diff --git a/src/render/renderer.test.ts b/src/render/renderer.test.ts new file mode 100644 index 0000000..68f2ec6 --- /dev/null +++ b/src/render/renderer.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from 'vitest'; +import { drawOverlay } from './renderer'; +import type { Segment, RenderStyle } from '../types'; + +class FakeCtx { + calls: string[] = []; + strokeStyle = ''; + lineWidth = 0; + beginPath() { this.calls.push('begin'); } + setLineDash(d: number[]) { this.calls.push(`dash:${d.length}`); } + moveTo(x: number, y: number) { this.calls.push(`move:${x},${y}`); } + lineTo(x: number, y: number) { this.calls.push(`line:${x},${y}`); } + stroke() { this.calls.push(`stroke:${this.strokeStyle}`); } +} + +const STYLE: RenderStyle = { cutColor: '#cut', rapidColor: '#rapid', lineWidth: 2 }; + +describe('drawOverlay', () => { + it('strokes a cut as a solid polyline in the cut colour', () => { + const ctx = new FakeCtx(); + const segs: Segment[] = [{ kind: 'cut', points: [[0, 0], [5, 5], [10, 0]] }]; + drawOverlay(ctx as unknown as CanvasRenderingContext2D, segs, STYLE); + expect(ctx.calls).toEqual(['begin', 'dash:0', 'move:0,0', 'line:5,5', 'line:10,0', 'stroke:#cut']); + }); + + it('strokes a rapid dashed in the rapid colour', () => { + const ctx = new FakeCtx(); + const segs: Segment[] = [{ kind: 'rapid', points: [[0, 0], [5, 0]] }]; + drawOverlay(ctx as unknown as CanvasRenderingContext2D, segs, STYLE); + expect(ctx.calls).toContain('dash:2'); + expect(ctx.calls).toContain('stroke:#rapid'); + }); + + it('skips degenerate segments', () => { + const ctx = new FakeCtx(); + drawOverlay(ctx as unknown as CanvasRenderingContext2D, [{ kind: 'cut', points: [[1, 1]] }], STYLE); + expect(ctx.calls).toEqual([]); + }); +}); diff --git a/src/render/renderer.ts b/src/render/renderer.ts new file mode 100644 index 0000000..0cf261c --- /dev/null +++ b/src/render/renderer.ts @@ -0,0 +1,28 @@ +import type { Segment, RenderStyle } from '../types'; + +/** Draw already-projected (image-pixel) segments onto a 2D context. */ +export function drawOverlay( + ctx: CanvasRenderingContext2D, + projected: Segment[], + style: RenderStyle, +): void { + for (const seg of projected) { + if (seg.points.length < 2) continue; + ctx.beginPath(); + ctx.setLineDash(seg.kind === 'rapid' ? [6, 4] : []); + ctx.strokeStyle = seg.kind === 'rapid' ? style.rapidColor : style.cutColor; + ctx.lineWidth = style.lineWidth; + const first = seg.points[0]!; + ctx.moveTo(first[0], first[1]); + for (let i = 1; i < seg.points.length; i++) { + ctx.lineTo(seg.points[i]![0], seg.points[i]![1]); + } + ctx.stroke(); + } +} + +/** Clear the whole canvas. */ +export function clearCanvas(ctx: CanvasRenderingContext2D, width: number, height: number): void { + ctx.setLineDash([]); + ctx.clearRect(0, 0, width, height); +}