Skip to content
Source
Sign inSign up

AnnotationStroke.test.ts

73 lines2.9 KB

1import type { PreviewAnnotationPoint } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3​
4import {
5 addStrokePoint,
6 openStrokePath,
7 startStroke,
8 strokeBounds,
9 STROKE_SEGMENTS_PER_PATH,
10} from "./AnnotationStroke.ts";
11​
12// The Draw tool used to rebuild the whole path and rescan every point on each move.
13const fullPath = (points: ReadonlyArray<PreviewAnnotationPoint>): string => {
14 let path = `M ${points[0]!.x} ${points[0]!.y}`;
15 for (let index = 1; index < points.length - 1; index += 1) {
16 const current = points[index]!;
17 const next = points[index + 1]!;
18 path += ` Q ${current.x} ${current.y} ${(current.x + next.x) / 2} ${(current.y + next.y) / 2}`;
19 }
20 const last = points[points.length - 1]!;
21 return `${path} L ${last.x} ${last.y}`;
22};
23​
24const fullBounds = (points: ReadonlyArray<PreviewAnnotationPoint>, width: number) => {
25 const xs = points.map((point) => point.x);
26 const ys = points.map((point) => point.y);
27 const padding = width + 3;
28 const left = Math.min(...xs) - padding;
29 const top = Math.min(...ys) - padding;
30 const right = Math.max(...xs) + padding;
31 const bottom = Math.max(...ys) + padding;
32 return { x: left, y: top, width: right - left, height: bottom - top };
33};
34​
35// Each later path starts with a move to where the previous one ended.
36const joinPaths = (paths: ReadonlyArray<string>): string =>
37 paths
38 .map((path, index) => {
39 if (index === 0) return path;
40 const resume = path.match(/^M (\S+ \S+)/)![1]!;
41 expect(paths[index - 1]!.endsWith(` ${resume}`)).toBe(true);
42 return path.slice(`M ${resume}`.length);
43 })
44 .join("");
45​
46describe("stroke geometry", () => {
47 it("draws the same curve and bounds as a full rescan at every length", () => {
48 const points = Array.from({ length: STROKE_SEGMENTS_PER_PATH * 2 + 30 }, (_, index) => ({
49 x: index * 3 + ((index * 7) % 11),
50 y: 200 + ((index * 13) % 29) - index,
51 }));
52 const geometry = startStroke(points[0]!);
53 const closedPaths: string[] = [];
54 for (let index = 1; index < points.length; index += 1) {
55 const closedPath = addStrokePoint(geometry, points[index]!);
56 if (closedPath !== null) closedPaths.push(closedPath);
57 const drawn = points.slice(0, index + 1);
58 expect(joinPaths([...closedPaths, openStrokePath(geometry)])).toBe(fullPath(drawn));
59 expect(strokeBounds(geometry, 4)).toEqual(fullBounds(drawn, 4));
60 }
61 expect(closedPaths).toHaveLength(2);
62 expect(geometry.points).toEqual(points);
63 });
64​
65 it("keeps the rewritten path short for a stroke past the engine's argument limit", () => {
66 const geometry = startStroke({ x: 0, y: 0 });
67 for (let index = 1; index < 250_000; index += 1) {
68 addStrokePoint(geometry, { x: index % 800, y: index % 600 });
69 }
70 expect(strokeBounds(geometry, 4)).toEqual({ x: -7, y: -7, width: 813, height: 613 });
71 expect(openStrokePath(geometry).length).toBeLessThan(STROKE_SEGMENTS_PER_PATH * 40);
72 });
73});