@1agh/maude 0.45.2 → 0.47.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/apps/studio/acp/bootstrap-brief.ts +8 -0
- package/apps/studio/acp/bridge.ts +151 -1
- package/apps/studio/annotations-layer.tsx +42 -0
- package/apps/studio/annotations-snap.ts +50 -10
- package/apps/studio/api.ts +760 -15
- package/apps/studio/artboard-guides-overlay.tsx +270 -0
- package/apps/studio/bin/_agent-browser-safe-config.json +1 -0
- package/apps/studio/bin/_agent-browser-safe.mjs +228 -0
- package/apps/studio/bin/_agent-browser-safe.test.mjs +165 -0
- package/apps/studio/bin/_curl-local.mjs +349 -0
- package/apps/studio/bin/_curl-local.test.mjs +280 -0
- package/apps/studio/bin/_pdf-playwright.mjs +35 -4
- package/apps/studio/bin/_png-playwright.mjs +38 -4
- package/apps/studio/bin/_pw-launch.mjs +52 -0
- package/apps/studio/bin/_pw-launch.test.mjs +90 -0
- package/apps/studio/bin/_smart-frames.mjs +419 -0
- package/apps/studio/bin/_smart-frames.test.mjs +140 -0
- package/apps/studio/bin/agent-browser-safe.sh +29 -0
- package/apps/studio/bin/curl-local.sh +28 -0
- package/apps/studio/bin/smart-frames.sh +30 -0
- package/apps/studio/canvas-cursors.ts +6 -0
- package/apps/studio/canvas-edit.ts +972 -6
- package/apps/studio/canvas-icons.tsx +13 -0
- package/apps/studio/canvas-lib.tsx +206 -11
- package/apps/studio/canvas-shell.tsx +809 -50
- package/apps/studio/client/app.jsx +1554 -206
- package/apps/studio/client/panels/ChatPanel.jsx +92 -3
- package/apps/studio/client/panels/SettingsPanel.jsx +211 -0
- package/apps/studio/client/panels/acp-capabilities.js +11 -0
- package/apps/studio/client/styles/3-shell-maude.css +39 -0
- package/apps/studio/client/styles/6-acp-chat.css +41 -0
- package/apps/studio/contextual-toolbar.tsx +5 -3
- package/apps/studio/dist/client.bundle.js +1627 -1627
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +20 -0
- package/apps/studio/export-dialog.tsx +138 -18
- package/apps/studio/exporters/pdf.ts +332 -18
- package/apps/studio/exporters/png.ts +45 -3
- package/apps/studio/footage/schema.ts +17 -1
- package/apps/studio/generation/gemma-models.ts +224 -0
- package/apps/studio/generation/prefs.ts +43 -0
- package/apps/studio/grid-track-handles.ts +179 -0
- package/apps/studio/handoff.ts +35 -0
- package/apps/studio/http.ts +331 -7
- package/apps/studio/input-router.tsx +73 -17
- package/apps/studio/print/marks.ts +113 -0
- package/apps/studio/print/units.ts +269 -0
- package/apps/studio/print-overlay-content.tsx +132 -0
- package/apps/studio/test/acp-mode-banner.test.ts +45 -0
- package/apps/studio/test/acp-session-allowed-tools.test.ts +187 -0
- package/apps/studio/test/annotations-snap.test.ts +56 -0
- package/apps/studio/test/artboard-guides-overlay.test.tsx +152 -0
- package/apps/studio/test/artboard-kinds.test.tsx +83 -0
- package/apps/studio/test/artboard-selection-attrs.test.ts +69 -0
- package/apps/studio/test/browse-posture.test.tsx +107 -0
- package/apps/studio/test/canvas-hide-chrome.test.ts +58 -0
- package/apps/studio/test/canvas-meta-api.test.ts +237 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +4 -0
- package/apps/studio/test/comment-mount.test.ts +2 -1
- package/apps/studio/test/component-map.test.ts +48 -0
- package/apps/studio/test/convert-to-absolute.test.ts +333 -0
- package/apps/studio/test/detach-component.test.ts +94 -0
- package/apps/studio/test/edit-scope-api.test.ts +8 -4
- package/apps/studio/test/element-structural-api.test.ts +74 -0
- package/apps/studio/test/element-structural-edit.test.ts +363 -0
- package/apps/studio/test/exporters/png.test.ts +49 -1
- package/apps/studio/test/grid-track-handles.test.ts +160 -0
- package/apps/studio/test/handoff.test.ts +48 -0
- package/apps/studio/test/input-router.test.ts +82 -8
- package/apps/studio/test/layers-synthetic-groups.test.ts +96 -0
- package/apps/studio/test/pdf-print-boxes.test.ts +326 -0
- package/apps/studio/test/print-marks.test.ts +113 -0
- package/apps/studio/test/print-units.test.ts +173 -0
- package/apps/studio/test/use-snap-guides.test.ts +81 -0
- package/apps/studio/test/use-tool-mode.test.tsx +10 -2
- package/apps/studio/tool-palette.tsx +3 -1
- package/apps/studio/use-canvas-media-drop.tsx +126 -0
- package/apps/studio/use-chrome-visibility.tsx +19 -0
- package/apps/studio/use-element-resize.tsx +24 -3
- package/apps/studio/use-grid-track-handles.tsx +364 -0
- package/apps/studio/use-keyboard-discipline.tsx +15 -0
- package/apps/studio/use-snap-guides.tsx +73 -5
- package/apps/studio/use-spacing-handles.tsx +9 -5
- package/apps/studio/use-tool-mode.tsx +30 -3
- package/apps/studio/web-overlay-content.tsx +52 -0
- package/apps/studio/whats-new.json +55 -0
- package/cli/commands/design.mjs +25 -1
- package/package.json +8 -8
- package/plugins/design/dependencies.json +35 -0
- package/plugins/design/templates/_shell.html +4 -0
|
@@ -17,10 +17,13 @@ const base = (over: Partial<ClassifyInput>): ClassifyInput => ({
|
|
|
17
17
|
...over,
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
describe('input-router / pointermove — move tool', () => {
|
|
21
|
-
|
|
20
|
+
describe('input-router / pointermove — move (select) tool', () => {
|
|
21
|
+
// feature-4 (browse/move split, DDR-187) — the Move tool is now a SELECT
|
|
22
|
+
// tool: a bare hover paints a TOP-level preview halo (Figma's hover outline),
|
|
23
|
+
// Cmd-hover previews the deepest element.
|
|
24
|
+
test('bare hover → hover preview with deep=false (top-level outline)', () => {
|
|
22
25
|
const action = classify(base({ type: 'pointermove', clientX: 10, clientY: 20 }));
|
|
23
|
-
expect(action
|
|
26
|
+
expect(action).toMatchObject({ kind: 'hover', deep: false });
|
|
24
27
|
});
|
|
25
28
|
|
|
26
29
|
test('cmd-hover → hover preview with deep=true', () => {
|
|
@@ -46,15 +49,22 @@ describe('input-router / pointermove — move tool', () => {
|
|
|
46
49
|
});
|
|
47
50
|
});
|
|
48
51
|
|
|
49
|
-
describe('input-router / pointerdown — move tool
|
|
50
|
-
|
|
52
|
+
describe('input-router / pointerdown — move (select) tool ladder', () => {
|
|
53
|
+
// feature-4 (browse/move split, DDR-187) — the full Figma select ladder.
|
|
54
|
+
test('bare left-click → select replace, deep=false (TOP-level object)', () => {
|
|
51
55
|
const action = classify(base({ type: 'pointerdown', button: 0, clientX: 100, clientY: 200 }));
|
|
52
|
-
expect(action
|
|
56
|
+
expect(action).toEqual({
|
|
57
|
+
kind: 'select',
|
|
58
|
+
mode: 'replace',
|
|
59
|
+
deep: false,
|
|
60
|
+
clientX: 100,
|
|
61
|
+
clientY: 200,
|
|
62
|
+
});
|
|
53
63
|
});
|
|
54
64
|
|
|
55
|
-
test('shift+left-click (no cmd) →
|
|
65
|
+
test('shift+left-click (no cmd) → select add, deep=false (add TOP-level)', () => {
|
|
56
66
|
const action = classify(base({ type: 'pointerdown', button: 0, shiftKey: true }));
|
|
57
|
-
expect(action
|
|
67
|
+
expect(action).toMatchObject({ kind: 'select', mode: 'add', deep: false });
|
|
58
68
|
});
|
|
59
69
|
|
|
60
70
|
test('cmd+left-click → select replace, deep=true (nested single)', () => {
|
|
@@ -128,6 +138,70 @@ describe('input-router / pointerdown — tool-aware', () => {
|
|
|
128
138
|
});
|
|
129
139
|
});
|
|
130
140
|
|
|
141
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
142
|
+
// feature-4 (browse/move split, DDR-187) — POSTURE INVARIANTS. The browse tool
|
|
143
|
+
// is the boot default and must be a PURE native pass-through: it claims NOTHING,
|
|
144
|
+
// so a freshly-opened mock stays alive (buttons click, links follow). This
|
|
145
|
+
// promotes the old `input-router.tsx` comment-invariant ("bare clicks pass
|
|
146
|
+
// through") into an enforced gate. If any of these flip to a claim, an existing
|
|
147
|
+
// canvas silently loses its native interactivity on boot.
|
|
148
|
+
describe('input-router / browse tool — pure pass-through (boot default)', () => {
|
|
149
|
+
const browse = (over: Partial<ClassifyInput>): ClassifyInput =>
|
|
150
|
+
base({ activeTool: 'browse', ...over });
|
|
151
|
+
|
|
152
|
+
test('bare left-click → no-op (native button/link/input fires)', () => {
|
|
153
|
+
expect(classify(browse({ type: 'pointerdown', button: 0, clientX: 5, clientY: 6 })).kind).toBe(
|
|
154
|
+
'no-op'
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('cmd+left-click → select deep (the escape hatch; consumer flips tool to move)', () => {
|
|
159
|
+
// User steer 2026-07-19 — "I clicked to edit" shouldn't require V first.
|
|
160
|
+
expect(classify(browse({ type: 'pointerdown', button: 0, metaKey: true }))).toMatchObject({
|
|
161
|
+
kind: 'select',
|
|
162
|
+
mode: 'replace',
|
|
163
|
+
deep: true,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('shift+left-click → no-op (Shift alone is NOT the escape hatch)', () => {
|
|
168
|
+
expect(classify(browse({ type: 'pointerdown', button: 0, shiftKey: true })).kind).toBe('no-op');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('bare hover → no-op; Cmd-hover previews deep (escape-hatch affordance)', () => {
|
|
172
|
+
expect(classify(browse({ type: 'pointermove', clientX: 1, clientY: 2 })).kind).toBe('no-op');
|
|
173
|
+
expect(classify(browse({ type: 'pointermove', metaKey: true }))).toMatchObject({
|
|
174
|
+
kind: 'hover',
|
|
175
|
+
deep: true,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('right-click → still context-menu (chrome, not canvas content)', () => {
|
|
180
|
+
expect(classify(browse({ type: 'pointerdown', button: 2, clientX: 3, clientY: 4 })).kind).toBe(
|
|
181
|
+
'context-menu'
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('V keydown → tool move (the router stays live in browse so V switches)', () => {
|
|
186
|
+
expect(classify(browse({ type: 'keydown', key: 'v' }))).toEqual({
|
|
187
|
+
kind: 'tool',
|
|
188
|
+
tool: 'move',
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// The select action must NEVER be produced from a non-select tool on a bare
|
|
194
|
+
// gesture. Guards the "select leaked into comment/draw/hand/browse" regression.
|
|
195
|
+
describe('input-router / select never leaks into non-select tools', () => {
|
|
196
|
+
const nonSelectBare: Tool[] = ['browse', 'hand', 'comment', 'pen', 'shape', 'sticky', 'eraser'];
|
|
197
|
+
for (const activeTool of nonSelectBare) {
|
|
198
|
+
test(`bare left-click in ${activeTool} → not a select`, () => {
|
|
199
|
+
const action = classify(base({ type: 'pointerdown', button: 0, activeTool }));
|
|
200
|
+
expect(action.kind).not.toBe('select');
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
131
205
|
describe('input-router / contextmenu event', () => {
|
|
132
206
|
test('always opens menu + carries cursor coords', () => {
|
|
133
207
|
const action = classify(base({ type: 'contextmenu', clientX: 77, clientY: 88 }));
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// layers-synthetic-groups — feature-4 T7. The serializer that feeds the shell's
|
|
2
|
+
// Layers panel must emit SYNTHETIC group rows for unstamped wrappers that hold
|
|
3
|
+
// stamped descendants (so the tree mirrors real nesting), while still omitting
|
|
4
|
+
// purely-decorative wrappers with no addressable content. This is the fix for
|
|
5
|
+
// the "some layers don't show up / the tree is flat" report.
|
|
6
|
+
//
|
|
7
|
+
// Needs a live DOM (children / querySelector / closest) — register happy-dom.
|
|
8
|
+
|
|
9
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
10
|
+
import { GlobalRegistrator } from '@happy-dom/global-registrator';
|
|
11
|
+
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
GlobalRegistrator.register();
|
|
14
|
+
});
|
|
15
|
+
afterAll(() => {
|
|
16
|
+
GlobalRegistrator.unregister();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
import { serializeArtboardTree } from '../canvas-shell.tsx';
|
|
20
|
+
|
|
21
|
+
function root(html: string): Element {
|
|
22
|
+
const el = document.createElement('div');
|
|
23
|
+
el.className = 'dc-artboard-body';
|
|
24
|
+
el.innerHTML = html;
|
|
25
|
+
return el;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('serializeArtboardTree — synthetic group rows for unstamped wrappers', () => {
|
|
29
|
+
test('unstamped wrapper WITH a stamped descendant → synthetic group parent (nesting preserved)', () => {
|
|
30
|
+
const tree = serializeArtboardTree(
|
|
31
|
+
root('<div class="hero-wrap"><button data-cd-id="aabbccdd">Go</button></div>')
|
|
32
|
+
);
|
|
33
|
+
expect(tree).toHaveLength(1);
|
|
34
|
+
const group = tree[0]!;
|
|
35
|
+
expect(group.synthetic).toBe(true);
|
|
36
|
+
expect(group.type).toBe('group');
|
|
37
|
+
expect(group.id.startsWith('__grp:')).toBe(true);
|
|
38
|
+
expect(group.tag).toBe('div');
|
|
39
|
+
// The stamped button is now a CHILD of the group (not hoisted to the root).
|
|
40
|
+
expect(group.children).toHaveLength(1);
|
|
41
|
+
expect(group.children[0]!.id).toBe('aabbccdd');
|
|
42
|
+
expect(group.children[0]!.synthetic).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('unstamped wrapper with NO stamped descendant → omitted (no decorative noise)', () => {
|
|
46
|
+
const tree = serializeArtboardTree(root('<div class="deco"><span>just text</span></div>'));
|
|
47
|
+
expect(tree).toHaveLength(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('stamped child directly → a normal (non-synthetic) row, no group wrapper', () => {
|
|
51
|
+
const tree = serializeArtboardTree(root('<section data-cd-id="11112222">Hi</section>'));
|
|
52
|
+
expect(tree).toHaveLength(1);
|
|
53
|
+
expect(tree[0]!.synthetic).toBeUndefined();
|
|
54
|
+
expect(tree[0]!.id).toBe('11112222');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('nested unstamped wrappers → nested synthetic groups (each rung shown)', () => {
|
|
58
|
+
const tree = serializeArtboardTree(
|
|
59
|
+
root('<div class="outer"><div class="inner"><a data-cd-id="deadbeef">L</a></div></div>')
|
|
60
|
+
);
|
|
61
|
+
expect(tree).toHaveLength(1);
|
|
62
|
+
const outer = tree[0]!;
|
|
63
|
+
expect(outer.synthetic).toBe(true);
|
|
64
|
+
expect(outer.children).toHaveLength(1);
|
|
65
|
+
const inner = outer.children[0]!;
|
|
66
|
+
expect(inner.synthetic).toBe(true);
|
|
67
|
+
expect(inner.children[0]!.id).toBe('deadbeef');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('synthetic group ids are unique across same-signature siblings (stable keys)', () => {
|
|
71
|
+
const tree = serializeArtboardTree(
|
|
72
|
+
root(
|
|
73
|
+
'<div class="col"><span data-cd-id="aaaa1111">a</span></div>' +
|
|
74
|
+
'<div class="col"><span data-cd-id="bbbb2222">b</span></div>'
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
expect(tree).toHaveLength(2);
|
|
78
|
+
expect(tree[0]!.id).not.toBe(tree[1]!.id);
|
|
79
|
+
expect(tree[0]!.synthetic).toBe(true);
|
|
80
|
+
expect(tree[1]!.synthetic).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('mixed: a stamped element AND a sibling unstamped-wrapper-with-content both surface', () => {
|
|
84
|
+
const tree = serializeArtboardTree(
|
|
85
|
+
root(
|
|
86
|
+
'<h1 data-cd-id="head0001">Title</h1>' +
|
|
87
|
+
'<div class="body"><p data-cd-id="para0001">copy</p></div>'
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
expect(tree).toHaveLength(2);
|
|
91
|
+
expect(tree[0]!.id).toBe('head0001');
|
|
92
|
+
expect(tree[0]!.synthetic).toBeUndefined();
|
|
93
|
+
expect(tree[1]!.synthetic).toBe(true);
|
|
94
|
+
expect(tree[1]!.children[0]!.id).toBe('para0001');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// feature-2-print-artboards T5 — the load-bearing golden gate: MediaBox ⊇
|
|
2
|
+
// BleedBox ⊇ TrimBox with exact expected pt values, plus marks presence.
|
|
3
|
+
// Exercises pdf-lib directly (no Playwright/Chromium needed — box-setting
|
|
4
|
+
// and mark-drawing are pure pdf-lib API calls); the page size mirrors what
|
|
5
|
+
// Chromium's page.pdf() would actually produce for an artboard authored via
|
|
6
|
+
// resolvePrintArtboard (96dpi CSS px → pt, same ratio as mm → pt).
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from 'bun:test';
|
|
9
|
+
import { PDFDocument } from 'pdf-lib';
|
|
10
|
+
|
|
11
|
+
import { readArtboardPrintProp } from '../canvas-edit.ts';
|
|
12
|
+
import {
|
|
13
|
+
applyPageFit,
|
|
14
|
+
applyPrintBoxesAndMarks,
|
|
15
|
+
artboardIdFromCssPath,
|
|
16
|
+
type PdfPrintOptions,
|
|
17
|
+
parsePageFit,
|
|
18
|
+
parsePdfPrintOptions,
|
|
19
|
+
resolvePdfDeviceScale,
|
|
20
|
+
} from '../exporters/pdf.ts';
|
|
21
|
+
import { requiredSlugPt } from '../print/marks.ts';
|
|
22
|
+
import {
|
|
23
|
+
getPaperPreset,
|
|
24
|
+
mmToPt,
|
|
25
|
+
pxToPt,
|
|
26
|
+
resolvePrintArtboard,
|
|
27
|
+
trimSizeMm,
|
|
28
|
+
} from '../print/units.ts';
|
|
29
|
+
|
|
30
|
+
const NO_MARKS: PdfPrintOptions = {
|
|
31
|
+
marks: { crop: false, registration: false, colorBars: false, pageInfo: false },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe('pdf.ts — artboardIdFromCssPath', () => {
|
|
35
|
+
test('extracts the id from the artboard-by-id selector shape', () => {
|
|
36
|
+
expect(artboardIdFromCssPath('[data-dc-screen="home"]')).toBe('home');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('a descendant / :first-of-type selector yields null', () => {
|
|
40
|
+
expect(artboardIdFromCssPath('.some .descendant')).toBeNull();
|
|
41
|
+
expect(artboardIdFromCssPath('[data-dc-screen]:first-of-type')).toBeNull();
|
|
42
|
+
expect(artboardIdFromCssPath('[data-dc-screen]')).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('pdf.ts — parsePdfPrintOptions', () => {
|
|
47
|
+
test('absent/malformed → null (print post-pass off)', () => {
|
|
48
|
+
expect(parsePdfPrintOptions(undefined)).toBeNull();
|
|
49
|
+
expect(parsePdfPrintOptions(null)).toBeNull();
|
|
50
|
+
expect(parsePdfPrintOptions('nope')).toBeNull();
|
|
51
|
+
expect(parsePdfPrintOptions([1, 2])).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('valid shape passes through with marks defaulting false', () => {
|
|
55
|
+
expect(parsePdfPrintOptions({})).toEqual({
|
|
56
|
+
includeBleed: undefined,
|
|
57
|
+
marks: { crop: false, registration: false, colorBars: false, pageInfo: false },
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('marks flags coerce to strict booleans (non-true → false)', () => {
|
|
62
|
+
expect(parsePdfPrintOptions({ marks: { crop: 'yes', registration: 1 } })).toEqual({
|
|
63
|
+
includeBleed: undefined,
|
|
64
|
+
marks: { crop: false, registration: false, colorBars: false, pageInfo: false },
|
|
65
|
+
});
|
|
66
|
+
expect(parsePdfPrintOptions({ marks: { crop: true, registration: true } })).toEqual({
|
|
67
|
+
includeBleed: undefined,
|
|
68
|
+
marks: { crop: true, registration: true, colorBars: false, pageInfo: false },
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('includeBleed passes through only when a strict boolean', () => {
|
|
73
|
+
expect(parsePdfPrintOptions({ includeBleed: false })?.includeBleed).toBe(false);
|
|
74
|
+
expect(parsePdfPrintOptions({ includeBleed: 'false' })?.includeBleed).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('pdf.ts — parsePageFit', () => {
|
|
79
|
+
test('a known paper id passes through', () => {
|
|
80
|
+
expect(parsePageFit('a4')).toBe('a4');
|
|
81
|
+
expect(parsePageFit('letter')).toBe('letter');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('unknown id / non-string → null', () => {
|
|
85
|
+
expect(parsePageFit('not-a-paper')).toBeNull();
|
|
86
|
+
expect(parsePageFit(42)).toBeNull();
|
|
87
|
+
expect(parsePageFit(undefined)).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Dogfood follow-up (2026-07-16) — a "vector PDF" still embeds raster
|
|
92
|
+
// content (dropped photos, large-format art) as a bitmap whose density is
|
|
93
|
+
// set by the capturing context's deviceScaleFactor; `dpi` controls that.
|
|
94
|
+
describe('pdf.ts — resolvePdfDeviceScale (raster-content density, dogfood follow-up)', () => {
|
|
95
|
+
test("no dpi → 1× (today's unchanged default — NOT png.ts's 2× default)", () => {
|
|
96
|
+
expect(resolvePdfDeviceScale({})).toBe(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('dpi=300 → 3.125× (300/96)', () => {
|
|
100
|
+
expect(resolvePdfDeviceScale({ dpi: 300 })).toBeCloseTo(300 / 96, 10);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('dpi=600 → 6.25×', () => {
|
|
104
|
+
expect(resolvePdfDeviceScale({ dpi: 600 })).toBeCloseTo(600 / 96, 10);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("a bare `scale` option (PNG's legacy field) is ignored — PDF has no scale concept", () => {
|
|
108
|
+
expect(resolvePdfDeviceScale({ scale: 3 })).toBe(1);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('pdf-print-boxes — GOLDEN: A4 + 3mm bleed, no marks', () => {
|
|
113
|
+
test('MediaBox === BleedBox (full page); TrimBox insets by bleedPt exactly', async () => {
|
|
114
|
+
const resolved = resolvePrintArtboard({ paper: 'a4', bleedMm: 3 });
|
|
115
|
+
const pageWidthPt = pxToPt(resolved.widthPx);
|
|
116
|
+
const pageHeightPt = pxToPt(resolved.heightPx);
|
|
117
|
+
|
|
118
|
+
const doc = await PDFDocument.create();
|
|
119
|
+
const page = doc.addPage([pageWidthPt, pageHeightPt]);
|
|
120
|
+
applyPrintBoxesAndMarks(page, 3, NO_MARKS);
|
|
121
|
+
|
|
122
|
+
const bleedPt = mmToPt(3);
|
|
123
|
+
const media = page.getMediaBox();
|
|
124
|
+
const bleed = page.getBleedBox();
|
|
125
|
+
const trim = page.getTrimBox();
|
|
126
|
+
|
|
127
|
+
// No marks requested → no slug → MediaBox is exactly the bleed box.
|
|
128
|
+
expect(media.x).toBeCloseTo(0, 6);
|
|
129
|
+
expect(media.y).toBeCloseTo(0, 6);
|
|
130
|
+
expect(media.width).toBeCloseTo(pageWidthPt, 6);
|
|
131
|
+
expect(media.height).toBeCloseTo(pageHeightPt, 6);
|
|
132
|
+
|
|
133
|
+
expect(bleed.x).toBeCloseTo(0, 6);
|
|
134
|
+
expect(bleed.y).toBeCloseTo(0, 6);
|
|
135
|
+
expect(bleed.width).toBeCloseTo(pageWidthPt, 6);
|
|
136
|
+
expect(bleed.height).toBeCloseTo(pageHeightPt, 6);
|
|
137
|
+
|
|
138
|
+
expect(trim.x).toBeCloseTo(bleedPt, 6);
|
|
139
|
+
expect(trim.y).toBeCloseTo(bleedPt, 6);
|
|
140
|
+
expect(trim.width).toBeCloseTo(pageWidthPt - 2 * bleedPt, 6);
|
|
141
|
+
expect(trim.height).toBeCloseTo(pageHeightPt - 2 * bleedPt, 6);
|
|
142
|
+
|
|
143
|
+
// The load-bearing nesting invariant, independent of the exact numbers above.
|
|
144
|
+
expect(media.x).toBeLessThanOrEqual(bleed.x);
|
|
145
|
+
expect(media.y).toBeLessThanOrEqual(bleed.y);
|
|
146
|
+
expect(media.x + media.width).toBeGreaterThanOrEqual(bleed.x + bleed.width);
|
|
147
|
+
expect(bleed.x).toBeLessThanOrEqual(trim.x);
|
|
148
|
+
expect(bleed.y).toBeLessThanOrEqual(trim.y);
|
|
149
|
+
expect(bleed.x + bleed.width).toBeGreaterThanOrEqual(trim.x + trim.width);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('pdf-print-boxes — GOLDEN: A4 + 3mm bleed + crop + registration marks', () => {
|
|
154
|
+
test('MediaBox enlarges by the exact required slug, negative origin, content-box unmoved', async () => {
|
|
155
|
+
const resolved = resolvePrintArtboard({ paper: 'a4', bleedMm: 3 });
|
|
156
|
+
const pageWidthPt = pxToPt(resolved.widthPx);
|
|
157
|
+
const pageHeightPt = pxToPt(resolved.heightPx);
|
|
158
|
+
|
|
159
|
+
const doc = await PDFDocument.create();
|
|
160
|
+
const page = doc.addPage([pageWidthPt, pageHeightPt]);
|
|
161
|
+
const printOpts: PdfPrintOptions = {
|
|
162
|
+
marks: { crop: true, registration: true, colorBars: false, pageInfo: false },
|
|
163
|
+
};
|
|
164
|
+
applyPrintBoxesAndMarks(page, 3, printOpts);
|
|
165
|
+
|
|
166
|
+
const bleedPt = mmToPt(3);
|
|
167
|
+
const slugPt = requiredSlugPt({ crop: true, registration: true });
|
|
168
|
+
const media = page.getMediaBox();
|
|
169
|
+
const bleed = page.getBleedBox();
|
|
170
|
+
const trim = page.getTrimBox();
|
|
171
|
+
|
|
172
|
+
// Exact expected pt values — the golden assertion.
|
|
173
|
+
expect(media.x).toBeCloseTo(-slugPt, 6);
|
|
174
|
+
expect(media.y).toBeCloseTo(-slugPt, 6);
|
|
175
|
+
expect(media.width).toBeCloseTo(pageWidthPt + 2 * slugPt, 6);
|
|
176
|
+
expect(media.height).toBeCloseTo(pageHeightPt + 2 * slugPt, 6);
|
|
177
|
+
// MediaBox's negative origin is the marks slug ONLY — bleed box content
|
|
178
|
+
// coordinates are untouched (still start at the page's own (0,0)).
|
|
179
|
+
expect(bleed.x).toBeCloseTo(0, 6);
|
|
180
|
+
expect(bleed.y).toBeCloseTo(0, 6);
|
|
181
|
+
expect(bleed.width).toBeCloseTo(pageWidthPt, 6);
|
|
182
|
+
expect(bleed.height).toBeCloseTo(pageHeightPt, 6);
|
|
183
|
+
expect(trim.x).toBeCloseTo(bleedPt, 6);
|
|
184
|
+
expect(trim.y).toBeCloseTo(bleedPt, 6);
|
|
185
|
+
expect(trim.width).toBeCloseTo(pageWidthPt - 2 * bleedPt, 6);
|
|
186
|
+
expect(trim.height).toBeCloseTo(pageHeightPt - 2 * bleedPt, 6);
|
|
187
|
+
|
|
188
|
+
// MediaBox ⊇ BleedBox ⊇ TrimBox, strictly (marks slug > 0 here).
|
|
189
|
+
expect(media.x).toBeLessThan(bleed.x);
|
|
190
|
+
expect(media.width + media.x).toBeGreaterThan(bleed.width + bleed.x);
|
|
191
|
+
expect(bleed.x).toBeLessThan(trim.x);
|
|
192
|
+
expect(bleed.width + bleed.x).toBeGreaterThan(trim.width + trim.x);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('marks present as vector ops — saved PDF is measurably larger with marks than without', async () => {
|
|
196
|
+
const resolved = resolvePrintArtboard({ paper: 'a4', bleedMm: 3 });
|
|
197
|
+
const pageWidthPt = pxToPt(resolved.widthPx);
|
|
198
|
+
const pageHeightPt = pxToPt(resolved.heightPx);
|
|
199
|
+
|
|
200
|
+
const withoutMarks = await PDFDocument.create();
|
|
201
|
+
applyPrintBoxesAndMarks(withoutMarks.addPage([pageWidthPt, pageHeightPt]), 3, NO_MARKS);
|
|
202
|
+
const withoutBytes = await withoutMarks.save();
|
|
203
|
+
|
|
204
|
+
const withMarks = await PDFDocument.create();
|
|
205
|
+
applyPrintBoxesAndMarks(withMarks.addPage([pageWidthPt, pageHeightPt]), 3, {
|
|
206
|
+
marks: { crop: true, registration: true, colorBars: false, pageInfo: false },
|
|
207
|
+
});
|
|
208
|
+
const withBytes = await withMarks.save();
|
|
209
|
+
|
|
210
|
+
// 8 crop-mark line segments + 4 registration circles (each an approximated
|
|
211
|
+
// bezier ellipse) + 8 crosshair segments is a non-trivial amount of vector
|
|
212
|
+
// content — the byte-size delta is a reliable proxy without depending on
|
|
213
|
+
// pdf-lib's internal (and version-specific) content-stream compression
|
|
214
|
+
// format for a literal operator-string grep.
|
|
215
|
+
expect(withBytes.byteLength).toBeGreaterThan(withoutBytes.byteLength);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('pdf-print-boxes — includeBleed:false crops the bleed strip out', () => {
|
|
220
|
+
test('MediaBox/BleedBox collapse onto TrimBox (degenerate but valid nesting)', async () => {
|
|
221
|
+
const resolved = resolvePrintArtboard({ paper: 'a4', bleedMm: 3 });
|
|
222
|
+
const pageWidthPt = pxToPt(resolved.widthPx);
|
|
223
|
+
const pageHeightPt = pxToPt(resolved.heightPx);
|
|
224
|
+
const bleedPt = mmToPt(3);
|
|
225
|
+
|
|
226
|
+
const doc = await PDFDocument.create();
|
|
227
|
+
const page = doc.addPage([pageWidthPt, pageHeightPt]);
|
|
228
|
+
applyPrintBoxesAndMarks(page, 3, { ...NO_MARKS, includeBleed: false });
|
|
229
|
+
|
|
230
|
+
const media = page.getMediaBox();
|
|
231
|
+
const bleed = page.getBleedBox();
|
|
232
|
+
const trim = page.getTrimBox();
|
|
233
|
+
|
|
234
|
+
expect(media.x).toBeCloseTo(bleedPt, 6);
|
|
235
|
+
expect(media.y).toBeCloseTo(bleedPt, 6);
|
|
236
|
+
expect(media.width).toBeCloseTo(pageWidthPt - 2 * bleedPt, 6);
|
|
237
|
+
expect(bleed.x).toBeCloseTo(trim.x, 6);
|
|
238
|
+
expect(bleed.width).toBeCloseTo(trim.width, 6);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// RCA issue-pdf-print-export-marks-missing — the shape that shipped broken:
|
|
243
|
+
// two `kind="print"` artboards sharing one `print` spec via a top-level const
|
|
244
|
+
// (AlligatorsAcko.tsx's actual authoring pattern), not a repeated inline
|
|
245
|
+
// literal per artboard. The multi-page loop in exporters/pdf.ts `run()`
|
|
246
|
+
// re-resolves readArtboardPrintProp independently per written page (keyed by
|
|
247
|
+
// the artboard's own data-dc-screen id — see safeArtboardFilename), so
|
|
248
|
+
// asserting it resolves per-artboard-id here is the load-bearing regression
|
|
249
|
+
// guard: both pages must get the SAME resolved geometry, not just page 1.
|
|
250
|
+
describe('readArtboardPrintProp — shared const print spec resolves for every page (multi-page correlation)', () => {
|
|
251
|
+
const canvas = [
|
|
252
|
+
'const A1_PRINT = { paper: "a1", orientation: "portrait", bleedMm: 3 } as const;',
|
|
253
|
+
'export default function Demo() {',
|
|
254
|
+
' return (',
|
|
255
|
+
' <DesignCanvas>',
|
|
256
|
+
' <DCArtboard id="acko-front" label="A" kind="print" width={816} height={1146} print={A1_PRINT as any}>',
|
|
257
|
+
' <div>front</div>',
|
|
258
|
+
' </DCArtboard>',
|
|
259
|
+
' <DCArtboard id="acko-back" label="B" kind="print" width={816} height={1146} print={A1_PRINT as any}>',
|
|
260
|
+
' <div>back</div>',
|
|
261
|
+
' </DCArtboard>',
|
|
262
|
+
' </DesignCanvas>',
|
|
263
|
+
' );',
|
|
264
|
+
'}',
|
|
265
|
+
].join('\n');
|
|
266
|
+
|
|
267
|
+
test('both artboard ids resolve to the same non-null print geometry, driving non-degenerate boxes+marks on each page', async () => {
|
|
268
|
+
const front = readArtboardPrintProp('/abs/AlligatorsAcko.tsx', canvas, 'acko-front');
|
|
269
|
+
const back = readArtboardPrintProp('/abs/AlligatorsAcko.tsx', canvas, 'acko-back');
|
|
270
|
+
expect(front).toEqual({ paper: 'a1', orientation: 'portrait', bleedMm: 3 });
|
|
271
|
+
expect(back).toEqual(front);
|
|
272
|
+
|
|
273
|
+
const printOpts: PdfPrintOptions = {
|
|
274
|
+
marks: { crop: true, registration: true, colorBars: false, pageInfo: false },
|
|
275
|
+
};
|
|
276
|
+
for (const printProp of [front, back]) {
|
|
277
|
+
if (!printProp) throw new Error('expected a resolved print prop');
|
|
278
|
+
const bleedMm = typeof printProp.bleedMm === 'number' ? printProp.bleedMm : 0;
|
|
279
|
+
const resolved = resolvePrintArtboard({ paper: printProp.paper as string, bleedMm });
|
|
280
|
+
const pageWidthPt = pxToPt(resolved.widthPx);
|
|
281
|
+
const pageHeightPt = pxToPt(resolved.heightPx);
|
|
282
|
+
const doc = await PDFDocument.create();
|
|
283
|
+
const page = doc.addPage([pageWidthPt, pageHeightPt]);
|
|
284
|
+
applyPrintBoxesAndMarks(page, bleedMm, printOpts);
|
|
285
|
+
const media = page.getMediaBox();
|
|
286
|
+
const trim = page.getTrimBox();
|
|
287
|
+
// Non-degenerate: TrimBox strictly inside MediaBox on every side — the
|
|
288
|
+
// exact thing that silently failed to happen when printProp was null.
|
|
289
|
+
expect(media.width).toBeGreaterThan(trim.width);
|
|
290
|
+
expect(media.height).toBeGreaterThan(trim.height);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe('pdf-print-boxes — applyPageFit (non-print artboard scale-to-paper)', () => {
|
|
296
|
+
test('the resulting page is sized to the target paper, content preserved', async () => {
|
|
297
|
+
const doc = await PDFDocument.create();
|
|
298
|
+
// A wide, non-print-sized artboard (e.g. a 1440×1024 digital screen at 96dpi).
|
|
299
|
+
const origWidthPt = pxToPt(1440);
|
|
300
|
+
const origHeightPt = pxToPt(1024);
|
|
301
|
+
doc.addPage([origWidthPt, origHeightPt]);
|
|
302
|
+
expect(doc.getPageCount()).toBe(1);
|
|
303
|
+
|
|
304
|
+
await applyPageFit(doc, 0, 'a4');
|
|
305
|
+
|
|
306
|
+
expect(doc.getPageCount()).toBe(1); // swapped in place, not appended
|
|
307
|
+
const page = doc.getPage(0);
|
|
308
|
+
// Expected size mirrors applyPageFit's OWN derivation (mmToPt of the
|
|
309
|
+
// preset's raw mm dims via trimSizeMm) — NOT a px-rounded roundtrip via
|
|
310
|
+
// resolvePrintArtboard, which would introduce a spurious sub-pt mismatch.
|
|
311
|
+
const preset = getPaperPreset('a4');
|
|
312
|
+
if (!preset) throw new Error('a4 preset missing');
|
|
313
|
+
const { widthMm, heightMm } = trimSizeMm(preset, 'portrait');
|
|
314
|
+
const { width, height } = page.getSize();
|
|
315
|
+
expect(width).toBeCloseTo(mmToPt(widthMm), 6);
|
|
316
|
+
expect(height).toBeCloseTo(mmToPt(heightMm), 6);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('unknown paper id is a no-op (never throws, never mutates)', async () => {
|
|
320
|
+
const doc = await PDFDocument.create();
|
|
321
|
+
doc.addPage([100, 100]);
|
|
322
|
+
await applyPageFit(doc, 0, 'not-a-paper');
|
|
323
|
+
expect(doc.getPageCount()).toBe(1);
|
|
324
|
+
expect(doc.getPage(0).getSize()).toEqual({ width: 100, height: 100 });
|
|
325
|
+
});
|
|
326
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// feature-2-print-artboards T5 — print/marks.ts pure geometry unit tests.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'bun:test';
|
|
4
|
+
|
|
5
|
+
import { computeMarksGeometry, MARK_STROKE_PT, requiredSlugPt } from '../print/marks.ts';
|
|
6
|
+
import { mmToPt } from '../print/units.ts';
|
|
7
|
+
|
|
8
|
+
const PAGE_W = 600; // pt, arbitrary
|
|
9
|
+
const PAGE_H = 800;
|
|
10
|
+
const BLEED = mmToPt(3);
|
|
11
|
+
|
|
12
|
+
describe('print/marks — requiredSlugPt', () => {
|
|
13
|
+
test('neither crop nor registration → 0', () => {
|
|
14
|
+
expect(requiredSlugPt({})).toBe(0);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('crop only → the mark length', () => {
|
|
18
|
+
expect(requiredSlugPt({ crop: true })).toBeCloseTo(mmToPt(3.5), 6);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('registration reaches further than crop alone', () => {
|
|
22
|
+
const cropOnly = requiredSlugPt({ crop: true });
|
|
23
|
+
const both = requiredSlugPt({ crop: true, registration: true });
|
|
24
|
+
expect(both).toBeGreaterThan(cropOnly);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('print/marks — computeMarksGeometry', () => {
|
|
29
|
+
test('crop:false, registration:false → both arrays empty', () => {
|
|
30
|
+
const g = computeMarksGeometry({ pageWidthPt: PAGE_W, pageHeightPt: PAGE_H, bleedPt: BLEED });
|
|
31
|
+
expect(g.cropMarks).toEqual([]);
|
|
32
|
+
expect(g.registrationMarks).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('crop:true → exactly 8 segments (2 per corner × 4 corners)', () => {
|
|
36
|
+
const g = computeMarksGeometry({
|
|
37
|
+
pageWidthPt: PAGE_W,
|
|
38
|
+
pageHeightPt: PAGE_H,
|
|
39
|
+
bleedPt: BLEED,
|
|
40
|
+
crop: true,
|
|
41
|
+
});
|
|
42
|
+
expect(g.cropMarks.length).toBe(8);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('every crop-mark segment lies OUTSIDE the bleed box (page) bounds', () => {
|
|
46
|
+
const g = computeMarksGeometry({
|
|
47
|
+
pageWidthPt: PAGE_W,
|
|
48
|
+
pageHeightPt: PAGE_H,
|
|
49
|
+
bleedPt: BLEED,
|
|
50
|
+
crop: true,
|
|
51
|
+
});
|
|
52
|
+
for (const seg of g.cropMarks) {
|
|
53
|
+
const outside = (x: number, y: number) => x < 0 || x > PAGE_W || y < 0 || y > PAGE_H;
|
|
54
|
+
// At least one endpoint of every mark segment must be outside the page —
|
|
55
|
+
// the whole point of a crop mark is to be visible in the bleed/slug area.
|
|
56
|
+
expect(outside(seg.x1, seg.y1) || outside(seg.x2, seg.y2)).toBe(true);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('crop-mark segments align to the trim line (bleed inset)', () => {
|
|
61
|
+
const g = computeMarksGeometry({
|
|
62
|
+
pageWidthPt: PAGE_W,
|
|
63
|
+
pageHeightPt: PAGE_H,
|
|
64
|
+
bleedPt: BLEED,
|
|
65
|
+
crop: true,
|
|
66
|
+
});
|
|
67
|
+
const trimX0 = BLEED;
|
|
68
|
+
const trimX1 = PAGE_W - BLEED;
|
|
69
|
+
const trimY0 = BLEED;
|
|
70
|
+
const trimY1 = PAGE_H - BLEED;
|
|
71
|
+
const xs = g.cropMarks.filter((s) => s.x1 === s.x2).map((s) => s.x1);
|
|
72
|
+
const ys = g.cropMarks.filter((s) => s.y1 === s.y2).map((s) => s.y1);
|
|
73
|
+
expect(new Set(xs)).toEqual(new Set([trimX0, trimX1]));
|
|
74
|
+
expect(new Set(ys)).toEqual(new Set([trimY0, trimY1]));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('registration:true → 4 marks, each a circle + 2-segment crosshair', () => {
|
|
78
|
+
const g = computeMarksGeometry({
|
|
79
|
+
pageWidthPt: PAGE_W,
|
|
80
|
+
pageHeightPt: PAGE_H,
|
|
81
|
+
bleedPt: BLEED,
|
|
82
|
+
registration: true,
|
|
83
|
+
});
|
|
84
|
+
expect(g.registrationMarks.length).toBe(4);
|
|
85
|
+
for (const m of g.registrationMarks) {
|
|
86
|
+
expect(m.crosshair.length).toBe(2);
|
|
87
|
+
expect(m.circle.r).toBeGreaterThan(0);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('registration marks sit beyond the crop marks (no visual collision)', () => {
|
|
92
|
+
const g = computeMarksGeometry({
|
|
93
|
+
pageWidthPt: PAGE_W,
|
|
94
|
+
pageHeightPt: PAGE_H,
|
|
95
|
+
bleedPt: BLEED,
|
|
96
|
+
crop: true,
|
|
97
|
+
registration: true,
|
|
98
|
+
});
|
|
99
|
+
const topCenterReg = g.registrationMarks.find((m) => m.circle.cy > PAGE_H);
|
|
100
|
+
expect(topCenterReg).toBeDefined();
|
|
101
|
+
const topCropTip = Math.max(
|
|
102
|
+
...g.cropMarks.filter((s) => s.y1 > PAGE_H || s.y2 > PAGE_H).map((s) => Math.max(s.y1, s.y2))
|
|
103
|
+
);
|
|
104
|
+
expect((topCenterReg?.circle.cy ?? 0) - (topCenterReg?.circle.r ?? 0)).toBeGreaterThan(
|
|
105
|
+
topCropTip
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('MARK_STROKE_PT is a small positive value (0.25mm)', () => {
|
|
111
|
+
expect(MARK_STROKE_PT).toBeCloseTo(mmToPt(0.25), 6);
|
|
112
|
+
expect(MARK_STROKE_PT).toBeGreaterThan(0);
|
|
113
|
+
});
|