@hraness/slopcamera 3.2.8 → 3.3.1

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.
Files changed (150) hide show
  1. package/README.md +29 -9
  2. package/apps/desktop/application/context.ts +6 -0
  3. package/apps/desktop/application/default-registry.ts +26 -0
  4. package/apps/desktop/application/operation.ts +14 -1
  5. package/apps/desktop/application/operations/index.ts +4 -0
  6. package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
  7. package/apps/desktop/application/operations/spatial-direction.ts +222 -0
  8. package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
  9. package/apps/desktop/application/operations/spatial-review.ts +194 -0
  10. package/apps/desktop/application/operations/spatial-scene.ts +25 -1
  11. package/apps/desktop/application/registry.ts +2 -2
  12. package/apps/desktop/application/spatial-asset-admission.ts +201 -0
  13. package/apps/desktop/application/spatial-assets.ts +160 -31
  14. package/apps/desktop/application/spatial-render.ts +217 -20
  15. package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
  16. package/apps/desktop/application/spatial-review.ts +361 -0
  17. package/apps/desktop/application/spatial-spz.ts +48 -3
  18. package/apps/desktop/application/spatial-world-import.ts +26 -5
  19. package/apps/desktop/application/spatial-world-metadata.ts +105 -0
  20. package/apps/desktop/cli/args.ts +514 -11
  21. package/apps/desktop/cli/capability-manifest.ts +374 -0
  22. package/apps/desktop/cli/cinema-renderer.ts +608 -0
  23. package/apps/desktop/cli/command-host-resources.ts +29 -1
  24. package/apps/desktop/cli/commands.ts +1188 -4
  25. package/apps/desktop/cli/gateway-review-provider.ts +334 -0
  26. package/apps/desktop/cli/help.ts +172 -6
  27. package/apps/desktop/cli/main.ts +3 -3
  28. package/apps/desktop/cli/portable-surface.ts +6 -1
  29. package/apps/desktop/cli/spatial-asset-service.ts +124 -0
  30. package/apps/desktop/cli/spatial-design-service.ts +237 -0
  31. package/apps/desktop/cli/spatial-generate-service.ts +370 -0
  32. package/apps/desktop/cli/spatial-scene-service.ts +268 -4
  33. package/apps/desktop/cli/workflow-code.ts +1 -1
  34. package/apps/desktop/code/semantic-builder.ts +24 -0
  35. package/apps/desktop/contracts/cinema.ts +736 -0
  36. package/apps/desktop/contracts/index.ts +1 -0
  37. package/apps/desktop/contracts/spatial-asset.ts +16 -0
  38. package/apps/desktop/contracts/spatial-world.ts +26 -3
  39. package/apps/desktop/core/cinema-plan.ts +1320 -0
  40. package/apps/desktop/core/index.ts +1 -0
  41. package/apps/desktop/core/storage.ts +75 -0
  42. package/apps/desktop/dist/cli/main.js +725 -273
  43. package/apps/desktop/html-overlay/spatial.ts +619 -57
  44. package/apps/desktop/workflows/cinematic-world.ts +127 -0
  45. package/apps/desktop/workflows/index.ts +6 -0
  46. package/dist/cli.js +25 -429
  47. package/dist/code/advanced.js +1 -269
  48. package/dist/code/index.js +16 -2239
  49. package/dist/generate.js +1 -23
  50. package/dist/host-resources.js +1 -25
  51. package/dist/index-0bbesn4m.js +35 -0
  52. package/dist/index-296qhpf2.js +3 -0
  53. package/dist/index-3cxf2c58.js +3 -0
  54. package/dist/index-77fjfg9f.js +3 -0
  55. package/dist/index-80a36bc0.js +3 -0
  56. package/dist/index-a3vvs3rf.js +3 -0
  57. package/dist/index-b3geqczn.js +5 -0
  58. package/dist/index-cgpkyjqa.js +3 -0
  59. package/dist/index-f0ptv8dm.js +7 -0
  60. package/dist/index-grk47tgs.js +3 -0
  61. package/dist/index-qry58nj2.js +10 -0
  62. package/dist/index.js +1 -226
  63. package/dist/operations.js +1 -28
  64. package/dist/skill-install-gx7afxsd.js +2 -0
  65. package/dist/vectorize/worker.js +1 -141
  66. package/dist/workflow.js +1 -15
  67. package/docs/README.md +5 -1
  68. package/package.json +7 -5
  69. package/skills/slopcamera/SKILL.md +6 -1
  70. package/skills/slopcamera/references/directed-scenes.md +22 -2
  71. package/skills/slopcamera/references/gateway-media.md +15 -1
  72. package/skills/slopcamera/references/image-galleries.md +166 -0
  73. package/skills/slopcamera/references/install.md +1 -1
  74. package/skills/slopcamera/references/parametric-design.md +14 -0
  75. package/skills/slopcamera/references/scene-building.md +32 -0
  76. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  77. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  78. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  79. package/src/capability-manifest.ts +361 -0
  80. package/src/cli.ts +222 -4
  81. package/src/code/canonical-json.ts +9 -0
  82. package/src/generate.ts +2 -2
  83. package/src/icon.ts +1111 -0
  84. package/src/image-gallery.ts +1049 -0
  85. package/src/index.ts +11 -0
  86. package/src/mcp/boundary.ts +11 -11
  87. package/src/mcp/index.ts +3 -0
  88. package/src/mcp/server.ts +2 -1
  89. package/src/mcp/tools.ts +1642 -130
  90. package/src/mcp/types.ts +19 -0
  91. package/src/operations.ts +412 -0
  92. package/src/portable-capability-manifest.ts +67 -0
  93. package/src/scene-gallery.ts +184 -0
  94. package/src/spatial-scene/asset-admission.ts +183 -0
  95. package/src/spatial-scene/audit-rendered.ts +692 -0
  96. package/src/spatial-scene/audit.ts +617 -0
  97. package/src/spatial-scene/behavior-audit.ts +252 -0
  98. package/src/spatial-scene/behavior-bake.ts +233 -0
  99. package/src/spatial-scene/behavior-fns.ts +585 -0
  100. package/src/spatial-scene/behavior-gallery.ts +109 -0
  101. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  102. package/src/spatial-scene/behavior-trace.ts +325 -0
  103. package/src/spatial-scene/behavior.ts +618 -0
  104. package/src/spatial-scene/build.ts +632 -0
  105. package/src/spatial-scene/camera-rig.ts +125 -0
  106. package/src/spatial-scene/camera-track.ts +4 -2
  107. package/src/spatial-scene/character.ts +124 -0
  108. package/src/spatial-scene/contracts.ts +75 -7
  109. package/src/spatial-scene/design-templates.ts +351 -0
  110. package/src/spatial-scene/design.ts +327 -0
  111. package/src/spatial-scene/direction-compile.ts +584 -0
  112. package/src/spatial-scene/direction.ts +122 -0
  113. package/src/spatial-scene/effects.ts +149 -0
  114. package/src/spatial-scene/evaluate.ts +109 -29
  115. package/src/spatial-scene/gallery.ts +277 -0
  116. package/src/spatial-scene/generate.ts +239 -0
  117. package/src/spatial-scene/geometry-native.ts +165 -0
  118. package/src/spatial-scene/geometry.ts +1216 -0
  119. package/src/spatial-scene/gltf.ts +600 -77
  120. package/src/spatial-scene/identity.ts +55 -2
  121. package/src/spatial-scene/index.ts +36 -0
  122. package/src/spatial-scene/inspect.ts +35 -6
  123. package/src/spatial-scene/material-lighting.ts +609 -0
  124. package/src/spatial-scene/math.ts +21 -4
  125. package/src/spatial-scene/motion-evidence.ts +88 -0
  126. package/src/spatial-scene/parametric.ts +765 -0
  127. package/src/spatial-scene/particle-preparation.ts +183 -0
  128. package/src/spatial-scene/particle.ts +195 -0
  129. package/src/spatial-scene/patch.ts +109 -15
  130. package/src/spatial-scene/performance.ts +1613 -0
  131. package/src/spatial-scene/probe.ts +137 -0
  132. package/src/spatial-scene/recipe-pack.ts +86 -0
  133. package/src/spatial-scene/render-effects.ts +240 -0
  134. package/src/spatial-scene/review.ts +639 -0
  135. package/src/spatial-scene/simulation-bake.ts +206 -0
  136. package/src/spatial-scene/simulation.ts +187 -0
  137. package/src/spatial-scene/solve.ts +366 -0
  138. package/src/spatial-scene/temporal-audit.ts +354 -0
  139. package/src/version.ts +1 -1
  140. package/dist/index-42zsesc1.js +0 -2257
  141. package/dist/index-7131pg1c.js +0 -1820
  142. package/dist/index-7308egqr.js +0 -48
  143. package/dist/index-8txs6fkn.js +0 -712
  144. package/dist/index-fava6pge.js +0 -192
  145. package/dist/index-h1k0fnjq.js +0 -2363
  146. package/dist/index-r7gdhmsp.js +0 -424
  147. package/dist/index-sh6xbav6.js +0 -1219
  148. package/dist/index-z1w83f81.js +0 -4
  149. package/dist/index-zfnddgay.js +0 -2029
  150. package/dist/skill-install-dh0ntqba.js +0 -10
