@1agh/maude 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -1
- package/cli/bin/maude.mjs +3 -0
- package/cli/commands/cache.mjs +181 -0
- package/cli/commands/cache.test.mjs +166 -0
- package/cli/commands/design-link.test.mjs +32 -2
- package/cli/commands/design.mjs +95 -4
- package/cli/commands/design.test.mjs +56 -0
- package/cli/commands/help.mjs +34 -0
- package/cli/commands/hub.mjs +255 -30
- package/cli/commands/hub.test.mjs +126 -2
- package/cli/commands/init.mjs +3 -0
- package/cli/commands/preflight.mjs +11 -0
- package/cli/commands/preflight.test.mjs +46 -0
- package/cli/commands/scenario-report.mjs +45 -0
- package/cli/lib/cache.mjs +407 -0
- package/cli/lib/cache.test.mjs +303 -0
- package/cli/lib/design-link.mjs +74 -10
- package/cli/lib/flow-design-integration.test.mjs +165 -0
- package/cli/lib/gitignore-block.mjs +90 -0
- package/cli/lib/gitignore-block.test.mjs +123 -0
- package/cli/lib/plugin-cli-reachability.test.mjs +56 -0
- package/cli/lib/preflight.mjs +41 -10
- package/package.json +8 -8
- package/plugins/design/dependencies.json +30 -2
- package/plugins/design/dev-server/ai-banner.tsx +2 -2
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +763 -123
- package/plugins/design/dev-server/annotations-layer.tsx +1624 -208
- package/plugins/design/dev-server/api.ts +123 -4
- package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_html-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pdf-playwright.mjs +25 -8
- package/plugins/design/dev-server/bin/_png-playwright.mjs +20 -20
- package/plugins/design/dev-server/bin/_pptx-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pw-launch.mjs +61 -0
- package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_svg-playwright.mjs +125 -19
- package/plugins/design/dev-server/bin/preflight.sh +21 -10
- package/plugins/design/dev-server/bin/prep.sh +211 -0
- package/plugins/design/dev-server/bin/scenario-report.mjs +209 -0
- package/plugins/design/dev-server/bin/screenshot.sh +18 -1
- package/plugins/design/dev-server/bin/smoke.sh +208 -23
- package/plugins/design/dev-server/canvas-arrowheads.ts +220 -0
- package/plugins/design/dev-server/canvas-comment-mount.tsx +8 -24
- package/plugins/design/dev-server/canvas-cursors.ts +173 -0
- package/plugins/design/dev-server/canvas-icons.tsx +299 -0
- package/plugins/design/dev-server/canvas-lib.tsx +25 -21
- package/plugins/design/dev-server/canvas-meta.schema.json +20 -0
- package/plugins/design/dev-server/canvas-shell.tsx +404 -66
- package/plugins/design/dev-server/client/app.jsx +1231 -416
- package/plugins/design/dev-server/client/styles/3-shell.css +10 -0
- package/plugins/design/dev-server/collab/index.ts +87 -9
- package/plugins/design/dev-server/collab/persistence.ts +34 -3
- package/plugins/design/dev-server/collab/registry.ts +80 -2
- package/plugins/design/dev-server/collab/room.ts +21 -8
- package/plugins/design/dev-server/context-menu.tsx +167 -23
- package/plugins/design/dev-server/context.ts +24 -0
- package/plugins/design/dev-server/contextual-toolbar.tsx +7 -7
- package/plugins/design/dev-server/dist/client.bundle.js +364 -29
- package/plugins/design/dev-server/dist/comment-mount.js +78 -33
- package/plugins/design/dev-server/dist/styles.css +16 -0
- package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
- package/plugins/design/dev-server/export-dialog.tsx +208 -18
- package/plugins/design/dev-server/exporters/html.ts +4 -1
- package/plugins/design/dev-server/exporters/index.ts +4 -1
- package/plugins/design/dev-server/exporters/pdf.ts +7 -1
- package/plugins/design/dev-server/exporters/png.ts +21 -2
- package/plugins/design/dev-server/exporters/pptx.ts +330 -201
- package/plugins/design/dev-server/exporters/scope.ts +107 -11
- package/plugins/design/dev-server/exporters/svg.ts +4 -1
- package/plugins/design/dev-server/fs-watch.ts +1 -0
- package/plugins/design/dev-server/http.ts +299 -28
- package/plugins/design/dev-server/input-router.tsx +35 -5
- package/plugins/design/dev-server/participants-chrome.tsx +10 -10
- package/plugins/design/dev-server/server.ts +123 -1
- package/plugins/design/dev-server/sync/agent.ts +95 -0
- package/plugins/design/dev-server/sync/codec.ts +155 -0
- package/plugins/design/dev-server/sync/connection-state.ts +203 -0
- package/plugins/design/dev-server/sync/index.ts +479 -35
- package/plugins/design/dev-server/sync/materialize.ts +62 -0
- package/plugins/design/dev-server/sync/migrate-seed.ts +163 -0
- package/plugins/design/dev-server/sync/origins.ts +57 -0
- package/plugins/design/dev-server/sync/projection.ts +368 -0
- package/plugins/design/dev-server/sync/status.ts +115 -0
- package/plugins/design/dev-server/sync/untrusted.ts +153 -0
- package/plugins/design/dev-server/test/_helpers.ts +6 -2
- package/plugins/design/dev-server/test/annotations-draw-modifiers.test.ts +206 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +311 -0
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +519 -0
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +162 -0
- package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +76 -0
- package/plugins/design/dev-server/test/canvas-route.test.ts +4 -1
- package/plugins/design/dev-server/test/collab-reseed-guard.test.ts +78 -0
- package/plugins/design/dev-server/test/collab-room.test.ts +21 -10
- package/plugins/design/dev-server/test/csp-canvas-shell.test.ts +46 -0
- package/plugins/design/dev-server/test/exporters/png.test.ts +24 -1
- package/plugins/design/dev-server/test/exporters/pptx-deck.test.ts +112 -0
- package/plugins/design/dev-server/test/exporters/pw-launch.test.ts +49 -0
- package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
- package/plugins/design/dev-server/test/fixtures/phase-20-annotations.svg +1 -0
- package/plugins/design/dev-server/test/fixtures/phase-21-annotations.svg +1 -0
- package/plugins/design/dev-server/test/input-router.test.ts +32 -4
- package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +132 -0
- package/plugins/design/dev-server/test/shared-doc-convergence.test.ts +265 -0
- package/plugins/design/dev-server/test/shared-doc-foundation.test.ts +63 -0
- package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +160 -0
- package/plugins/design/dev-server/test/shared-doc-projection.test.ts +238 -0
- package/plugins/design/dev-server/test/sync-connection-state.test.ts +146 -0
- package/plugins/design/dev-server/test/sync-meta-codec.test.ts +123 -0
- package/plugins/design/dev-server/test/sync-runtime.test.ts +531 -4
- package/plugins/design/dev-server/test/sync-status.test.ts +80 -0
- package/plugins/design/dev-server/test/sync-untrusted.test.ts +104 -0
- package/plugins/design/dev-server/test/use-annotation-resize.test.ts +200 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +39 -13
- package/plugins/design/dev-server/tool-palette.tsx +155 -24
- package/plugins/design/dev-server/undo-hud.tsx +4 -4
- package/plugins/design/dev-server/undo-stack.ts +20 -4
- package/plugins/design/dev-server/use-annotation-resize.tsx +219 -61
- package/plugins/design/dev-server/use-tool-mode.tsx +68 -19
- package/plugins/design/dev-server/ws.ts +40 -1
- package/plugins/design/templates/_shell.html +85 -10
- package/plugins/design/templates/canvas.tsx.template +13 -0
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +14 -7
- package/plugins/flow/.claude-plugin/config.schema.json +5 -0
- package/plugins/flow/templates/ai-skeleton/README.md +1 -1
- package/plugins/flow/templates/ai-skeleton/gitignore +10 -0
- package/plugins/flow/templates/ai-skeleton/scenarios/README.md +3 -1
- package/plugins/flow/templates/ai-skeleton/workflows.config.json +2 -1
|
@@ -13,12 +13,18 @@ import {
|
|
|
13
13
|
type ArrowStroke,
|
|
14
14
|
type EllipseStroke,
|
|
15
15
|
type PenStroke,
|
|
16
|
+
type PolygonStroke,
|
|
16
17
|
type RectStroke,
|
|
18
|
+
STICKY_PALETTE,
|
|
19
|
+
type StickyStroke,
|
|
17
20
|
type Stroke,
|
|
18
21
|
type TextStroke,
|
|
19
22
|
arrowHeadPoints,
|
|
20
23
|
penPathD,
|
|
24
|
+
polygonPoints,
|
|
25
|
+
polygonVertices,
|
|
21
26
|
rid,
|
|
27
|
+
strokeBBox,
|
|
22
28
|
strokeHitTest,
|
|
23
29
|
strokesShallowEqual,
|
|
24
30
|
strokesToSvg,
|
|
@@ -461,3 +467,308 @@ describe('annotations-layer / strokesShallowEqual (drag no-op gate)', () => {
|
|
|
461
467
|
expect(strokesShallowEqual([], [])).toBe(true);
|
|
462
468
|
});
|
|
463
469
|
});
|
|
470
|
+
|
|
471
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
472
|
+
// Phase 21 — sticky / standalone-text / rect-radius / arrow-heads write path.
|
|
473
|
+
// Parse + byte-identical round-trip live in annotations-roundtrip.test.ts
|
|
474
|
+
// (they need a DOMParser, registered there via happy-dom).
|
|
475
|
+
|
|
476
|
+
describe('annotations-layer / Phase 21 sticky serialization', () => {
|
|
477
|
+
const sticky: StickyStroke = {
|
|
478
|
+
id: 'st1',
|
|
479
|
+
tool: 'sticky',
|
|
480
|
+
color: STICKY_PALETTE[0],
|
|
481
|
+
x: 40,
|
|
482
|
+
y: 50,
|
|
483
|
+
w: 200,
|
|
484
|
+
h: 160,
|
|
485
|
+
text: 'approve copy?',
|
|
486
|
+
fontSize: 14,
|
|
487
|
+
cornerRadius: 8,
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
test('sticky → <g data-tool="sticky"> with rect + inert <text> body', () => {
|
|
491
|
+
const svg = strokesToSvg([sticky]);
|
|
492
|
+
expect(svg).toContain('data-id="st1"');
|
|
493
|
+
expect(svg).toContain('data-tool="sticky"');
|
|
494
|
+
expect(svg).toContain('data-r="8"');
|
|
495
|
+
expect(svg).toContain('data-fs="14"');
|
|
496
|
+
expect(svg).toContain(`fill="${STICKY_PALETTE[0]}"`);
|
|
497
|
+
expect(svg).toContain('<rect x="40" y="50" width="200" height="160" rx="8" ry="8"/>');
|
|
498
|
+
// Body text lives in an allowlisted <text> (survives sanitizeAnnotationSvg,
|
|
499
|
+
// which strips <foreignObject>). NEVER a foreignObject in the persisted SVG.
|
|
500
|
+
expect(svg).not.toContain('foreignObject');
|
|
501
|
+
expect(svg).toContain('>approve copy?</text>');
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test('sticky default color is the muted yellow paper tint (slot 0); 10 muted tints (Phase 24)', () => {
|
|
505
|
+
expect(STICKY_PALETTE[0]).toBe('#fce8a6');
|
|
506
|
+
expect(STICKY_PALETTE).toHaveLength(10);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test('sticky body text is HTML-escaped (no tag injection)', () => {
|
|
510
|
+
const svg = strokesToSvg([{ ...sticky, text: '<script>x</script>' }]);
|
|
511
|
+
expect(svg).toContain('<script>x</script>');
|
|
512
|
+
expect(svg).not.toMatch(/<script[\s>]/);
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
describe('annotations-layer / Phase 21 rect corner radius serialization', () => {
|
|
517
|
+
const base: RectStroke = {
|
|
518
|
+
id: 'r',
|
|
519
|
+
tool: 'rect',
|
|
520
|
+
color: '#1d6cf0',
|
|
521
|
+
width: 2,
|
|
522
|
+
x: 0,
|
|
523
|
+
y: 0,
|
|
524
|
+
w: 50,
|
|
525
|
+
h: 50,
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
test('cornerRadius 0 (or absent) emits NO rx/ry/data-r (byte-compat)', () => {
|
|
529
|
+
expect(strokesToSvg([base])).not.toContain('rx=');
|
|
530
|
+
expect(strokesToSvg([{ ...base, cornerRadius: 0 }])).not.toContain('data-r=');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test('cornerRadius 8 emits rx/ry + data-r', () => {
|
|
534
|
+
const svg = strokesToSvg([{ ...base, cornerRadius: 8 }]);
|
|
535
|
+
expect(svg).toContain('rx="8" ry="8" data-r="8"');
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test('cornerRadius 999 (pill) round-trips the literal value', () => {
|
|
539
|
+
expect(strokesToSvg([{ ...base, cornerRadius: 999 }])).toContain('data-r="999"');
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
describe('annotations-layer / Phase 21 arrow heads + dash serialization', () => {
|
|
544
|
+
const base: ArrowStroke = {
|
|
545
|
+
id: 'a',
|
|
546
|
+
tool: 'arrow',
|
|
547
|
+
color: '#1a8f3e',
|
|
548
|
+
width: 2,
|
|
549
|
+
x1: 0,
|
|
550
|
+
y1: 0,
|
|
551
|
+
x2: 100,
|
|
552
|
+
y2: 0,
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
test('default arrow (endHead triangle, no start, solid) emits the legacy form', () => {
|
|
556
|
+
const svg = strokesToSvg([base]);
|
|
557
|
+
// Exactly one polyline (the end head), no data-* head attrs, no dasharray.
|
|
558
|
+
expect(svg.match(/<polyline/g) ?? []).toHaveLength(1);
|
|
559
|
+
expect(svg).not.toContain('data-start-head');
|
|
560
|
+
expect(svg).not.toContain('data-end-head');
|
|
561
|
+
expect(svg).not.toContain('data-dash');
|
|
562
|
+
expect(svg).not.toContain('stroke-dasharray');
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test('no heads (line) emits zero polylines + data-end-head="none"', () => {
|
|
566
|
+
const svg = strokesToSvg([{ ...base, startHead: 'none', endHead: 'none' }]);
|
|
567
|
+
expect(svg.match(/<polyline/g) ?? []).toHaveLength(0);
|
|
568
|
+
expect(svg).toContain('data-end-head="none"');
|
|
569
|
+
expect(svg).not.toContain('data-start-head');
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('both heads emit two polylines + data-start-head="triangle"', () => {
|
|
573
|
+
const svg = strokesToSvg([{ ...base, startHead: 'triangle', endHead: 'triangle' }]);
|
|
574
|
+
expect(svg.match(/<polyline/g) ?? []).toHaveLength(2);
|
|
575
|
+
expect(svg).toContain('data-start-head="triangle"');
|
|
576
|
+
expect(svg).not.toContain('data-end-head'); // triangle is the default
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test('start-only head: one polyline + both head data-attrs', () => {
|
|
580
|
+
const svg = strokesToSvg([{ ...base, startHead: 'triangle', endHead: 'none' }]);
|
|
581
|
+
expect(svg.match(/<polyline/g) ?? []).toHaveLength(1);
|
|
582
|
+
expect(svg).toContain('data-start-head="triangle"');
|
|
583
|
+
expect(svg).toContain('data-end-head="none"');
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
test('dashed arrow emits stroke-dasharray + data-dash="1"', () => {
|
|
587
|
+
const svg = strokesToSvg([{ ...base, dashed: true }]);
|
|
588
|
+
expect(svg).toContain('stroke-dasharray="6 4"');
|
|
589
|
+
expect(svg).toContain('data-dash="1"');
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
describe('annotations-layer / Phase 21 standalone text serialization', () => {
|
|
594
|
+
test('standalone text writes x/y and OMITS data-anchor-id', () => {
|
|
595
|
+
const t: TextStroke = {
|
|
596
|
+
id: 't-std',
|
|
597
|
+
tool: 'text',
|
|
598
|
+
color: '#1a1a1a',
|
|
599
|
+
fontSize: 14,
|
|
600
|
+
text: 'label',
|
|
601
|
+
x: 120,
|
|
602
|
+
y: 80,
|
|
603
|
+
};
|
|
604
|
+
const svg = strokesToSvg([t]);
|
|
605
|
+
expect(svg).toContain('data-tool="text"');
|
|
606
|
+
expect(svg).toContain('x="120"');
|
|
607
|
+
expect(svg).toContain('y="80"');
|
|
608
|
+
expect(svg).not.toContain('data-anchor-id');
|
|
609
|
+
expect(svg).toContain('>label</text>');
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test('anchored text still writes data-anchor-id (back-compat, unchanged)', () => {
|
|
613
|
+
const t: TextStroke = {
|
|
614
|
+
id: 't-anc',
|
|
615
|
+
tool: 'text',
|
|
616
|
+
color: '#1a1a1a',
|
|
617
|
+
fontSize: 14,
|
|
618
|
+
text: 'anchored',
|
|
619
|
+
anchorId: 'r-host',
|
|
620
|
+
};
|
|
621
|
+
const svg = strokesToSvg([t]);
|
|
622
|
+
expect(svg).toContain('data-anchor-id="r-host"');
|
|
623
|
+
expect(svg).not.toContain('x=');
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
describe('annotations-layer / Phase 21 sticky + standalone-text geometry', () => {
|
|
628
|
+
const sticky: StickyStroke = {
|
|
629
|
+
id: 'st',
|
|
630
|
+
tool: 'sticky',
|
|
631
|
+
color: STICKY_PALETTE[2],
|
|
632
|
+
x: 10,
|
|
633
|
+
y: 20,
|
|
634
|
+
w: 200,
|
|
635
|
+
h: 160,
|
|
636
|
+
text: 'note',
|
|
637
|
+
fontSize: 14,
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
test('sticky bbox is its rect extent', () => {
|
|
641
|
+
expect(strokeBBox(sticky)).toEqual({ x: 10, y: 20, w: 200, h: 160 });
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test('sticky bbox normalizes negative extent (mid-drag)', () => {
|
|
645
|
+
expect(strokeBBox({ ...sticky, x: 100, y: 100, w: -40, h: -30 })).toEqual({
|
|
646
|
+
x: 60,
|
|
647
|
+
y: 70,
|
|
648
|
+
w: 40,
|
|
649
|
+
h: 30,
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test('sticky is a filled-rect hit anywhere inside', () => {
|
|
654
|
+
expect(strokeHitTest(sticky, 100, 100, 4)).toBe(true); // interior
|
|
655
|
+
expect(strokeHitTest(sticky, 10, 20, 4)).toBe(true); // corner
|
|
656
|
+
expect(strokeHitTest(sticky, 500, 500, 4)).toBe(false); // far outside
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test('standalone text has a synthetic selectable bbox at its (x, y)', () => {
|
|
660
|
+
const t: TextStroke = {
|
|
661
|
+
id: 't',
|
|
662
|
+
tool: 'text',
|
|
663
|
+
color: '#000',
|
|
664
|
+
fontSize: 14,
|
|
665
|
+
text: 'hello',
|
|
666
|
+
x: 50,
|
|
667
|
+
y: 60,
|
|
668
|
+
};
|
|
669
|
+
const bb = strokeBBox(t);
|
|
670
|
+
expect(bb?.x).toBe(50);
|
|
671
|
+
expect(bb?.y).toBe(60);
|
|
672
|
+
expect(bb?.w).toBeGreaterThan(0);
|
|
673
|
+
expect(bb?.h).toBeCloseTo(14 * 1.2, 4);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
test('standalone text is eraser-hittable; anchored text is not', () => {
|
|
677
|
+
const std: TextStroke = {
|
|
678
|
+
id: 't',
|
|
679
|
+
tool: 'text',
|
|
680
|
+
color: '#000',
|
|
681
|
+
fontSize: 14,
|
|
682
|
+
text: 'hi',
|
|
683
|
+
x: 0,
|
|
684
|
+
y: 0,
|
|
685
|
+
};
|
|
686
|
+
expect(strokeHitTest(std, 2, 2, 4)).toBe(true);
|
|
687
|
+
const anchored: TextStroke = {
|
|
688
|
+
id: 't2',
|
|
689
|
+
tool: 'text',
|
|
690
|
+
color: '#000',
|
|
691
|
+
fontSize: 14,
|
|
692
|
+
text: 'hi',
|
|
693
|
+
anchorId: 'host',
|
|
694
|
+
};
|
|
695
|
+
expect(strokeHitTest(anchored, 2, 2, 4)).toBe(false);
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
describe('annotations-layer / Phase 24 polygon geometry', () => {
|
|
700
|
+
test('polygonVertices span the full bbox for every shape', () => {
|
|
701
|
+
for (const shape of ['diamond', 'triangle', 'triangle-down'] as const) {
|
|
702
|
+
const pts = polygonVertices(shape, 10, 20, 80, 60);
|
|
703
|
+
const xs = pts.map((p) => p[0]);
|
|
704
|
+
const ys = pts.map((p) => p[1]);
|
|
705
|
+
expect(Math.min(...xs)).toBeCloseTo(10, 4);
|
|
706
|
+
expect(Math.max(...xs)).toBeCloseTo(90, 4);
|
|
707
|
+
expect(Math.min(...ys)).toBeCloseTo(20, 4);
|
|
708
|
+
expect(Math.max(...ys)).toBeCloseTo(80, 4);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test('diamond has 4 vertices; triangles have 3', () => {
|
|
713
|
+
expect(polygonVertices('diamond', 0, 0, 10, 10)).toHaveLength(4);
|
|
714
|
+
expect(polygonVertices('triangle', 0, 0, 10, 10)).toHaveLength(3);
|
|
715
|
+
expect(polygonVertices('triangle-down', 0, 0, 10, 10)).toHaveLength(3);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
test('polygonPoints serializes vertices as an SVG points string', () => {
|
|
719
|
+
expect(polygonPoints('diamond', 0, 0, 10, 10)).toBe('5,0 10,5 5,10 0,5');
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
test('polygon serializes as <polygon data-tool="polygon" data-shape=...>', () => {
|
|
723
|
+
const p: PolygonStroke = {
|
|
724
|
+
id: 'pg',
|
|
725
|
+
tool: 'polygon',
|
|
726
|
+
shape: 'triangle',
|
|
727
|
+
color: '#e5484d',
|
|
728
|
+
width: 2,
|
|
729
|
+
x: 0,
|
|
730
|
+
y: 0,
|
|
731
|
+
w: 40,
|
|
732
|
+
h: 40,
|
|
733
|
+
};
|
|
734
|
+
const svg = strokesToSvg([p]);
|
|
735
|
+
expect(svg).toContain('data-tool="polygon"');
|
|
736
|
+
expect(svg).toContain('data-shape="triangle"');
|
|
737
|
+
expect(svg).toContain('points="20,0 40,40 0,40"');
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test('polygon bbox is its normalized extent', () => {
|
|
741
|
+
const p: PolygonStroke = {
|
|
742
|
+
id: 'pg',
|
|
743
|
+
tool: 'polygon',
|
|
744
|
+
shape: 'diamond',
|
|
745
|
+
color: '#000',
|
|
746
|
+
width: 2,
|
|
747
|
+
x: 100,
|
|
748
|
+
y: 100,
|
|
749
|
+
w: -40,
|
|
750
|
+
h: -30,
|
|
751
|
+
};
|
|
752
|
+
expect(strokeBBox(p)).toEqual({ x: 60, y: 70, w: 40, h: 30 });
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
test('filled diamond hit-tests inside; stroke-only hits the edge not the centre', () => {
|
|
756
|
+
const filled: PolygonStroke = {
|
|
757
|
+
id: 'd',
|
|
758
|
+
tool: 'polygon',
|
|
759
|
+
shape: 'diamond',
|
|
760
|
+
color: '#000',
|
|
761
|
+
width: 2,
|
|
762
|
+
x: 0,
|
|
763
|
+
y: 0,
|
|
764
|
+
w: 100,
|
|
765
|
+
h: 100,
|
|
766
|
+
fill: '#eee',
|
|
767
|
+
};
|
|
768
|
+
expect(strokeHitTest(filled, 50, 50, 4)).toBe(true); // centre, filled
|
|
769
|
+
expect(strokeHitTest(filled, 5, 5, 4)).toBe(false); // outside the diamond (in a bbox corner)
|
|
770
|
+
const outline: PolygonStroke = { ...filled, fill: null };
|
|
771
|
+
expect(strokeHitTest(outline, 50, 50, 4)).toBe(false); // centre, not filled
|
|
772
|
+
expect(strokeHitTest(outline, 25, 25, 4)).toBe(true); // on the NW edge midpoint line
|
|
773
|
+
});
|
|
774
|
+
});
|