@bovinphang/craft-common 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +12 -2
- package/dist/content/catalog.d.ts +4 -0
- package/dist/content/resources.d.ts +2 -0
- package/dist/content/types.d.ts +21 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +668 -0
- package/dist/install/identity.d.ts +5 -0
- package/dist/install/planner.d.ts +6 -0
- package/dist/install/transaction.d.ts +7 -0
- package/dist/install/types.d.ts +44 -0
- package/docs/consuming-common.md +33 -0
- package/localized/zh-CN/methods/debug.md +13 -0
- package/localized/zh-CN/methods/doc-sync.md +12 -0
- package/localized/zh-CN/methods/refactoring.md +16 -0
- package/localized/zh-CN/methods/source-driven-development.md +19 -0
- package/localized/zh-CN/methods/tdd.md +13 -0
- package/localized/zh-CN/skills/cc-debug-methodology/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-debug-methodology/references/method.md +13 -0
- package/localized/zh-CN/skills/cc-doc-sync-core/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-doc-sync-core/references/method.md +12 -0
- package/localized/zh-CN/skills/cc-drawio-studio/SKILL.md +73 -0
- package/localized/zh-CN/skills/cc-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
- package/localized/zh-CN/skills/cc-drawio-studio/data/brand-icons.json +878 -0
- package/localized/zh-CN/skills/cc-drawio-studio/data/shape-index.json +83570 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/data-residency.md +27 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/diagram-patterns.md +60 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/flowchart-quality.md +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/layout-and-export.md +72 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/troubleshooting.md +29 -0
- package/localized/zh-CN/skills/cc-drawio-studio/references/xml-and-mermaid.md +58 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/brand-symbols.mjs +99 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/diagram-lint.mjs +205 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/diagram-url.mjs +102 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/drawio-export.mjs +134 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/layout-graph.mjs +107 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/png-embed-fix.mjs +26 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-go-packages.mjs +36 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-js-modules.mjs +52 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-python-classes.mjs +30 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-python-modules.mjs +55 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/shape-query.mjs +121 -0
- package/localized/zh-CN/skills/cc-drawio-studio/scripts/studio-core.mjs +139 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/built-in/corporate.json +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/built-in/default.json +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/built-in/handdrawn.json +49 -0
- package/localized/zh-CN/skills/cc-drawio-studio/styles/schema.json +112 -0
- package/localized/zh-CN/skills/cc-refactoring-core/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-refactoring-core/references/method.md +16 -0
- package/localized/zh-CN/skills/cc-source-driven-development/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-source-driven-development/references/method.md +19 -0
- package/localized/zh-CN/skills/cc-tdd-methodology/SKILL.md +8 -0
- package/localized/zh-CN/skills/cc-tdd-methodology/references/method.md +13 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/SKILL.md +34 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/interactive-diagram.html +371 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/README.md +14 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/architecture.json +121 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/invalid-explicit.json +41 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/sequence.json +53 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/sequence.mmd +21 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/workflow.json +98 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/assets/quality-examples/workflow.mmd +7 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/references/diagram-workflows.md +81 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/references/html-technical-diagrams.md +243 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/references/png-qa-autofix.md +37 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/diagram-browser.mjs +153 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/diagram-layout.mjs +176 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/export-diagram.mjs +332 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/interactive-diagram-server.mjs +654 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/mermaid-render.mjs +64 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/png-qa.mjs +462 -0
- package/localized/zh-CN/skills/cc-technical-diagrams/scripts/tech-diagram-render.mjs +1537 -0
- package/localized/zh-CN/skills/eval_queries.json +160 -0
- package/localized/zh-CN/skills/metadata.json +223 -0
- package/localized/zh-CN/skills/relations.json +53 -0
- package/methods/debug.md +13 -0
- package/methods/doc-sync.md +12 -0
- package/methods/refactoring.md +16 -0
- package/methods/source-driven-development.md +19 -0
- package/methods/tdd.md +13 -0
- package/package.json +51 -4
- package/skills/cc-debug-methodology/SKILL.md +8 -0
- package/skills/cc-debug-methodology/references/method.md +13 -0
- package/skills/cc-doc-sync-core/SKILL.md +8 -0
- package/skills/cc-doc-sync-core/references/method.md +12 -0
- package/skills/cc-drawio-studio/SKILL.md +73 -0
- package/skills/cc-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
- package/skills/cc-drawio-studio/data/brand-icons.json +878 -0
- package/skills/cc-drawio-studio/data/shape-index.json +83570 -0
- package/skills/cc-drawio-studio/references/data-residency.md +27 -0
- package/skills/cc-drawio-studio/references/diagram-patterns.md +60 -0
- package/skills/cc-drawio-studio/references/flowchart-quality.md +49 -0
- package/skills/cc-drawio-studio/references/layout-and-export.md +72 -0
- package/skills/cc-drawio-studio/references/troubleshooting.md +29 -0
- package/skills/cc-drawio-studio/references/xml-and-mermaid.md +58 -0
- package/skills/cc-drawio-studio/scripts/brand-symbols.mjs +99 -0
- package/skills/cc-drawio-studio/scripts/diagram-lint.mjs +205 -0
- package/skills/cc-drawio-studio/scripts/diagram-url.mjs +102 -0
- package/skills/cc-drawio-studio/scripts/drawio-export.mjs +134 -0
- package/skills/cc-drawio-studio/scripts/layout-graph.mjs +107 -0
- package/skills/cc-drawio-studio/scripts/png-embed-fix.mjs +26 -0
- package/skills/cc-drawio-studio/scripts/scan-go-packages.mjs +36 -0
- package/skills/cc-drawio-studio/scripts/scan-js-modules.mjs +52 -0
- package/skills/cc-drawio-studio/scripts/scan-python-classes.mjs +30 -0
- package/skills/cc-drawio-studio/scripts/scan-python-modules.mjs +55 -0
- package/skills/cc-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
- package/skills/cc-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
- package/skills/cc-drawio-studio/scripts/shape-query.mjs +121 -0
- package/skills/cc-drawio-studio/scripts/studio-core.mjs +139 -0
- package/skills/cc-drawio-studio/styles/built-in/corporate.json +49 -0
- package/skills/cc-drawio-studio/styles/built-in/default.json +49 -0
- package/skills/cc-drawio-studio/styles/built-in/handdrawn.json +49 -0
- package/skills/cc-drawio-studio/styles/schema.json +112 -0
- package/skills/cc-refactoring-core/SKILL.md +8 -0
- package/skills/cc-refactoring-core/references/method.md +16 -0
- package/skills/cc-source-driven-development/SKILL.md +8 -0
- package/skills/cc-source-driven-development/references/method.md +19 -0
- package/skills/cc-tdd-methodology/SKILL.md +8 -0
- package/skills/cc-tdd-methodology/references/method.md +13 -0
- package/skills/cc-technical-diagrams/SKILL.md +34 -0
- package/skills/cc-technical-diagrams/assets/interactive-diagram.html +371 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/README.md +14 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/architecture.json +121 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/invalid-explicit.json +41 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/sequence.json +53 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/sequence.mmd +21 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/workflow.json +98 -0
- package/skills/cc-technical-diagrams/assets/quality-examples/workflow.mmd +7 -0
- package/skills/cc-technical-diagrams/references/diagram-workflows.md +81 -0
- package/skills/cc-technical-diagrams/references/html-technical-diagrams.md +243 -0
- package/skills/cc-technical-diagrams/references/png-qa-autofix.md +37 -0
- package/skills/cc-technical-diagrams/scripts/diagram-browser.mjs +153 -0
- package/skills/cc-technical-diagrams/scripts/diagram-layout.mjs +176 -0
- package/skills/cc-technical-diagrams/scripts/export-diagram.mjs +332 -0
- package/skills/cc-technical-diagrams/scripts/interactive-diagram-server.mjs +654 -0
- package/skills/cc-technical-diagrams/scripts/mermaid-render.mjs +64 -0
- package/skills/cc-technical-diagrams/scripts/png-qa.mjs +462 -0
- package/skills/cc-technical-diagrams/scripts/tech-diagram-render.mjs +1537 -0
- package/skills/eval_queries.json +160 -0
- package/skills/metadata.json +223 -0
- package/skills/relations.json +53 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @typedef {{x:number,y:number,width:number,height:number}} Rect */
|
|
4
|
+
/** @typedef {[number,number]} Point */
|
|
5
|
+
/** @typedef {{code:string,id:string,message:string}} QualityIssue */
|
|
6
|
+
/** @typedef {{lines:string[],width:number,height:number,fontSize:number,lineHeight:number}} TextLayout */
|
|
7
|
+
/** @typedef {Rect & {id:string,label:string,sublabel:string,type:string,actor?:string,step?:string,shape?:"box"|"diamond"|"terminal"}} Box */
|
|
8
|
+
|
|
9
|
+
/** @param {string} text @param {number} fontSize */
|
|
10
|
+
function textWidth(text, fontSize) {
|
|
11
|
+
return [...new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text)]
|
|
12
|
+
.reduce((sum, item) => sum + (/^[\x00-\x7f]+$/.test(item.segment) ? fontSize * 0.6 : fontSize), 0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @param {string} text @param {number} maxWidth @param {number} [fontSize] @param {number} [lineHeight] @returns {TextLayout} */
|
|
16
|
+
export function layoutText(text, maxWidth, fontSize = 14, lineHeight = 20) {
|
|
17
|
+
const limit = Math.max(fontSize, maxWidth);
|
|
18
|
+
/** @type {string[]} */
|
|
19
|
+
const lines = [];
|
|
20
|
+
for (const paragraph of text.split(/\r?\n/)) {
|
|
21
|
+
let line = "";
|
|
22
|
+
for (const { segment: word } of new Intl.Segmenter(undefined, { granularity: "word" }).segment(paragraph)) {
|
|
23
|
+
if (line && textWidth(word,fontSize) <= limit && textWidth(line+word,fontSize)>limit) {lines.push(line);line="";}
|
|
24
|
+
for (const { segment } of new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(word)) {
|
|
25
|
+
if (line && textWidth(line + segment, fontSize) > limit) {
|
|
26
|
+
lines.push(line);
|
|
27
|
+
line = "";
|
|
28
|
+
}
|
|
29
|
+
line += segment;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
lines.push(line);
|
|
33
|
+
}
|
|
34
|
+
return { lines, width: Math.max(0, ...lines.map((line) => textWidth(line, fontSize))), height: lines.length * lineHeight, fontSize, lineHeight };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @param {Box} source @param {{width:boolean,height:boolean}} explicit */
|
|
38
|
+
export function fitNodeText(source, explicit) {
|
|
39
|
+
const box = { ...source };
|
|
40
|
+
if (![box.width, box.height].every((v) => Number.isFinite(v) && v > 0)) throw new Error(`Invalid size for node "${box.id}".`);
|
|
41
|
+
const diamond = box.shape === "diamond" || box.type === "decision";
|
|
42
|
+
const semantic = ["agent", "model", "memory", "vectorstore", "graphdb", "tool", "document", "queue", "browser", "user", "gateway"].includes(box.type);
|
|
43
|
+
if (!explicit.width) box.width = Math.max(box.width, diamond ? 240 : 200);
|
|
44
|
+
const available = diamond ? box.width * 0.5 - 12 : (box.type === "queue" ? box.width - 64 : box.width - 32);
|
|
45
|
+
const main = layoutText(box.label, available);
|
|
46
|
+
const sub = layoutText(box.sublabel, available, 12, 16);
|
|
47
|
+
const header = ["database", "vectorstore"].includes(box.type) ? 30 : box.type === "queue" ? 20 : semantic ? 46 : box.actor ? 40 : 20;
|
|
48
|
+
const contentHeight = main.height + (box.sublabel ? sub.height + 6 : 0);
|
|
49
|
+
const needed = diamond ? (contentHeight + 24) * 2 : header + contentHeight + (["database", "vectorstore"].includes(box.type) ? 30 : 18);
|
|
50
|
+
if (!explicit.height) box.height = Math.max(box.height, needed, 72);
|
|
51
|
+
/** @type {QualityIssue[]} */
|
|
52
|
+
const issues = [];
|
|
53
|
+
if (box.height < needed || available < main.width) issues.push({ code: "node-text-overflow", id: box.id, message: `Node "${box.id}" has an explicit size too small for its complete text.` });
|
|
54
|
+
return { box, labels: [main, sub], issues };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @param {Rect} box @param {'top'|'right'|'bottom'|'left'} side @returns {Point} */
|
|
58
|
+
export function boundaryPort(box, side) {
|
|
59
|
+
if (side === "top") return [box.x + box.width / 2, box.y];
|
|
60
|
+
if (side === "bottom") return [box.x + box.width / 2, box.y + box.height];
|
|
61
|
+
if (side === "left") return [box.x, box.y + box.height / 2];
|
|
62
|
+
return [box.x + box.width, box.y + box.height / 2];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Strict interior intersection; boundary contact is allowed. @param {Point} a @param {Point} b @param {Rect} r */
|
|
66
|
+
export function segmentHits(a, b, r) {
|
|
67
|
+
let lo = 0, hi = 1;
|
|
68
|
+
for (const axis of [0, 1]) {
|
|
69
|
+
const start = axis === 0 ? r.x : r.y;
|
|
70
|
+
const end = start + (axis === 0 ? r.width : r.height);
|
|
71
|
+
const delta = b[axis] - a[axis];
|
|
72
|
+
if (Math.abs(delta) < 1e-8) {
|
|
73
|
+
if (a[axis] <= start + 1e-6 || a[axis] >= end - 1e-6) return false;
|
|
74
|
+
} else {
|
|
75
|
+
const t1 = (start - a[axis]) / delta, t2 = (end - a[axis]) / delta;
|
|
76
|
+
lo = Math.max(lo, Math.min(t1, t2)); hi = Math.min(hi, Math.max(t1, t2));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return hi - lo > 1e-6;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** @param {Rect} a @param {Rect} b */
|
|
83
|
+
export function overlaps(a, b) {
|
|
84
|
+
return Math.min(a.x + a.width, b.x + b.width) > Math.max(a.x, b.x) + 1e-6 && Math.min(a.y + a.height, b.y + b.height) > Math.max(a.y, b.y) + 1e-6;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @param {Point[]} points @returns {Point[]} */
|
|
88
|
+
function simplify(points) {
|
|
89
|
+
return points.filter((p, i) => {
|
|
90
|
+
if (i && p[0] === points[i - 1][0] && p[1] === points[i - 1][1]) return false;
|
|
91
|
+
if (!i || i === points.length - 1) return true;
|
|
92
|
+
const prev = points[i - 1], next = points[i + 1];
|
|
93
|
+
return !((prev[0] === p[0] && next[0] === p[0]) || (prev[1] === p[1] && next[1] === p[1]));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @param {Rect & {id:string}} from @param {Rect & {id:string}} to @param {Rect[]} obstacles @param {Point[][]} occupied @param {Point[]} [waypoints] */
|
|
98
|
+
export function routeConnector(from, to, obstacles, occupied, waypoints) {
|
|
99
|
+
const horizontal = Math.abs(to.x - from.x) >= Math.abs(to.y - from.y);
|
|
100
|
+
const forward = horizontal ? to.x >= from.x : to.y >= from.y;
|
|
101
|
+
const start = boundaryPort(from, horizontal ? (forward ? "right" : "left") : (forward ? "bottom" : "top"));
|
|
102
|
+
const end = boundaryPort(to, horizontal ? (forward ? "left" : "right") : (forward ? "top" : "bottom"));
|
|
103
|
+
/** @type {QualityIssue[]} */
|
|
104
|
+
const issues = [];
|
|
105
|
+
if (waypoints) {
|
|
106
|
+
const points = [start, ...waypoints, end];
|
|
107
|
+
if (points.some((p, i) => i > 0 && obstacles.some((r) => segmentHits(points[i - 1], p, r)))) issues.push({ code: "route-obstructed", id: `${from.id}-${to.id}`, message: "Explicit waypoints cross a non-endpoint node." });
|
|
108
|
+
return { points, issues };
|
|
109
|
+
}
|
|
110
|
+
if (from.id === to.id) {
|
|
111
|
+
const x = from.x + from.width + 40 + occupied.length * 4;
|
|
112
|
+
return { points: /** @type {Point[]} */ ([[from.x + from.width, from.y + from.height / 3], [x, from.y + from.height / 3], [x, from.y + from.height * 2 / 3], [from.x + from.width, from.y + from.height * 2 / 3]]), issues };
|
|
113
|
+
}
|
|
114
|
+
const padded = obstacles.map((r) => ({ x: r.x - 12, y: r.y - 12, width: r.width + 24, height: r.height + 24 }));
|
|
115
|
+
const xSet = new Set([start[0], end[0], 28]), ySet = new Set([start[1], end[1], 28]);
|
|
116
|
+
for (const r of [...padded, from, to]) { xSet.add(r.x - 16); xSet.add(r.x + r.width + 16); ySet.add(r.y - 16); ySet.add(r.y + r.height + 16); }
|
|
117
|
+
for (const route of occupied) for (const [x, y] of route) { xSet.add(x - 16); xSet.add(x + 16); ySet.add(y - 16); ySet.add(y + 16); }
|
|
118
|
+
const xs = [...xSet].filter((x) => x >= 24).sort((a, b) => a - b), ys = [...ySet].filter((y) => y >= 24).sort((a, b) => a - b);
|
|
119
|
+
if (xs.length > 128 || ys.length > 128) return { points: [start, end], issues: [{ code: "route-unresolved", id: `${from.id}-${to.id}`, message: "Routing grid exceeded its bounded search budget." }] };
|
|
120
|
+
const nx = xs.length, ny = ys.length;
|
|
121
|
+
const startId = ys.indexOf(start[1]) * nx + xs.indexOf(start[0]), endId = ys.indexOf(end[1]) * nx + xs.indexOf(end[0]);
|
|
122
|
+
/** @param {number} id @returns {Point} */
|
|
123
|
+
const point = (id) => [xs[id % nx], ys[Math.floor(id / nx)]];
|
|
124
|
+
/** @type {Array<{node:number,dir:number,cost:number,key:string}>} */
|
|
125
|
+
const pending = [{ node: startId, dir: 0, cost: 0, key: `${startId}:0` }];
|
|
126
|
+
const best = new Map([[`${startId}:0`, 0]]), previous = new Map();
|
|
127
|
+
let finish = "";
|
|
128
|
+
while (pending.length) {
|
|
129
|
+
pending.sort((a, b) => a.cost - b.cost || a.node - b.node || a.dir - b.dir);
|
|
130
|
+
const current = pending.shift();
|
|
131
|
+
if (!current || current.cost !== best.get(current.key)) continue;
|
|
132
|
+
if (current.node === endId) { finish = current.key; break; }
|
|
133
|
+
const ix = current.node % nx, iy = Math.floor(current.node / nx);
|
|
134
|
+
for (const [dx, dy] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
|
|
135
|
+
if (ix + dx < 0 || ix + dx >= nx || iy + dy < 0 || iy + dy >= ny) continue;
|
|
136
|
+
const next = (iy + dy) * nx + ix + dx, a = point(current.node), b = point(next);
|
|
137
|
+
if ([...padded, from, to].some((r) => segmentHits(a, b, r))) continue;
|
|
138
|
+
const dir = dx ? 1 : 2;
|
|
139
|
+
let penalty = 0;
|
|
140
|
+
for (const route of occupied) for (let i = 1; i < route.length; i++) {
|
|
141
|
+
const p = route[i - 1], q = route[i];
|
|
142
|
+
if ((a[0] === b[0] && p[0] === q[0] && a[0] === p[0] && Math.min(b[1], a[1]) < Math.max(p[1], q[1]) && Math.max(b[1], a[1]) > Math.min(p[1], q[1])) || (a[1] === b[1] && p[1] === q[1] && a[1] === p[1] && Math.min(b[0], a[0]) < Math.max(p[0], q[0]) && Math.max(b[0], a[0]) > Math.min(p[0], q[0]))) penalty += 80;
|
|
143
|
+
}
|
|
144
|
+
const cost = current.cost + Math.abs(b[0] - a[0]) + Math.abs(b[1] - a[1]) + (current.dir && current.dir !== dir ? 24 : 0) + penalty;
|
|
145
|
+
const key = `${next}:${dir}`;
|
|
146
|
+
if (cost < (best.get(key) ?? Infinity)) { best.set(key, cost); previous.set(key, current.key); pending.push({ node: next, dir, cost, key }); }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!finish) return { points: [start, end], issues: [{ code: "route-unresolved", id: `${from.id}-${to.id}`, message: "No obstacle-free route exists without moving explicit nodes." }] };
|
|
150
|
+
/** @type {Point[]} */
|
|
151
|
+
const route = [];
|
|
152
|
+
for (let key = finish; key; key = previous.get(key)) route.unshift(point(Number(key.split(":")[0])));
|
|
153
|
+
return { points: simplify(route), issues };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** @param {Point[]} points @param {TextLayout} layout @param {Rect[]} obstacles @param {Point[][]} [routes] */
|
|
157
|
+
export function placeEdgeLabel(points, layout, obstacles, routes = []) {
|
|
158
|
+
const lengths = points.slice(1).map((p, i) => Math.hypot(p[0] - points[i][0], p[1] - points[i][1]));
|
|
159
|
+
let remaining = lengths.reduce((a, b) => a + b, 0) / 2;
|
|
160
|
+
let cx = points[0][0], cy = points[0][1];
|
|
161
|
+
for (let i = 0; i < lengths.length; i++) {
|
|
162
|
+
if (remaining <= lengths[i]) {
|
|
163
|
+
const ratio = lengths[i] ? remaining / lengths[i] : 0;
|
|
164
|
+
cx = points[i][0] + (points[i + 1][0] - points[i][0]) * ratio;
|
|
165
|
+
cy = points[i][1] + (points[i + 1][1] - points[i][1]) * ratio;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
remaining -= lengths[i];
|
|
169
|
+
}
|
|
170
|
+
let rect = { x: cx - layout.width / 2, y: cy - layout.height - 12, width: layout.width, height: layout.height };
|
|
171
|
+
for (const offset of [12,24,36,52,72,96]) for (const direction of ["above","below","left","right"]) {
|
|
172
|
+
const candidate = direction === "above" ? {...rect,y:cy-layout.height-offset} : direction === "below" ? {...rect,y:cy+offset} : direction === "left" ? {...rect,x:cx-layout.width-offset,y:cy-layout.height/2} : {...rect,x:cx+offset,y:cy-layout.height/2};
|
|
173
|
+
if (candidate.x >= 24 && candidate.y >= 24 && !obstacles.some((r) => overlaps(candidate, r)) && !routes.some(route=>route.slice(1).some((point,index)=>segmentHits(route[index],point,{x:candidate.x-4,y:candidate.y-4,width:candidate.width+8,height:candidate.height+8})))) return { rect: candidate, issues: /** @type {QualityIssue[]} */ ([]) };
|
|
174
|
+
}
|
|
175
|
+
return { rect, issues: [{ code: "edge-label-collision", id: "edge", message: "No free label position near route midpoint." }] };
|
|
176
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
import { analyzeManifest } from "./png-qa.mjs";
|
|
5
|
+
import { withDiagramPage } from "./diagram-browser.mjs";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
/** @typedef {{id:string,x:number,y:number,width:number,height:number,[key:string]:unknown}} LabelRect */
|
|
11
|
+
/** @typedef {{canvas:{width:number,height:number},boxes:LabelRect[],groups?:LabelRect[],labels?:LabelRect[],connectors:Array<{points:Array<[number,number]>,[key:string]:unknown}>,[key:string]:unknown}} Manifest */
|
|
12
|
+
|
|
13
|
+
const SUPPORTED_FORMATS = new Set(["svg", "png", "jpg", "jpeg"]);
|
|
14
|
+
const JPEG_FORMATS = new Set(["jpg", "jpeg"]);
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const args = parseArgs(process.argv.slice(2));
|
|
18
|
+
const inputPath = path.resolve(requireArg(args, "input"));
|
|
19
|
+
const format = requireArg(args, "format").toLowerCase();
|
|
20
|
+
const scale = args.scale === undefined ? 2 : parseScale(args.scale);
|
|
21
|
+
const background = args.background ?? (args.theme === "dark" ? "#0f172a" : "#ffffff");
|
|
22
|
+
|
|
23
|
+
if (!SUPPORTED_FORMATS.has(format)) {
|
|
24
|
+
throw usageError(`Unsupported --format "${format}". Expected svg, png, jpg, or jpeg.`);
|
|
25
|
+
}
|
|
26
|
+
if (!fs.existsSync(inputPath)) {
|
|
27
|
+
throw usageError(`Input file does not exist: ${inputPath}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let svg = extractSvg(inputPath);
|
|
31
|
+
const theme = args.theme ?? "light";
|
|
32
|
+
if (theme !== "light" && theme !== "dark") throw usageError("--theme must be light or dark");
|
|
33
|
+
svg = svg.replace(/data-theme=["'][^"']*["']/, `data-theme="${theme}"`);
|
|
34
|
+
/** @type {Manifest|null} */
|
|
35
|
+
const manifest = args.manifest ? JSON.parse(fs.readFileSync(path.resolve(args.manifest), "utf8")) : null;
|
|
36
|
+
const outputPath = path.resolve(resolveOutputPath(inputPath, format, args.output));
|
|
37
|
+
|
|
38
|
+
for(const metadataPath of [args["output-manifest"],args["qa-report"]]) if(metadataPath) fs.mkdirSync(path.dirname(path.resolve(metadataPath)),{recursive:true});
|
|
39
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
40
|
+
if (format === "svg") {
|
|
41
|
+
fs.writeFileSync(outputPath, `${svg.trim()}\n`, "utf8");
|
|
42
|
+
if (manifest && args["output-manifest"]) fs.writeFileSync(path.resolve(args["output-manifest"]), JSON.stringify({...manifest,coordinateSpace:"svg",scale:1,measurement:"estimated"},null,2)+"\n");
|
|
43
|
+
if (args["qa-report"]) fs.writeFileSync(path.resolve(args["qa-report"]),JSON.stringify({status:"estimated",measurement:"estimated",theme,issues:manifest?.issues??[],visualQA:"pending"},null,2)+"\n");
|
|
44
|
+
} else {
|
|
45
|
+
const dimensions = measureSvg(svg);
|
|
46
|
+
const pageHtml = `<!doctype html><meta charset="utf-8"><style>html,body{margin:0;padding:0;background:${background};}svg.tech-diagram,body>svg{display:block;width:${dimensions.width}px;min-width:0;height:${dimensions.height}px;}</style>${svg}`;
|
|
47
|
+
const result = await withDiagramPage(pageHtml, { theme, timeoutMs: 30000 }, async (session) => {
|
|
48
|
+
await session.evaluate(`(async()=>{let last="";for(let i=0;i<8;i++){let next=Array.from(document.querySelectorAll('[data-label-id]')).map(el=>el.getBBox().width).join(',');if(next===last)return;last=next;await new Promise(r=>requestAnimationFrame(r));}throw new Error('Text layout did not stabilize');})()`);
|
|
49
|
+
const labels = await session.evaluate(`Array.from(document.querySelectorAll('[data-label-id]')).map(el=>{const r=el.getBBox();return {id:el.getAttribute('data-label-id'),x:r.x,y:r.y,width:r.width,height:r.height,fontFamily:getComputedStyle(el).fontFamily,fontSize:getComputedStyle(el).fontSize};})`);
|
|
50
|
+
const png = await session.screenshot(dimensions.width, dimensions.height, scale);
|
|
51
|
+
return { png, labels };
|
|
52
|
+
});
|
|
53
|
+
if (result.png.length < 24 || result.png.readUInt32BE(16) !== Math.ceil(dimensions.width * scale) || result.png.readUInt32BE(20) !== Math.ceil(dimensions.height * scale)) throw usageError("Browser raster export failed: unexpected image dimensions.");
|
|
54
|
+
if(manifest) {
|
|
55
|
+
const measured=/** @type {LabelRect[]} */ (result.labels);
|
|
56
|
+
const missing=(manifest.labels??[]).filter(label=>!measured.some(actual=>actual.id===label.id));
|
|
57
|
+
if(missing.length) throw usageError("Browser measurement missing manifest labels: "+missing.map(label=>label.id).join(", "));
|
|
58
|
+
}
|
|
59
|
+
if (JPEG_FORMATS.has(format)) {
|
|
60
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "fec-diagram-jpeg-"));
|
|
61
|
+
try { const pngPath = path.join(temp, "image.png"); fs.writeFileSync(pngPath, result.png); convertPngToJpeg(pngPath, outputPath); } finally { fs.rmSync(temp, { recursive: true, force: true }); }
|
|
62
|
+
} else fs.writeFileSync(outputPath, result.png);
|
|
63
|
+
if (manifest) {
|
|
64
|
+
manifest.labels = (manifest.labels ?? []).map((label) => ({ ...label, .../** @type {LabelRect[]} */ (result.labels).find((actual) => actual.id === label.id) }));
|
|
65
|
+
manifest.measurement = "browser";
|
|
66
|
+
if (args["output-manifest"]) fs.writeFileSync(path.resolve(args["output-manifest"]), JSON.stringify(scaleManifest(manifest, scale), null, 2) + "\n");
|
|
67
|
+
}
|
|
68
|
+
/** @type {Array<{code:string,message:string}>} */
|
|
69
|
+
const boundaryIssues=[];
|
|
70
|
+
if(manifest) analyzeManifest({width:Math.ceil(dimensions.width*scale),height:Math.ceil(dimensions.height*scale)},scaleManifest(manifest,scale),boundaryIssues,new Set(),false);
|
|
71
|
+
if (args["qa-report"]) fs.writeFileSync(path.resolve(args["qa-report"]), JSON.stringify({ status: boundaryIssues.length ? "partial" : "measured", issues:boundaryIssues, measurement: "browser", theme, width: dimensions.width * scale, height: dimensions.height * scale, note: "Run PNG QA and inspect the final image; browser measurement alone is not approval." }, null, 2) + "\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(`Exported ${format.toUpperCase()} diagram: ${outputPath}`);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string[]} argv
|
|
82
|
+
* @returns {Record<string, string | undefined>}
|
|
83
|
+
*/
|
|
84
|
+
function parseArgs(argv) {
|
|
85
|
+
/** @type {Record<string, string | undefined>} */
|
|
86
|
+
const parsed = {};
|
|
87
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
88
|
+
const token = argv[index];
|
|
89
|
+
if (!token.startsWith("--")) {
|
|
90
|
+
throw usageError(`Unexpected positional argument "${token}".`);
|
|
91
|
+
}
|
|
92
|
+
const key = token.slice(2);
|
|
93
|
+
const value = argv[index + 1];
|
|
94
|
+
if (!value || value.startsWith("--")) {
|
|
95
|
+
throw usageError(`Missing value for --${key}.`);
|
|
96
|
+
}
|
|
97
|
+
parsed[key] = value;
|
|
98
|
+
index += 1;
|
|
99
|
+
}
|
|
100
|
+
return parsed;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {Record<string, string | undefined>} args
|
|
105
|
+
* @param {string} key
|
|
106
|
+
*/
|
|
107
|
+
function requireArg(args, key) {
|
|
108
|
+
const value = args[key];
|
|
109
|
+
if (!value) throw usageError(`Missing required --${key}.`);
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} value
|
|
115
|
+
*/
|
|
116
|
+
function parseScale(value) {
|
|
117
|
+
const scale = Number(value);
|
|
118
|
+
if (!Number.isFinite(scale) || scale <= 0) {
|
|
119
|
+
throw usageError("--scale must be a positive number.");
|
|
120
|
+
}
|
|
121
|
+
return scale;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {string} inputPath
|
|
126
|
+
* @param {string} format
|
|
127
|
+
* @param {string | undefined} output
|
|
128
|
+
*/
|
|
129
|
+
function resolveOutputPath(inputPath, format, output) {
|
|
130
|
+
if (output) return output;
|
|
131
|
+
const extension = JPEG_FORMATS.has(format) ? ".jpg" : `.${format}`;
|
|
132
|
+
return inputPath.replace(/\.[^.\\/]+$/, "") + extension;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {string} inputPath
|
|
137
|
+
*/
|
|
138
|
+
function extractSvg(inputPath) {
|
|
139
|
+
const content = fs.readFileSync(inputPath, "utf8");
|
|
140
|
+
if (inputPath.toLowerCase().endsWith(".svg")) {
|
|
141
|
+
if (!/<svg[\s>]/i.test(content)) {
|
|
142
|
+
throw usageError("SVG input does not contain an <svg> root.");
|
|
143
|
+
}
|
|
144
|
+
return content;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const svgStart = content.search(/<svg[\s>]/i);
|
|
148
|
+
if (svgStart === -1) {
|
|
149
|
+
throw usageError("HTML input does not contain an inline <svg> element.");
|
|
150
|
+
}
|
|
151
|
+
const afterStart = content.slice(svgStart);
|
|
152
|
+
const svgEnd = afterStart.search(/<\/svg>/i);
|
|
153
|
+
if (svgEnd === -1) {
|
|
154
|
+
throw usageError("HTML input contains an <svg> start tag without a closing </svg>.");
|
|
155
|
+
}
|
|
156
|
+
return afterStart.slice(0, svgEnd + "</svg>".length);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {string} svg
|
|
161
|
+
* @param {string} outputPath
|
|
162
|
+
* @param {string} format
|
|
163
|
+
* @param {number} scale
|
|
164
|
+
* @param {string} background
|
|
165
|
+
*/
|
|
166
|
+
/**
|
|
167
|
+
* @param {string} pngPath
|
|
168
|
+
* @param {string} outputPath
|
|
169
|
+
*/
|
|
170
|
+
function convertPngToJpeg(pngPath, outputPath) {
|
|
171
|
+
const converters = [
|
|
172
|
+
() => convertWithMagick(pngPath, outputPath),
|
|
173
|
+
() => convertWithFfmpeg(pngPath, outputPath),
|
|
174
|
+
() => process.platform === "win32" ? convertWithPowerShell(pngPath, outputPath) : false,
|
|
175
|
+
() => process.platform === "darwin" ? convertWithSips(pngPath, outputPath) : false,
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
for (const convert of converters) {
|
|
179
|
+
try {
|
|
180
|
+
if (convert() && fs.existsSync(outputPath) && fs.statSync(outputPath).size > 0) return;
|
|
181
|
+
} catch {
|
|
182
|
+
// Try the next system converter.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
throw usageError("JPG/JPEG export needs a local image converter after Chromium captures the SVG. Install ImageMagick, FFmpeg, macOS sips, or use SVG/PNG output.");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {string} pngPath
|
|
191
|
+
* @param {string} outputPath
|
|
192
|
+
*/
|
|
193
|
+
function convertWithMagick(pngPath, outputPath) {
|
|
194
|
+
for (const command of ["magick", "convert"]) {
|
|
195
|
+
const result = spawnSync(command, [pngPath, "-background", "white", "-alpha", "remove", "-quality", "95", outputPath], {
|
|
196
|
+
encoding: "utf8",
|
|
197
|
+
timeout: 15000,
|
|
198
|
+
windowsHide: true,
|
|
199
|
+
});
|
|
200
|
+
if (result.status === 0) return true;
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param {string} pngPath
|
|
207
|
+
* @param {string} outputPath
|
|
208
|
+
*/
|
|
209
|
+
function convertWithFfmpeg(pngPath, outputPath) {
|
|
210
|
+
const result = spawnSync("ffmpeg", ["-y", "-i", pngPath, "-q:v", "2", outputPath], {
|
|
211
|
+
encoding: "utf8",
|
|
212
|
+
timeout: 15000,
|
|
213
|
+
windowsHide: true,
|
|
214
|
+
});
|
|
215
|
+
return result.status === 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @param {string} pngPath
|
|
220
|
+
* @param {string} outputPath
|
|
221
|
+
*/
|
|
222
|
+
function convertWithPowerShell(pngPath, outputPath) {
|
|
223
|
+
const script = [
|
|
224
|
+
"Add-Type -AssemblyName System.Drawing",
|
|
225
|
+
`$img = [System.Drawing.Image]::FromFile(${quotePowerShell(pngPath)})`,
|
|
226
|
+
"$bmp = New-Object System.Drawing.Bitmap $img.Width, $img.Height",
|
|
227
|
+
"$graphics = [System.Drawing.Graphics]::FromImage($bmp)",
|
|
228
|
+
"$graphics.Clear([System.Drawing.Color]::White)",
|
|
229
|
+
"$graphics.DrawImage($img, 0, 0, $img.Width, $img.Height)",
|
|
230
|
+
`$bmp.Save(${quotePowerShell(outputPath)}, [System.Drawing.Imaging.ImageFormat]::Jpeg)`,
|
|
231
|
+
"$graphics.Dispose(); $bmp.Dispose(); $img.Dispose()",
|
|
232
|
+
].join("; ");
|
|
233
|
+
const result = spawnSync("powershell", ["-NoProfile", "-Command", script], {
|
|
234
|
+
encoding: "utf8",
|
|
235
|
+
timeout: 15000,
|
|
236
|
+
windowsHide: true,
|
|
237
|
+
});
|
|
238
|
+
return result.status === 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {string} pngPath
|
|
243
|
+
* @param {string} outputPath
|
|
244
|
+
*/
|
|
245
|
+
function convertWithSips(pngPath, outputPath) {
|
|
246
|
+
fs.copyFileSync(pngPath, outputPath);
|
|
247
|
+
const result = spawnSync("sips", ["-s", "format", "jpeg", outputPath, "--out", outputPath], {
|
|
248
|
+
encoding: "utf8",
|
|
249
|
+
timeout: 15000,
|
|
250
|
+
});
|
|
251
|
+
return result.status === 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {string} value
|
|
256
|
+
*/
|
|
257
|
+
function quotePowerShell(value) {
|
|
258
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {string} svg
|
|
263
|
+
*/
|
|
264
|
+
function measureSvg(svg) {
|
|
265
|
+
const openTag = svg.match(/<svg\b[^>]*>/i)?.[0] ?? "";
|
|
266
|
+
const width = numberAttr(openTag, "width");
|
|
267
|
+
const height = numberAttr(openTag, "height");
|
|
268
|
+
if (width && height) return { width, height };
|
|
269
|
+
|
|
270
|
+
const viewBox = openTag.match(/\bviewBox=(["'])(.*?)\1/i)?.[2]?.trim().split(/\s+/).map(Number);
|
|
271
|
+
if (viewBox && viewBox.length === 4 && viewBox.every(Number.isFinite)) {
|
|
272
|
+
return { width: viewBox[2], height: viewBox[3] };
|
|
273
|
+
}
|
|
274
|
+
return { width: 1280, height: 720 };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {string} tag
|
|
279
|
+
* @param {string} name
|
|
280
|
+
*/
|
|
281
|
+
function numberAttr(tag, name) {
|
|
282
|
+
const value = tag.match(new RegExp(`\\b${name}=(["'])(.*?)\\1`, "i"))?.[2];
|
|
283
|
+
if (!value) return null;
|
|
284
|
+
const number = Number.parseFloat(value);
|
|
285
|
+
return Number.isFinite(number) && number > 0 ? number : null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @param {string} svg
|
|
290
|
+
* @param {number} width
|
|
291
|
+
* @param {number} height
|
|
292
|
+
* @param {number} scale
|
|
293
|
+
* @param {string} background
|
|
294
|
+
*/
|
|
295
|
+
function renderScreenshotHtml(svg, width, height, scale, background) {
|
|
296
|
+
return `<!doctype html>
|
|
297
|
+
<html lang="en">
|
|
298
|
+
<head>
|
|
299
|
+
<meta charset="utf-8">
|
|
300
|
+
<style>
|
|
301
|
+
html, body {
|
|
302
|
+
margin: 0;
|
|
303
|
+
width: ${Math.ceil(width * scale)}px;
|
|
304
|
+
height: ${Math.ceil(height * scale)}px;
|
|
305
|
+
overflow: hidden;
|
|
306
|
+
background: ${background};
|
|
307
|
+
}
|
|
308
|
+
svg {
|
|
309
|
+
display: block;
|
|
310
|
+
width: ${Math.ceil(width * scale)}px;
|
|
311
|
+
height: ${Math.ceil(height * scale)}px;
|
|
312
|
+
}
|
|
313
|
+
</style>
|
|
314
|
+
</head>
|
|
315
|
+
<body>${svg}</body>
|
|
316
|
+
</html>
|
|
317
|
+
`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @param {string} message
|
|
322
|
+
*/
|
|
323
|
+
function usageError(message) {
|
|
324
|
+
return new Error(message);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** @param {Manifest} manifest @param {number} scale */
|
|
328
|
+
function scaleManifest(manifest, scale) {
|
|
329
|
+
/** @param {LabelRect} r */
|
|
330
|
+
const rect = (r) => ({ ...r, x: r.x * scale, y: r.y * scale, width: r.width * scale, height: r.height * scale });
|
|
331
|
+
return { ...manifest, coordinateSpace: "png", scale, canvas: { width: Math.ceil(manifest.canvas.width * scale), height: Math.ceil(manifest.canvas.height * scale) }, boxes: manifest.boxes.map(rect), groups: (manifest.groups ?? []).map(rect), labels: (manifest.labels ?? []).map(rect), connectors: manifest.connectors.map((c) => ({ ...c, points: c.points.map(([x, y]) => [x * scale, y * scale]) })) };
|
|
332
|
+
}
|