@@ -1,2363 +0,0 @@
1
- // @bun
2
- import {
3
- vectorizeImage
4
- } from "./index-zfnddgay.js";
5
- import {
6
- generateSlopcameraImageFile,
7
- slopcameraMaximumPromptBytes
8
- } from "./index-r7gdhmsp.js";
9
- import {
10
- createDefaultHostResourceCoordinator
11
- } from "./index-sh6xbav6.js";
12
-
13
- // src/operations.ts
14
- import { randomUUID } from "crypto";
15
- import { mkdir, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
16
- import { dirname, join, resolve } from "path";
17
-
18
- // src/icons.ts
19
- var shared = 'fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"';
20
- var builtInIcons = Object.freeze({
21
- brain: {
22
- viewBox: "0 0 24 24",
23
- body: `<path ${shared} d="M9.5 4.5A3.5 3.5 0 0 0 6 8v.35A3.5 3.5 0 0 0 5.5 15 3.5 3.5 0 0 0 9 19.5M14.5 4.5A3.5 3.5 0 0 1 18 8v.35a3.5 3.5 0 0 1 .5 6.65 3.5 3.5 0 0 1-3.5 4.5M9.5 4.5v15M14.5 4.5v15M6 9.5h3.5M14.5 14.5H18"/>`
24
- },
25
- check: {
26
- viewBox: "0 0 24 24",
27
- body: `<path ${shared} d="m5 12 4.25 4.25L19 6.5"/>`
28
- },
29
- code: {
30
- viewBox: "0 0 24 24",
31
- body: `<path ${shared} d="m8.5 7-5 5 5 5M15.5 7l5 5-5 5M13.5 4l-3 16"/>`
32
- },
33
- database: {
34
- viewBox: "0 0 24 24",
35
- body: `<ellipse ${shared} cx="12" cy="5" rx="7.5" ry="3"/><path ${shared} d="M4.5 5v7c0 1.66 3.36 3 7.5 3s7.5-1.34 7.5-3V5M4.5 12v7c0 1.66 3.36 3 7.5 3s7.5-1.34 7.5-3v-7"/>`
36
- },
37
- document: {
38
- viewBox: "0 0 24 24",
39
- body: `<path ${shared} d="M6 2.75h8l4 4V21.25H6zM14 2.75v4h4M9 11h6M9 15h6"/>`
40
- },
41
- globe: {
42
- viewBox: "0 0 24 24",
43
- body: `<circle ${shared} cx="12" cy="12" r="9"/><path ${shared} d="M3 12h18M12 3c2.4 2.45 3.5 5.45 3.5 9S14.4 18.55 12 21M12 3C9.6 5.45 8.5 8.45 8.5 12s1.1 6.55 3.5 9"/>`
44
- },
45
- search: {
46
- viewBox: "0 0 24 24",
47
- body: `<circle ${shared} cx="10.5" cy="10.5" r="6.5"/><path ${shared} d="m15.25 15.25 5 5"/>`
48
- },
49
- server: {
50
- viewBox: "0 0 24 24",
51
- body: `<rect ${shared} x="3" y="3.5" width="18" height="7" rx="2"/><rect ${shared} x="3" y="13.5" width="18" height="7" rx="2"/><path ${shared} d="M7 7h.01M7 17h.01M11 7h6M11 17h6"/>`
52
- },
53
- shield: {
54
- viewBox: "0 0 24 24",
55
- body: `<path ${shared} d="M12 2.75 20 6v5.5c0 5.25-3.35 8.2-8 10-4.65-1.8-8-4.75-8-10V6z"/>`
56
- },
57
- user: {
58
- viewBox: "0 0 24 24",
59
- body: `<circle ${shared} cx="12" cy="8" r="4"/><path ${shared} d="M4.5 21c.5-4.2 3-6.5 7.5-6.5s7 2.3 7.5 6.5"/>`
60
- }
61
- });
62
- function sanitizeIcon(icon) {
63
- const dangerous = /<\s*(script|style|foreignObject|iframe|object|embed|image|use|a)\b|on[a-z]+\s*=|(?:xlink:)?href\s*=|style\s*=|javascript:|url\s*\(/i;
64
- if (dangerous.test(icon.body)) {
65
- throw new Error("Icon body contains executable or externally embedded SVG content");
66
- }
67
- if (!/^[-+.\d\s]+$/.test(icon.viewBox.trim())) {
68
- throw new Error(`Invalid icon viewBox: ${icon.viewBox}`);
69
- }
70
- return icon;
71
- }
72
-
73
- // src/label-layout.ts
74
- var defaultLabelFontSize = 22;
75
- var defaultLabelWeight = 600;
76
- var defaultLineHeightRatio = 1.35;
77
- var defaultRowGap = 8;
78
- var iconLabelGap = 12;
79
- function characterWidthRatio(fontFamily) {
80
- return fontFamily === "mono" ? 0.62 : 0.56;
81
- }
82
- function estimateDiagramTextWidth(text, fontSize, fontFamily = "default") {
83
- return text.length * fontSize * characterWidthRatio(fontFamily);
84
- }
85
- function wrapDiagramText(text, maxWidth, fontSize, fontFamily = "default") {
86
- const explicitLines = text.split(`
87
- `);
88
- const maxCharacters = Math.max(1, Math.floor(maxWidth / (fontSize * characterWidthRatio(fontFamily))));
89
- const lines = [];
90
- for (const explicitLine of explicitLines) {
91
- if (explicitLine.length <= maxCharacters) {
92
- lines.push(explicitLine);
93
- continue;
94
- }
95
- const words = explicitLine.split(/\s+/);
96
- let current = "";
97
- for (const word of words) {
98
- const candidate = current === "" ? word : `${current} ${word}`;
99
- if (candidate.length <= maxCharacters || current === "") {
100
- current = candidate;
101
- } else {
102
- lines.push(current);
103
- current = word;
104
- }
105
- }
106
- if (current !== "")
107
- lines.push(current);
108
- }
109
- return lines.length === 0 ? [""] : lines;
110
- }
111
- function resolveRows(shape) {
112
- if (shape.labelRows !== undefined) {
113
- return shape.labelRows.map((row) => ({
114
- text: row.text,
115
- fontSize: row.fontSize ?? defaultLabelFontSize,
116
- fontFamily: row.fontFamily ?? "default",
117
- weight: row.weight ?? defaultLabelWeight,
118
- lineHeight: (row.fontSize ?? defaultLabelFontSize) * defaultLineHeightRatio
119
- }));
120
- }
121
- if (shape.label === undefined)
122
- return [];
123
- const fontSize = shape.labelFontSize ?? defaultLabelFontSize;
124
- return [
125
- {
126
- text: shape.label,
127
- fontSize,
128
- fontFamily: shape.labelFontFamily ?? "default",
129
- weight: shape.labelWeight ?? defaultLabelWeight,
130
- lineHeight: fontSize * defaultLineHeightRatio
131
- }
132
- ];
133
- }
134
- function layoutBoxContent(shape, resolvedIconSize) {
135
- const textWidth = Math.max(1, shape.width - 32);
136
- const pendingRows = resolveRows(shape).map((row) => {
137
- const lines = wrapDiagramText(row.text, textWidth, row.fontSize, row.fontFamily);
138
- const height = lines.length * row.lineHeight;
139
- return { ...row, lines, height };
140
- });
141
- const rowGap = shape.labelRows === undefined ? 0 : shape.labelRowGap ?? defaultRowGap;
142
- const rowsHeight = pendingRows.reduce((total, row) => total + row.height, 0) + Math.max(0, pendingRows.length - 1) * rowGap;
143
- const hasIcon = resolvedIconSize !== undefined;
144
- const gap = hasIcon && pendingRows.length > 0 ? iconLabelGap : 0;
145
- const contentHeight = (resolvedIconSize ?? 0) + gap + rowsHeight;
146
- let cursorY = shape.y + (shape.height - contentHeight) / 2;
147
- const icon = resolvedIconSize === undefined ? undefined : {
148
- x: shape.x + (shape.width - resolvedIconSize) / 2,
149
- y: cursorY,
150
- size: resolvedIconSize
151
- };
152
- if (hasIcon)
153
- cursorY += resolvedIconSize + gap;
154
- const rows = pendingRows.map((row) => {
155
- const resolved = { ...row, y: cursorY };
156
- cursorY += row.height + rowGap;
157
- return resolved;
158
- });
159
- return {
160
- ...icon === undefined ? {} : { icon },
161
- rows
162
- };
163
- }
164
-
165
- // src/render.ts
166
- import { readFile } from "fs/promises";
167
- import { extname, isAbsolute } from "path";
168
- import { fileURLToPath } from "url";
169
- import { Resvg } from "@resvg/resvg-js";
170
-
171
- // src/assets/fonts/nebula-sans/NebulaSans-Bold.otf
172
- var NebulaSans_Bold_default = "./NebulaSans-Bold-26se8aek.otf";
173
-
174
- // src/assets/fonts/nebula-sans/NebulaSans-Bold.woff2
175
- var NebulaSans_Bold_default2 = "./NebulaSans-Bold-bcz7y08t.woff2";
176
-
177
- // src/assets/fonts/nebula-sans/NebulaSans-Book.otf
178
- var NebulaSans_Book_default = "./NebulaSans-Book-8cenzchw.otf";
179
-
180
- // src/assets/fonts/nebula-sans/NebulaSans-Book.woff2
181
- var NebulaSans_Book_default2 = "./NebulaSans-Book-5ax05zvn.woff2";
182
-
183
- // src/theme.ts
184
- var tones = [
185
- "neutral",
186
- "blue",
187
- "orange",
188
- "green",
189
- "red",
190
- "purple",
191
- "yellow"
192
- ];
193
- var defaults = {
194
- light: {
195
- background: "#ffffff",
196
- foreground: "#18181b",
197
- muted: "#71717a",
198
- stroke: "#27272a",
199
- tones: {
200
- neutral: { fill: "#f4f4f5", stroke: "#52525b", text: "#18181b" },
201
- blue: { fill: "#dbeafe", stroke: "#2563eb", text: "#1e3a8a" },
202
- orange: { fill: "#ffedd5", stroke: "#ea580c", text: "#7c2d12" },
203
- green: { fill: "#dcfce7", stroke: "#16a34a", text: "#14532d" },
204
- red: { fill: "#fee2e2", stroke: "#dc2626", text: "#7f1d1d" },
205
- purple: { fill: "#f3e8ff", stroke: "#9333ea", text: "#581c87" },
206
- yellow: { fill: "#fef9c3", stroke: "#ca8a04", text: "#713f12" }
207
- }
208
- },
209
- dark: {
210
- background: "#09090b",
211
- foreground: "#fafafa",
212
- muted: "#a1a1aa",
213
- stroke: "#d4d4d8",
214
- tones: {
215
- neutral: { fill: "#27272a", stroke: "#a1a1aa", text: "#fafafa" },
216
- blue: { fill: "#172554", stroke: "#60a5fa", text: "#dbeafe" },
217
- orange: { fill: "#431407", stroke: "#fb923c", text: "#ffedd5" },
218
- green: { fill: "#052e16", stroke: "#4ade80", text: "#dcfce7" },
219
- red: { fill: "#450a0a", stroke: "#f87171", text: "#fee2e2" },
220
- purple: { fill: "#3b0764", stroke: "#c084fc", text: "#f3e8ff" },
221
- yellow: { fill: "#422006", stroke: "#facc15", text: "#fef9c3" }
222
- }
223
- }
224
- };
225
- function mergeTheme(base, override) {
226
- if (override === undefined)
227
- return base;
228
- return {
229
- background: override.background ?? base.background,
230
- foreground: override.foreground ?? base.foreground,
231
- muted: override.muted ?? base.muted,
232
- stroke: override.stroke ?? base.stroke,
233
- tones: Object.fromEntries(tones.map((tone) => [
234
- tone,
235
- {
236
- fill: override.tones?.[tone]?.fill ?? base.tones[tone].fill,
237
- stroke: override.tones?.[tone]?.stroke ?? base.tones[tone].stroke,
238
- text: override.tones?.[tone]?.text ?? base.tones[tone].text
239
- }
240
- ]))
241
- };
242
- }
243
- function resolveTheme(mode, config) {
244
- return mergeTheme(defaults[mode], config.theme?.[mode]);
245
- }
246
-
247
- // src/render.ts
248
- var bundledAssetPath = (asset) => isAbsolute(asset) ? asset : fileURLToPath(new URL(asset, import.meta.url));
249
- var defaultFont = Object.freeze({
250
- family: "Nebula Sans",
251
- files: Object.freeze([
252
- Object.freeze({
253
- embed: true,
254
- path: bundledAssetPath(NebulaSans_Book_default2),
255
- style: "normal",
256
- weight: 400
257
- }),
258
- Object.freeze({
259
- embed: true,
260
- path: bundledAssetPath(NebulaSans_Bold_default2),
261
- style: "normal",
262
- weight: 700
263
- }),
264
- Object.freeze({
265
- embed: false,
266
- path: bundledAssetPath(NebulaSans_Book_default),
267
- style: "normal",
268
- weight: 400
269
- }),
270
- Object.freeze({
271
- embed: false,
272
- path: bundledAssetPath(NebulaSans_Bold_default),
273
- style: "normal",
274
- weight: 700
275
- })
276
- ])
277
- });
278
- var escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
279
- function fontMime(filePath) {
280
- switch (extname(filePath).toLowerCase()) {
281
- case ".otf":
282
- return "font/otf";
283
- case ".woff":
284
- return "font/woff";
285
- case ".woff2":
286
- return "font/woff2";
287
- default:
288
- return "font/ttf";
289
- }
290
- }
291
- async function fontCss(font) {
292
- if (font.files === undefined)
293
- return "";
294
- const faces = await Promise.all(font.files.filter((file) => file.embed === true).map(async (file) => {
295
- const encoded = (await readFile(file.path)).toString("base64");
296
- return `@font-face{font-family:"${escapeXml(font.family)}";src:url(data:${fontMime(file.path)};base64,${encoded});font-weight:${file.weight ?? 400};font-style:${file.style ?? "normal"};}`;
297
- }));
298
- return faces.join("");
299
- }
300
- function textSvg(options) {
301
- const lines = options.lines ?? wrapDiagramText(options.text, options.width, options.fontSize);
302
- const lineHeight = options.lineHeight ?? options.fontSize * 1.25;
303
- const anchor = options.align;
304
- const x = anchor === "middle" ? options.x + options.width / 2 : anchor === "end" ? options.x + options.width : options.x;
305
- const dominantBaseline = options.centerLineBoxes ? "central" : "hanging";
306
- return `<text x="${x}" y="${options.y}" text-anchor="${anchor}" dominant-baseline="${dominantBaseline}" fill="${options.color}" opacity="${options.opacity}" font-family="${escapeXml(options.family)}" font-size="${options.fontSize}" font-weight="${options.weight}">${lines.map((line, index) => options.centerLineBoxes ? `<tspan x="${x}" y="${options.y + (index + 0.5) * lineHeight}">${escapeXml(line)}</tspan>` : `<tspan x="${x}" dy="${index === 0 ? 0 : lineHeight}">${escapeXml(line)}</tspan>`).join("")}</text>`;
307
- }
308
- function iconSvg(options) {
309
- const icon = sanitizeIcon(options.icon);
310
- const parts = icon.viewBox.trim().split(/\s+/).map(Number);
311
- const width = (parts[2] ?? 24) - (parts[0] ?? 0);
312
- const height = (parts[3] ?? 24) - (parts[1] ?? 0);
313
- const scale = options.size / Math.max(width, height);
314
- return `<g color="${options.color}" opacity="${options.opacity}" transform="translate(${options.x} ${options.y}) scale(${scale}) translate(${-Number(parts[0] ?? 0)} ${-Number(parts[1] ?? 0)})">${icon.body}</g>`;
315
- }
316
- function boxSvg(shape, theme, families, icons) {
317
- const tone = theme.tones[shape.tone ?? "neutral"];
318
- const strokeWidth = shape.strokeWidth ?? 2;
319
- const opacity = shape.opacity ?? 1;
320
- const radius = shape.type === "ellipse" ? 0 : Math.min(shape.radius ?? 22, shape.height / 2);
321
- const geometry = shape.type === "ellipse" ? `<ellipse cx="${shape.x + shape.width / 2}" cy="${shape.y + shape.height / 2}" rx="${shape.width / 2}" ry="${shape.height / 2}" fill="${shape.fill === false ? "none" : tone.fill}" stroke="${tone.stroke}" stroke-width="${strokeWidth}"/>` : `<rect x="${shape.x}" y="${shape.y}" width="${shape.width}" height="${shape.height}" rx="${radius}" fill="${shape.fill === false ? "none" : tone.fill}" stroke="${tone.stroke}" stroke-width="${strokeWidth}"/>`;
322
- const icon = shape.icon === undefined ? undefined : icons[shape.icon];
323
- if (shape.icon !== undefined && icon === undefined) {
324
- throw new Error(`Unknown icon "${shape.icon}" on shape ${shape.id}`);
325
- }
326
- const iconSize = Math.min(shape.iconSize ?? 52, shape.height * 0.45, shape.width * 0.32);
327
- const content = layoutBoxContent(shape, icon === undefined ? undefined : iconSize);
328
- const iconMarkup = icon === undefined || content.icon === undefined ? "" : iconSvg({
329
- icon,
330
- x: content.icon.x,
331
- y: content.icon.y,
332
- size: content.icon.size,
333
- color: tone.text,
334
- opacity: 1
335
- });
336
- const labelMarkup = content.rows.map((row) => textSvg({
337
- text: row.text,
338
- x: shape.x + 16,
339
- y: row.y,
340
- width: shape.width - 32,
341
- fontSize: row.fontSize,
342
- weight: row.weight,
343
- align: "middle",
344
- color: tone.text,
345
- opacity: 1,
346
- family: families[row.fontFamily],
347
- lineHeight: row.lineHeight,
348
- lines: row.lines,
349
- centerLineBoxes: true
350
- })).join("");
351
- return `<g data-shape-id="${escapeXml(shape.id)}" opacity="${opacity}">${geometry}${iconMarkup}${labelMarkup}</g>`;
352
- }
353
- function pointForAnchor(shape, anchor, toward, position) {
354
- const center = { x: shape.x + shape.width / 2, y: shape.y + shape.height / 2 };
355
- let resolved = anchor ?? "auto";
356
- if (resolved === "auto") {
357
- const dx = toward.x - center.x;
358
- const dy = toward.y - center.y;
359
- resolved = Math.abs(dx / shape.width) >= Math.abs(dy / shape.height) ? dx >= 0 ? "right" : "left" : dy >= 0 ? "bottom" : "top";
360
- }
361
- const resolvedPosition = position ?? 0.5;
362
- if (shape.type === "ellipse") {
363
- const projected = (resolvedPosition - 0.5) * 2;
364
- const radial = Math.sqrt(Math.max(0, 1 - projected * projected)) / 2;
365
- const normalized = resolved === "top" ? { x: resolvedPosition, y: 0.5 - radial } : resolved === "bottom" ? { x: resolvedPosition, y: 0.5 + radial } : resolved === "left" ? { x: 0.5 - radial, y: resolvedPosition } : { x: 0.5 + radial, y: resolvedPosition };
366
- return {
367
- x: shape.x + shape.width * normalized.x,
368
- y: shape.y + shape.height * normalized.y,
369
- normalized
370
- };
371
- }
372
- switch (resolved) {
373
- case "top":
374
- return {
375
- x: shape.x + shape.width * resolvedPosition,
376
- y: shape.y,
377
- normalized: { x: resolvedPosition, y: 0 }
378
- };
379
- case "right":
380
- return {
381
- x: shape.x + shape.width,
382
- y: shape.y + shape.height * resolvedPosition,
383
- normalized: { x: 1, y: resolvedPosition }
384
- };
385
- case "bottom":
386
- return {
387
- x: shape.x + shape.width * resolvedPosition,
388
- y: shape.y + shape.height,
389
- normalized: { x: resolvedPosition, y: 1 }
390
- };
391
- case "left":
392
- return {
393
- x: shape.x,
394
- y: shape.y + shape.height * resolvedPosition,
395
- normalized: { x: 0, y: resolvedPosition }
396
- };
397
- }
398
- }
399
- function resolveEdge(spec, edge) {
400
- const boxes = new Map(spec.shapes.filter((shape) => shape.type === "rect" || shape.type === "ellipse").map((shape) => [shape.id, shape]));
401
- const from = boxes.get(edge.from);
402
- const to = boxes.get(edge.to);
403
- if (from === undefined || to === undefined) {
404
- throw new Error(`Edge ${edge.id} references a missing box`);
405
- }
406
- const fromCenter = { x: from.x + from.width / 2, y: from.y + from.height / 2 };
407
- const toCenter = { x: to.x + to.width / 2, y: to.y + to.height / 2 };
408
- const start = pointForAnchor(from, edge.start, toCenter, edge.startPosition);
409
- const end = pointForAnchor(to, edge.end, fromCenter, edge.endPosition);
410
- const dx = end.x - start.x;
411
- const dy = end.y - start.y;
412
- const length = Math.hypot(dx, dy) || 1;
413
- const bend = edge.bend ?? 0;
414
- return {
415
- edge,
416
- from,
417
- to,
418
- start,
419
- end,
420
- control: {
421
- x: (start.x + end.x) / 2 + -dy / length * bend,
422
- y: (start.y + end.y) / 2 + dx / length * bend
423
- }
424
- };
425
- }
426
- function resolveEdgeLabel(resolved) {
427
- const { edge, start, end, control } = resolved;
428
- const t = edge.labelPosition ?? 0.5;
429
- const oneMinusT = 1 - t;
430
- const point = {
431
- x: oneMinusT * oneMinusT * start.x + 2 * oneMinusT * t * control.x + t * t * end.x,
432
- y: oneMinusT * oneMinusT * start.y + 2 * oneMinusT * t * control.y + t * t * end.y
433
- };
434
- const tangent = {
435
- x: 2 * oneMinusT * (control.x - start.x) + 2 * t * (end.x - control.x),
436
- y: 2 * oneMinusT * (control.y - start.y) + 2 * t * (end.y - control.y)
437
- };
438
- const length = Math.hypot(tangent.x, tangent.y) || 1;
439
- const offset = edge.labelOffset ?? -14;
440
- return {
441
- x: point.x + -tangent.y / length * offset,
442
- y: point.y + tangent.x / length * offset
443
- };
444
- }
445
- function edgeSvg(resolved, theme, families) {
446
- const { edge, start, end, control } = resolved;
447
- const toneName = edge.tone ?? "neutral";
448
- const tone = theme.tones[toneName];
449
- const marker = edge.arrowhead === "none" ? "" : edge.arrowhead === "triangle" ? `url(#arrow-triangle-${toneName})` : `url(#arrow-open-${toneName})`;
450
- const path = `M ${start.x} ${start.y} Q ${control.x} ${control.y} ${end.x} ${end.y}`;
451
- const labelPoint = resolveEdgeLabel(resolved);
452
- const label = edge.label === undefined ? "" : `<text x="${labelPoint.x}" y="${labelPoint.y}" text-anchor="middle" dominant-baseline="central" fill="${tone.text}" stroke="${theme.background}" stroke-width="6" paint-order="stroke" font-family="${escapeXml(families[edge.labelFontFamily ?? "default"])}" font-size="${edge.labelFontSize ?? 18}" font-weight="${edge.labelWeight ?? 600}">${escapeXml(edge.label)}</text>`;
453
- return `<g data-edge-id="${escapeXml(edge.id)}"><path d="${path}" fill="none" stroke="${tone.stroke}" stroke-width="3" stroke-linecap="round" marker-end="${marker}"/>${label}</g>`;
454
- }
455
- function shapeSvg(shape, theme, families, icons) {
456
- if (shape.type === "rect" || shape.type === "ellipse") {
457
- return boxSvg(shape, theme, families, icons);
458
- }
459
- if (shape.type === "line") {
460
- return `<line data-shape-id="${escapeXml(shape.id)}" x1="${shape.x}" y1="${shape.y}" x2="${shape.x2}" y2="${shape.y2}" stroke="${theme.tones[shape.tone ?? "neutral"].stroke}" stroke-width="${shape.strokeWidth ?? 3}" stroke-linecap="round" opacity="${shape.opacity ?? 1}"/>`;
461
- }
462
- const text = shape;
463
- return textSvg({
464
- text: text.text,
465
- x: text.x,
466
- y: text.y,
467
- width: text.width ?? Math.max(text.text.length * (text.fontSize ?? 24) * 0.58, 12),
468
- fontSize: text.fontSize ?? 24,
469
- weight: text.weight ?? 500,
470
- align: text.align ?? "start",
471
- color: theme.tones[text.tone ?? "neutral"].text,
472
- opacity: text.opacity ?? 1,
473
- family: families[text.fontFamily ?? "default"]
474
- });
475
- }
476
- async function renderSvg(spec, mode, config) {
477
- const theme = resolveTheme(mode, config);
478
- const font = config.font ?? defaultFont;
479
- const families = {
480
- default: font.family,
481
- mono: font.monoFamily ?? "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace"
482
- };
483
- const icons = config.icons ?? {};
484
- const embeddedFonts = await fontCss(font);
485
- const markerDefinitions = Object.entries(theme.tones).map(([toneName, tone]) => `<marker id="arrow-open-${toneName}" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto" markerUnits="strokeWidth"><path d="M2 2 10 6 2 10" fill="none" stroke="${tone.stroke}" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></marker><marker id="arrow-triangle-${toneName}" markerWidth="11" markerHeight="11" refX="9" refY="5.5" orient="auto" markerUnits="strokeWidth"><path d="M1 1 10 5.5 1 10z" fill="${tone.stroke}"/></marker>`).join("");
486
- const edgeMarkup = (spec.edges ?? []).map((edge) => edgeSvg(resolveEdge(spec, edge), theme, families)).join("");
487
- const shapeMarkup = spec.shapes.map((shape) => shapeSvg(shape, theme, families, icons)).join("");
488
- const svg = [
489
- `<svg xmlns="http://www.w3.org/2000/svg" width="${spec.canvas.width}" height="${spec.canvas.height}" viewBox="0 0 ${spec.canvas.width} ${spec.canvas.height}" role="img" aria-labelledby="diagram-title" color-scheme="${mode}">`,
490
- `<title id="diagram-title">${escapeXml(spec.name)}</title>`,
491
- `<style>${embeddedFonts}text{font-kerning:normal;text-rendering:geometricPrecision}</style>`,
492
- `<rect width="100%" height="100%" fill="${theme.background}"/>`,
493
- `<defs>${markerDefinitions}</defs>`,
494
- edgeMarkup,
495
- shapeMarkup,
496
- "</svg>"
497
- ].join("");
498
- return { mode, svg, width: spec.canvas.width, height: spec.canvas.height };
499
- }
500
- function renderPng(rendered, config, scale = 2) {
501
- const font = config.font ?? defaultFont;
502
- const fontFiles = font.files?.map((file) => file.path) ?? [];
503
- const renderer = new Resvg(rendered.svg, {
504
- fitTo: { mode: "zoom", value: scale },
505
- font: {
506
- loadSystemFonts: true,
507
- ...fontFiles.length === 0 ? {} : { fontFiles },
508
- defaultFontFamily: font.family
509
- }
510
- });
511
- return renderer.render().asPng();
512
- }
513
-
514
- // src/lint.ts
515
- function boxOutsideCanvas(shape, spec) {
516
- return shape.x < 0 || shape.y < 0 || shape.x + shape.width > spec.canvas.width || shape.y + shape.height > spec.canvas.height;
517
- }
518
- function lintDiagram(spec) {
519
- const findings = [];
520
- const boxes = spec.shapes.filter((shape) => shape.type === "rect" || shape.type === "ellipse");
521
- for (const shape of boxes) {
522
- if (boxOutsideCanvas(shape, spec)) {
523
- findings.push({
524
- code: "outside-canvas",
525
- message: `${shape.id} extends beyond the canvas`,
526
- shapeIds: [shape.id]
527
- });
528
- }
529
- const labels = shape.labelRows?.map((row) => row.text) ?? (shape.label === undefined ? [] : [shape.label]);
530
- const longLabel = labels.find((label) => label.length > 32);
531
- if (longLabel !== undefined) {
532
- findings.push({
533
- code: "long-label",
534
- message: `${shape.id} has a ${longLabel.length}-character label; prefer a short noun phrase`,
535
- shapeIds: [shape.id]
536
- });
537
- }
538
- const iconSize = shape.icon === undefined ? undefined : Math.min(shape.iconSize ?? 52, shape.height * 0.45, shape.width * 0.32);
539
- const content = layoutBoxContent(shape, iconSize);
540
- const contentTop = content.icon?.y ?? content.rows[0]?.y;
541
- const finalRow = content.rows.at(-1);
542
- const contentBottom = finalRow === undefined ? content.icon === undefined ? undefined : content.icon.y + content.icon.size : finalRow.y + finalRow.height;
543
- const contentWidth = Math.max(1, shape.width - 32);
544
- const exceedsWidth = content.rows.some((row) => row.lines.some((line) => estimateDiagramTextWidth(line, row.fontSize, row.fontFamily) > contentWidth));
545
- if (contentTop !== undefined && contentBottom !== undefined && (contentTop < shape.y || contentBottom > shape.y + shape.height || exceedsWidth)) {
546
- findings.push({
547
- code: "label-overflow",
548
- message: `${shape.id} label and icon content exceeds the box bounds; increase the box or reduce authored sizes`,
549
- shapeIds: [shape.id]
550
- });
551
- }
552
- if (shape.width < 120 || shape.height < 64) {
553
- findings.push({
554
- code: "small-target",
555
- message: `${shape.id} is small enough to make its label or icon hard to scan`,
556
- shapeIds: [shape.id]
557
- });
558
- }
559
- }
560
- if (boxes.length > 9) {
561
- findings.push({
562
- code: "too-many-elements",
563
- message: `The diagram has ${boxes.length} primary shapes; consider a higher-level visual`,
564
- shapeIds: boxes.map((shape) => shape.id)
565
- });
566
- }
567
- const sharedTerminals = new Map;
568
- for (const edge of spec.edges ?? []) {
569
- const resolved = resolveEdge(spec, edge);
570
- for (const terminal of [
571
- { kind: "start", point: resolved.start, shapeId: edge.from },
572
- { kind: "end", point: resolved.end, shapeId: edge.to }
573
- ]) {
574
- const key = `${terminal.kind}:${terminal.shapeId}:${terminal.point.x.toFixed(3)}:${terminal.point.y.toFixed(3)}`;
575
- const group = sharedTerminals.get(key) ?? [];
576
- group.push({ edgeId: edge.id, shapeId: terminal.shapeId });
577
- sharedTerminals.set(key, group);
578
- }
579
- const length = Math.hypot(resolved.end.x - resolved.start.x, resolved.end.y - resolved.start.y);
580
- if (length < 96) {
581
- findings.push({
582
- code: "short-arrow",
583
- message: `${edge.id} is ${Math.round(length)}px long; leave more space between connected shapes`,
584
- shapeIds: [edge.from, edge.to]
585
- });
586
- }
587
- if (edge.label !== undefined && edge.label.length > 24) {
588
- findings.push({
589
- code: "long-edge-label",
590
- message: `${edge.id} has a long connector label; prefer one short relation`,
591
- shapeIds: [edge.from, edge.to]
592
- });
593
- }
594
- }
595
- for (const group of sharedTerminals.values()) {
596
- if (group.length < 2)
597
- continue;
598
- findings.push({
599
- code: "shared-edge-port",
600
- message: `${group.map(({ edgeId }) => edgeId).join(", ")} share one connector port; set distinct startPosition or endPosition values`,
601
- shapeIds: [group[0].shapeId]
602
- });
603
- }
604
- return findings;
605
- }
606
-
607
- // src/types.ts
608
- var diagramVersion = 1;
609
-
610
- // src/layout.ts
611
- var stackLayoutDefaults = {
612
- gap: 160,
613
- padding: 64,
614
- align: "center"
615
- };
616
- var coordinatePrecision = 3;
617
- var coordinateScale = 10 ** coordinatePrecision;
618
-
619
- class StackLayoutError extends Error {
620
- issues;
621
- constructor(issues) {
622
- super(`Invalid stack layout:
623
- ${issues.map((issue) => `- ${issue}`).join(`
624
- `)}`);
625
- this.name = "StackLayoutError";
626
- this.issues = issues;
627
- }
628
- }
629
- function coordinateFromUnits(value) {
630
- return value / coordinateScale;
631
- }
632
- function normalizedScaledCoordinate(value) {
633
- const scaled = value * coordinateScale;
634
- return normalizedGridValue(scaled);
635
- }
636
- function normalizedGridValue(value) {
637
- const nearest = Math.round(value);
638
- return Math.abs(value - nearest) <= 0.000000001 ? nearest : value;
639
- }
640
- function floorGridValue(value) {
641
- return Math.floor(normalizedGridValue(value));
642
- }
643
- function ceilGridValue(value) {
644
- return Math.ceil(normalizedGridValue(value));
645
- }
646
- function coordinateUnits(value, label, positive, rounding, issues) {
647
- if (!Number.isFinite(value) || (positive ? value <= 0 : value < 0)) {
648
- issues.push(positive ? `${label} must be positive` : `${label} must not be negative`);
649
- return 0;
650
- }
651
- const scaled = normalizedScaledCoordinate(value);
652
- const units = rounding === "ceil" ? ceilGridValue(scaled) : floorGridValue(scaled);
653
- if (!Number.isSafeInteger(units)) {
654
- issues.push(`${label} must be a finite coordinate within the supported range`);
655
- return 0;
656
- }
657
- if (positive && units < 1) {
658
- issues.push(`${label} must occupy at least ${coordinateFromUnits(1)}px in the layout grid`);
659
- return 0;
660
- }
661
- return units;
662
- }
663
- function positionedShape(shape, mainPosition, crossPosition, horizontal) {
664
- return {
665
- ...shape,
666
- x: coordinateFromUnits(horizontal ? mainPosition : crossPosition),
667
- y: coordinateFromUnits(horizontal ? crossPosition : mainPosition)
668
- };
669
- }
670
- function authoredMainSize(shape, horizontal) {
671
- return horizontal ? shape.width : shape.height;
672
- }
673
- function emittedMainPlacement(shapes, horizontal, gap, issues) {
674
- if (shapes.length === 0)
675
- return { offsets: [], span: 0 };
676
- const offsets = [];
677
- let offset = 0;
678
- for (const [index, shape] of shapes.entries()) {
679
- offsets.push(offset);
680
- if (index === shapes.length - 1)
681
- break;
682
- const advance = coordinateUnits(authoredMainSize(shape, horizontal) + gap, `space after shape ${shape.id}`, true, "ceil", issues);
683
- offset += advance;
684
- if (!Number.isSafeInteger(offset)) {
685
- issues.push("stack positions exceed the supported coordinate range");
686
- return { offsets, span: Number.POSITIVE_INFINITY };
687
- }
688
- }
689
- const last = shapes.at(-1);
690
- const span = offset + (last === undefined ? 0 : normalizedScaledCoordinate(authoredMainSize(last, horizontal)));
691
- if (!Number.isFinite(span) || !Number.isSafeInteger(Math.ceil(span))) {
692
- issues.push("stack extent exceeds the supported coordinate range");
693
- }
694
- return { offsets, span };
695
- }
696
- function clampedGridPosition(ideal, minimum, maximum) {
697
- return Math.min(maximum, Math.max(minimum, Math.round(ideal)));
698
- }
699
- function resolvedAnchors(fromIndex, toIndex, horizontal) {
700
- const forward = fromIndex < toIndex;
701
- if (horizontal) {
702
- return forward ? { start: "right", end: "left" } : { start: "left", end: "right" };
703
- }
704
- return forward ? { start: "bottom", end: "top" } : { start: "top", end: "bottom" };
705
- }
706
- function resolvedEdge(edge, indexes, horizontal) {
707
- const fromIndex = indexes.get(edge.from);
708
- const toIndex = indexes.get(edge.to);
709
- if (fromIndex === undefined || toIndex === undefined) {
710
- throw new StackLayoutError([
711
- `edge ${edge.id} must reference shapes that belong to the stack`
712
- ]);
713
- }
714
- const anchors = resolvedAnchors(fromIndex, toIndex, horizontal);
715
- return {
716
- ...edge,
717
- start: anchors.start,
718
- end: anchors.end,
719
- bend: 0
720
- };
721
- }
722
- function stackStructureIssues(source) {
723
- const issues = [];
724
- if (source.shapes.length < 1 || source.shapes.length > 9) {
725
- issues.push("stack layouts must contain between 1 and 9 shapes");
726
- }
727
- const indexes = new Map;
728
- const recordIds = new Set;
729
- for (const [index, shape] of source.shapes.entries()) {
730
- if (recordIds.has(shape.id)) {
731
- issues.push(`shape id ${shape.id} is duplicated`);
732
- } else {
733
- indexes.set(shape.id, index);
734
- recordIds.add(shape.id);
735
- }
736
- }
737
- const connectedPairs = new Set;
738
- for (const stackEdge of source.edges ?? []) {
739
- const edge = stackEdge;
740
- if (recordIds.has(edge.id)) {
741
- issues.push(`edge id ${edge.id} is duplicated`);
742
- }
743
- recordIds.add(edge.id);
744
- const fromIndex = indexes.get(edge.from);
745
- const toIndex = indexes.get(edge.to);
746
- if (fromIndex === undefined) {
747
- issues.push(`edge ${edge.id} has unknown from id ${edge.from}`);
748
- }
749
- if (toIndex === undefined) {
750
- issues.push(`edge ${edge.id} has unknown to id ${edge.to}`);
751
- }
752
- if (fromIndex !== undefined && toIndex !== undefined) {
753
- if (fromIndex === toIndex) {
754
- issues.push(`edge ${edge.id} cannot connect a shape to itself`);
755
- } else {
756
- if (Math.abs(fromIndex - toIndex) !== 1) {
757
- issues.push(`edge ${edge.id} must connect adjacent stack shapes`);
758
- }
759
- const pair = [fromIndex, toIndex].sort((left, right) => left - right).join(":");
760
- if (connectedPairs.has(pair)) {
761
- issues.push(`edge ${edge.id} duplicates a connection between the same stack shapes`);
762
- }
763
- connectedPairs.add(pair);
764
- }
765
- }
766
- if (edge.start !== undefined && edge.start !== "auto") {
767
- issues.push(`edge ${edge.id}.start must be auto or omitted in a stack layout`);
768
- }
769
- if (edge.end !== undefined && edge.end !== "auto") {
770
- issues.push(`edge ${edge.id}.end must be auto or omitted in a stack layout`);
771
- }
772
- if (edge.bend !== undefined && edge.bend !== 0) {
773
- issues.push(`edge ${edge.id}.bend must be 0 or omitted in a stack layout`);
774
- }
775
- }
776
- return issues;
777
- }
778
- function resolveStackLayout(source) {
779
- const issues = [...stackStructureIssues(source)];
780
- const horizontal = source.layout.direction === "horizontal";
781
- coordinateUnits(source.canvas.width, "canvas width", true, "floor", issues);
782
- coordinateUnits(source.canvas.height, "canvas height", true, "floor", issues);
783
- const gapValue = source.layout.gap ?? stackLayoutDefaults.gap;
784
- coordinateUnits(gapValue, "stack gap", false, "ceil", issues);
785
- const paddingValue = source.canvas.padding ?? stackLayoutDefaults.padding;
786
- const padding = coordinateUnits(paddingValue, "canvas padding", false, "ceil", issues);
787
- for (const shape of source.shapes) {
788
- coordinateUnits(shape.width, `shape ${shape.id} width`, true, "ceil", issues);
789
- coordinateUnits(shape.height, `shape ${shape.id} height`, true, "ceil", issues);
790
- }
791
- const align = source.layout.align ?? stackLayoutDefaults.align;
792
- const canvasMainValue = horizontal ? source.canvas.width : source.canvas.height;
793
- const canvasCrossValue = horizontal ? source.canvas.height : source.canvas.width;
794
- const mainPlacement = emittedMainPlacement(source.shapes, horizontal, gapValue, issues);
795
- const maximumMainStart = floorGridValue(normalizedScaledCoordinate(canvasMainValue - paddingValue) - mainPlacement.span);
796
- if (padding > maximumMainStart) {
797
- const requiredMain = ceilGridValue(mainPlacement.span);
798
- const availableMain = floorGridValue(normalizedScaledCoordinate(canvasMainValue - paddingValue * 2));
799
- issues.push(`${source.layout.direction} stack needs ${coordinateFromUnits(requiredMain)}px but only ${coordinateFromUnits(availableMain)}px remain inside ${paddingValue}px padding`);
800
- }
801
- const crossStarts = source.shapes.map((shape) => {
802
- const authoredSize = horizontal ? shape.height : shape.width;
803
- const maximum = floorGridValue(normalizedScaledCoordinate(canvasCrossValue - paddingValue - authoredSize));
804
- if (padding > maximum) {
805
- const requiredCross = ceilGridValue(normalizedScaledCoordinate(authoredSize));
806
- const availableCross = floorGridValue(normalizedScaledCoordinate(canvasCrossValue - paddingValue * 2));
807
- issues.push(`shape ${shape.id} needs ${coordinateFromUnits(requiredCross)}px on the cross axis but only ${coordinateFromUnits(availableCross)}px remain inside ${paddingValue}px padding`);
808
- }
809
- return { authoredSize, maximum };
810
- });
811
- if (issues.length > 0)
812
- throw new StackLayoutError(issues);
813
- const mainStart = clampedGridPosition((normalizedScaledCoordinate(canvasMainValue) - mainPlacement.span) / 2, padding, maximumMainStart);
814
- const shapes = source.shapes.map((shape, index) => {
815
- const crossStart = crossStarts[index];
816
- if (crossStart === undefined) {
817
- throw new StackLayoutError([
818
- `shape ${shape.id} has no cross-axis placement`
819
- ]);
820
- }
821
- const crossPosition = align === "start" ? padding : align === "end" ? crossStart.maximum : clampedGridPosition(normalizedScaledCoordinate((canvasCrossValue - crossStart.authoredSize) / 2), padding, crossStart.maximum);
822
- const mainPosition = mainStart + (mainPlacement.offsets[index] ?? 0);
823
- const positioned = positionedShape(shape, mainPosition, crossPosition, horizontal);
824
- return positioned;
825
- });
826
- const indexes = new Map(shapes.map((shape, index) => [shape.id, index]));
827
- const edges = source.edges?.map((edge) => resolvedEdge(edge, indexes, horizontal));
828
- return {
829
- ...source.$schema === undefined ? {} : { $schema: source.$schema },
830
- version: source.version,
831
- name: source.name,
832
- canvas: {
833
- width: source.canvas.width,
834
- height: source.canvas.height,
835
- padding: paddingValue
836
- },
837
- shapes,
838
- ...edges === undefined ? {} : { edges }
839
- };
840
- }
841
- function resolveDiagramSource(source) {
842
- return "layout" in source ? resolveStackLayout(source) : source;
843
- }
844
-
845
- // src/parse.ts
846
- var tones2 = new Set([
847
- "neutral",
848
- "blue",
849
- "orange",
850
- "green",
851
- "red",
852
- "purple",
853
- "yellow"
854
- ]);
855
- var anchors = new Set(["auto", "top", "right", "bottom", "left"]);
856
- var fontFamilies = new Set(["default", "mono"]);
857
- var stackDirections = new Set(["horizontal", "vertical"]);
858
- var stackAlignments = new Set(["start", "center", "end"]);
859
- var idPattern = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
860
- var namePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
861
-
862
- class DiagramValidationError extends Error {
863
- issues;
864
- constructor(issues) {
865
- super(`Invalid diagram specification:
866
- ${issues.map((issue) => `- ${issue}`).join(`
867
- `)}`);
868
- this.name = "DiagramValidationError";
869
- this.issues = issues;
870
- }
871
- }
872
- function isRecord(value) {
873
- return typeof value === "object" && value !== null && !Array.isArray(value);
874
- }
875
- function readString(record, key, at, issues) {
876
- const value = record[key];
877
- if (typeof value !== "string") {
878
- issues.push(`${at}.${key} must be a string`);
879
- return;
880
- }
881
- return value;
882
- }
883
- function readOptionalString(record, key, at, issues) {
884
- const value = record[key];
885
- if (value === undefined)
886
- return;
887
- if (typeof value !== "string") {
888
- issues.push(`${at}.${key} must be a string when present`);
889
- return;
890
- }
891
- return value;
892
- }
893
- function readNumber(record, key, at, issues, options = {}) {
894
- const value = record[key];
895
- if (typeof value !== "number" || !Number.isFinite(value)) {
896
- issues.push(`${at}.${key} must be a finite number`);
897
- return;
898
- }
899
- if (options.positive && value <= 0) {
900
- issues.push(`${at}.${key} must be greater than zero`);
901
- return;
902
- }
903
- if (options.nonNegative && value < 0) {
904
- issues.push(`${at}.${key} must be zero or greater`);
905
- return;
906
- }
907
- return value;
908
- }
909
- function readOptionalNumber(record, key, at, issues, options = {}) {
910
- if (record[key] === undefined)
911
- return;
912
- return readNumber(record, key, at, issues, options);
913
- }
914
- function readOptionalTone(record, key, at, issues) {
915
- const value = record[key];
916
- if (value === undefined)
917
- return;
918
- if (typeof value !== "string" || !tones2.has(value)) {
919
- issues.push(`${at}.${key} must be one of ${[...tones2].join(", ")}`);
920
- return;
921
- }
922
- return value;
923
- }
924
- function readOptionalFontFamily(record, key, at, issues) {
925
- const value = record[key];
926
- if (value === undefined)
927
- return;
928
- if (typeof value !== "string" || !fontFamilies.has(value)) {
929
- issues.push(`${at}.${key} must be default or mono`);
930
- return;
931
- }
932
- return value;
933
- }
934
- function readOptionalWeight(record, key, at, issues) {
935
- const value = record[key];
936
- if (value === undefined)
937
- return;
938
- if (typeof value !== "number" || ![400, 500, 600, 700].includes(value)) {
939
- issues.push(`${at}.${key} must be 400, 500, 600, or 700`);
940
- return;
941
- }
942
- return value;
943
- }
944
- function parseLabelRows(value, at, issues) {
945
- if (value === undefined)
946
- return;
947
- if (!Array.isArray(value)) {
948
- issues.push(`${at}.labelRows must be an array when present`);
949
- return;
950
- }
951
- if (value.length < 1 || value.length > 4) {
952
- issues.push(`${at}.labelRows must contain between 1 and 4 rows`);
953
- }
954
- const rows = [];
955
- for (const [index, candidate] of value.entries()) {
956
- const rowAt = `${at}.labelRows[${index}]`;
957
- if (!isRecord(candidate)) {
958
- issues.push(`${rowAt} must be an object`);
959
- continue;
960
- }
961
- validateKnownKeys(candidate, new Set(["text", "fontSize", "fontFamily", "weight"]), rowAt, issues);
962
- const text = readString(candidate, "text", rowAt, issues);
963
- const fontSize = readOptionalNumber(candidate, "fontSize", rowAt, issues, {
964
- positive: true
965
- });
966
- const fontFamily = readOptionalFontFamily(candidate, "fontFamily", rowAt, issues);
967
- const weight = readOptionalWeight(candidate, "weight", rowAt, issues);
968
- if (text !== undefined) {
969
- rows.push({
970
- text,
971
- ...fontSize === undefined ? {} : { fontSize },
972
- ...fontFamily === undefined ? {} : { fontFamily },
973
- ...weight === undefined ? {} : { weight }
974
- });
975
- }
976
- }
977
- return rows.length === 0 ? undefined : rows;
978
- }
979
- function validateKnownKeys(record, allowed, at, issues) {
980
- for (const key of Object.keys(record)) {
981
- if (!allowed.has(key))
982
- issues.push(`${at}.${key} is not supported`);
983
- }
984
- }
985
- function parseBoxLabelContent(value, at, issues) {
986
- const label = readOptionalString(value, "label", at, issues);
987
- const labelFontSize = readOptionalNumber(value, "labelFontSize", at, issues, {
988
- positive: true
989
- });
990
- const labelFontFamily = readOptionalFontFamily(value, "labelFontFamily", at, issues);
991
- const labelWeight = readOptionalWeight(value, "labelWeight", at, issues);
992
- const labelRows = parseLabelRows(value.labelRows, at, issues);
993
- const labelRowGap = readOptionalNumber(value, "labelRowGap", at, issues, {
994
- nonNegative: true
995
- });
996
- const hasLegacyStyle = value.labelFontSize !== undefined || value.labelFontFamily !== undefined || value.labelWeight !== undefined;
997
- if (value.label === undefined && hasLegacyStyle) {
998
- issues.push(`${at} cannot style a label that is not present`);
999
- }
1000
- if (value.labelRows !== undefined && value.label !== undefined) {
1001
- issues.push(`${at}.label and ${at}.labelRows are mutually exclusive`);
1002
- }
1003
- if (value.labelRows !== undefined && hasLegacyStyle) {
1004
- issues.push(`${at}.labelRows cannot be combined with legacy label styling`);
1005
- }
1006
- if (value.labelRows === undefined && value.labelRowGap !== undefined) {
1007
- issues.push(`${at}.labelRowGap requires labelRows`);
1008
- }
1009
- if (labelRows !== undefined) {
1010
- return {
1011
- labelRows,
1012
- ...labelRowGap === undefined ? {} : { labelRowGap }
1013
- };
1014
- }
1015
- if (label !== undefined) {
1016
- return {
1017
- label,
1018
- ...labelFontSize === undefined ? {} : { labelFontSize },
1019
- ...labelFontFamily === undefined ? {} : { labelFontFamily },
1020
- ...labelWeight === undefined ? {} : { labelWeight }
1021
- };
1022
- }
1023
- return {};
1024
- }
1025
- function parseBase(record, at, issues) {
1026
- const id = readString(record, "id", at, issues);
1027
- if (id !== undefined && !idPattern.test(id)) {
1028
- issues.push(`${at}.id must contain only letters, numbers, underscores, or hyphens`);
1029
- }
1030
- const opacity = readOptionalNumber(record, "opacity", at, issues, { nonNegative: true });
1031
- if (opacity !== undefined && opacity > 1)
1032
- issues.push(`${at}.opacity must not exceed 1`);
1033
- const x = readNumber(record, "x", at, issues);
1034
- const y = readNumber(record, "y", at, issues);
1035
- const tone = readOptionalTone(record, "tone", at, issues);
1036
- return {
1037
- ...id === undefined ? {} : { id },
1038
- ...x === undefined ? {} : { x },
1039
- ...y === undefined ? {} : { y },
1040
- ...tone === undefined ? {} : { tone },
1041
- ...opacity === undefined ? {} : { opacity }
1042
- };
1043
- }
1044
- function parseShape(value, index, issues) {
1045
- const at = `shapes[${index}]`;
1046
- if (!isRecord(value)) {
1047
- issues.push(`${at} must be an object`);
1048
- return null;
1049
- }
1050
- const type = readString(value, "type", at, issues);
1051
- const base = parseBase(value, at, issues);
1052
- if (base.id === undefined || base.x === undefined || base.y === undefined || type === undefined) {
1053
- return null;
1054
- }
1055
- const requiredBase = {
1056
- id: base.id,
1057
- x: base.x,
1058
- y: base.y,
1059
- ...base.tone === undefined ? {} : { tone: base.tone },
1060
- ...base.opacity === undefined ? {} : { opacity: base.opacity }
1061
- };
1062
- if (type === "rect" || type === "ellipse") {
1063
- validateKnownKeys(value, new Set([
1064
- "id",
1065
- "type",
1066
- "x",
1067
- "y",
1068
- "tone",
1069
- "opacity",
1070
- "width",
1071
- "height",
1072
- "radius",
1073
- "label",
1074
- "labelFontSize",
1075
- "labelFontFamily",
1076
- "labelWeight",
1077
- "labelRows",
1078
- "labelRowGap",
1079
- "icon",
1080
- "iconSize",
1081
- "strokeWidth",
1082
- "fill"
1083
- ]), at, issues);
1084
- const width = readNumber(value, "width", at, issues, { positive: true });
1085
- const height = readNumber(value, "height", at, issues, { positive: true });
1086
- const labelContent = parseBoxLabelContent(value, at, issues);
1087
- const fill = value.fill;
1088
- if (fill !== undefined && typeof fill !== "boolean")
1089
- issues.push(`${at}.fill must be a boolean`);
1090
- if (width === undefined || height === undefined)
1091
- return null;
1092
- return {
1093
- ...requiredBase,
1094
- type,
1095
- width,
1096
- height,
1097
- ...readOptionalNumber(value, "radius", at, issues, { nonNegative: true }) === undefined ? {} : { radius: value.radius },
1098
- ...labelContent,
1099
- ...readOptionalString(value, "icon", at, issues) === undefined ? {} : { icon: value.icon },
1100
- ...readOptionalNumber(value, "iconSize", at, issues, { positive: true }) === undefined ? {} : { iconSize: value.iconSize },
1101
- ...readOptionalNumber(value, "strokeWidth", at, issues, { nonNegative: true }) === undefined ? {} : { strokeWidth: value.strokeWidth },
1102
- ...typeof fill === "boolean" ? { fill } : {}
1103
- };
1104
- }
1105
- if (type === "text") {
1106
- validateKnownKeys(value, new Set([
1107
- "id",
1108
- "type",
1109
- "x",
1110
- "y",
1111
- "tone",
1112
- "opacity",
1113
- "text",
1114
- "width",
1115
- "fontSize",
1116
- "fontFamily",
1117
- "weight",
1118
- "align"
1119
- ]), at, issues);
1120
- const text = readString(value, "text", at, issues);
1121
- const fontFamily = readOptionalFontFamily(value, "fontFamily", at, issues);
1122
- const weight = readOptionalWeight(value, "weight", at, issues);
1123
- const align = value.align;
1124
- if (align !== undefined && !["start", "middle", "end"].includes(align)) {
1125
- issues.push(`${at}.align must be start, middle, or end`);
1126
- }
1127
- if (text === undefined)
1128
- return null;
1129
- return {
1130
- ...requiredBase,
1131
- type,
1132
- text,
1133
- ...readOptionalNumber(value, "width", at, issues, { positive: true }) === undefined ? {} : { width: value.width },
1134
- ...readOptionalNumber(value, "fontSize", at, issues, { positive: true }) === undefined ? {} : { fontSize: value.fontSize },
1135
- ...fontFamily === undefined ? {} : { fontFamily },
1136
- ...weight === undefined ? {} : { weight },
1137
- ...align === undefined ? {} : { align }
1138
- };
1139
- }
1140
- if (type === "line") {
1141
- validateKnownKeys(value, new Set(["id", "type", "x", "y", "tone", "opacity", "x2", "y2", "strokeWidth"]), at, issues);
1142
- const x2 = readNumber(value, "x2", at, issues);
1143
- const y2 = readNumber(value, "y2", at, issues);
1144
- if (x2 === undefined || y2 === undefined)
1145
- return null;
1146
- return {
1147
- ...requiredBase,
1148
- type,
1149
- x2,
1150
- y2,
1151
- ...readOptionalNumber(value, "strokeWidth", at, issues, { positive: true }) === undefined ? {} : { strokeWidth: value.strokeWidth }
1152
- };
1153
- }
1154
- issues.push(`${at}.type must be rect, ellipse, text, or line`);
1155
- return null;
1156
- }
1157
- function parseStackShape(value, index, issues) {
1158
- const at = `shapes[${index}]`;
1159
- if (!isRecord(value)) {
1160
- issues.push(`${at} must be an object`);
1161
- return null;
1162
- }
1163
- validateKnownKeys(value, new Set([
1164
- "id",
1165
- "type",
1166
- "tone",
1167
- "opacity",
1168
- "width",
1169
- "height",
1170
- "radius",
1171
- "label",
1172
- "labelFontSize",
1173
- "labelFontFamily",
1174
- "labelWeight",
1175
- "labelRows",
1176
- "labelRowGap",
1177
- "icon",
1178
- "iconSize",
1179
- "strokeWidth",
1180
- "fill"
1181
- ]), at, issues);
1182
- const id = readString(value, "id", at, issues);
1183
- if (id !== undefined && !idPattern.test(id)) {
1184
- issues.push(`${at}.id must contain only letters, numbers, underscores, or hyphens`);
1185
- }
1186
- const type = readString(value, "type", at, issues);
1187
- if (type !== undefined && type !== "rect" && type !== "ellipse") {
1188
- issues.push(`${at}.type must be rect or ellipse in a stack layout`);
1189
- }
1190
- const width = readNumber(value, "width", at, issues, { positive: true });
1191
- const height = readNumber(value, "height", at, issues, { positive: true });
1192
- const labelContent = parseBoxLabelContent(value, at, issues);
1193
- const tone = readOptionalTone(value, "tone", at, issues);
1194
- const opacity = readOptionalNumber(value, "opacity", at, issues, { nonNegative: true });
1195
- if (opacity !== undefined && opacity > 1)
1196
- issues.push(`${at}.opacity must not exceed 1`);
1197
- const radius = readOptionalNumber(value, "radius", at, issues, { nonNegative: true });
1198
- const icon = readOptionalString(value, "icon", at, issues);
1199
- const iconSize = readOptionalNumber(value, "iconSize", at, issues, { positive: true });
1200
- const strokeWidth = readOptionalNumber(value, "strokeWidth", at, issues, {
1201
- nonNegative: true
1202
- });
1203
- const fill = value.fill;
1204
- if (fill !== undefined && typeof fill !== "boolean")
1205
- issues.push(`${at}.fill must be a boolean`);
1206
- if (id === undefined || type !== "rect" && type !== "ellipse" || width === undefined || height === undefined) {
1207
- return null;
1208
- }
1209
- return {
1210
- id,
1211
- type,
1212
- width,
1213
- height,
1214
- ...tone === undefined ? {} : { tone },
1215
- ...opacity === undefined ? {} : { opacity },
1216
- ...radius === undefined ? {} : { radius },
1217
- ...labelContent,
1218
- ...icon === undefined ? {} : { icon },
1219
- ...iconSize === undefined ? {} : { iconSize },
1220
- ...strokeWidth === undefined ? {} : { strokeWidth },
1221
- ...typeof fill === "boolean" ? { fill } : {}
1222
- };
1223
- }
1224
- function parseEdge(value, index, issues) {
1225
- const at = `edges[${index}]`;
1226
- if (!isRecord(value)) {
1227
- issues.push(`${at} must be an object`);
1228
- return null;
1229
- }
1230
- validateKnownKeys(value, new Set([
1231
- "id",
1232
- "from",
1233
- "to",
1234
- "label",
1235
- "tone",
1236
- "start",
1237
- "end",
1238
- "bend",
1239
- "arrowhead",
1240
- "startPosition",
1241
- "endPosition",
1242
- "labelFontSize",
1243
- "labelFontFamily",
1244
- "labelWeight",
1245
- "labelPosition",
1246
- "labelOffset"
1247
- ]), at, issues);
1248
- const id = readString(value, "id", at, issues);
1249
- const from = readString(value, "from", at, issues);
1250
- const to = readString(value, "to", at, issues);
1251
- if (id !== undefined && !idPattern.test(id))
1252
- issues.push(`${at}.id has unsupported characters`);
1253
- const start = value.start;
1254
- const end = value.end;
1255
- if (start !== undefined && (typeof start !== "string" || !anchors.has(start))) {
1256
- issues.push(`${at}.start must be auto, top, right, bottom, or left`);
1257
- }
1258
- if (end !== undefined && (typeof end !== "string" || !anchors.has(end))) {
1259
- issues.push(`${at}.end must be auto, top, right, bottom, or left`);
1260
- }
1261
- const startPosition = readOptionalNumber(value, "startPosition", at, issues, {
1262
- nonNegative: true
1263
- });
1264
- if (startPosition !== undefined && startPosition > 1) {
1265
- issues.push(`${at}.startPosition must not exceed 1`);
1266
- }
1267
- const endPosition = readOptionalNumber(value, "endPosition", at, issues, {
1268
- nonNegative: true
1269
- });
1270
- if (endPosition !== undefined && endPosition > 1) {
1271
- issues.push(`${at}.endPosition must not exceed 1`);
1272
- }
1273
- const arrowhead = value.arrowhead;
1274
- if (arrowhead !== undefined && !["arrow", "triangle", "none"].includes(arrowhead)) {
1275
- issues.push(`${at}.arrowhead must be arrow, triangle, or none`);
1276
- }
1277
- const label = readOptionalString(value, "label", at, issues);
1278
- const labelFontSize = readOptionalNumber(value, "labelFontSize", at, issues, {
1279
- positive: true
1280
- });
1281
- const labelFontFamily = readOptionalFontFamily(value, "labelFontFamily", at, issues);
1282
- const labelWeight = readOptionalWeight(value, "labelWeight", at, issues);
1283
- const labelPosition = readOptionalNumber(value, "labelPosition", at, issues, {
1284
- nonNegative: true
1285
- });
1286
- if (labelPosition !== undefined && labelPosition > 1) {
1287
- issues.push(`${at}.labelPosition must not exceed 1`);
1288
- }
1289
- const labelOffset = readOptionalNumber(value, "labelOffset", at, issues);
1290
- if (value.label === undefined && [
1291
- value.labelFontSize,
1292
- value.labelFontFamily,
1293
- value.labelWeight,
1294
- value.labelPosition,
1295
- value.labelOffset
1296
- ].some((candidate) => candidate !== undefined)) {
1297
- issues.push(`${at} cannot style or position a label that is not present`);
1298
- }
1299
- if (id === undefined || from === undefined || to === undefined)
1300
- return null;
1301
- return {
1302
- id,
1303
- from,
1304
- to,
1305
- ...label === undefined ? {} : {
1306
- label,
1307
- ...labelFontSize === undefined ? {} : { labelFontSize },
1308
- ...labelFontFamily === undefined ? {} : { labelFontFamily },
1309
- ...labelWeight === undefined ? {} : { labelWeight },
1310
- ...labelPosition === undefined ? {} : { labelPosition },
1311
- ...labelOffset === undefined ? {} : { labelOffset }
1312
- },
1313
- ...readOptionalTone(value, "tone", at, issues) === undefined ? {} : { tone: value.tone },
1314
- ...start === undefined ? {} : { start },
1315
- ...end === undefined ? {} : { end },
1316
- ...startPosition === undefined ? {} : { startPosition },
1317
- ...endPosition === undefined ? {} : { endPosition },
1318
- ...readOptionalNumber(value, "bend", at, issues) === undefined ? {} : { bend: value.bend },
1319
- ...arrowhead === undefined ? {} : { arrowhead }
1320
- };
1321
- }
1322
- function parseStackLayout(value, issues) {
1323
- if (!isRecord(value)) {
1324
- issues.push("layout must be an object");
1325
- return null;
1326
- }
1327
- validateKnownKeys(value, new Set(["type", "direction", "gap", "align"]), "layout", issues);
1328
- if (value.type !== "stack")
1329
- issues.push("layout.type must be stack");
1330
- const direction = value.direction;
1331
- if (typeof direction !== "string" || !stackDirections.has(direction)) {
1332
- issues.push("layout.direction must be horizontal or vertical");
1333
- }
1334
- const gap = readOptionalNumber(value, "gap", "layout", issues, { nonNegative: true });
1335
- const align = value.align;
1336
- if (align !== undefined && (typeof align !== "string" || !stackAlignments.has(align))) {
1337
- issues.push("layout.align must be start, center, or end");
1338
- }
1339
- if (value.type !== "stack" || typeof direction !== "string" || !stackDirections.has(direction)) {
1340
- return null;
1341
- }
1342
- return {
1343
- type: "stack",
1344
- direction,
1345
- ...gap === undefined ? {} : { gap },
1346
- ...align === undefined || !stackAlignments.has(align) ? {} : { align }
1347
- };
1348
- }
1349
- function parseDiagramSource(value) {
1350
- const issues = [];
1351
- if (!isRecord(value))
1352
- throw new DiagramValidationError(["root must be an object"]);
1353
- const isStackSource = "layout" in value;
1354
- validateKnownKeys(value, new Set([
1355
- "$schema",
1356
- "version",
1357
- "name",
1358
- "canvas",
1359
- "shapes",
1360
- "edges",
1361
- ...isStackSource ? ["layout"] : []
1362
- ]), "root", issues);
1363
- if (value.version !== diagramVersion)
1364
- issues.push(`version must be ${diagramVersion}`);
1365
- const name = readString(value, "name", "root", issues);
1366
- if (name !== undefined && !namePattern.test(name)) {
1367
- issues.push("name must be lowercase kebab-case");
1368
- }
1369
- const canvasValue = value.canvas;
1370
- let canvas = null;
1371
- if (!isRecord(canvasValue)) {
1372
- issues.push("canvas must be an object");
1373
- } else {
1374
- validateKnownKeys(canvasValue, new Set(["width", "height", "padding"]), "canvas", issues);
1375
- const width = readNumber(canvasValue, "width", "canvas", issues, { positive: true });
1376
- const height = readNumber(canvasValue, "height", "canvas", issues, { positive: true });
1377
- const padding = readOptionalNumber(canvasValue, "padding", "canvas", issues, {
1378
- nonNegative: true
1379
- });
1380
- if (width !== undefined && height !== undefined) {
1381
- canvas = { width, height, ...padding === undefined ? {} : { padding } };
1382
- }
1383
- }
1384
- const layout = isStackSource ? parseStackLayout(value.layout, issues) : null;
1385
- const shapesValue = value.shapes;
1386
- const positionedShapes = [];
1387
- const stackShapes = [];
1388
- if (!Array.isArray(shapesValue)) {
1389
- issues.push("shapes must be an array");
1390
- } else {
1391
- if (isStackSource && (shapesValue.length < 1 || shapesValue.length > 9)) {
1392
- issues.push("stack layouts must contain between 1 and 9 shapes");
1393
- }
1394
- for (const [index, shape] of shapesValue.entries()) {
1395
- if (isStackSource) {
1396
- const parsed = parseStackShape(shape, index, issues);
1397
- if (parsed !== null)
1398
- stackShapes.push(parsed);
1399
- } else {
1400
- const parsed = parseShape(shape, index, issues);
1401
- if (parsed !== null)
1402
- positionedShapes.push(parsed);
1403
- }
1404
- }
1405
- }
1406
- const edgesValue = value.edges;
1407
- const edges = [];
1408
- if (edgesValue !== undefined) {
1409
- if (!Array.isArray(edgesValue)) {
1410
- issues.push("edges must be an array when present");
1411
- } else {
1412
- for (const [index, edge] of edgesValue.entries()) {
1413
- const parsed = parseEdge(edge, index, issues);
1414
- if (parsed !== null)
1415
- edges.push(parsed);
1416
- }
1417
- }
1418
- }
1419
- const shapes = isStackSource ? stackShapes : positionedShapes;
1420
- const allIds = new Set;
1421
- for (const [kind, records] of [
1422
- ["shape", shapes],
1423
- ["edge", edges]
1424
- ]) {
1425
- for (const record of records) {
1426
- if (allIds.has(record.id))
1427
- issues.push(`${kind} id ${record.id} is duplicated`);
1428
- allIds.add(record.id);
1429
- }
1430
- }
1431
- const connectableIds = new Set(shapes.filter((shape) => shape.type === "rect" || shape.type === "ellipse").map((shape) => shape.id));
1432
- for (const edge of edges) {
1433
- if (!connectableIds.has(edge.from))
1434
- issues.push(`edge ${edge.id} has unknown or non-connectable from id ${edge.from}`);
1435
- if (!connectableIds.has(edge.to))
1436
- issues.push(`edge ${edge.id} has unknown or non-connectable to id ${edge.to}`);
1437
- if (edge.from === edge.to)
1438
- issues.push(`edge ${edge.id} cannot connect a shape to itself`);
1439
- }
1440
- if (isStackSource) {
1441
- const indexes = new Map(stackShapes.map((shape, index) => [shape.id, index]));
1442
- const connectedPairs = new Set;
1443
- for (const edge of edges) {
1444
- const fromIndex = indexes.get(edge.from);
1445
- const toIndex = indexes.get(edge.to);
1446
- if (fromIndex !== undefined && toIndex !== undefined && Math.abs(fromIndex - toIndex) !== 1) {
1447
- issues.push(`edge ${edge.id} must connect adjacent stack shapes`);
1448
- }
1449
- if (fromIndex !== undefined && toIndex !== undefined && fromIndex !== toIndex) {
1450
- const pair = [fromIndex, toIndex].sort((left, right) => left - right).join(":");
1451
- if (connectedPairs.has(pair)) {
1452
- issues.push(`edge ${edge.id} duplicates a connection between the same stack shapes`);
1453
- }
1454
- connectedPairs.add(pair);
1455
- }
1456
- if (edge.start !== undefined && edge.start !== "auto") {
1457
- issues.push(`edge ${edge.id}.start must be auto or omitted in a stack layout`);
1458
- }
1459
- if (edge.end !== undefined && edge.end !== "auto") {
1460
- issues.push(`edge ${edge.id}.end must be auto or omitted in a stack layout`);
1461
- }
1462
- if (edge.bend !== undefined && edge.bend !== 0) {
1463
- issues.push(`edge ${edge.id}.bend must be 0 or omitted in a stack layout`);
1464
- }
1465
- if (edge.startPosition !== undefined) {
1466
- issues.push(`edge ${edge.id}.startPosition is not supported in a stack layout`);
1467
- }
1468
- if (edge.endPosition !== undefined) {
1469
- issues.push(`edge ${edge.id}.endPosition is not supported in a stack layout`);
1470
- }
1471
- }
1472
- }
1473
- if (issues.length > 0 || name === undefined || canvas === null || isStackSource && layout === null) {
1474
- throw new DiagramValidationError(issues);
1475
- }
1476
- const common = {
1477
- ..."$schema" in value && typeof value.$schema === "string" ? { $schema: value.$schema } : {},
1478
- version: diagramVersion,
1479
- name,
1480
- canvas
1481
- };
1482
- if (isStackSource) {
1483
- const stackEdges = edges.map((edge) => ({
1484
- id: edge.id,
1485
- from: edge.from,
1486
- to: edge.to,
1487
- ...edge.label === undefined ? {} : { label: edge.label },
1488
- ...edge.tone === undefined ? {} : { tone: edge.tone },
1489
- ...edge.start === "auto" ? { start: edge.start } : {},
1490
- ...edge.end === "auto" ? { end: edge.end } : {},
1491
- ...edge.bend === 0 ? { bend: edge.bend } : {},
1492
- ...edge.arrowhead === undefined ? {} : { arrowhead: edge.arrowhead },
1493
- ...edge.label === undefined ? {} : {
1494
- ...edge.labelFontSize === undefined ? {} : { labelFontSize: edge.labelFontSize },
1495
- ...edge.labelFontFamily === undefined ? {} : { labelFontFamily: edge.labelFontFamily },
1496
- ...edge.labelWeight === undefined ? {} : { labelWeight: edge.labelWeight },
1497
- ...edge.labelPosition === undefined ? {} : { labelPosition: edge.labelPosition },
1498
- ...edge.labelOffset === undefined ? {} : { labelOffset: edge.labelOffset }
1499
- }
1500
- }));
1501
- return {
1502
- ...common,
1503
- layout,
1504
- shapes: stackShapes,
1505
- ...edgesValue === undefined ? {} : { edges: stackEdges }
1506
- };
1507
- }
1508
- return {
1509
- ...common,
1510
- shapes: positionedShapes,
1511
- ...edgesValue === undefined ? {} : { edges }
1512
- };
1513
- }
1514
- function parseDiagramSpec(value) {
1515
- return resolveDiagramSource(parseDiagramSource(value));
1516
- }
1517
-
1518
- // src/tldr.ts
1519
- var schema = {
1520
- schemaVersion: 2,
1521
- sequences: {
1522
- "com.tldraw.store": 5,
1523
- "com.tldraw.asset": 1,
1524
- "com.tldraw.camera": 1,
1525
- "com.tldraw.document": 2,
1526
- "com.tldraw.instance": 26,
1527
- "com.tldraw.instance_page_state": 5,
1528
- "com.tldraw.page": 1,
1529
- "com.tldraw.instance_presence": 6,
1530
- "com.tldraw.pointer": 1,
1531
- "com.tldraw.shape": 4,
1532
- "com.tldraw.user": 1,
1533
- "com.tldraw.asset.image": 6,
1534
- "com.tldraw.asset.video": 5,
1535
- "com.tldraw.asset.bookmark": 2,
1536
- "com.tldraw.shape.group": 0,
1537
- "com.tldraw.shape.text": 4,
1538
- "com.tldraw.shape.bookmark": 2,
1539
- "com.tldraw.shape.draw": 5,
1540
- "com.tldraw.shape.geo": 11,
1541
- "com.tldraw.shape.note": 13,
1542
- "com.tldraw.shape.line": 5,
1543
- "com.tldraw.shape.frame": 1,
1544
- "com.tldraw.shape.arrow": 8,
1545
- "com.tldraw.shape.highlight": 4,
1546
- "com.tldraw.shape.embed": 4,
1547
- "com.tldraw.shape.image": 5,
1548
- "com.tldraw.shape.video": 4,
1549
- "com.tldraw.binding.arrow": 1
1550
- }
1551
- };
1552
- var tldrawColors = {
1553
- neutral: "black",
1554
- blue: "blue",
1555
- orange: "orange",
1556
- green: "green",
1557
- red: "red",
1558
- purple: "violet",
1559
- yellow: "yellow"
1560
- };
1561
- function richText(text, weight = 400) {
1562
- return {
1563
- type: "doc",
1564
- content: text.split(`
1565
- `).map((line) => ({
1566
- type: "paragraph",
1567
- ...line === "" ? {} : {
1568
- content: [
1569
- {
1570
- type: "text",
1571
- text: line,
1572
- ...weight < 600 ? {} : { marks: [{ type: "bold" }] }
1573
- }
1574
- ]
1575
- }
1576
- }))
1577
- };
1578
- }
1579
- function authoredShapeId(id) {
1580
- return `shape:${id}`;
1581
- }
1582
- function generatedShapeId(kind, id) {
1583
- return `shape:slopcamera:${kind}:${id}`;
1584
- }
1585
- function shapeMeta(sourceId) {
1586
- return { diagram: { version: 1, sourceId } };
1587
- }
1588
- function indexKey(index) {
1589
- if (!Number.isSafeInteger(index) || index < 0) {
1590
- throw new Error(`A .tldr index must be a non-negative safe integer, received ${index}`);
1591
- }
1592
- const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
1593
- let width = 1;
1594
- let remaining = index;
1595
- let capacity = digits.length;
1596
- while (remaining >= capacity) {
1597
- remaining -= capacity;
1598
- width += 1;
1599
- capacity *= digits.length;
1600
- if (width > 26 || !Number.isSafeInteger(capacity)) {
1601
- throw new Error("A .tldr export contains too many generated records");
1602
- }
1603
- }
1604
- let suffix = "";
1605
- for (let place = 0;place < width; place += 1) {
1606
- suffix = `${digits[remaining % digits.length]}${suffix}`;
1607
- remaining = Math.floor(remaining / digits.length);
1608
- }
1609
- return `${String.fromCharCode(97 + width - 1)}${suffix}`;
1610
- }
1611
- function baseShape(shape, index) {
1612
- return {
1613
- x: shape.x,
1614
- y: shape.y,
1615
- rotation: 0,
1616
- isLocked: false,
1617
- opacity: shape.opacity ?? 1,
1618
- meta: shapeMeta(shape.id),
1619
- id: authoredShapeId(shape.id),
1620
- parentId: "page:page",
1621
- index: indexKey(index),
1622
- typeName: "shape"
1623
- };
1624
- }
1625
- function textShape(options) {
1626
- return {
1627
- x: options.x,
1628
- y: options.y,
1629
- rotation: 0,
1630
- isLocked: false,
1631
- opacity: options.opacity ?? 1,
1632
- meta: shapeMeta(options.sourceId),
1633
- id: options.recordId,
1634
- type: "text",
1635
- props: {
1636
- color: tldrawColors[options.tone],
1637
- size: options.size,
1638
- w: options.width,
1639
- font: options.fontFamily === "mono" ? "mono" : "sans",
1640
- textAlign: options.align,
1641
- autoSize: false,
1642
- scale: options.scale ?? 1,
1643
- richText: richText(options.text, options.weight)
1644
- },
1645
- parentId: "page:page",
1646
- index: indexKey(options.index),
1647
- typeName: "shape"
1648
- };
1649
- }
1650
- function svgIconAsset(icon, color) {
1651
- const clean = sanitizeIcon(icon);
1652
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="${clean.viewBox}" fill="none" color="${color}">${clean.body}</svg>`;
1653
- return `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
1654
- }
1655
- var tldrawTextFontSizes = {
1656
- s: 18,
1657
- m: 24,
1658
- l: 36,
1659
- xl: 44
1660
- };
1661
- function scaledTldrawSize(fontSize) {
1662
- const size = fontSize < 21 ? "s" : fontSize < 30 ? "m" : fontSize < 40 ? "l" : "xl";
1663
- return { size, scale: fontSize / tldrawTextFontSizes[size] };
1664
- }
1665
- function serializeTldr(spec, config) {
1666
- const records = [
1667
- {
1668
- gridSize: 10,
1669
- name: spec.name,
1670
- meta: { diagram: { version: 1 } },
1671
- id: "document:document",
1672
- typeName: "document"
1673
- },
1674
- {
1675
- meta: {},
1676
- id: "page:page",
1677
- name: "Page 1",
1678
- index: "a1",
1679
- typeName: "page"
1680
- }
1681
- ];
1682
- const icons = config.icons ?? {};
1683
- const lightTheme = resolveTheme("light", config);
1684
- let generatedIndex = 1;
1685
- for (const shape of spec.shapes) {
1686
- generatedIndex += 1;
1687
- if (shape.type === "rect" || shape.type === "ellipse") {
1688
- const box = shape;
1689
- const hasIcon = box.icon !== undefined;
1690
- const hasSeparateLabel = box.labelRows !== undefined || box.label !== undefined && (hasIcon || box.labelFontSize !== undefined || box.labelFontFamily !== undefined || box.labelWeight !== undefined);
1691
- records.push({
1692
- ...baseShape(box, generatedIndex),
1693
- type: "geo",
1694
- props: {
1695
- w: box.width,
1696
- h: box.height,
1697
- geo: box.type === "ellipse" ? "ellipse" : "rectangle",
1698
- dash: "solid",
1699
- growY: 0,
1700
- url: "",
1701
- scale: 1,
1702
- color: tldrawColors[box.tone ?? "neutral"],
1703
- labelColor: "black",
1704
- fill: box.fill === false ? "none" : "solid",
1705
- size: (box.strokeWidth ?? 2) >= 4 ? "l" : "m",
1706
- font: "sans",
1707
- align: "middle",
1708
- verticalAlign: "middle",
1709
- richText: richText(hasSeparateLabel ? "" : box.label ?? "", box.labelWeight)
1710
- }
1711
- });
1712
- const iconSize = Math.min(box.iconSize ?? 52, box.height * 0.45, box.width * 0.32);
1713
- const content = layoutBoxContent(box, hasIcon ? iconSize : undefined);
1714
- if (box.icon !== undefined) {
1715
- const icon = icons[box.icon];
1716
- if (icon === undefined)
1717
- throw new Error(`Unknown icon "${box.icon}" on shape ${box.id}`);
1718
- if (content.icon === undefined)
1719
- throw new Error(`Missing icon layout for shape ${box.id}`);
1720
- const assetId = `asset:icon-${box.id}`;
1721
- records.push({
1722
- id: assetId,
1723
- type: "image",
1724
- typeName: "asset",
1725
- props: {
1726
- name: `${box.icon}.svg`,
1727
- src: svgIconAsset(icon, lightTheme.tones[box.tone ?? "neutral"].text),
1728
- w: 96,
1729
- h: 96,
1730
- mimeType: "image/svg+xml",
1731
- isAnimated: false
1732
- },
1733
- meta: { diagram: { version: 1, icon: box.icon } }
1734
- });
1735
- generatedIndex += 1;
1736
- records.push({
1737
- x: content.icon.x,
1738
- y: content.icon.y,
1739
- rotation: 0,
1740
- isLocked: false,
1741
- opacity: box.opacity ?? 1,
1742
- meta: shapeMeta(box.id),
1743
- id: generatedShapeId("box-icon", box.id),
1744
- type: "image",
1745
- props: {
1746
- w: iconSize,
1747
- h: iconSize,
1748
- assetId,
1749
- playing: true,
1750
- url: "",
1751
- crop: null,
1752
- flipX: false,
1753
- flipY: false,
1754
- altText: box.labelRows?.map((row) => row.text).join(" ") ?? box.label ?? box.icon
1755
- },
1756
- parentId: "page:page",
1757
- index: indexKey(generatedIndex),
1758
- typeName: "shape"
1759
- });
1760
- }
1761
- if (hasSeparateLabel) {
1762
- for (const [rowIndex, row] of content.rows.entries()) {
1763
- const textStyle = scaledTldrawSize(row.fontSize);
1764
- generatedIndex += 1;
1765
- records.push(textShape({
1766
- recordId: generatedShapeId("box-label", `${box.id}:${rowIndex + 1}`),
1767
- sourceId: box.id,
1768
- x: box.x + 16,
1769
- y: row.y,
1770
- width: Math.max(1, box.width - 32) / textStyle.scale,
1771
- text: row.lines.join(`
1772
- `),
1773
- tone: box.tone ?? "neutral",
1774
- size: textStyle.size,
1775
- align: "middle",
1776
- index: generatedIndex,
1777
- scale: textStyle.scale,
1778
- fontFamily: row.fontFamily,
1779
- weight: row.weight,
1780
- ...box.opacity === undefined ? {} : { opacity: box.opacity }
1781
- }));
1782
- }
1783
- }
1784
- continue;
1785
- }
1786
- if (shape.type === "text") {
1787
- const text = shape;
1788
- const fontSize = text.fontSize ?? 24;
1789
- const textStyle = scaledTldrawSize(fontSize);
1790
- const width = text.width ?? Math.max(8, text.text.length * fontSize * 0.58);
1791
- records.push(textShape({
1792
- recordId: authoredShapeId(text.id),
1793
- sourceId: text.id,
1794
- x: text.x,
1795
- y: text.y,
1796
- width: width / textStyle.scale,
1797
- text: text.text,
1798
- tone: text.tone ?? "neutral",
1799
- size: textStyle.size,
1800
- align: text.align ?? "start",
1801
- index: generatedIndex,
1802
- scale: textStyle.scale,
1803
- ...text.fontFamily === undefined ? {} : { fontFamily: text.fontFamily },
1804
- ...text.weight === undefined ? {} : { weight: text.weight },
1805
- ...text.opacity === undefined ? {} : { opacity: text.opacity }
1806
- }));
1807
- continue;
1808
- }
1809
- const line = shape;
1810
- records.push({
1811
- ...baseShape(line, generatedIndex),
1812
- type: "line",
1813
- props: {
1814
- dash: "solid",
1815
- size: (line.strokeWidth ?? 3) >= 4 ? "l" : "m",
1816
- color: tldrawColors[line.tone ?? "neutral"],
1817
- spline: "line",
1818
- points: {
1819
- a1: { id: "a1", index: "a1", x: 0, y: 0 },
1820
- a2: {
1821
- id: "a2",
1822
- index: "a2",
1823
- x: line.x2 - line.x,
1824
- y: line.y2 - line.y
1825
- }
1826
- },
1827
- scale: 1
1828
- }
1829
- });
1830
- }
1831
- for (const edge of spec.edges ?? []) {
1832
- const resolved = resolveEdge(spec, edge);
1833
- generatedIndex += 1;
1834
- const arrowId = authoredShapeId(edge.id);
1835
- records.push({
1836
- x: resolved.start.x,
1837
- y: resolved.start.y,
1838
- rotation: 0,
1839
- isLocked: false,
1840
- opacity: 1,
1841
- meta: shapeMeta(edge.id),
1842
- id: arrowId,
1843
- type: "arrow",
1844
- props: {
1845
- kind: "arc",
1846
- elbowMidPoint: 0.5,
1847
- dash: "solid",
1848
- size: "m",
1849
- fill: "none",
1850
- color: tldrawColors[edge.tone ?? "neutral"],
1851
- labelColor: tldrawColors[edge.tone ?? "neutral"],
1852
- bend: (edge.bend ?? 0) / 2,
1853
- start: { x: 0, y: 0 },
1854
- end: {
1855
- x: resolved.end.x - resolved.start.x,
1856
- y: resolved.end.y - resolved.start.y
1857
- },
1858
- arrowheadStart: "none",
1859
- arrowheadEnd: edge.arrowhead ?? "arrow",
1860
- richText: richText(""),
1861
- labelPosition: edge.labelPosition ?? 0.5,
1862
- font: edge.labelFontFamily === "mono" ? "mono" : "sans",
1863
- scale: 1
1864
- },
1865
- parentId: "page:page",
1866
- index: indexKey(generatedIndex),
1867
- typeName: "shape"
1868
- });
1869
- if (edge.label !== undefined) {
1870
- const labelPoint = resolveEdgeLabel(resolved);
1871
- const fontSize = edge.labelFontSize ?? 18;
1872
- const fontFamily = edge.labelFontFamily ?? "default";
1873
- const textStyle = scaledTldrawSize(fontSize);
1874
- const widthRatio = fontFamily === "mono" ? 0.62 : 0.56;
1875
- const width = Math.max(fontSize, edge.label.length * fontSize * widthRatio);
1876
- generatedIndex += 1;
1877
- records.push(textShape({
1878
- recordId: generatedShapeId("edge-label", edge.id),
1879
- sourceId: edge.id,
1880
- x: labelPoint.x - width / 2,
1881
- y: labelPoint.y - fontSize * 1.35 / 2,
1882
- width: width / textStyle.scale,
1883
- text: edge.label,
1884
- tone: edge.tone ?? "neutral",
1885
- size: textStyle.size,
1886
- align: "middle",
1887
- index: generatedIndex,
1888
- scale: textStyle.scale,
1889
- fontFamily,
1890
- weight: edge.labelWeight ?? 600
1891
- }));
1892
- }
1893
- records.push({
1894
- meta: {},
1895
- id: `binding:${edge.id}-start`,
1896
- fromId: arrowId,
1897
- toId: authoredShapeId(edge.from),
1898
- type: "arrow",
1899
- props: {
1900
- isPrecise: edge.startPosition !== undefined,
1901
- isExact: false,
1902
- normalizedAnchor: resolved.start.normalized,
1903
- snap: "none",
1904
- terminal: "start"
1905
- },
1906
- typeName: "binding"
1907
- }, {
1908
- meta: {},
1909
- id: `binding:${edge.id}-end`,
1910
- fromId: arrowId,
1911
- toId: authoredShapeId(edge.to),
1912
- type: "arrow",
1913
- props: {
1914
- isPrecise: edge.endPosition !== undefined,
1915
- isExact: false,
1916
- normalizedAnchor: resolved.end.normalized,
1917
- snap: "none",
1918
- terminal: "end"
1919
- },
1920
- typeName: "binding"
1921
- });
1922
- }
1923
- return `${JSON.stringify({ tldrawFileFormatVersion: 1, schema, records }, null, 2)}
1924
- `;
1925
- }
1926
- // src/operations.ts
1927
- var slopcameraOperationCodes = [
1928
- "slopcamera.diagram.check",
1929
- "slopcamera.diagram.render",
1930
- "slopcamera.image.vectorize",
1931
- "slopcamera.image.generate"
1932
- ];
1933
-
1934
- class SlopcameraOperationError extends Error {
1935
- code;
1936
- constructor(code, message) {
1937
- super(`[${code}] ${message}`);
1938
- this.name = "SlopcameraOperationError";
1939
- this.code = code;
1940
- }
1941
- }
1942
- var modelSchema = {
1943
- type: "string",
1944
- minLength: 3,
1945
- maxLength: 256,
1946
- pattern: "^[a-zA-Z0-9][a-zA-Z0-9._-]*/[a-zA-Z0-9][a-zA-Z0-9._:-]*$"
1947
- };
1948
- var pathSchema = {
1949
- type: "string",
1950
- minLength: 1,
1951
- maxLength: 4096
1952
- };
1953
- function deepFreeze(value) {
1954
- if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
1955
- return value;
1956
- }
1957
- for (const nested of Object.values(value))
1958
- deepFreeze(nested);
1959
- return Object.freeze(value);
1960
- }
1961
- var slopcameraOperationRegistry = deepFreeze([
1962
- {
1963
- code: "slopcamera.diagram.check",
1964
- title: "Check diagram",
1965
- description: "Parse and lint a checked Slopcamera diagram source without changing its files.",
1966
- execution: "local",
1967
- authentication: "none",
1968
- destructive: false,
1969
- idempotent: true,
1970
- inputSchema: {
1971
- type: "object",
1972
- additionalProperties: false,
1973
- required: ["path"],
1974
- properties: { path: pathSchema }
1975
- },
1976
- resources: [
1977
- { resource: "cpu", amount: 1 },
1978
- { resource: "local-io", amount: 1 }
1979
- ]
1980
- },
1981
- {
1982
- code: "slopcamera.diagram.render",
1983
- title: "Render diagram",
1984
- description: "Render a checked Slopcamera diagram source to its replaceable light, dark, PNG, SVG, and tldraw artifacts.",
1985
- execution: "local",
1986
- authentication: "none",
1987
- destructive: true,
1988
- idempotent: true,
1989
- inputSchema: {
1990
- type: "object",
1991
- additionalProperties: false,
1992
- required: ["path"],
1993
- properties: {
1994
- path: pathSchema,
1995
- outDirectory: pathSchema,
1996
- scale: {
1997
- type: "number",
1998
- exclusiveMinimum: 0,
1999
- maximum: 4
2000
- }
2001
- }
2002
- },
2003
- resources: [
2004
- { resource: "cpu", amount: 1 },
2005
- { resource: "local-io", amount: 1 }
2006
- ]
2007
- },
2008
- {
2009
- code: "slopcamera.image.vectorize",
2010
- title: "Vectorize image",
2011
- description: "Convert a local caller-owned raster into a bounded inert SVG without authentication or network access.",
2012
- execution: "local",
2013
- authentication: "none",
2014
- destructive: true,
2015
- idempotent: false,
2016
- inputSchema: {
2017
- type: "object",
2018
- additionalProperties: false,
2019
- required: ["inputPath", "outputPath"],
2020
- properties: {
2021
- inputPath: pathSchema,
2022
- outputPath: pathSchema,
2023
- duotone: {
2024
- type: "array",
2025
- minItems: 2,
2026
- maxItems: 2,
2027
- items: {
2028
- type: "string",
2029
- pattern: "^#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?$"
2030
- }
2031
- },
2032
- alphaCutoff: { type: "integer", minimum: 1, maximum: 64 },
2033
- timeoutMs: { type: "integer", minimum: 1, maximum: 300000 }
2034
- }
2035
- },
2036
- resources: [
2037
- { resource: "cpu", amount: 1 },
2038
- { resource: "local-io", amount: 1 }
2039
- ]
2040
- },
2041
- {
2042
- code: "slopcamera.image.generate",
2043
- title: "Generate image",
2044
- description: "Generate one bounded image directly through Vercel AI Gateway with an environment credential and no client retry.",
2045
- execution: "gateway",
2046
- authentication: "environment",
2047
- destructive: true,
2048
- idempotent: false,
2049
- inputSchema: {
2050
- type: "object",
2051
- additionalProperties: false,
2052
- required: ["model", "prompt", "outputPath"],
2053
- properties: {
2054
- model: modelSchema,
2055
- prompt: {
2056
- type: "string",
2057
- minLength: 1,
2058
- maxLength: slopcameraMaximumPromptBytes
2059
- },
2060
- outputPath: pathSchema
2061
- }
2062
- },
2063
- resources: [
2064
- { resource: "local-io", amount: 1 },
2065
- { resource: "network", amount: 1 },
2066
- { resource: "paid-call", amount: 1 }
2067
- ],
2068
- transport: {
2069
- method: "POST",
2070
- authority: "https://ai-gateway.vercel.sh/v4/ai",
2071
- authorization: "bearer",
2072
- retry: "never"
2073
- }
2074
- }
2075
- ]);
2076
- function operationFailure(message) {
2077
- throw new SlopcameraOperationError("INVALID_OPERATION_INPUT", message);
2078
- }
2079
- function isRecord2(value) {
2080
- return typeof value === "object" && value !== null && !Array.isArray(value);
2081
- }
2082
- function record(value, allowedKeys) {
2083
- if (!isRecord2(value))
2084
- operationFailure("Operation input must be an object.");
2085
- const unknown = Object.keys(value).filter((key) => !allowedKeys.includes(key));
2086
- if (unknown.length > 0) {
2087
- operationFailure(`Unsupported operation input field: ${unknown[0]}.`);
2088
- }
2089
- return value;
2090
- }
2091
- function pathValue(value, name) {
2092
- if (typeof value !== "string" || value.length < 1 || value.length > 4096 || value.includes("\x00")) {
2093
- operationFailure(`${name} must be a non-empty bounded local path.`);
2094
- }
2095
- return value;
2096
- }
2097
- function parseCheck(value) {
2098
- const input = record(value, ["path"]);
2099
- return { path: pathValue(input.path, "path") };
2100
- }
2101
- function parseRender(value) {
2102
- const input = record(value, ["path", "outDirectory", "scale"]);
2103
- const scale = input.scale;
2104
- if (scale !== undefined && (typeof scale !== "number" || !Number.isFinite(scale) || scale <= 0 || scale > 4)) {
2105
- operationFailure("scale must be greater than zero and no more than 4.");
2106
- }
2107
- return {
2108
- path: pathValue(input.path, "path"),
2109
- ...input.outDirectory === undefined ? {} : { outDirectory: pathValue(input.outDirectory, "outDirectory") },
2110
- ...scale === undefined ? {} : { scale }
2111
- };
2112
- }
2113
- function parseVectorize(value) {
2114
- const input = record(value, [
2115
- "inputPath",
2116
- "outputPath",
2117
- "duotone",
2118
- "alphaCutoff",
2119
- "timeoutMs"
2120
- ]);
2121
- const inputPath = pathValue(input.inputPath, "inputPath");
2122
- const outputPath = pathValue(input.outputPath, "outputPath");
2123
- if (!outputPath.toLowerCase().endsWith(".svg")) {
2124
- operationFailure("outputPath must end in .svg.");
2125
- }
2126
- const duotone = input.duotone;
2127
- if (duotone !== undefined && (!Array.isArray(duotone) || duotone.length !== 2 || duotone.some((color) => typeof color !== "string" || !/^#[a-f0-9]{3}(?:[a-f0-9]{3})?$/iu.test(color)))) {
2128
- operationFailure("duotone must contain exactly two #rgb or #rrggbb colors.");
2129
- }
2130
- const alphaCutoff = input.alphaCutoff;
2131
- if (alphaCutoff !== undefined && (!Number.isInteger(alphaCutoff) || alphaCutoff < 1 || alphaCutoff > 64)) {
2132
- operationFailure("alphaCutoff must be an integer from 1 through 64.");
2133
- }
2134
- const timeoutMs = input.timeoutMs;
2135
- if (timeoutMs !== undefined && (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300000)) {
2136
- operationFailure("timeoutMs must be an integer from 1 through 300000.");
2137
- }
2138
- return {
2139
- inputPath,
2140
- outputPath,
2141
- ...duotone === undefined ? {} : { duotone },
2142
- ...alphaCutoff === undefined ? {} : { alphaCutoff },
2143
- ...timeoutMs === undefined ? {} : { timeoutMs }
2144
- };
2145
- }
2146
- function parseGenerate(value) {
2147
- const input = record(value, ["model", "prompt", "outputPath"]);
2148
- if (typeof input.model !== "string" || input.model.length > 256 || !/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(input.model)) {
2149
- operationFailure("model must be a bounded Vercel AI Gateway provider/model id.");
2150
- }
2151
- if (typeof input.prompt !== "string" || input.prompt.trim().length < 1 || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(input.prompt) || Buffer.byteLength(input.prompt, "utf8") > slopcameraMaximumPromptBytes) {
2152
- operationFailure(`prompt must be non-empty and no more than ${slopcameraMaximumPromptBytes} UTF-8 bytes.`);
2153
- }
2154
- const outputPath = pathValue(input.outputPath, "outputPath");
2155
- if (!/\.(?:jpe?g|png|webp)$/iu.test(outputPath)) {
2156
- operationFailure("outputPath must end in .png, .jpg, .jpeg, or .webp.");
2157
- }
2158
- return {
2159
- model: input.model,
2160
- prompt: input.prompt,
2161
- outputPath
2162
- };
2163
- }
2164
- function parseSlopcameraOperationInput(code, input) {
2165
- switch (code) {
2166
- case "slopcamera.diagram.check":
2167
- return parseCheck(input);
2168
- case "slopcamera.diagram.render":
2169
- return parseRender(input);
2170
- case "slopcamera.image.vectorize":
2171
- return parseVectorize(input);
2172
- case "slopcamera.image.generate":
2173
- return parseGenerate(input);
2174
- default:
2175
- throw new SlopcameraOperationError("INVALID_OPERATION", "Unknown Slopcamera operation code.");
2176
- }
2177
- }
2178
- function isSlopcameraOperationCode(value) {
2179
- return slopcameraOperationCodes.includes(value);
2180
- }
2181
- function slopcameraOperationHostResourceClaims(code) {
2182
- const descriptor = slopcameraOperationRegistry.find((candidate) => candidate.code === code);
2183
- if (descriptor === undefined) {
2184
- throw new SlopcameraOperationError("INVALID_OPERATION", "Unknown Slopcamera operation code.");
2185
- }
2186
- return descriptor.resources;
2187
- }
2188
- function searchSlopcameraOperations(query = "", limit = slopcameraOperationRegistry.length) {
2189
- if (typeof query !== "string" || query.length > 200 || /[\u0000-\u001f\u007f]/u.test(query) || !Number.isInteger(limit) || limit < 1 || limit > 20) {
2190
- throw new SlopcameraOperationError("INVALID_SEARCH", "Search requires a bounded query and a limit from 1 through 20.");
2191
- }
2192
- const terms = query.toLowerCase().split(/\s+/u).filter((term) => term.length > 0);
2193
- return slopcameraOperationRegistry.filter((operation) => {
2194
- const haystack = `${operation.code} ${operation.title} ${operation.description}`.toLowerCase();
2195
- return terms.every((term) => haystack.includes(term));
2196
- }).slice(0, limit);
2197
- }
2198
- function operationDependenciesWithLease(dependencies, lease) {
2199
- const inheritedFileDescriptors = [
2200
- ...dependencies.inheritedFileDescriptors ?? [],
2201
- lease.inheritedFileDescriptor
2202
- ].filter((descriptor, index, descriptors) => descriptors.indexOf(descriptor) === index);
2203
- if (inheritedFileDescriptors.length > 16 || inheritedFileDescriptors.some((descriptor) => !Number.isSafeInteger(descriptor) || descriptor < 0 || descriptor > 2147483647)) {
2204
- throw new SlopcameraOperationError("INVALID_OPERATION_INPUT", "Operation host-resource inheritance exceeds its descriptor bound.");
2205
- }
2206
- const {
2207
- hostResourceCoordinator: _hostResourceCoordinator,
2208
- signal: _signal,
2209
- waitTimeoutMilliseconds: _waitTimeoutMilliseconds,
2210
- ...operationDependencies
2211
- } = dependencies;
2212
- return {
2213
- ...operationDependencies,
2214
- inheritedFileDescriptors
2215
- };
2216
- }
2217
- async function withSlopcameraOperationHostAdmission(code, callback, options = {}) {
2218
- const coordinator = options.hostResourceCoordinator ?? createDefaultHostResourceCoordinator();
2219
- return await coordinator.withLease(slopcameraOperationHostResourceClaims(code), async (lease) => {
2220
- await lease.assertOwned();
2221
- return await callback(lease);
2222
- }, {
2223
- ...options.signal === undefined ? {} : { signal: options.signal },
2224
- ...options.waitTimeoutMilliseconds === undefined ? {} : { waitTimeoutMilliseconds: options.waitTimeoutMilliseconds }
2225
- });
2226
- }
2227
- var operationBuiltInConfig = Object.freeze({
2228
- icons: builtInIcons
2229
- });
2230
- async function readOperationDiagram(path) {
2231
- const absolutePath = resolve(path);
2232
- let value;
2233
- try {
2234
- value = JSON.parse(await readFile2(absolutePath, "utf8"));
2235
- } catch (cause) {
2236
- throw new SlopcameraOperationError("INVALID_OPERATION_INPUT", "Diagram source could not be read as JSON.");
2237
- }
2238
- const spec = parseDiagramSpec(value);
2239
- for (const shape of spec.shapes) {
2240
- if ((shape.type === "rect" || shape.type === "ellipse") && shape.icon !== undefined && !Object.hasOwn(builtInIcons, shape.icon)) {
2241
- throw new SlopcameraOperationError("INVALID_OPERATION_INPUT", "Diagram requests an unavailable built-in icon.");
2242
- }
2243
- }
2244
- return { absolutePath, spec };
2245
- }
2246
- async function atomicOperationWrite(path, value) {
2247
- const temporaryPath = join(dirname(path), `.${randomUUID()}.slopcamera-operation.tmp`);
2248
- try {
2249
- await writeFile(temporaryPath, value, { flag: "wx" });
2250
- await rename(temporaryPath, path);
2251
- } finally {
2252
- await rm(temporaryPath, { force: true }).catch(() => {
2253
- return;
2254
- });
2255
- }
2256
- }
2257
- async function checkOperationDiagram(path) {
2258
- const { spec } = await readOperationDiagram(path);
2259
- return {
2260
- findings: lintDiagram(spec),
2261
- configPath: null
2262
- };
2263
- }
2264
- async function renderOperationDiagram(input) {
2265
- const { absolutePath, spec } = await readOperationDiagram(input.path);
2266
- const outputDirectory = resolve(input.outDirectory ?? dirname(absolutePath));
2267
- const scale = input.scale ?? 2;
2268
- const [light, dark] = await Promise.all([
2269
- renderSvg(spec, "light", operationBuiltInConfig),
2270
- renderSvg(spec, "dark", operationBuiltInConfig)
2271
- ]);
2272
- const [lightPng, darkPng] = [
2273
- renderPng(light, operationBuiltInConfig, scale),
2274
- renderPng(dark, operationBuiltInConfig, scale)
2275
- ];
2276
- const artifacts = {
2277
- spec: absolutePath,
2278
- tldr: join(outputDirectory, `${spec.name}.tldr`),
2279
- lightSvg: join(outputDirectory, `${spec.name}.light.svg`),
2280
- darkSvg: join(outputDirectory, `${spec.name}.dark.svg`),
2281
- lightPng: join(outputDirectory, `${spec.name}.light.png`),
2282
- darkPng: join(outputDirectory, `${spec.name}.dark.png`)
2283
- };
2284
- await mkdir(outputDirectory, { recursive: true });
2285
- await Promise.all([
2286
- atomicOperationWrite(artifacts.tldr, serializeTldr(spec, operationBuiltInConfig)),
2287
- atomicOperationWrite(artifacts.lightSvg, light.svg),
2288
- atomicOperationWrite(artifacts.darkSvg, dark.svg),
2289
- atomicOperationWrite(artifacts.lightPng, lightPng),
2290
- atomicOperationWrite(artifacts.darkPng, darkPng)
2291
- ]);
2292
- return {
2293
- artifacts,
2294
- findings: lintDiagram(spec),
2295
- configPath: null
2296
- };
2297
- }
2298
- async function executeSlopcameraOperationUncoordinated(code, value, dependencies = {}) {
2299
- const input = parseSlopcameraOperationInput(code, value);
2300
- switch (code) {
2301
- case "slopcamera.diagram.check": {
2302
- const options = input;
2303
- return await checkOperationDiagram(options.path);
2304
- }
2305
- case "slopcamera.diagram.render": {
2306
- const options = input;
2307
- return await renderOperationDiagram(options);
2308
- }
2309
- case "slopcamera.image.vectorize": {
2310
- const options = input;
2311
- const result = await vectorizeImage(options.inputPath, {
2312
- outputPath: options.outputPath,
2313
- ...options.duotone === undefined ? {} : { duotone: options.duotone },
2314
- ...options.alphaCutoff === undefined ? {} : { alphaCutoff: options.alphaCutoff },
2315
- ...options.timeoutMs === undefined ? {} : { limits: { maxDurationMs: options.timeoutMs } },
2316
- ...dependencies.inheritedFileDescriptors === undefined ? {} : {
2317
- inheritedFileDescriptors: dependencies.inheritedFileDescriptors
2318
- }
2319
- });
2320
- if (result.outputPath === null) {
2321
- throw new SlopcameraOperationError("INVALID_OPERATION_INPUT", "Vectorization did not publish its required output.");
2322
- }
2323
- return {
2324
- outputPath: result.outputPath,
2325
- receipt: result.receipt
2326
- };
2327
- }
2328
- case "slopcamera.image.generate": {
2329
- const options = input;
2330
- return await generateSlopcameraImageFile({
2331
- ...options,
2332
- ...dependencies.signal === undefined ? {} : { signal: dependencies.signal }
2333
- }, dependencies);
2334
- }
2335
- default:
2336
- throw new SlopcameraOperationError("INVALID_OPERATION", "Unknown Slopcamera operation code.");
2337
- }
2338
- }
2339
- async function executeSlopcameraOperationWithLease(code, value, lease, dependencies = {}) {
2340
- await lease.assertOwned();
2341
- const available = new Map;
2342
- for (const claim of lease.claims) {
2343
- if (typeof claim.resource !== "string" || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(claim.resource) || !Number.isSafeInteger(claim.amount) || claim.amount < 1) {
2344
- throw new SlopcameraOperationError("INVALID_OPERATION", "The active host-resource lease contains invalid claims.");
2345
- }
2346
- const total = (available.get(claim.resource) ?? 0) + claim.amount;
2347
- if (!Number.isSafeInteger(total)) {
2348
- throw new SlopcameraOperationError("INVALID_OPERATION", "The active host-resource lease contains invalid claims.");
2349
- }
2350
- available.set(claim.resource, total);
2351
- }
2352
- const missing = slopcameraOperationHostResourceClaims(code).filter((claim) => (available.get(claim.resource) ?? 0) < claim.amount);
2353
- if (missing.length > 0) {
2354
- throw new SlopcameraOperationError("INVALID_OPERATION", `The active host-resource lease does not cover ${missing.map((claim) => `${claim.resource}:${String(claim.amount)}`).join(", ")}.`);
2355
- }
2356
- return await executeSlopcameraOperationUncoordinated(code, value, operationDependenciesWithLease(dependencies, lease));
2357
- }
2358
- async function executeSlopcameraOperation(code, value, dependencies = {}) {
2359
- const input = parseSlopcameraOperationInput(code, value);
2360
- return await withSlopcameraOperationHostAdmission(code, async (lease) => await executeSlopcameraOperationUncoordinated(code, input, operationDependenciesWithLease(dependencies, lease)), dependencies);
2361
- }
2362
-
2363
- export { builtInIcons, sanitizeIcon, resolveEdge, renderSvg, renderPng, lintDiagram, stackLayoutDefaults, StackLayoutError, resolveStackLayout, resolveDiagramSource, DiagramValidationError, parseDiagramSource, parseDiagramSpec, serializeTldr, slopcameraOperationCodes, SlopcameraOperationError, slopcameraOperationRegistry, parseSlopcameraOperationInput, isSlopcameraOperationCode, slopcameraOperationHostResourceClaims, searchSlopcameraOperations, withSlopcameraOperationHostAdmission, executeSlopcameraOperationWithLease, executeSlopcameraOperation };