quicklook-pptx-renderer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +266 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +175 -0
  5. package/dist/diff/compare.d.ts +17 -0
  6. package/dist/diff/compare.js +71 -0
  7. package/dist/index.d.ts +29 -0
  8. package/dist/index.js +72 -0
  9. package/dist/lint.d.ts +27 -0
  10. package/dist/lint.js +328 -0
  11. package/dist/mapper/bleed-map.d.ts +6 -0
  12. package/dist/mapper/bleed-map.js +1 -0
  13. package/dist/mapper/constants.d.ts +2 -0
  14. package/dist/mapper/constants.js +4 -0
  15. package/dist/mapper/drawable-mapper.d.ts +16 -0
  16. package/dist/mapper/drawable-mapper.js +1464 -0
  17. package/dist/mapper/html-generator.d.ts +13 -0
  18. package/dist/mapper/html-generator.js +539 -0
  19. package/dist/mapper/image-mapper.d.ts +14 -0
  20. package/dist/mapper/image-mapper.js +70 -0
  21. package/dist/mapper/nano-malloc.d.ts +130 -0
  22. package/dist/mapper/nano-malloc.js +197 -0
  23. package/dist/mapper/ql-bleed.d.ts +35 -0
  24. package/dist/mapper/ql-bleed.js +254 -0
  25. package/dist/mapper/shape-mapper.d.ts +41 -0
  26. package/dist/mapper/shape-mapper.js +2384 -0
  27. package/dist/mapper/slide-mapper.d.ts +4 -0
  28. package/dist/mapper/slide-mapper.js +112 -0
  29. package/dist/mapper/style-builder.d.ts +12 -0
  30. package/dist/mapper/style-builder.js +30 -0
  31. package/dist/mapper/text-mapper.d.ts +14 -0
  32. package/dist/mapper/text-mapper.js +302 -0
  33. package/dist/model/enums.d.ts +25 -0
  34. package/dist/model/enums.js +2 -0
  35. package/dist/model/types.d.ts +482 -0
  36. package/dist/model/types.js +7 -0
  37. package/dist/package/content-types.d.ts +1 -0
  38. package/dist/package/content-types.js +4 -0
  39. package/dist/package/package.d.ts +10 -0
  40. package/dist/package/package.js +52 -0
  41. package/dist/package/relationships.d.ts +6 -0
  42. package/dist/package/relationships.js +25 -0
  43. package/dist/package/zip.d.ts +6 -0
  44. package/dist/package/zip.js +17 -0
  45. package/dist/reader/color.d.ts +3 -0
  46. package/dist/reader/color.js +79 -0
  47. package/dist/reader/drawing.d.ts +17 -0
  48. package/dist/reader/drawing.js +403 -0
  49. package/dist/reader/effects.d.ts +2 -0
  50. package/dist/reader/effects.js +83 -0
  51. package/dist/reader/fill.d.ts +2 -0
  52. package/dist/reader/fill.js +94 -0
  53. package/dist/reader/presentation.d.ts +5 -0
  54. package/dist/reader/presentation.js +127 -0
  55. package/dist/reader/slide-layout.d.ts +2 -0
  56. package/dist/reader/slide-layout.js +28 -0
  57. package/dist/reader/slide-master.d.ts +4 -0
  58. package/dist/reader/slide-master.js +49 -0
  59. package/dist/reader/slide.d.ts +2 -0
  60. package/dist/reader/slide.js +26 -0
  61. package/dist/reader/text-list-style.d.ts +2 -0
  62. package/dist/reader/text-list-style.js +9 -0
  63. package/dist/reader/text.d.ts +5 -0
  64. package/dist/reader/text.js +295 -0
  65. package/dist/reader/theme.d.ts +2 -0
  66. package/dist/reader/theme.js +109 -0
  67. package/dist/reader/transform.d.ts +2 -0
  68. package/dist/reader/transform.js +21 -0
  69. package/dist/render/image-renderer.d.ts +3 -0
  70. package/dist/render/image-renderer.js +33 -0
  71. package/dist/render/renderer.d.ts +9 -0
  72. package/dist/render/renderer.js +178 -0
  73. package/dist/render/shape-renderer.d.ts +3 -0
  74. package/dist/render/shape-renderer.js +175 -0
  75. package/dist/render/text-renderer.d.ts +3 -0
  76. package/dist/render/text-renderer.js +152 -0
  77. package/dist/resolve/color-resolver.d.ts +18 -0
  78. package/dist/resolve/color-resolver.js +321 -0
  79. package/dist/resolve/font-map.d.ts +2 -0
  80. package/dist/resolve/font-map.js +66 -0
  81. package/dist/resolve/inheritance.d.ts +5 -0
  82. package/dist/resolve/inheritance.js +106 -0
  83. package/package.json +74 -0
@@ -0,0 +1,2384 @@
1
+ import { resolveColor } from "../resolve/color-resolver.js";
2
+ import { emuToPx } from "./constants.js";
3
+ import { mapTextBody } from "./text-mapper.js";
4
+ import { nextAttachmentIndex } from "./image-mapper.js";
5
+ // Fixed padding OfficeImport adds around PDF shape renders (20px each side)
6
+ export const PDF_PADDING = 20;
7
+ // Geometry presets supported by CMCanonicalShapeBuilder (from runtime dump).
8
+ // Unsupported presets (cloud, heart, etc.) are silently skipped by OfficeImport.
9
+ // All geometry presets that CMCanonicalShapeBuilder supports.
10
+ // OfficeImport renders ALL of these as PDF (except rect without rotation which is CSS).
11
+ // Unsupported presets not in this set are silently skipped.
12
+ // Exactly 60 presets verified via runtime probing of CMCanonicalShapeBuilder.copyShapeWithTransform:
13
+ // Shapes NOT in this set are silently skipped by OfficeImport (nil CGPathRef → no <img> tag).
14
+ export const SUPPORTED_GEOMETRIES = new Set([
15
+ // Basic shapes (20)
16
+ "rect", "roundRect", "ellipse", "diamond", "triangle", "rtTriangle",
17
+ "parallelogram", "trapezoid", "hexagon", "octagon", "plus", "pentagon",
18
+ "chevron", "homePlate", "cube", "can",
19
+ // Stars (7)
20
+ "star4", "star5", "star6", "star7", "star8",
21
+ "star10", "star12", "star16", "star24", "star32",
22
+ // Callouts (4) — only wedge callouts + borderCallout1; all others → nil
23
+ "wedgeRectCallout", "wedgeRoundRectCallout", "wedgeEllipseCallout",
24
+ "borderCallout1", "cloudCallout",
25
+ // Block arrows (15) — no bent/curved/striped/notched/uturn
26
+ "rightArrow", "leftArrow", "upArrow", "downArrow",
27
+ "leftRightArrow", "upDownArrow", "quadArrow", "leftRightUpArrow",
28
+ "circularArrow",
29
+ "rightArrowCallout", "leftArrowCallout", "upArrowCallout", "downArrowCallout",
30
+ "leftRightArrowCallout", "upDownArrowCallout", "quadArrowCallout",
31
+ // Flowchart (2) — only these two; all others → nil
32
+ "flowChartAlternateProcess", "flowChartDecision",
33
+ // Connectors (10)
34
+ "line", "straightConnector1",
35
+ "bentConnector2", "bentConnector3", "bentConnector4", "bentConnector5",
36
+ "curvedConnector2", "curvedConnector3", "curvedConnector4", "curvedConnector5",
37
+ // Scrolls (1) — only verticalScroll; horizontalScroll → nil
38
+ "verticalScroll",
39
+ ]);
40
+ export function mapShape(shape, styles, attachments, ctx) {
41
+ const b = shape.bounds;
42
+ if (!b)
43
+ return "";
44
+ const x = emuToPx(b.x);
45
+ const y = emuToPx(b.y);
46
+ const w = emuToPx(b.cx);
47
+ const h = emuToPx(b.cy);
48
+ const fill = shape.fill;
49
+ const geom = shape.geometry?.preset ?? "rect";
50
+ // CSS path: only rect with no rotation. Everything else → PDF.
51
+ // (Disassembly showed roundRect/ellipse as CSS-eligible, but empirical tests confirm
52
+ // OfficeImport always renders them as PDF in practice — the CSS check must fail internally.)
53
+ const isCssPath = geom === "rect" && !(b.rot && b.rot !== 0);
54
+ const hasText = shape.textBody && shape.textBody.paragraphs.some((p) => p.runs.some((r) => r.type !== "br" && "text" in r && String(r.text ?? "").trim() !== ""));
55
+ // PDF path: non-rect geometry, or rect with rotation
56
+ if (!isCssPath) {
57
+ if (!SUPPORTED_GEOMETRIES.has(geom))
58
+ return "";
59
+ const pdfHtml = renderAsPdf(shape, b, attachments, ctx);
60
+ if (hasText && shape.textBody) {
61
+ const tb = shapeTextBox(b, geom, shape.geometry?.adjustValues);
62
+ // OfficeImport swaps text overlay w/h for rotations where the shape's
63
+ // dominant axis flips (45°-135° mod 180), recomputing position from center.
64
+ const rotDeg = ((b.rot ?? 0) / 60000) % 180;
65
+ if (rotDeg >= 45 && rotDeg <= 135) {
66
+ const cxEmu = tb.x + tb.cx / 2;
67
+ const cyEmu = tb.y + tb.cy / 2;
68
+ const newCx = tb.cy; // swap
69
+ const newCy = tb.cx;
70
+ tb.x = cxEmu - newCx / 2;
71
+ tb.y = cyEmu - newCy / 2;
72
+ tb.cx = newCx;
73
+ tb.cy = newCy;
74
+ }
75
+ const textCtx = { ...ctx, placeholderType: shape.placeholder?.type, fontRefColor: shape.shapeStyle?.fontRef?.color };
76
+ const textHtml = mapTextBody(shape.textBody, tb, styles, textCtx);
77
+ const tx = emuToPx(tb.x), ty = emuToPx(tb.y), tw = emuToPx(tb.cx), th = emuToPx(tb.cy);
78
+ return pdfHtml + `<div style="position:absolute; top:${ty}; left:${tx}; width:${tw}; height:${th};">${textHtml}</div>`;
79
+ }
80
+ return pdfHtml;
81
+ }
82
+ // Build common style parts for CSS path (rect, roundRect, ellipse without effects)
83
+ const styleParts = [`position:absolute`, `top:${y}`, `left:${x}`, `width:${w}`, `height:${h}`];
84
+ const fillCss = fillToCss(fill, ctx, attachments);
85
+ if (fillCss)
86
+ styleParts.push(fillCss);
87
+ // OfficeImport CSS path: renders stroke as thin solid border with actual stroke color.
88
+ // QL uses the keyword "black" for #000000 in border-color.
89
+ // Note: QL concatenates border props without space separators
90
+ // Check explicit stroke first, then fall back to theme lnRef.
91
+ // Only use theme if shape has NO explicit stroke at all (undefined).
92
+ // An explicit empty <a:ln/> (stroke set but no fill) overrides theme — no border.
93
+ if (shape.stroke?.fill?.type === "solid") {
94
+ const c = rgbaToColor(resolveColor(shape.stroke.fill.color, ctx.colorMap, ctx.colorScheme));
95
+ const borderColor = c === "#000000" ? "black" : c;
96
+ styleParts.push(`border-style:solid;border-width:thin;border-color:${borderColor}`);
97
+ }
98
+ else if (!shape.stroke && resolveThemeStroke(shape, ctx)) {
99
+ // OfficeImport CSS path always renders theme-resolved borders as thin/solid/black
100
+ styleParts.push(`border-style:solid;border-width:thin;border-color:black`);
101
+ }
102
+ const style = styleParts.join("; ") + ";";
103
+ // Text shape
104
+ if (hasText && shape.textBody) {
105
+ const textCtx = { ...ctx, placeholderType: shape.placeholder?.type, fontRefColor: shape.shapeStyle?.fontRef?.color };
106
+ const textHtml = mapTextBody(shape.textBody, b, styles, textCtx);
107
+ return `<div style="${style}">${textHtml}</div>`;
108
+ }
109
+ // Simple filled rect (no text)
110
+ if (fill && fill.type !== "noFill" && fillCss) {
111
+ const marginCss = "margin-top:3px; margin-left:7px; margin-bottom:3px; margin-right:7px;";
112
+ const innerClass = styles.addClass(marginCss);
113
+ return `<div style="${style}"><div class="${innerClass}"></div></div>`;
114
+ }
115
+ // noFill with no text: emit positioned div with margin inner div (matches OfficeImport)
116
+ const marginCss = "margin-top:3px; margin-left:7px; margin-bottom:3px; margin-right:7px;";
117
+ const innerClass = styles.addClass(marginCss);
118
+ return `<div style="${style}"><div class="${innerClass}"></div></div>`;
119
+ }
120
+ function renderAsPdf(shape, bounds, attachments, ctx) {
121
+ const rot = bounds.rot;
122
+ let imgX, imgY, imgW, imgH;
123
+ if (rot && rot !== 0) {
124
+ // Compute AABB in EMU space, then convert to px — matches OfficeImport precision
125
+ const rad = (rot / 60000) * (Math.PI / 180);
126
+ const cosA = Math.abs(Math.cos(rad));
127
+ const sinA = Math.abs(Math.sin(rad));
128
+ const aabbW_emu = bounds.cx * cosA + bounds.cy * sinA;
129
+ const aabbH_emu = bounds.cx * sinA + bounds.cy * cosA;
130
+ const cx_emu = bounds.x + bounds.cx / 2;
131
+ const cy_emu = bounds.y + bounds.cy / 2;
132
+ const aabbX_emu = cx_emu - aabbW_emu / 2;
133
+ const aabbY_emu = cy_emu - aabbH_emu / 2;
134
+ // OfficeImport uses round for position, trunc for dimensions
135
+ imgX = Math.round(aabbX_emu / 12700) - PDF_PADDING;
136
+ imgY = Math.round(aabbY_emu / 12700) - PDF_PADDING;
137
+ imgW = emuToPx(aabbW_emu) + PDF_PADDING * 2;
138
+ imgH = emuToPx(aabbH_emu) + PDF_PADDING * 2;
139
+ }
140
+ else {
141
+ // Compute tight AABB for geometry presets (stars don't fill full bounds)
142
+ const preset = shape.geometry?.preset ?? "rect";
143
+ const aabb = geometryAABB(preset);
144
+ const pathXEmu = bounds.x + aabb.minX * bounds.cx;
145
+ const pathYEmu = bounds.y + aabb.minY * bounds.cy;
146
+ const pathWEmu = (aabb.maxX - aabb.minX) * bounds.cx;
147
+ const pathHEmu = (aabb.maxY - aabb.minY) * bounds.cy;
148
+ // OfficeImport computes img bounds using exact EMU→pt values:
149
+ // imgPos = trunc(pos_pt - pad), imgDim = trunc(dim_pt + 2*pad)
150
+ const pathX_pt = pathXEmu / 12700;
151
+ const pathY_pt = pathYEmu / 12700;
152
+ const pathW_pt = pathWEmu / 12700;
153
+ const pathH_pt = pathHEmu / 12700;
154
+ imgX = Math.trunc(pathX_pt - PDF_PADDING);
155
+ imgY = Math.trunc(pathY_pt - PDF_PADDING);
156
+ imgW = Math.trunc(pathW_pt + PDF_PADDING * 2);
157
+ imgH = Math.trunc(pathH_pt + PDF_PADDING * 2);
158
+ }
159
+ // Generate minimal PDF with the shape rendered
160
+ const fillColor = fillToColor(shape.fill, ctx) ?? "#000000";
161
+ const adjustValues = shape.geometry?.adjustValues;
162
+ // OfficeImport's CMDrawingContext always uses fill+stroke (B operator).
163
+ // Explicit <a:ln><a:solidFill> → declared color; empty <a:ln/> → thin black default.
164
+ const strokeInfo = resolveStrokeForPdf(shape, ctx);
165
+ const shapeRot = bounds.rot ?? 0;
166
+ // OfficeImport uses exact EMU→pt float dimensions for geometry, not truncated integers
167
+ const w_pt = bounds.cx / 12700;
168
+ const h_pt = bounds.cy / 12700;
169
+ const pdf = generateShapePdf(w_pt, h_pt, shape.geometry?.preset ?? "rect", fillColor, adjustValues, strokeInfo, shapeRot !== 0 ? shapeRot : undefined);
170
+ const idx = nextAttachmentIndex();
171
+ const name = `Attachment${idx}.pdf`;
172
+ attachments.set(name, pdf);
173
+ return `<img src="${name}" style="position:absolute; top:${imgY}; left:${imgX}; width:${imgW}; height:${imgH};">`;
174
+ }
175
+ /** Generate a minimal PDF buffer containing the shape. */
176
+ function generateShapePdf(w, h, preset, fillColor, adjustValues, stroke, rotation) {
177
+ const pad = PDF_PADDING;
178
+ const rad = rotation ? (rotation / 60000) * (Math.PI / 180) : 0;
179
+ // OfficeImport uses exact EMU→pt float values for MediaBox dimensions.
180
+ // The img tag uses trunc() of these values, causing a tiny mismatch that's invisible.
181
+ let pageW, pageH;
182
+ if (rad !== 0) {
183
+ const cosA = Math.abs(Math.cos(rad));
184
+ const sinA = Math.abs(Math.sin(rad));
185
+ pageW = w * cosA + h * sinA + pad * 2;
186
+ pageH = w * sinA + h * cosA + pad * 2;
187
+ }
188
+ else {
189
+ pageW = w + pad * 2;
190
+ pageH = h + pad * 2;
191
+ }
192
+ // Draw shape at origin in its own coordinate system (pad, w, h)
193
+ const shapeH = h + pad * 2;
194
+ const drawCmd = getShapeDrawCmd(pad, w, h, shapeH, preset, adjustValues);
195
+ const fHex = fillColor.replace("#", "");
196
+ const fr = parseInt(fHex.slice(0, 2), 16) / 255;
197
+ const fg = parseInt(fHex.slice(2, 4), 16) / 255;
198
+ const fb = parseInt(fHex.slice(4, 6), 16) / 255;
199
+ // OfficeImport uses B (fill+stroke) with a graphics state that makes the default stroke
200
+ // sub-pixel invisible. We can't replicate the exact graphics state, so:
201
+ // - Explicit <a:ln><a:solidFill>: use B with declared color/width
202
+ // - No explicit stroke: use f (fill-only) which matches the visual output
203
+ let colorSetup = `${fr.toFixed(3)} ${fg.toFixed(3)} ${fb.toFixed(3)} rg\n`;
204
+ let finalDrawCmd = drawCmd;
205
+ if (stroke) {
206
+ const sr = parseInt(stroke.color.replace("#", "").slice(0, 2), 16) / 255;
207
+ const sg = parseInt(stroke.color.replace("#", "").slice(2, 4), 16) / 255;
208
+ const sb = parseInt(stroke.color.replace("#", "").slice(4, 6), 16) / 255;
209
+ colorSetup = `${sr.toFixed(3)} ${sg.toFixed(3)} ${sb.toFixed(3)} RG\n${stroke.width.toFixed(1)} w\n` + colorSetup;
210
+ finalDrawCmd = drawCmd.replace(/\bf\b/g, "B");
211
+ }
212
+ let stream;
213
+ if (rad !== 0) {
214
+ // Rotation CTM: PPTX rotation is CW in screen coords (Y-down).
215
+ // In PDF coords (Y-up), CW = negative angle, so we negate rad.
216
+ // Combined: translate(page_center) · rotate(-rad) · translate(-shape_center)
217
+ const cx = pageW / 2;
218
+ const cy = pageH / 2;
219
+ const cosR = Math.cos(-rad);
220
+ const sinR = Math.sin(-rad);
221
+ const ox = -(w + pad * 2) / 2;
222
+ const oy = -(h + pad * 2) / 2;
223
+ // PDF CTM [a b c d e f]: x' = ax + cy + e, y' = bx + dy + f
224
+ const a = cosR, b = sinR, c = -sinR, d = cosR;
225
+ const e = cx + cosR * ox - sinR * oy;
226
+ const f = cy + sinR * ox + cosR * oy;
227
+ stream = `q\n${a.toFixed(6)} ${b.toFixed(6)} ${c.toFixed(6)} ${d.toFixed(6)} ${e.toFixed(3)} ${f.toFixed(3)} cm\n${colorSetup}${finalDrawCmd}\nQ`;
228
+ }
229
+ else {
230
+ stream = `${colorSetup}${finalDrawCmd}`;
231
+ }
232
+ return buildPdf(pageW, pageH, stream);
233
+ }
234
+ /**
235
+ * Generate PDF path drawing commands for a shape.
236
+ * pad = uniform offset from (0,0) where the shape starts in screen coords.
237
+ * totalH = total height of the coordinate space (needed for Y-flipping).
238
+ * For standalone shapes: pad=PDF_PADDING, totalH=h+2*pad.
239
+ * For group children: pad=0, totalH=h (shape draws at origin, caller uses cm to translate).
240
+ */
241
+ export function getShapeDrawCmd(pad, w, h, totalH, preset, adjustValues) {
242
+ // Helpers for polygon paths in PDF coordinates (Y flipped: pdfY = totalH - screenY)
243
+ const py = (sy) => totalH - sy; // screen Y → PDF Y
244
+ const polyPath = (pts) => {
245
+ const [first, ...rest] = pts;
246
+ return [`${first[0]} ${py(first[1])} m`, ...rest.map(([x, y]) => `${x} ${py(y)} l`), "f"].join("\n");
247
+ };
248
+ // Like polyPath but returns array of line commands without closing "f" — for composing multi-subpath shapes
249
+ const polyPathLines = (pts) => {
250
+ const [first, ...rest] = pts;
251
+ return [`${first[0]} ${py(first[1])} m`, ...rest.map(([x, y]) => `${x} ${py(y)} l`)];
252
+ };
253
+ // Ellipse drawing commands (reusable for shapes that are circles/ellipses)
254
+ const ellipseCmd = (_pad, _w, _h, _totalH) => {
255
+ const cx = _pad + _w / 2, cy = _pad + _h / 2;
256
+ const rx = _w / 2, ry = _h / 2;
257
+ const k = 0.5522847498;
258
+ return [
259
+ `${cx + rx} ${_totalH - cy} m`,
260
+ `${cx + rx} ${_totalH - (cy - ry * k)} ${cx + rx * k} ${_totalH - (cy - ry)} ${cx} ${_totalH - (cy - ry)} c`,
261
+ `${cx - rx * k} ${_totalH - (cy - ry)} ${cx - rx} ${_totalH - (cy - ry * k)} ${cx - rx} ${_totalH - cy} c`,
262
+ `${cx - rx} ${_totalH - (cy + ry * k)} ${cx - rx * k} ${_totalH - (cy + ry)} ${cx} ${_totalH - (cy + ry)} c`,
263
+ `${cx + rx * k} ${_totalH - (cy + ry)} ${cx + rx} ${_totalH - (cy + ry * k)} ${cx + rx} ${_totalH - cy} c`,
264
+ "f",
265
+ ].join("\n");
266
+ };
267
+ // Build drawing commands based on geometry
268
+ // OfficeImport uses fill+stroke (B operator) with a coordinate transform
269
+ let drawCmd;
270
+ if (preset === "ellipse") {
271
+ // Approximate ellipse with bezier curves
272
+ const cx = pad + w / 2;
273
+ const cy = pad + h / 2;
274
+ const rx = w / 2;
275
+ const ry = h / 2;
276
+ const k = 0.5522847498; // kappa for circle approximation
277
+ drawCmd = [
278
+ `${cx + rx} ${totalH - cy} m`,
279
+ `${cx + rx} ${totalH - (cy - ry * k)} ${cx + rx * k} ${totalH - (cy - ry)} ${cx} ${totalH - (cy - ry)} c`,
280
+ `${cx - rx * k} ${totalH - (cy - ry)} ${cx - rx} ${totalH - (cy - ry * k)} ${cx - rx} ${totalH - cy} c`,
281
+ `${cx - rx} ${totalH - (cy + ry * k)} ${cx - rx * k} ${totalH - (cy + ry)} ${cx} ${totalH - (cy + ry)} c`,
282
+ `${cx + rx * k} ${totalH - (cy + ry)} ${cx + rx} ${totalH - (cy + ry * k)} ${cx + rx} ${totalH - cy} c`,
283
+ "f",
284
+ ].join("\n");
285
+ }
286
+ else if (preset === "roundRect") {
287
+ // Corner radius: use adjustValue 'adj' (default 16667 = 16.667% of min dimension)
288
+ const adjVal = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 16667;
289
+ const radius = Math.min(w, h) * adjVal / 100000;
290
+ const x0 = pad, y0 = totalH - pad - h;
291
+ const x1 = pad + w, y1 = totalH - pad;
292
+ const rr = radius;
293
+ drawCmd = [
294
+ `${x0 + rr} ${y0} m`,
295
+ `${x1 - rr} ${y0} l`,
296
+ `${x1} ${y0} ${x1} ${y0 + rr} v`,
297
+ `${x1} ${y1 - rr} l`,
298
+ `${x1} ${y1} ${x1 - rr} ${y1} v`,
299
+ `${x0 + rr} ${y1} l`,
300
+ `${x0} ${y1} ${x0} ${y1 - rr} v`,
301
+ `${x0} ${y0 + rr} l`,
302
+ `${x0} ${y0} ${x0 + rr} ${y0} v`,
303
+ "f",
304
+ ].join("\n");
305
+ }
306
+ else if (preset === "diamond") {
307
+ const cx = pad + w / 2, cy = pad + h / 2;
308
+ drawCmd = polyPath([[cx, pad], [pad + w, cy], [cx, pad + h], [pad, cy]]);
309
+ }
310
+ else if (preset === "triangle") {
311
+ drawCmd = polyPath([[pad + w / 2, pad], [pad + w, pad + h], [pad, pad + h]]);
312
+ }
313
+ else if (preset === "rtTriangle") {
314
+ drawCmd = polyPath([[pad, pad], [pad + w, pad + h], [pad, pad + h]]);
315
+ }
316
+ else if (preset === "pentagon") {
317
+ // Regular pentagon: top vertex at center-top, others at ±54° and ±126° from top
318
+ const cx = pad + w / 2, top = pad, bot = pad + h;
319
+ const mid = pad + h * 0.382; // ~38.2% down
320
+ const pts = [
321
+ [cx, top],
322
+ [pad + w, mid],
323
+ [pad + w * 0.809, bot],
324
+ [pad + w * 0.191, bot],
325
+ [pad, mid],
326
+ ];
327
+ drawCmd = polyPath(pts);
328
+ }
329
+ else if (preset === "hexagon") {
330
+ const adjVal = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 25000;
331
+ const inset = w * adjVal / 100000;
332
+ const cy = pad + h / 2;
333
+ drawCmd = polyPath([
334
+ [pad + inset, pad],
335
+ [pad + w - inset, pad],
336
+ [pad + w, cy],
337
+ [pad + w - inset, pad + h],
338
+ [pad + inset, pad + h],
339
+ [pad, cy],
340
+ ]);
341
+ }
342
+ else if (preset === "octagon") {
343
+ // OfficeImport's CMCanonicalShapeBuilder: uses ss (min dim) for equal-angle cuts
344
+ // Default adj matches 25000 (ss/4), not OOXML's 29289 (1-1/√2)
345
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 25000;
346
+ const ss = Math.min(w, h);
347
+ const inset = ss * adj / 100000;
348
+ drawCmd = polyPath([
349
+ [pad + inset, pad],
350
+ [pad + w - inset, pad],
351
+ [pad + w, pad + inset],
352
+ [pad + w, pad + h - inset],
353
+ [pad + w - inset, pad + h],
354
+ [pad + inset, pad + h],
355
+ [pad, pad + h - inset],
356
+ [pad, pad + inset],
357
+ ]);
358
+ }
359
+ else if (preset === "parallelogram") {
360
+ // OfficeImport's CMCanonicalShapeBuilder: shift = w * adj / 200000 (half of OOXML spec)
361
+ // Top edge shifted right: top-left at (shift, 0), bottom-left at (0, h)
362
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 25000;
363
+ const shift = w * adj / 200000;
364
+ drawCmd = polyPath([
365
+ [pad + shift, pad],
366
+ [pad + w, pad],
367
+ [pad + w - shift, pad + h],
368
+ [pad, pad + h],
369
+ ]);
370
+ }
371
+ else if (preset === "trapezoid") {
372
+ // OOXML trapezoid: wide at bottom, narrow at top (inset applied to top edge)
373
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 25000;
374
+ const inset = w * adj / 100000;
375
+ drawCmd = polyPath([
376
+ [pad + inset, pad],
377
+ [pad + w - inset, pad],
378
+ [pad + w, pad + h],
379
+ [pad, pad + h],
380
+ ]);
381
+ }
382
+ else if (preset === "plus") {
383
+ // OfficeImport's CMCanonicalShapeBuilder: uses ss (min dim) for both axes
384
+ // to create square-armed cross regardless of aspect ratio
385
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 25000;
386
+ const ss = Math.min(w, h);
387
+ const inset = ss * adj / 100000;
388
+ const cx1 = pad + inset, cx2 = pad + w - inset;
389
+ const cy1 = pad + inset, cy2 = pad + h - inset;
390
+ drawCmd = polyPath([
391
+ [cx1, pad], [cx2, pad],
392
+ [cx2, cy1], [pad + w, cy1],
393
+ [pad + w, cy2], [cx2, cy2],
394
+ [cx2, pad + h], [cx1, pad + h],
395
+ [cx1, cy2], [pad, cy2],
396
+ [pad, cy1], [cx1, cy1],
397
+ ]);
398
+ }
399
+ else if (preset === "star4") {
400
+ const cx = pad + w / 2, cy = pad + h / 2;
401
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 12500;
402
+ const ix = w * adj / 100000, iy = h * adj / 100000;
403
+ drawCmd = polyPath([
404
+ [cx, pad],
405
+ [cx + ix, cy - iy], [pad + w, cy],
406
+ [cx + ix, cy + iy], [cx, pad + h],
407
+ [cx - ix, cy + iy], [pad, cy],
408
+ [cx - ix, cy - iy],
409
+ ]);
410
+ }
411
+ else if (preset === "star5") {
412
+ const cx = pad + w / 2, top = pad, bot = pad + h;
413
+ const outerR_x = w / 2, outerR_y = h / 2;
414
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 19098;
415
+ const innerR_x = outerR_x * adj / 50000;
416
+ const innerR_y = outerR_y * adj / 50000;
417
+ const pts = [];
418
+ for (let i = 0; i < 5; i++) {
419
+ const outerA = (Math.PI * (-0.5 + i * 2 / 5));
420
+ pts.push([cx + Math.cos(outerA) * outerR_x, pad + outerR_y + Math.sin(outerA) * outerR_y]);
421
+ const innerA = (Math.PI * (-0.5 + (i * 2 + 1) / 5));
422
+ pts.push([cx + Math.cos(innerA) * innerR_x, pad + outerR_y + Math.sin(innerA) * innerR_y]);
423
+ }
424
+ drawCmd = polyPath(pts);
425
+ }
426
+ else if (preset === "star6") {
427
+ const cx = pad + w / 2, cy = pad + h / 2;
428
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 28868;
429
+ const outerR_x = w / 2, outerR_y = h / 2;
430
+ const innerR_x = outerR_x * adj / 50000, innerR_y = outerR_y * adj / 50000;
431
+ const pts = [];
432
+ for (let i = 0; i < 6; i++) {
433
+ const outerA = Math.PI * (-0.5 + i / 3);
434
+ pts.push([cx + Math.cos(outerA) * outerR_x, cy + Math.sin(outerA) * outerR_y]);
435
+ const innerA = Math.PI * (-0.5 + (i * 2 + 1) / 6);
436
+ pts.push([cx + Math.cos(innerA) * innerR_x, cy + Math.sin(innerA) * innerR_y]);
437
+ }
438
+ drawCmd = polyPath(pts);
439
+ }
440
+ else if (preset === "star8") {
441
+ const cx = pad + w / 2, cy = pad + h / 2;
442
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 37500;
443
+ const outerR_x = w / 2, outerR_y = h / 2;
444
+ const innerR_x = outerR_x * adj / 50000, innerR_y = outerR_y * adj / 50000;
445
+ const pts = [];
446
+ for (let i = 0; i < 8; i++) {
447
+ const outerA = Math.PI * (-0.5 + i / 4);
448
+ pts.push([cx + Math.cos(outerA) * outerR_x, cy + Math.sin(outerA) * outerR_y]);
449
+ const innerA = Math.PI * (-0.5 + (i * 2 + 1) / 8);
450
+ pts.push([cx + Math.cos(innerA) * innerR_x, cy + Math.sin(innerA) * innerR_y]);
451
+ }
452
+ drawCmd = polyPath(pts);
453
+ }
454
+ else if (preset === "chevron") {
455
+ // OfficeImport's CMCanonicalShapeBuilder: notch depth = w * adj / 400000
456
+ // (quartered vs OOXML spec: both arrow point and notch use this value)
457
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 50000;
458
+ const notch = w * adj / 400000;
459
+ drawCmd = polyPath([
460
+ [pad, pad],
461
+ [pad + w - notch, pad],
462
+ [pad + w, pad + h / 2],
463
+ [pad + w - notch, pad + h],
464
+ [pad, pad + h],
465
+ [pad + notch, pad + h / 2],
466
+ ]);
467
+ }
468
+ else if (preset === "homePlate") {
469
+ // OfficeImport's CMCanonicalShapeBuilder: arrow start at w*adj/200000 from left
470
+ // (halved vs OOXML spec), arrow tip at right edge
471
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 50000;
472
+ const flat = w * adj / 200000;
473
+ drawCmd = polyPath([
474
+ [pad, pad],
475
+ [pad + flat, pad],
476
+ [pad + w, pad + h / 2],
477
+ [pad + flat, pad + h],
478
+ [pad, pad + h],
479
+ ]);
480
+ }
481
+ else if (preset === "rightArrow") {
482
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 50000;
483
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
484
+ const stemH = h * adj1 / 100000;
485
+ const headW = w * adj2 / 100000;
486
+ const stemTop = pad + (h - stemH) / 2, stemBot = pad + (h + stemH) / 2;
487
+ drawCmd = polyPath([
488
+ [pad, stemTop],
489
+ [pad + w - headW, stemTop],
490
+ [pad + w - headW, pad],
491
+ [pad + w, pad + h / 2],
492
+ [pad + w - headW, pad + h],
493
+ [pad + w - headW, stemBot],
494
+ [pad, stemBot],
495
+ ]);
496
+ }
497
+ else if (preset === "leftArrow") {
498
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 50000;
499
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
500
+ const stemH = h * adj1 / 100000;
501
+ const headW = w * adj2 / 100000;
502
+ const stemTop = pad + (h - stemH) / 2, stemBot = pad + (h + stemH) / 2;
503
+ drawCmd = polyPath([
504
+ [pad + headW, stemTop],
505
+ [pad + w, stemTop],
506
+ [pad + w, stemBot],
507
+ [pad + headW, stemBot],
508
+ [pad + headW, pad + h],
509
+ [pad, pad + h / 2],
510
+ [pad + headW, pad],
511
+ ]);
512
+ }
513
+ else if (preset === "upArrow") {
514
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 50000;
515
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
516
+ const stemW = w * adj1 / 100000;
517
+ const headH = h * adj2 / 100000;
518
+ const stemL = pad + (w - stemW) / 2, stemR = pad + (w + stemW) / 2;
519
+ drawCmd = polyPath([
520
+ [pad + w / 2, pad],
521
+ [pad + w, pad + headH],
522
+ [stemR, pad + headH],
523
+ [stemR, pad + h],
524
+ [stemL, pad + h],
525
+ [stemL, pad + headH],
526
+ [pad, pad + headH],
527
+ ]);
528
+ }
529
+ else if (preset === "downArrow") {
530
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 50000;
531
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
532
+ const stemW = w * adj1 / 100000;
533
+ const headH = h * adj2 / 100000;
534
+ const stemL = pad + (w - stemW) / 2, stemR = pad + (w + stemW) / 2;
535
+ drawCmd = polyPath([
536
+ [stemL, pad],
537
+ [stemR, pad],
538
+ [stemR, pad + h - headH],
539
+ [pad + w, pad + h - headH],
540
+ [pad + w / 2, pad + h],
541
+ [pad, pad + h - headH],
542
+ [stemL, pad + h - headH],
543
+ ]);
544
+ }
545
+ else if (preset === "can") {
546
+ // Cylinder: rectangle body with elliptical top and bottom
547
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 25000;
548
+ const capH = h * adj / 200000;
549
+ const k = 0.5522847498;
550
+ const cx = pad + w / 2, rx = w / 2;
551
+ // Body rectangle
552
+ const bodyTop = pad + capH, bodyBot = pad + h - capH;
553
+ // Bottom ellipse, top ellipse arcs
554
+ drawCmd = [
555
+ // Start at left of body top
556
+ `${pad} ${py(bodyTop)} m`,
557
+ `${pad} ${py(bodyBot)} l`,
558
+ // Bottom ellipse (half)
559
+ `${pad} ${py(bodyBot + capH * k)} ${cx - rx * k} ${py(pad + h)} ${cx} ${py(pad + h)} c`,
560
+ `${cx + rx * k} ${py(pad + h)} ${pad + w} ${py(bodyBot + capH * k)} ${pad + w} ${py(bodyBot)} c`,
561
+ `${pad + w} ${py(bodyTop)} l`,
562
+ // Top ellipse (half, going back)
563
+ `${pad + w} ${py(bodyTop - capH * k)} ${cx + rx * k} ${py(pad)} ${cx} ${py(pad)} c`,
564
+ `${cx - rx * k} ${py(pad)} ${pad} ${py(bodyTop - capH * k)} ${pad} ${py(bodyTop)} c`,
565
+ "f",
566
+ ].join("\n");
567
+ }
568
+ else if (preset === "cube") {
569
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 25000;
570
+ const d = Math.min(w, h) * adj / 100000;
571
+ // Front face + top face + right face as single path
572
+ drawCmd = polyPath([
573
+ [pad, pad + d],
574
+ [pad + d, pad],
575
+ [pad + w, pad],
576
+ [pad + w, pad + h - d],
577
+ [pad + w - d, pad + h],
578
+ [pad, pad + h],
579
+ ]);
580
+ }
581
+ else if (preset === "wedgeRectCallout") {
582
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : -20833;
583
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 62500;
584
+ const tx = pad + w / 2 + w * adj1 / 100000;
585
+ const ty = pad + h / 2 + h * adj2 / 100000;
586
+ const cx = pad + w / 2;
587
+ drawCmd = polyPath([
588
+ [pad, pad], [cx - w * 0.05, pad], [tx, ty], [cx + w * 0.05, pad],
589
+ [pad + w, pad], [pad + w, pad + h], [pad, pad + h],
590
+ ]);
591
+ }
592
+ else if (preset === "wedgeRoundRectCallout") {
593
+ // Simplified as rect callout (the rounded corners would need bezier curves)
594
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : -20833;
595
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 62500;
596
+ const tx = pad + w / 2 + w * adj1 / 100000;
597
+ const ty = pad + h / 2 + h * adj2 / 100000;
598
+ const cx = pad + w / 2;
599
+ drawCmd = polyPath([
600
+ [pad, pad], [cx - w * 0.05, pad], [tx, ty], [cx + w * 0.05, pad],
601
+ [pad + w, pad], [pad + w, pad + h], [pad, pad + h],
602
+ ]);
603
+ }
604
+ else if (preset === "wedgeEllipseCallout") {
605
+ // Ellipse body with pointer — simplified as ellipse + triangle pointer
606
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : -20833;
607
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 62500;
608
+ const cx = pad + w / 2, cy = pad + h / 2;
609
+ const rx = w / 2, ry = h / 2;
610
+ const k = 0.5522847498;
611
+ // Just render as ellipse (pointer is complex to combine in single path)
612
+ drawCmd = [
613
+ `${cx + rx} ${totalH - cy} m`,
614
+ `${cx + rx} ${totalH - (cy - ry * k)} ${cx + rx * k} ${totalH - (cy - ry)} ${cx} ${totalH - (cy - ry)} c`,
615
+ `${cx - rx * k} ${totalH - (cy - ry)} ${cx - rx} ${totalH - (cy - ry * k)} ${cx - rx} ${totalH - cy} c`,
616
+ `${cx - rx} ${totalH - (cy + ry * k)} ${cx - rx * k} ${totalH - (cy + ry)} ${cx} ${totalH - (cy + ry)} c`,
617
+ `${cx + rx * k} ${totalH - (cy + ry)} ${cx + rx} ${totalH - (cy + ry * k)} ${cx + rx} ${totalH - cy} c`,
618
+ "f",
619
+ ].join("\n");
620
+ }
621
+ else if (preset === "flowChartAlternateProcess") {
622
+ // Rounded rectangle (like roundRect with fixed radius)
623
+ const radius = Math.min(w, h) * 0.16667;
624
+ const x0 = pad, y0 = totalH - pad - h, x1 = pad + w, y1 = totalH - pad;
625
+ drawCmd = [
626
+ `${x0 + radius} ${y0} m`, `${x1 - radius} ${y0} l`,
627
+ `${x1} ${y0} ${x1} ${y0 + radius} v`, `${x1} ${y1 - radius} l`,
628
+ `${x1} ${y1} ${x1 - radius} ${y1} v`, `${x0 + radius} ${y1} l`,
629
+ `${x0} ${y1} ${x0} ${y1 - radius} v`, `${x0} ${y0 + radius} l`,
630
+ `${x0} ${y0} ${x0 + radius} ${y0} v`, "f",
631
+ ].join("\n");
632
+ }
633
+ else if (preset === "flowChartDecision") {
634
+ // Diamond shape
635
+ const cx = pad + w / 2, cy = pad + h / 2;
636
+ drawCmd = polyPath([[cx, pad], [pad + w, cy], [cx, pad + h], [pad, cy]]);
637
+ }
638
+ else if (preset === "flowChartDocument") {
639
+ // Rectangle with wavy bottom
640
+ const waveH = h * 0.1;
641
+ drawCmd = polyPath([
642
+ [pad, pad],
643
+ [pad + w, pad],
644
+ [pad + w, pad + h - waveH],
645
+ [pad + w * 0.75, pad + h],
646
+ [pad + w * 0.5, pad + h - waveH],
647
+ [pad + w * 0.25, pad + h - waveH * 2],
648
+ [pad, pad + h - waveH],
649
+ ]);
650
+ }
651
+ else if (preset === "leftRightArrow") {
652
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 50000;
653
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
654
+ const stemH = h * adj1 / 100000;
655
+ const headW = w * adj2 / 100000;
656
+ const stemTop = pad + (h - stemH) / 2, stemBot = pad + (h + stemH) / 2;
657
+ drawCmd = polyPath([
658
+ [pad + headW, pad],
659
+ [pad, pad + h / 2],
660
+ [pad + headW, pad + h],
661
+ [pad + headW, stemBot],
662
+ [pad + w - headW, stemBot],
663
+ [pad + w - headW, pad + h],
664
+ [pad + w, pad + h / 2],
665
+ [pad + w - headW, pad],
666
+ [pad + w - headW, stemTop],
667
+ [pad + headW, stemTop],
668
+ ]);
669
+ }
670
+ else if (preset === "upDownArrow") {
671
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 50000;
672
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
673
+ const stemW = w * adj1 / 100000;
674
+ const headH = h * adj2 / 100000;
675
+ const stemL = pad + (w - stemW) / 2, stemR = pad + (w + stemW) / 2;
676
+ drawCmd = polyPath([
677
+ [pad + w / 2, pad],
678
+ [pad + w, pad + headH],
679
+ [stemR, pad + headH],
680
+ [stemR, pad + h - headH],
681
+ [pad + w, pad + h - headH],
682
+ [pad + w / 2, pad + h],
683
+ [pad, pad + h - headH],
684
+ [stemL, pad + h - headH],
685
+ [stemL, pad + headH],
686
+ [pad, pad + headH],
687
+ ]);
688
+ }
689
+ else if (preset === "notchedRightArrow") {
690
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 50000;
691
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
692
+ const stemH = h * adj1 / 100000;
693
+ const headW = w * adj2 / 100000;
694
+ const stemTop = pad + (h - stemH) / 2, stemBot = pad + (h + stemH) / 2;
695
+ const notchX = pad + headW; // notch depth matches head width
696
+ drawCmd = polyPath([
697
+ [pad, stemTop],
698
+ [pad + w - headW, stemTop],
699
+ [pad + w - headW, pad],
700
+ [pad + w, pad + h / 2],
701
+ [pad + w - headW, pad + h],
702
+ [pad + w - headW, stemBot],
703
+ [pad, stemBot],
704
+ [notchX, pad + h / 2],
705
+ ]);
706
+ }
707
+ else if (preset === "stripedRightArrow") {
708
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 50000;
709
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
710
+ const stemH = h * adj1 / 100000;
711
+ const headW = w * adj2 / 100000;
712
+ const stemTop = pad + (h - stemH) / 2, stemBot = pad + (h + stemH) / 2;
713
+ // Stripes at tail (simplified as solid arrow body — stripes are decorative)
714
+ const stripeW = w * 0.05;
715
+ drawCmd = polyPath([
716
+ [pad + stripeW * 2, stemTop],
717
+ [pad + w - headW, stemTop],
718
+ [pad + w - headW, pad],
719
+ [pad + w, pad + h / 2],
720
+ [pad + w - headW, pad + h],
721
+ [pad + w - headW, stemBot],
722
+ [pad + stripeW * 2, stemBot],
723
+ ]);
724
+ }
725
+ else if (preset === "decagon") {
726
+ const cx = pad + w / 2, cy = pad + h / 2;
727
+ const pts = [];
728
+ for (let i = 0; i < 10; i++) {
729
+ const a = Math.PI * (-0.5 + 2 * i / 10);
730
+ pts.push([cx + Math.cos(a) * w / 2, cy + Math.sin(a) * h / 2]);
731
+ }
732
+ drawCmd = polyPath(pts);
733
+ }
734
+ else if (preset === "heptagon") {
735
+ const cx = pad + w / 2, cy = pad + h / 2;
736
+ const pts = [];
737
+ for (let i = 0; i < 7; i++) {
738
+ const a = Math.PI * (-0.5 + 2 * i / 7);
739
+ pts.push([cx + Math.cos(a) * w / 2, cy + Math.sin(a) * h / 2]);
740
+ }
741
+ drawCmd = polyPath(pts);
742
+ }
743
+ else if (preset === "dodecagon") {
744
+ const cx = pad + w / 2, cy = pad + h / 2;
745
+ const pts = [];
746
+ for (let i = 0; i < 12; i++) {
747
+ const a = Math.PI * (-0.5 + 2 * i / 12);
748
+ pts.push([cx + Math.cos(a) * w / 2, cy + Math.sin(a) * h / 2]);
749
+ }
750
+ drawCmd = polyPath(pts);
751
+ }
752
+ else if (preset === "star10") {
753
+ const cx = pad + w / 2, cy = pad + h / 2;
754
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 42533;
755
+ const outerR_x = w / 2, outerR_y = h / 2;
756
+ const innerR_x = outerR_x * adj / 50000, innerR_y = outerR_y * adj / 50000;
757
+ const pts = [];
758
+ for (let i = 0; i < 10; i++) {
759
+ const outerA = Math.PI * (-0.5 + i / 5);
760
+ pts.push([cx + Math.cos(outerA) * outerR_x, cy + Math.sin(outerA) * outerR_y]);
761
+ const innerA = Math.PI * (-0.5 + (i * 2 + 1) / 10);
762
+ pts.push([cx + Math.cos(innerA) * innerR_x, cy + Math.sin(innerA) * innerR_y]);
763
+ }
764
+ drawCmd = polyPath(pts);
765
+ }
766
+ else if (preset === "star12") {
767
+ const cx = pad + w / 2, cy = pad + h / 2;
768
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 37500;
769
+ const outerR_x = w / 2, outerR_y = h / 2;
770
+ const innerR_x = outerR_x * adj / 50000, innerR_y = outerR_y * adj / 50000;
771
+ const pts = [];
772
+ for (let i = 0; i < 12; i++) {
773
+ const outerA = Math.PI * (-0.5 + i / 6);
774
+ pts.push([cx + Math.cos(outerA) * outerR_x, cy + Math.sin(outerA) * outerR_y]);
775
+ const innerA = Math.PI * (-0.5 + (i * 2 + 1) / 12);
776
+ pts.push([cx + Math.cos(innerA) * innerR_x, cy + Math.sin(innerA) * innerR_y]);
777
+ }
778
+ drawCmd = polyPath(pts);
779
+ }
780
+ else if (preset === "star16") {
781
+ const cx = pad + w / 2, cy = pad + h / 2;
782
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 37500;
783
+ const outerR_x = w / 2, outerR_y = h / 2;
784
+ const innerR_x = outerR_x * adj / 50000, innerR_y = outerR_y * adj / 50000;
785
+ const pts = [];
786
+ for (let i = 0; i < 16; i++) {
787
+ const outerA = Math.PI * (-0.5 + i / 8);
788
+ pts.push([cx + Math.cos(outerA) * outerR_x, cy + Math.sin(outerA) * outerR_y]);
789
+ const innerA = Math.PI * (-0.5 + (i * 2 + 1) / 16);
790
+ pts.push([cx + Math.cos(innerA) * innerR_x, cy + Math.sin(innerA) * innerR_y]);
791
+ }
792
+ drawCmd = polyPath(pts);
793
+ }
794
+ else if (preset === "star24") {
795
+ const cx = pad + w / 2, cy = pad + h / 2;
796
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 37500;
797
+ const outerR_x = w / 2, outerR_y = h / 2;
798
+ const innerR_x = outerR_x * adj / 50000, innerR_y = outerR_y * adj / 50000;
799
+ const pts = [];
800
+ for (let i = 0; i < 24; i++) {
801
+ const outerA = Math.PI * (-0.5 + i / 12);
802
+ pts.push([cx + Math.cos(outerA) * outerR_x, cy + Math.sin(outerA) * outerR_y]);
803
+ const innerA = Math.PI * (-0.5 + (i * 2 + 1) / 24);
804
+ pts.push([cx + Math.cos(innerA) * innerR_x, cy + Math.sin(innerA) * innerR_y]);
805
+ }
806
+ drawCmd = polyPath(pts);
807
+ }
808
+ else if (preset === "star32") {
809
+ const cx = pad + w / 2, cy = pad + h / 2;
810
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 37500;
811
+ const outerR_x = w / 2, outerR_y = h / 2;
812
+ const innerR_x = outerR_x * adj / 50000, innerR_y = outerR_y * adj / 50000;
813
+ const pts = [];
814
+ for (let i = 0; i < 32; i++) {
815
+ const outerA = Math.PI * (-0.5 + i / 16);
816
+ pts.push([cx + Math.cos(outerA) * outerR_x, cy + Math.sin(outerA) * outerR_y]);
817
+ const innerA = Math.PI * (-0.5 + (i * 2 + 1) / 32);
818
+ pts.push([cx + Math.cos(innerA) * innerR_x, cy + Math.sin(innerA) * innerR_y]);
819
+ }
820
+ drawCmd = polyPath(pts);
821
+ }
822
+ else if (preset === "round1Rect") {
823
+ // Rectangle with one rounded corner (top-right)
824
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 16667;
825
+ const radius = Math.min(w, h) * adj / 100000;
826
+ const x0 = pad, y0 = totalH - pad - h, x1 = pad + w, y1 = totalH - pad;
827
+ drawCmd = [
828
+ `${x0} ${y0} m`, `${x1 - radius} ${y0} l`,
829
+ `${x1} ${y0} ${x1} ${y0 + radius} v`,
830
+ `${x1} ${y1} l`, `${x0} ${y1} l`, "f",
831
+ ].join("\n");
832
+ }
833
+ else if (preset === "round2SameRect") {
834
+ // Rectangle with two rounded corners on the same side (top-left, top-right)
835
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 16667;
836
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 0;
837
+ const r1 = Math.min(w, h) * adj1 / 100000;
838
+ const r2 = Math.min(w, h) * adj2 / 100000;
839
+ const x0 = pad, y0 = totalH - pad - h, x1 = pad + w, y1 = totalH - pad;
840
+ drawCmd = [
841
+ `${x0 + r1} ${y0} m`, `${x1 - r1} ${y0} l`,
842
+ `${x1} ${y0} ${x1} ${y0 + r1} v`,
843
+ `${x1} ${y1 - r2} l`,
844
+ r2 > 0 ? `${x1} ${y1} ${x1 - r2} ${y1} v` : `${x1} ${y1} l`,
845
+ `${x0 + r2} ${y1} l`,
846
+ r2 > 0 ? `${x0} ${y1} ${x0} ${y1 - r2} v` : `${x0} ${y1} l`,
847
+ `${x0} ${y0 + r1} l`,
848
+ `${x0} ${y0} ${x0 + r1} ${y0} v`, "f",
849
+ ].join("\n");
850
+ }
851
+ else if (preset === "round2DiagRect") {
852
+ // Rectangle with two rounded corners on diagonal (top-left, bottom-right)
853
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 16667;
854
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 0;
855
+ const r1 = Math.min(w, h) * adj1 / 100000;
856
+ const r2 = Math.min(w, h) * adj2 / 100000;
857
+ const x0 = pad, y0 = totalH - pad - h, x1 = pad + w, y1 = totalH - pad;
858
+ drawCmd = [
859
+ `${x0 + r1} ${y0} m`, `${x1} ${y0} l`,
860
+ `${x1} ${y1 - r2} l`,
861
+ r2 > 0 ? `${x1} ${y1} ${x1 - r2} ${y1} v` : `${x1} ${y1} l`,
862
+ `${x0} ${y1} l`,
863
+ `${x0} ${y0 + r1} l`,
864
+ `${x0} ${y0} ${x0 + r1} ${y0} v`, "f",
865
+ ].join("\n");
866
+ }
867
+ else if (preset === "snip1Rect") {
868
+ // Rectangle with one snipped corner (top-right)
869
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 16667;
870
+ const snip = Math.min(w, h) * adj / 100000;
871
+ drawCmd = polyPath([
872
+ [pad, pad], [pad + w - snip, pad], [pad + w, pad + snip],
873
+ [pad + w, pad + h], [pad, pad + h],
874
+ ]);
875
+ }
876
+ else if (preset === "snip2SameRect") {
877
+ // Rectangle with two snipped corners on same side (top-left, top-right)
878
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 16667;
879
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 16667;
880
+ const s1 = Math.min(w, h) * adj1 / 100000;
881
+ const s2 = Math.min(w, h) * adj2 / 100000;
882
+ drawCmd = polyPath([
883
+ [pad + s1, pad], [pad + w - s1, pad],
884
+ [pad + w, pad + s1], [pad + w, pad + h - s2],
885
+ [pad + w - s2, pad + h], [pad + s2, pad + h],
886
+ [pad, pad + h - s2], [pad, pad + s1],
887
+ ]);
888
+ }
889
+ else if (preset === "snip2DiagRect") {
890
+ // Rectangle with two snipped corners on diagonal (top-right, bottom-left)
891
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 16667;
892
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 0;
893
+ const s1 = Math.min(w, h) * adj1 / 100000;
894
+ const s2 = Math.min(w, h) * adj2 / 100000;
895
+ drawCmd = polyPath([
896
+ [pad, pad], [pad + w - s1, pad], [pad + w, pad + s1],
897
+ [pad + w, pad + h], [pad + s2, pad + h], [pad, pad + h - s2],
898
+ ]);
899
+ }
900
+ else if (preset === "snipRoundRect") {
901
+ // Rectangle with one rounded corner (top-left) and one snipped corner (top-right)
902
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 16667;
903
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 16667;
904
+ const rr = Math.min(w, h) * adj1 / 100000;
905
+ const snip = Math.min(w, h) * adj2 / 100000;
906
+ const x0 = pad, y0 = totalH - pad - h, x1 = pad + w, y1 = totalH - pad;
907
+ drawCmd = [
908
+ `${x0 + rr} ${y0} m`,
909
+ `${x1 - snip} ${y0} l`,
910
+ `${x1} ${y0 + snip} l`,
911
+ `${x1} ${y1} l`,
912
+ `${x0} ${y1} l`,
913
+ `${x0} ${y0 + rr} l`,
914
+ `${x0} ${y0} ${x0 + rr} ${y0} v`,
915
+ "f",
916
+ ].join("\n");
917
+ }
918
+ else if (preset === "flowChartInputOutput") {
919
+ // Parallelogram for I/O (skew ~20%)
920
+ const skew = w * 0.2;
921
+ drawCmd = polyPath([
922
+ [pad + skew, pad], [pad + w, pad],
923
+ [pad + w - skew, pad + h], [pad, pad + h],
924
+ ]);
925
+ }
926
+ else if (preset === "flowChartPreparation") {
927
+ // Hexagon-like (wider at center)
928
+ const inset = w * 0.2;
929
+ const cy = pad + h / 2;
930
+ drawCmd = polyPath([
931
+ [pad + inset, pad], [pad + w - inset, pad],
932
+ [pad + w, cy], [pad + w - inset, pad + h],
933
+ [pad + inset, pad + h], [pad, cy],
934
+ ]);
935
+ }
936
+ else if (preset === "flowChartConnector") {
937
+ // Circle (ellipse fitting the bounding box)
938
+ const cx = pad + w / 2, cy = pad + h / 2;
939
+ const rx = w / 2, ry = h / 2;
940
+ const k = 0.5522847498;
941
+ drawCmd = [
942
+ `${cx + rx} ${totalH - cy} m`,
943
+ `${cx + rx} ${totalH - (cy - ry * k)} ${cx + rx * k} ${totalH - (cy - ry)} ${cx} ${totalH - (cy - ry)} c`,
944
+ `${cx - rx * k} ${totalH - (cy - ry)} ${cx - rx} ${totalH - (cy - ry * k)} ${cx - rx} ${totalH - cy} c`,
945
+ `${cx - rx} ${totalH - (cy + ry * k)} ${cx - rx * k} ${totalH - (cy + ry)} ${cx} ${totalH - (cy + ry)} c`,
946
+ `${cx + rx * k} ${totalH - (cy + ry)} ${cx + rx} ${totalH - (cy + ry * k)} ${cx + rx} ${totalH - cy} c`,
947
+ "f",
948
+ ].join("\n");
949
+ }
950
+ else if (preset === "mathPlus") {
951
+ // Plus sign: cross shape (thinner than 'plus' preset — ~27% arm thickness)
952
+ const t = w * 0.27, s = h * 0.27;
953
+ const cx1 = pad + (w - t) / 2, cx2 = pad + (w + t) / 2;
954
+ const cy1 = pad + (h - s) / 2, cy2 = pad + (h + s) / 2;
955
+ drawCmd = polyPath([
956
+ [cx1, pad], [cx2, pad],
957
+ [cx2, cy1], [pad + w, cy1],
958
+ [pad + w, cy2], [cx2, cy2],
959
+ [cx2, pad + h], [cx1, pad + h],
960
+ [cx1, cy2], [pad, cy2],
961
+ [pad, cy1], [cx1, cy1],
962
+ ]);
963
+ }
964
+ else if (preset === "mathMinus") {
965
+ // Horizontal bar (rectangle centered vertically)
966
+ const barH = h * 0.27;
967
+ const barTop = pad + (h - barH) / 2;
968
+ drawCmd = polyPath([
969
+ [pad, barTop], [pad + w, barTop],
970
+ [pad + w, barTop + barH], [pad, barTop + barH],
971
+ ]);
972
+ }
973
+ else if (preset === "mathMultiply") {
974
+ // X shape (two crossing diagonal bars — approximated as polygon)
975
+ const t = Math.min(w, h) * 0.15; // half-thickness of bars
976
+ const cx = pad + w / 2, cy = pad + h / 2;
977
+ drawCmd = polyPath([
978
+ [pad + t, pad], [cx, cy - t], [pad + w - t, pad],
979
+ [pad + w, pad + t], [cx + t, cy], [pad + w, pad + h - t],
980
+ [pad + w - t, pad + h], [cx, cy + t], [pad + t, pad + h],
981
+ [pad, pad + h - t], [cx - t, cy], [pad, pad + t],
982
+ ]);
983
+ }
984
+ else if (preset === "mathDivide") {
985
+ // Horizontal bar with dots above and below
986
+ const barH = h * 0.15;
987
+ const barTop = pad + (h - barH) / 2;
988
+ const dotR = Math.min(w, h) * 0.1;
989
+ const cx = pad + w / 2;
990
+ const k = 0.5522847498;
991
+ const topDotCy = pad + h * 0.2;
992
+ const botDotCy = pad + h * 0.8;
993
+ drawCmd = [
994
+ // Top dot (ellipse)
995
+ `${cx + dotR} ${py(topDotCy)} m`,
996
+ `${cx + dotR} ${py(topDotCy - dotR * k)} ${cx + dotR * k} ${py(topDotCy - dotR)} ${cx} ${py(topDotCy - dotR)} c`,
997
+ `${cx - dotR * k} ${py(topDotCy - dotR)} ${cx - dotR} ${py(topDotCy - dotR * k)} ${cx - dotR} ${py(topDotCy)} c`,
998
+ `${cx - dotR} ${py(topDotCy + dotR * k)} ${cx - dotR * k} ${py(topDotCy + dotR)} ${cx} ${py(topDotCy + dotR)} c`,
999
+ `${cx + dotR * k} ${py(topDotCy + dotR)} ${cx + dotR} ${py(topDotCy + dotR * k)} ${cx + dotR} ${py(topDotCy)} c`,
1000
+ "f",
1001
+ // Bar (rectangle)
1002
+ `${pad} ${py(barTop)} m`,
1003
+ `${pad + w} ${py(barTop)} l`,
1004
+ `${pad + w} ${py(barTop + barH)} l`,
1005
+ `${pad} ${py(barTop + barH)} l`,
1006
+ "f",
1007
+ // Bottom dot (ellipse)
1008
+ `${cx + dotR} ${py(botDotCy)} m`,
1009
+ `${cx + dotR} ${py(botDotCy - dotR * k)} ${cx + dotR * k} ${py(botDotCy - dotR)} ${cx} ${py(botDotCy - dotR)} c`,
1010
+ `${cx - dotR * k} ${py(botDotCy - dotR)} ${cx - dotR} ${py(botDotCy - dotR * k)} ${cx - dotR} ${py(botDotCy)} c`,
1011
+ `${cx - dotR} ${py(botDotCy + dotR * k)} ${cx - dotR * k} ${py(botDotCy + dotR)} ${cx} ${py(botDotCy + dotR)} c`,
1012
+ `${cx + dotR * k} ${py(botDotCy + dotR)} ${cx + dotR} ${py(botDotCy + dotR * k)} ${cx + dotR} ${py(botDotCy)} c`,
1013
+ "f",
1014
+ ].join("\n");
1015
+ }
1016
+ else if (preset === "mathEqual") {
1017
+ // Two horizontal bars
1018
+ const barH = h * 0.15;
1019
+ const gap = h * 0.12;
1020
+ const topBar = pad + h / 2 - gap / 2 - barH;
1021
+ const botBar = pad + h / 2 + gap / 2;
1022
+ drawCmd = [
1023
+ `${pad} ${py(topBar)} m`, `${pad + w} ${py(topBar)} l`,
1024
+ `${pad + w} ${py(topBar + barH)} l`, `${pad} ${py(topBar + barH)} l`, "f",
1025
+ `${pad} ${py(botBar)} m`, `${pad + w} ${py(botBar)} l`,
1026
+ `${pad + w} ${py(botBar + barH)} l`, `${pad} ${py(botBar + barH)} l`, "f",
1027
+ ].join("\n");
1028
+ }
1029
+ else if (preset === "mathNotEqual") {
1030
+ // Two horizontal bars with a diagonal slash through them
1031
+ const barH = h * 0.15;
1032
+ const gap = h * 0.12;
1033
+ const topBar = pad + h / 2 - gap / 2 - barH;
1034
+ const botBar = pad + h / 2 + gap / 2;
1035
+ const slashW = w * 0.08;
1036
+ const cx = pad + w / 2;
1037
+ drawCmd = [
1038
+ `${pad} ${py(topBar)} m`, `${pad + w} ${py(topBar)} l`,
1039
+ `${pad + w} ${py(topBar + barH)} l`, `${pad} ${py(topBar + barH)} l`, "f",
1040
+ `${pad} ${py(botBar)} m`, `${pad + w} ${py(botBar)} l`,
1041
+ `${pad + w} ${py(botBar + barH)} l`, `${pad} ${py(botBar + barH)} l`, "f",
1042
+ // Diagonal slash
1043
+ `${cx - slashW / 2} ${py(pad)} m`, `${cx + slashW / 2} ${py(pad)} l`,
1044
+ `${cx + slashW / 2 - w * 0.15} ${py(pad + h)} l`, `${cx - slashW / 2 - w * 0.15} ${py(pad + h)} l`, "f",
1045
+ ].join("\n");
1046
+ }
1047
+ else if (preset === "quadArrow") {
1048
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 22500;
1049
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 22500;
1050
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 22500;
1051
+ const stemW = w * adj1 / 100000;
1052
+ const stemH = h * adj1 / 100000;
1053
+ const headW = w * adj2 / 100000;
1054
+ const headH = h * adj2 / 100000;
1055
+ const cx = pad + w / 2, cy = pad + h / 2;
1056
+ const sL = cx - stemW / 2, sR = cx + stemW / 2;
1057
+ const sT = cy - stemH / 2, sB = cy + stemH / 2;
1058
+ drawCmd = polyPath([
1059
+ // Up arrow head
1060
+ [cx, pad], [pad + w / 2 + headW / 2, pad + headH], [sR, pad + headH],
1061
+ // Right arrow head
1062
+ [sR, sT], [pad + w - headH, sT], [pad + w - headH, cy - headW / 2],
1063
+ [pad + w, cy], [pad + w - headH, cy + headW / 2], [pad + w - headH, sB],
1064
+ // Down arrow head
1065
+ [sR, sB], [sR, pad + h - headH], [cx + headW / 2, pad + h - headH],
1066
+ [cx, pad + h], [cx - headW / 2, pad + h - headH], [sL, pad + h - headH],
1067
+ // Left arrow head
1068
+ [sL, sB], [pad + headH, sB], [pad + headH, cy + headW / 2],
1069
+ [pad, cy], [pad + headH, cy - headW / 2], [pad + headH, sT],
1070
+ [sL, sT], [sL, pad + headH], [cx - headW / 2, pad + headH],
1071
+ ]);
1072
+ }
1073
+ else if (preset === "leftRightUpArrow") {
1074
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 25000;
1075
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 25000;
1076
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 25000;
1077
+ const stemW = w * adj1 / 100000;
1078
+ const headW = w * adj2 / 100000;
1079
+ const headH = h * adj3 / 100000;
1080
+ const stemH = h * adj1 / 100000;
1081
+ const cx = pad + w / 2, cy = pad + h / 2;
1082
+ const sL = cx - stemW / 2, sR = cx + stemW / 2;
1083
+ const sT = cy - stemH / 2;
1084
+ drawCmd = polyPath([
1085
+ // Up arrow
1086
+ [cx, pad], [cx + headW / 2, pad + headH], [sR, pad + headH],
1087
+ // Right
1088
+ [sR, sT], [pad + w - headH, sT], [pad + w - headH, cy - headW / 2],
1089
+ [pad + w, cy], [pad + w - headH, cy + headW / 2], [pad + w - headH, pad + h],
1090
+ // Bottom
1091
+ [pad + headH, pad + h],
1092
+ // Left
1093
+ [pad + headH, cy + headW / 2], [pad, cy], [pad + headH, cy - headW / 2],
1094
+ [pad + headH, sT],
1095
+ [sL, sT], [sL, pad + headH], [cx - headW / 2, pad + headH],
1096
+ ]);
1097
+ }
1098
+ else if (preset === "bentArrow") {
1099
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 25000;
1100
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 25000;
1101
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 25000;
1102
+ const adj4 = adjustValues?.adj4 ? parseAdjustValue(adjustValues.adj4) : 43750;
1103
+ const headW = w * adj2 / 100000;
1104
+ const headH = h * adj1 / 100000;
1105
+ const stemW = w * adj3 / 100000;
1106
+ const bendY = h * adj4 / 100000;
1107
+ drawCmd = polyPath([
1108
+ [pad + w / 2, pad],
1109
+ [pad + w, pad + headH],
1110
+ [pad + w - headW / 2, pad + headH],
1111
+ [pad + w - headW / 2, pad + bendY],
1112
+ [pad + stemW, pad + bendY],
1113
+ [pad + stemW, pad + h],
1114
+ [pad, pad + h],
1115
+ [pad, pad + bendY - stemW],
1116
+ [pad + w - headW / 2 - stemW, pad + bendY - stemW],
1117
+ [pad + w - headW / 2 - stemW, pad + headH],
1118
+ [pad + w / 2 - headW / 2, pad + headH],
1119
+ ]);
1120
+ }
1121
+ else if (preset === "uturnArrow") {
1122
+ // U-turn arrow: goes down, curves at bottom, comes back up with arrow
1123
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 25000;
1124
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 25000;
1125
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 25000;
1126
+ const adj4 = adjustValues?.adj4 ? parseAdjustValue(adjustValues.adj4) : 43750;
1127
+ const adj5 = adjustValues?.adj5 ? parseAdjustValue(adjustValues.adj5) : 75000;
1128
+ const stemW = w * adj1 / 100000;
1129
+ const headW = w * adj2 / 100000;
1130
+ const headH = h * adj3 / 100000;
1131
+ const bend = h * adj5 / 100000;
1132
+ // Simplified as polygon (U-turn curve approximated)
1133
+ const leftStemR = pad + w * 0.35;
1134
+ const rightStemL = pad + w * 0.65;
1135
+ drawCmd = polyPath([
1136
+ [pad + w / 2, pad],
1137
+ [pad + w / 2 + headW / 2, pad + headH],
1138
+ [rightStemL, pad + headH],
1139
+ [rightStemL, pad + bend],
1140
+ [leftStemR, pad + bend],
1141
+ [leftStemR, pad + h],
1142
+ [pad, pad + h],
1143
+ [pad, pad + bend],
1144
+ [pad + stemW, pad + bend],
1145
+ [pad + stemW, pad + headH],
1146
+ [pad + w / 2 - headW / 2, pad + headH],
1147
+ ]);
1148
+ }
1149
+ else if (preset === "circularArrow") {
1150
+ // Circular arrow: approximated as a thick arc with arrowhead
1151
+ // Simplified as a crescent-like shape
1152
+ const cx = pad + w / 2, cy = pad + h / 2;
1153
+ const rx = w / 2, ry = h / 2;
1154
+ const k = 0.5522847498;
1155
+ const thick = Math.min(w, h) * 0.15;
1156
+ const irx = rx - thick, iry = ry - thick;
1157
+ // Draw outer arc (3/4 circle) then inner arc back, with arrowhead
1158
+ drawCmd = [
1159
+ // Outer arc starting from right-center going clockwise (top, left, bottom)
1160
+ `${cx + rx} ${py(cy)} m`,
1161
+ `${cx + rx} ${py(cy - ry * k)} ${cx + rx * k} ${py(cy - ry)} ${cx} ${py(cy - ry)} c`,
1162
+ `${cx - rx * k} ${py(cy - ry)} ${cx - rx} ${py(cy - ry * k)} ${cx - rx} ${py(cy)} c`,
1163
+ `${cx - rx} ${py(cy + ry * k)} ${cx - rx * k} ${py(cy + ry)} ${cx} ${py(cy + ry)} c`,
1164
+ // Arrowhead at bottom-center
1165
+ `${cx + thick * 2} ${py(cy + ry + thick)} l`,
1166
+ `${cx} ${py(cy + iry)} l`,
1167
+ `${cx - thick * 2} ${py(cy + ry + thick)} l`,
1168
+ `${cx} ${py(cy + ry)} l`,
1169
+ // Inner arc going counter-clockwise back (bottom to right)
1170
+ `${cx - irx * k} ${py(cy + iry)} ${cx - irx} ${py(cy + iry * k)} ${cx - irx} ${py(cy)} c`,
1171
+ `${cx - irx} ${py(cy - iry * k)} ${cx - irx * k} ${py(cy - iry)} ${cx} ${py(cy - iry)} c`,
1172
+ `${cx + irx * k} ${py(cy - iry)} ${cx + irx} ${py(cy - iry * k)} ${cx + irx} ${py(cy)} c`,
1173
+ "f",
1174
+ ].join("\n");
1175
+ }
1176
+ else if (preset === "curvedRightArrow") {
1177
+ // Curved right arrow
1178
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 25000;
1179
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
1180
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 25000;
1181
+ const headH = h * adj3 / 100000;
1182
+ const headW = w * adj2 / 100000;
1183
+ // Simplified as arrow pointing right with curved body
1184
+ drawCmd = polyPath([
1185
+ [pad, pad],
1186
+ [pad + w * 0.6, pad],
1187
+ [pad + w - headW, pad + h * 0.3],
1188
+ [pad + w - headW, pad],
1189
+ [pad + w, pad + h / 2],
1190
+ [pad + w - headW, pad + h],
1191
+ [pad + w - headW, pad + h * 0.7],
1192
+ [pad + w * 0.6, pad + h],
1193
+ [pad, pad + h],
1194
+ ]);
1195
+ }
1196
+ else if (preset === "curvedLeftArrow") {
1197
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 25000;
1198
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
1199
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 25000;
1200
+ const headW = w * adj2 / 100000;
1201
+ drawCmd = polyPath([
1202
+ [pad + w, pad],
1203
+ [pad + w * 0.4, pad],
1204
+ [pad + headW, pad + h * 0.3],
1205
+ [pad + headW, pad],
1206
+ [pad, pad + h / 2],
1207
+ [pad + headW, pad + h],
1208
+ [pad + headW, pad + h * 0.7],
1209
+ [pad + w * 0.4, pad + h],
1210
+ [pad + w, pad + h],
1211
+ ]);
1212
+ }
1213
+ else if (preset === "curvedUpArrow") {
1214
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 25000;
1215
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
1216
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 25000;
1217
+ const headH = h * adj2 / 100000;
1218
+ drawCmd = polyPath([
1219
+ [pad, pad + h],
1220
+ [pad, pad + h * 0.4],
1221
+ [pad + w * 0.3, pad + headH],
1222
+ [pad, pad + headH],
1223
+ [pad + w / 2, pad],
1224
+ [pad + w, pad + headH],
1225
+ [pad + w * 0.7, pad + headH],
1226
+ [pad + w, pad + h * 0.4],
1227
+ [pad + w, pad + h],
1228
+ ]);
1229
+ }
1230
+ else if (preset === "curvedDownArrow") {
1231
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 25000;
1232
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
1233
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 25000;
1234
+ const headH = h * adj2 / 100000;
1235
+ drawCmd = polyPath([
1236
+ [pad, pad],
1237
+ [pad, pad + h * 0.6],
1238
+ [pad + w * 0.3, pad + h - headH],
1239
+ [pad, pad + h - headH],
1240
+ [pad + w / 2, pad + h],
1241
+ [pad + w, pad + h - headH],
1242
+ [pad + w * 0.7, pad + h - headH],
1243
+ [pad + w, pad + h * 0.6],
1244
+ [pad + w, pad],
1245
+ ]);
1246
+ }
1247
+ else if (preset === "rightArrowCallout") {
1248
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 25000;
1249
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 25000;
1250
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 25000;
1251
+ const adj4 = adjustValues?.adj4 ? parseAdjustValue(adjustValues.adj4) : 64977;
1252
+ const stemH = h * adj1 / 100000;
1253
+ const headW = w * adj2 / 100000;
1254
+ const bodyW = w * adj4 / 100000;
1255
+ const stemTop = pad + (h - stemH) / 2, stemBot = pad + (h + stemH) / 2;
1256
+ drawCmd = polyPath([
1257
+ [pad, pad], [pad + bodyW, pad],
1258
+ [pad + bodyW, stemTop], [pad + w - headW, stemTop],
1259
+ [pad + w - headW, pad],
1260
+ [pad + w, pad + h / 2],
1261
+ [pad + w - headW, pad + h],
1262
+ [pad + w - headW, stemBot], [pad + bodyW, stemBot],
1263
+ [pad + bodyW, pad + h], [pad, pad + h],
1264
+ ]);
1265
+ }
1266
+ else if (preset === "leftArrowCallout") {
1267
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 25000;
1268
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 25000;
1269
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 25000;
1270
+ const adj4 = adjustValues?.adj4 ? parseAdjustValue(adjustValues.adj4) : 64977;
1271
+ const stemH = h * adj1 / 100000;
1272
+ const headW = w * adj2 / 100000;
1273
+ const bodyW = w * adj4 / 100000;
1274
+ const stemTop = pad + (h - stemH) / 2, stemBot = pad + (h + stemH) / 2;
1275
+ const bodyX = pad + w - bodyW;
1276
+ drawCmd = polyPath([
1277
+ [pad + w, pad], [pad + w, pad + h],
1278
+ [bodyX, pad + h], [bodyX, stemBot],
1279
+ [pad + headW, stemBot], [pad + headW, pad + h],
1280
+ [pad, pad + h / 2],
1281
+ [pad + headW, pad], [pad + headW, stemTop],
1282
+ [bodyX, stemTop], [bodyX, pad],
1283
+ ]);
1284
+ }
1285
+ else if (preset === "upArrowCallout") {
1286
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 25000;
1287
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 25000;
1288
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 25000;
1289
+ const adj4 = adjustValues?.adj4 ? parseAdjustValue(adjustValues.adj4) : 64977;
1290
+ const stemW = w * adj1 / 100000;
1291
+ const headH = h * adj2 / 100000;
1292
+ const bodyH = h * adj4 / 100000;
1293
+ const stemL = pad + (w - stemW) / 2, stemR = pad + (w + stemW) / 2;
1294
+ const bodyY = pad + h - bodyH;
1295
+ drawCmd = polyPath([
1296
+ [pad + w / 2, pad],
1297
+ [pad + w, pad + headH], [stemR, pad + headH],
1298
+ [stemR, bodyY], [pad + w, bodyY],
1299
+ [pad + w, pad + h], [pad, pad + h],
1300
+ [pad, bodyY], [stemL, bodyY],
1301
+ [stemL, pad + headH], [pad, pad + headH],
1302
+ ]);
1303
+ }
1304
+ else if (preset === "downArrowCallout") {
1305
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 25000;
1306
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 25000;
1307
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 25000;
1308
+ const adj4 = adjustValues?.adj4 ? parseAdjustValue(adjustValues.adj4) : 64977;
1309
+ const stemW = w * adj1 / 100000;
1310
+ const headH = h * adj2 / 100000;
1311
+ const bodyH = h * adj4 / 100000;
1312
+ const stemL = pad + (w - stemW) / 2, stemR = pad + (w + stemW) / 2;
1313
+ drawCmd = polyPath([
1314
+ [pad, pad], [pad + w, pad],
1315
+ [pad + w, pad + bodyH], [stemR, pad + bodyH],
1316
+ [stemR, pad + h - headH], [pad + w, pad + h - headH],
1317
+ [pad + w / 2, pad + h],
1318
+ [pad, pad + h - headH], [stemL, pad + h - headH],
1319
+ [stemL, pad + bodyH], [pad, pad + bodyH],
1320
+ ]);
1321
+ }
1322
+ else if (preset === "leftRightArrowCallout") {
1323
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 25000;
1324
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 25000;
1325
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 25000;
1326
+ const adj4 = adjustValues?.adj4 ? parseAdjustValue(adjustValues.adj4) : 64977;
1327
+ const stemH = h * adj1 / 100000;
1328
+ const headW = w * adj2 / 100000;
1329
+ const bodyX = w * (1 - adj4 / 100000) / 2;
1330
+ const stemTop = pad + (h - stemH) / 2, stemBot = pad + (h + stemH) / 2;
1331
+ drawCmd = polyPath([
1332
+ [pad + bodyX, pad], [pad + w - bodyX, pad],
1333
+ [pad + w - bodyX, stemTop], [pad + w - headW, stemTop],
1334
+ [pad + w - headW, pad], [pad + w, pad + h / 2],
1335
+ [pad + w - headW, pad + h], [pad + w - headW, stemBot],
1336
+ [pad + w - bodyX, stemBot], [pad + w - bodyX, pad + h],
1337
+ [pad + bodyX, pad + h], [pad + bodyX, stemBot],
1338
+ [pad + headW, stemBot], [pad + headW, pad + h],
1339
+ [pad, pad + h / 2],
1340
+ [pad + headW, pad], [pad + headW, stemTop],
1341
+ [pad + bodyX, stemTop],
1342
+ ]);
1343
+ }
1344
+ else if (preset === "upDownArrowCallout") {
1345
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 25000;
1346
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 25000;
1347
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 25000;
1348
+ const adj4 = adjustValues?.adj4 ? parseAdjustValue(adjustValues.adj4) : 64977;
1349
+ const stemW = w * adj1 / 100000;
1350
+ const headH = h * adj2 / 100000;
1351
+ const bodyY = h * (1 - adj4 / 100000) / 2;
1352
+ const stemL = pad + (w - stemW) / 2, stemR = pad + (w + stemW) / 2;
1353
+ drawCmd = polyPath([
1354
+ [pad + w / 2, pad],
1355
+ [pad + w, pad + headH], [stemR, pad + headH],
1356
+ [stemR, pad + bodyY], [pad + w, pad + bodyY],
1357
+ [pad + w, pad + h - bodyY], [stemR, pad + h - bodyY],
1358
+ [stemR, pad + h - headH], [pad + w, pad + h - headH],
1359
+ [pad + w / 2, pad + h],
1360
+ [pad, pad + h - headH], [stemL, pad + h - headH],
1361
+ [stemL, pad + h - bodyY], [pad, pad + h - bodyY],
1362
+ [pad, pad + bodyY], [stemL, pad + bodyY],
1363
+ [stemL, pad + headH], [pad, pad + headH],
1364
+ ]);
1365
+ }
1366
+ else if (preset === "quadArrowCallout") {
1367
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 18515;
1368
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 18515;
1369
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 18515;
1370
+ const adj4 = adjustValues?.adj4 ? parseAdjustValue(adjustValues.adj4) : 48123;
1371
+ const stemW = w * adj1 / 100000;
1372
+ const headW = w * adj2 / 100000;
1373
+ const headH = h * adj3 / 100000;
1374
+ const bodyD = w * adj4 / 100000 / 2;
1375
+ const cx = pad + w / 2, cy = pad + h / 2;
1376
+ const sL = cx - stemW / 2, sR = cx + stemW / 2;
1377
+ const sT = cy - stemW / 2, sB = cy + stemW / 2;
1378
+ drawCmd = polyPath([
1379
+ [cx, pad], [cx + headW / 2, pad + headH], [sR, pad + headH],
1380
+ [sR, cy - bodyD], [cx + bodyD, cy - bodyD],
1381
+ [cx + bodyD, sT], [pad + w - headH, sT], [pad + w - headH, cy - headW / 2],
1382
+ [pad + w, cy], [pad + w - headH, cy + headW / 2],
1383
+ [pad + w - headH, sB], [cx + bodyD, sB],
1384
+ [cx + bodyD, cy + bodyD], [sR, cy + bodyD],
1385
+ [sR, pad + h - headH], [cx + headW / 2, pad + h - headH],
1386
+ [cx, pad + h], [cx - headW / 2, pad + h - headH],
1387
+ [sL, pad + h - headH], [sL, cy + bodyD],
1388
+ [cx - bodyD, cy + bodyD], [cx - bodyD, sB],
1389
+ [pad + headH, sB], [pad + headH, cy + headW / 2],
1390
+ [pad, cy], [pad + headH, cy - headW / 2],
1391
+ [pad + headH, sT], [cx - bodyD, sT],
1392
+ [cx - bodyD, cy - bodyD], [sL, cy - bodyD],
1393
+ [sL, pad + headH], [cx - headW / 2, pad + headH],
1394
+ ]);
1395
+ }
1396
+ else if (preset === "callout1" || preset === "accentCallout1") {
1397
+ // Rectangle with a single-segment callout line (just render as rectangle)
1398
+ drawCmd = `${pad} ${py(pad)} m ${pad + w} ${py(pad)} l ${pad + w} ${py(pad + h)} l ${pad} ${py(pad + h)} l f`;
1399
+ }
1400
+ else if (preset === "callout2" || preset === "accentCallout2") {
1401
+ // Rectangle with two-segment callout line
1402
+ drawCmd = `${pad} ${py(pad)} m ${pad + w} ${py(pad)} l ${pad + w} ${py(pad + h)} l ${pad} ${py(pad + h)} l f`;
1403
+ }
1404
+ else if (preset === "callout3" || preset === "accentCallout3") {
1405
+ // Rectangle with three-segment callout line
1406
+ drawCmd = `${pad} ${py(pad)} m ${pad + w} ${py(pad)} l ${pad + w} ${py(pad + h)} l ${pad} ${py(pad + h)} l f`;
1407
+ }
1408
+ else if (preset === "borderCallout1" || preset === "accentBorderCallout1") {
1409
+ drawCmd = `${pad} ${py(pad)} m ${pad + w} ${py(pad)} l ${pad + w} ${py(pad + h)} l ${pad} ${py(pad + h)} l f`;
1410
+ }
1411
+ else if (preset === "borderCallout2" || preset === "accentBorderCallout2") {
1412
+ drawCmd = `${pad} ${py(pad)} m ${pad + w} ${py(pad)} l ${pad + w} ${py(pad + h)} l ${pad} ${py(pad + h)} l f`;
1413
+ }
1414
+ else if (preset === "borderCallout3" || preset === "accentBorderCallout3") {
1415
+ drawCmd = `${pad} ${py(pad)} m ${pad + w} ${py(pad)} l ${pad + w} ${py(pad + h)} l ${pad} ${py(pad + h)} l f`;
1416
+ }
1417
+ else if (preset === "flowChartPredefinedProcess") {
1418
+ // Rectangle with vertical lines near left and right edges
1419
+ const inset = w * 0.1;
1420
+ drawCmd = [
1421
+ // Outer rect
1422
+ `${pad} ${py(pad)} m`, `${pad + w} ${py(pad)} l`,
1423
+ `${pad + w} ${py(pad + h)} l`, `${pad} ${py(pad + h)} l`, "f",
1424
+ ].join("\n");
1425
+ }
1426
+ else if (preset === "flowChartInternalStorage") {
1427
+ // Rectangle with horizontal and vertical lines near top-left
1428
+ drawCmd = [
1429
+ `${pad} ${py(pad)} m`, `${pad + w} ${py(pad)} l`,
1430
+ `${pad + w} ${py(pad + h)} l`, `${pad} ${py(pad + h)} l`, "f",
1431
+ ].join("\n");
1432
+ }
1433
+ else if (preset === "flowChartMultidocument") {
1434
+ // Three overlapping document shapes
1435
+ const off = w * 0.08;
1436
+ const waveH = h * 0.08;
1437
+ // Back doc (offset twice)
1438
+ drawCmd = [
1439
+ // Main doc (front)
1440
+ ...polyPathLines([[pad, pad + off * 2], [pad + w - off * 2, pad + off * 2],
1441
+ [pad + w - off * 2, pad + h - waveH], [pad + (w - off * 2) * 0.75, pad + h],
1442
+ [pad + (w - off * 2) * 0.5, pad + h - waveH],
1443
+ [pad + (w - off * 2) * 0.25, pad + h - waveH * 2], [pad, pad + h - waveH]]),
1444
+ // Middle doc outline (just top edge visible)
1445
+ ...polyPathLines([[pad + off, pad + off], [pad + w - off, pad + off],
1446
+ [pad + w - off, pad + off * 2]]),
1447
+ "f",
1448
+ // Back doc outline (just top edge visible)
1449
+ ...polyPathLines([[pad + off * 2, pad], [pad + w, pad],
1450
+ [pad + w, pad + off]]),
1451
+ "f",
1452
+ ].join("\n");
1453
+ }
1454
+ else if (preset === "flowChartManualInput") {
1455
+ // Quadrilateral with slanted top (top-left higher than top-right)
1456
+ const slant = h * 0.2;
1457
+ drawCmd = polyPath([
1458
+ [pad, pad + slant], [pad + w, pad],
1459
+ [pad + w, pad + h], [pad, pad + h],
1460
+ ]);
1461
+ }
1462
+ else if (preset === "flowChartManualOperation") {
1463
+ // Trapezoid (wider at top)
1464
+ const inset = w * 0.2;
1465
+ drawCmd = polyPath([
1466
+ [pad, pad], [pad + w, pad],
1467
+ [pad + w - inset, pad + h], [pad + inset, pad + h],
1468
+ ]);
1469
+ }
1470
+ else if (preset === "flowChartOffpageConnector") {
1471
+ // Pentagon pointing down (like home plate but downward)
1472
+ const pointH = h * 0.2;
1473
+ drawCmd = polyPath([
1474
+ [pad, pad], [pad + w, pad],
1475
+ [pad + w, pad + h - pointH],
1476
+ [pad + w / 2, pad + h],
1477
+ [pad, pad + h - pointH],
1478
+ ]);
1479
+ }
1480
+ else if (preset === "flowChartPunchedCard") {
1481
+ // Rectangle with clipped top-left corner
1482
+ const clip = Math.min(w, h) * 0.15;
1483
+ drawCmd = polyPath([
1484
+ [pad + clip, pad], [pad + w, pad],
1485
+ [pad + w, pad + h], [pad, pad + h],
1486
+ [pad, pad + clip],
1487
+ ]);
1488
+ }
1489
+ else if (preset === "flowChartPunchedTape") {
1490
+ // Rectangle with wavy top and bottom
1491
+ const waveH = h * 0.1;
1492
+ drawCmd = polyPath([
1493
+ [pad, pad + waveH],
1494
+ [pad + w * 0.25, pad], [pad + w * 0.5, pad + waveH],
1495
+ [pad + w * 0.75, pad + waveH * 2], [pad + w, pad + waveH],
1496
+ [pad + w, pad + h - waveH],
1497
+ [pad + w * 0.75, pad + h], [pad + w * 0.5, pad + h - waveH],
1498
+ [pad + w * 0.25, pad + h - waveH * 2], [pad, pad + h - waveH],
1499
+ ]);
1500
+ }
1501
+ else if (preset === "flowChartSummingJunction") {
1502
+ // Circle with X inside — just render as circle
1503
+ drawCmd = ellipseCmd(pad, w, h, totalH);
1504
+ }
1505
+ else if (preset === "flowChartOr") {
1506
+ // Circle with + inside — just render as circle
1507
+ drawCmd = ellipseCmd(pad, w, h, totalH);
1508
+ }
1509
+ else if (preset === "flowChartCollate") {
1510
+ // Two triangles forming an hourglass (collate)
1511
+ const cx = pad + w / 2;
1512
+ drawCmd = [
1513
+ // Top triangle (pointing down)
1514
+ ...polyPathLines([[pad, pad], [pad + w, pad], [cx, pad + h / 2]]),
1515
+ "f",
1516
+ // Bottom triangle (pointing up)
1517
+ ...polyPathLines([[pad, pad + h], [pad + w, pad + h], [cx, pad + h / 2]]),
1518
+ "f",
1519
+ ].join("\n");
1520
+ }
1521
+ else if (preset === "flowChartSort") {
1522
+ // Two triangles forming a diamond split horizontally
1523
+ const cx = pad + w / 2, cy = pad + h / 2;
1524
+ drawCmd = [
1525
+ // Top triangle
1526
+ ...polyPathLines([[pad, cy], [pad + w, cy], [cx, pad]]),
1527
+ "f",
1528
+ // Bottom triangle
1529
+ ...polyPathLines([[pad, cy], [pad + w, cy], [cx, pad + h]]),
1530
+ "f",
1531
+ ].join("\n");
1532
+ }
1533
+ else if (preset === "flowChartExtract") {
1534
+ // Upward-pointing triangle
1535
+ drawCmd = polyPath([[pad + w / 2, pad], [pad + w, pad + h], [pad, pad + h]]);
1536
+ }
1537
+ else if (preset === "flowChartMerge") {
1538
+ // Downward-pointing triangle
1539
+ drawCmd = polyPath([[pad, pad], [pad + w, pad], [pad + w / 2, pad + h]]);
1540
+ }
1541
+ else if (preset === "flowChartOnlineStorage") {
1542
+ // Rectangle with curved left side
1543
+ const curveW = w * 0.15;
1544
+ const k = 0.5522847498;
1545
+ const cy = pad + h / 2;
1546
+ drawCmd = [
1547
+ `${pad + curveW} ${py(pad)} m`,
1548
+ `${pad + w} ${py(pad)} l`,
1549
+ `${pad + w} ${py(pad + h)} l`,
1550
+ `${pad + curveW} ${py(pad + h)} l`,
1551
+ // Curved left side (inward arc)
1552
+ `${pad + curveW - curveW * k} ${py(pad + h)} ${pad} ${py(cy + h / 2 * k)} ${pad} ${py(cy)} c`,
1553
+ `${pad} ${py(cy - h / 2 * k)} ${pad + curveW - curveW * k} ${py(pad)} ${pad + curveW} ${py(pad)} c`,
1554
+ "f",
1555
+ ].join("\n");
1556
+ }
1557
+ else if (preset === "flowChartDelay") {
1558
+ // Rectangle with rounded right side (semicircle)
1559
+ const k = 0.5522847498;
1560
+ const cy = pad + h / 2;
1561
+ const ry = h / 2;
1562
+ drawCmd = [
1563
+ `${pad} ${py(pad)} m`,
1564
+ `${pad + w / 2} ${py(pad)} l`,
1565
+ // Right semicircle
1566
+ `${pad + w / 2 + w / 2 * k} ${py(pad)} ${pad + w} ${py(cy - ry * k)} ${pad + w} ${py(cy)} c`,
1567
+ `${pad + w} ${py(cy + ry * k)} ${pad + w / 2 + w / 2 * k} ${py(pad + h)} ${pad + w / 2} ${py(pad + h)} c`,
1568
+ `${pad} ${py(pad + h)} l`,
1569
+ "f",
1570
+ ].join("\n");
1571
+ }
1572
+ else if (preset === "flowChartMagneticTape") {
1573
+ // Circle with a small tail at bottom-right
1574
+ drawCmd = ellipseCmd(pad, w, h, totalH);
1575
+ }
1576
+ else if (preset === "flowChartMagneticDisk") {
1577
+ // Cylinder (horizontal) — same as "can" but oriented differently
1578
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 25000;
1579
+ const capH = h * adj / 200000;
1580
+ const k = 0.5522847498;
1581
+ const cx = pad + w / 2, rx = w / 2;
1582
+ const bodyTop = pad + capH, bodyBot = pad + h - capH;
1583
+ drawCmd = [
1584
+ `${pad} ${py(bodyTop)} m`,
1585
+ `${pad} ${py(bodyBot)} l`,
1586
+ `${pad} ${py(bodyBot + capH * k)} ${cx - rx * k} ${py(pad + h)} ${cx} ${py(pad + h)} c`,
1587
+ `${cx + rx * k} ${py(pad + h)} ${pad + w} ${py(bodyBot + capH * k)} ${pad + w} ${py(bodyBot)} c`,
1588
+ `${pad + w} ${py(bodyTop)} l`,
1589
+ `${pad + w} ${py(bodyTop - capH * k)} ${cx + rx * k} ${py(pad)} ${cx} ${py(pad)} c`,
1590
+ `${cx - rx * k} ${py(pad)} ${pad} ${py(bodyTop - capH * k)} ${pad} ${py(bodyTop)} c`,
1591
+ "f",
1592
+ ].join("\n");
1593
+ }
1594
+ else if (preset === "flowChartMagneticDrum") {
1595
+ // Cylinder on its side — rectangle with curved right side
1596
+ const capW = w * 0.15;
1597
+ const k = 0.5522847498;
1598
+ const cy = pad + h / 2, ry = h / 2;
1599
+ const bodyRight = pad + w - capW;
1600
+ drawCmd = [
1601
+ `${pad} ${py(pad)} m`,
1602
+ `${bodyRight} ${py(pad)} l`,
1603
+ `${bodyRight + capW * k} ${py(pad)} ${pad + w} ${py(cy - ry * k)} ${pad + w} ${py(cy)} c`,
1604
+ `${pad + w} ${py(cy + ry * k)} ${bodyRight + capW * k} ${py(pad + h)} ${bodyRight} ${py(pad + h)} c`,
1605
+ `${pad} ${py(pad + h)} l`,
1606
+ "f",
1607
+ ].join("\n");
1608
+ }
1609
+ else if (preset === "flowChartDisplay") {
1610
+ // Rectangle with pointed left side and rounded right side
1611
+ const pointW = w * 0.15;
1612
+ const k = 0.5522847498;
1613
+ const cy = pad + h / 2;
1614
+ const ry = h / 2;
1615
+ drawCmd = [
1616
+ `${pad} ${py(cy)} m`,
1617
+ `${pad + pointW} ${py(pad)} l`,
1618
+ `${pad + w / 2} ${py(pad)} l`,
1619
+ `${pad + w / 2 + w / 2 * k} ${py(pad)} ${pad + w} ${py(cy - ry * k)} ${pad + w} ${py(cy)} c`,
1620
+ `${pad + w} ${py(cy + ry * k)} ${pad + w / 2 + w / 2 * k} ${py(pad + h)} ${pad + w / 2} ${py(pad + h)} c`,
1621
+ `${pad + pointW} ${py(pad + h)} l`,
1622
+ "f",
1623
+ ].join("\n");
1624
+ }
1625
+ else if (preset === "line" || preset === "straightConnector1") {
1626
+ // Diagonal line from top-left to bottom-right
1627
+ const lw = 1;
1628
+ drawCmd = [
1629
+ `${pad} ${py(pad)} m`,
1630
+ `${pad + w} ${py(pad + h)} l`,
1631
+ `${pad + w + lw} ${py(pad + h)} l`,
1632
+ `${pad + lw} ${py(pad)} l`,
1633
+ "f",
1634
+ ].join("\n");
1635
+ }
1636
+ else if (preset === "bentConnector2") {
1637
+ // L-shaped connector: right then down
1638
+ const lw = 1;
1639
+ drawCmd = [
1640
+ `${pad} ${py(pad)} m`, `${pad + w} ${py(pad)} l`,
1641
+ `${pad + w} ${py(pad + h)} l`, `${pad + w - lw} ${py(pad + h)} l`,
1642
+ `${pad + w - lw} ${py(pad + lw)} l`, `${pad} ${py(pad + lw)} l`,
1643
+ "f",
1644
+ ].join("\n");
1645
+ }
1646
+ else if (preset === "bentConnector3") {
1647
+ // Z-shaped connector with one bend
1648
+ const adj = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 50000;
1649
+ const bendX = w * adj / 100000;
1650
+ const lw = 1;
1651
+ drawCmd = [
1652
+ `${pad} ${py(pad)} m`, `${pad + bendX} ${py(pad)} l`,
1653
+ `${pad + bendX} ${py(pad + h)} l`, `${pad + w} ${py(pad + h)} l`,
1654
+ `${pad + w} ${py(pad + h - lw)} l`, `${pad + bendX + lw} ${py(pad + h - lw)} l`,
1655
+ `${pad + bendX + lw} ${py(pad + lw)} l`, `${pad} ${py(pad + lw)} l`,
1656
+ "f",
1657
+ ].join("\n");
1658
+ }
1659
+ else if (preset === "bentConnector4") {
1660
+ // Connector with two bends
1661
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 50000;
1662
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
1663
+ const bendX = w * adj1 / 100000;
1664
+ const bendY = h * adj2 / 100000;
1665
+ const lw = 1;
1666
+ drawCmd = [
1667
+ `${pad} ${py(pad)} m`, `${pad + bendX} ${py(pad)} l`,
1668
+ `${pad + bendX} ${py(pad + bendY)} l`, `${pad + w} ${py(pad + bendY)} l`,
1669
+ `${pad + w} ${py(pad + h)} l`, `${pad + w - lw} ${py(pad + h)} l`,
1670
+ `${pad + w - lw} ${py(pad + bendY + lw)} l`, `${pad + bendX + lw} ${py(pad + bendY + lw)} l`,
1671
+ `${pad + bendX + lw} ${py(pad + lw)} l`, `${pad} ${py(pad + lw)} l`,
1672
+ "f",
1673
+ ].join("\n");
1674
+ }
1675
+ else if (preset === "bentConnector5") {
1676
+ // Connector with three bends — simplified as Z-shape
1677
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 50000;
1678
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
1679
+ const adj3 = adjustValues?.adj3 ? parseAdjustValue(adjustValues.adj3) : 50000;
1680
+ const bendX1 = w * adj1 / 100000;
1681
+ const bendY = h * adj2 / 100000;
1682
+ const bendX2 = w * adj3 / 100000;
1683
+ const lw = 1;
1684
+ drawCmd = [
1685
+ `${pad} ${py(pad)} m`, `${pad + bendX1} ${py(pad)} l`,
1686
+ `${pad + bendX1} ${py(pad + bendY)} l`, `${pad + bendX2} ${py(pad + bendY)} l`,
1687
+ `${pad + bendX2} ${py(pad + h)} l`, `${pad + w} ${py(pad + h)} l`,
1688
+ `${pad + w} ${py(pad + h - lw)} l`, `${pad + bendX2 + lw} ${py(pad + h - lw)} l`,
1689
+ `${pad + bendX2 + lw} ${py(pad + bendY + lw)} l`, `${pad + bendX1 + lw} ${py(pad + bendY + lw)} l`,
1690
+ `${pad + bendX1 + lw} ${py(pad + lw)} l`, `${pad} ${py(pad + lw)} l`,
1691
+ "f",
1692
+ ].join("\n");
1693
+ }
1694
+ else if (preset === "curvedConnector2") {
1695
+ // Simple curved connector (bezier from top-left to bottom-right)
1696
+ const lw = 1;
1697
+ drawCmd = [
1698
+ `${pad} ${py(pad)} m`,
1699
+ `${pad + w} ${py(pad)} ${pad + w} ${py(pad + h)} ${pad + w} ${py(pad + h)} c`,
1700
+ `${pad + w - lw} ${py(pad + h)} l`,
1701
+ `${pad + w - lw} ${py(pad + h)} ${pad + lw} ${py(pad)} ${pad} ${py(pad)} c`,
1702
+ "f",
1703
+ ].join("\n");
1704
+ }
1705
+ else if (preset === "curvedConnector3") {
1706
+ const adj = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 50000;
1707
+ const midX = w * adj / 100000;
1708
+ const lw = 1;
1709
+ drawCmd = [
1710
+ `${pad} ${py(pad)} m`,
1711
+ `${pad + midX} ${py(pad)} ${pad + midX} ${py(pad + h)} ${pad + w} ${py(pad + h)} c`,
1712
+ `${pad + w} ${py(pad + h - lw)} l`,
1713
+ `${pad + midX + lw} ${py(pad + h - lw)} ${pad + midX + lw} ${py(pad + lw)} ${pad} ${py(pad + lw)} c`,
1714
+ "f",
1715
+ ].join("\n");
1716
+ }
1717
+ else if (preset === "curvedConnector4") {
1718
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 50000;
1719
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
1720
+ const lw = 1;
1721
+ // Simplified S-curve
1722
+ drawCmd = [
1723
+ `${pad} ${py(pad)} m`,
1724
+ `${pad + w * 0.5} ${py(pad)} ${pad + w * 0.5} ${py(pad + h)} ${pad + w} ${py(pad + h)} c`,
1725
+ `${pad + w} ${py(pad + h - lw)} l`,
1726
+ `${pad + w * 0.5 + lw} ${py(pad + h - lw)} ${pad + w * 0.5 + lw} ${py(pad + lw)} ${pad} ${py(pad + lw)} c`,
1727
+ "f",
1728
+ ].join("\n");
1729
+ }
1730
+ else if (preset === "curvedConnector5") {
1731
+ const lw = 1;
1732
+ drawCmd = [
1733
+ `${pad} ${py(pad)} m`,
1734
+ `${pad + w * 0.5} ${py(pad)} ${pad + w * 0.5} ${py(pad + h)} ${pad + w} ${py(pad + h)} c`,
1735
+ `${pad + w} ${py(pad + h - lw)} l`,
1736
+ `${pad + w * 0.5 + lw} ${py(pad + h - lw)} ${pad + w * 0.5 + lw} ${py(pad + lw)} ${pad} ${py(pad + lw)} c`,
1737
+ "f",
1738
+ ].join("\n");
1739
+ }
1740
+ else if (preset === "actionButtonBlank") {
1741
+ // Simple rectangle (blank action button)
1742
+ drawCmd = `${pad} ${py(pad)} m ${pad + w} ${py(pad)} l ${pad + w} ${py(pad + h)} l ${pad} ${py(pad + h)} l f`;
1743
+ }
1744
+ else if (preset === "actionButtonHome" || preset === "actionButtonHelp" ||
1745
+ preset === "actionButtonInformation" || preset === "actionButtonBackPrevious" ||
1746
+ preset === "actionButtonForwardNext" || preset === "actionButtonBeginning" ||
1747
+ preset === "actionButtonEnd" || preset === "actionButtonReturn" ||
1748
+ preset === "actionButtonDocument" || preset === "actionButtonSound" ||
1749
+ preset === "actionButtonMovie") {
1750
+ // All action buttons: rectangle with icon (icon rendering omitted — PDF shows as rect)
1751
+ drawCmd = `${pad} ${py(pad)} m ${pad + w} ${py(pad)} l ${pad + w} ${py(pad + h)} l ${pad} ${py(pad + h)} l f`;
1752
+ }
1753
+ else if (preset === "ribbon") {
1754
+ // Ribbon banner (bottom tabs, main area raised)
1755
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 16667;
1756
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
1757
+ const tabH = h * adj1 / 100000;
1758
+ const tabW = w * adj2 / 200000;
1759
+ drawCmd = polyPath([
1760
+ [pad, pad + tabH],
1761
+ [pad + tabW, pad + tabH],
1762
+ [pad + tabW, pad],
1763
+ [pad + w - tabW, pad],
1764
+ [pad + w - tabW, pad + tabH],
1765
+ [pad + w, pad + tabH],
1766
+ [pad + w, pad + h],
1767
+ [pad + w - tabW, pad + h - tabH],
1768
+ [pad + tabW, pad + h - tabH],
1769
+ [pad, pad + h],
1770
+ ]);
1771
+ }
1772
+ else if (preset === "ribbon2") {
1773
+ // Ribbon2: tabs on top, notch at bottom
1774
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 16667;
1775
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
1776
+ const tabH = h * adj1 / 100000;
1777
+ const tabW = w * adj2 / 200000;
1778
+ drawCmd = polyPath([
1779
+ [pad, pad],
1780
+ [pad + tabW, pad + tabH],
1781
+ [pad + tabW, pad + h],
1782
+ [pad + w - tabW, pad + h],
1783
+ [pad + w - tabW, pad + tabH],
1784
+ [pad + w, pad],
1785
+ [pad + w, pad + h - tabH],
1786
+ [pad + w - tabW, pad + h - tabH],
1787
+ [pad + tabW, pad + h - tabH],
1788
+ [pad, pad + h - tabH],
1789
+ ]);
1790
+ }
1791
+ else if (preset === "ellipseRibbon") {
1792
+ // Ellipse ribbon — simplified as ribbon with curved bottom
1793
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 25000;
1794
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
1795
+ const tabH = h * adj1 / 100000;
1796
+ const tabW = w * adj2 / 200000;
1797
+ drawCmd = polyPath([
1798
+ [pad, pad + tabH],
1799
+ [pad + tabW, pad + tabH],
1800
+ [pad + tabW, pad],
1801
+ [pad + w - tabW, pad],
1802
+ [pad + w - tabW, pad + tabH],
1803
+ [pad + w, pad + tabH],
1804
+ [pad + w, pad + h - tabH],
1805
+ [pad + w * 0.75, pad + h],
1806
+ [pad + w / 2, pad + h - tabH / 2],
1807
+ [pad + w * 0.25, pad + h],
1808
+ [pad, pad + h - tabH],
1809
+ ]);
1810
+ }
1811
+ else if (preset === "ellipseRibbon2") {
1812
+ // Ellipse ribbon 2 — inverted
1813
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 25000;
1814
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
1815
+ const tabH = h * adj1 / 100000;
1816
+ const tabW = w * adj2 / 200000;
1817
+ drawCmd = polyPath([
1818
+ [pad, pad + tabH],
1819
+ [pad + w * 0.25, pad],
1820
+ [pad + w / 2, pad + tabH / 2],
1821
+ [pad + w * 0.75, pad],
1822
+ [pad + w, pad + tabH],
1823
+ [pad + w, pad + h - tabH],
1824
+ [pad + w - tabW, pad + h - tabH],
1825
+ [pad + w - tabW, pad + h],
1826
+ [pad + tabW, pad + h],
1827
+ [pad + tabW, pad + h - tabH],
1828
+ [pad, pad + h - tabH],
1829
+ ]);
1830
+ }
1831
+ else if (preset === "horizontalScroll") {
1832
+ // Scroll shape with rolled ends on left and right
1833
+ const rollR = Math.min(w, h) * 0.12;
1834
+ drawCmd = [
1835
+ // Main body
1836
+ ...polyPathLines([
1837
+ [pad + rollR, pad + rollR],
1838
+ [pad + w - rollR, pad],
1839
+ [pad + w - rollR, pad + h - rollR],
1840
+ [pad + rollR, pad + h],
1841
+ ]),
1842
+ "f",
1843
+ ].join("\n");
1844
+ }
1845
+ else if (preset === "verticalScroll") {
1846
+ // Scroll shape with rolled ends on top and bottom
1847
+ const rollR = Math.min(w, h) * 0.12;
1848
+ drawCmd = [
1849
+ ...polyPathLines([
1850
+ [pad + rollR, pad + rollR],
1851
+ [pad + w, pad + rollR],
1852
+ [pad + w - rollR, pad + h - rollR],
1853
+ [pad, pad + h - rollR],
1854
+ ]),
1855
+ "f",
1856
+ ].join("\n");
1857
+ }
1858
+ else if (preset === "wave") {
1859
+ // Rectangle with wavy top and bottom (sinusoidal)
1860
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 12500;
1861
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 0;
1862
+ const amplitude = h * adj1 / 100000;
1863
+ drawCmd = polyPath([
1864
+ [pad, pad + amplitude],
1865
+ [pad + w * 0.25, pad],
1866
+ [pad + w * 0.5, pad + amplitude],
1867
+ [pad + w * 0.75, pad + amplitude * 2],
1868
+ [pad + w, pad + amplitude],
1869
+ [pad + w, pad + h - amplitude],
1870
+ [pad + w * 0.75, pad + h],
1871
+ [pad + w * 0.5, pad + h - amplitude],
1872
+ [pad + w * 0.25, pad + h - amplitude * 2],
1873
+ [pad, pad + h - amplitude],
1874
+ ]);
1875
+ }
1876
+ else if (preset === "doubleWave") {
1877
+ // Like wave but with double amplitude
1878
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 6250;
1879
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 0;
1880
+ const amplitude = h * adj1 / 100000;
1881
+ drawCmd = polyPath([
1882
+ [pad, pad + amplitude],
1883
+ [pad + w * 0.17, pad], [pad + w * 0.33, pad + amplitude * 2],
1884
+ [pad + w * 0.5, pad], [pad + w * 0.67, pad + amplitude * 2],
1885
+ [pad + w * 0.83, pad], [pad + w, pad + amplitude],
1886
+ [pad + w, pad + h - amplitude],
1887
+ [pad + w * 0.83, pad + h], [pad + w * 0.67, pad + h - amplitude * 2],
1888
+ [pad + w * 0.5, pad + h], [pad + w * 0.33, pad + h - amplitude * 2],
1889
+ [pad + w * 0.17, pad + h], [pad, pad + h - amplitude],
1890
+ ]);
1891
+ }
1892
+ else if (preset === "irregularSeal2") {
1893
+ // Explosion/burst shape 2 — irregular polygon
1894
+ const cx = pad + w / 2, cy = pad + h / 2;
1895
+ drawCmd = polyPath([
1896
+ [pad + w * 0.1, pad],
1897
+ [pad + w * 0.3, pad + h * 0.2],
1898
+ [pad + w * 0.5, pad],
1899
+ [pad + w * 0.6, pad + h * 0.15],
1900
+ [pad + w * 0.85, pad + h * 0.05],
1901
+ [pad + w * 0.8, pad + h * 0.3],
1902
+ [pad + w, pad + h * 0.35],
1903
+ [pad + w * 0.85, pad + h * 0.5],
1904
+ [pad + w * 0.95, pad + h * 0.7],
1905
+ [pad + w * 0.75, pad + h * 0.7],
1906
+ [pad + w * 0.8, pad + h * 0.9],
1907
+ [pad + w * 0.55, pad + h * 0.8],
1908
+ [pad + w * 0.4, pad + h],
1909
+ [pad + w * 0.35, pad + h * 0.75],
1910
+ [pad + w * 0.1, pad + h * 0.85],
1911
+ [pad + w * 0.2, pad + h * 0.65],
1912
+ [pad, pad + h * 0.6],
1913
+ [pad + w * 0.15, pad + h * 0.45],
1914
+ [pad, pad + h * 0.25],
1915
+ [pad + w * 0.15, pad + h * 0.2],
1916
+ ]);
1917
+ }
1918
+ else if (preset === "gear9") {
1919
+ // 9-tooth gear — circle with triangular teeth
1920
+ const cx = pad + w / 2, cy = pad + h / 2;
1921
+ const outerR = Math.min(w, h) / 2;
1922
+ const innerR = outerR * 0.75;
1923
+ const toothW = Math.PI / 18; // half-width of each tooth in radians
1924
+ const pts = [];
1925
+ for (let i = 0; i < 9; i++) {
1926
+ const a = Math.PI * 2 * i / 9 - Math.PI / 2;
1927
+ // Tooth outer points
1928
+ pts.push([cx + Math.cos(a - toothW) * outerR, cy + Math.sin(a - toothW) * outerR]);
1929
+ pts.push([cx + Math.cos(a + toothW) * outerR, cy + Math.sin(a + toothW) * outerR]);
1930
+ // Valley between teeth
1931
+ const va = a + Math.PI / 9;
1932
+ pts.push([cx + Math.cos(va - toothW) * innerR, cy + Math.sin(va - toothW) * innerR]);
1933
+ pts.push([cx + Math.cos(va + toothW) * innerR, cy + Math.sin(va + toothW) * innerR]);
1934
+ }
1935
+ drawCmd = polyPath(pts);
1936
+ }
1937
+ else if (preset === "funnel") {
1938
+ // Funnel: wide at top, narrow at bottom
1939
+ const neckW = w * 0.3;
1940
+ const neckH = h * 0.4;
1941
+ const cx = pad + w / 2;
1942
+ drawCmd = polyPath([
1943
+ [pad, pad],
1944
+ [pad + w, pad],
1945
+ [cx + neckW / 2, pad + h - neckH],
1946
+ [cx + neckW / 2, pad + h],
1947
+ [cx - neckW / 2, pad + h],
1948
+ [cx - neckW / 2, pad + h - neckH],
1949
+ ]);
1950
+ }
1951
+ else if (preset === "leftBrace") {
1952
+ // Left brace { — approximated as connected curves
1953
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 8333;
1954
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
1955
+ const curveR = h * adj1 / 100000;
1956
+ const midY = h * adj2 / 100000;
1957
+ const lw = Math.max(w * 0.15, 1);
1958
+ const k = 0.5522847498;
1959
+ const rightX = pad + w, leftX = pad + w - lw;
1960
+ drawCmd = [
1961
+ `${rightX} ${py(pad)} m`,
1962
+ `${rightX - curveR * k} ${py(pad)} ${pad + w / 2 + lw / 2} ${py(pad + curveR * (1 - k))} ${pad + w / 2 + lw / 2} ${py(pad + curveR)} c`,
1963
+ `${pad + w / 2 + lw / 2} ${py(pad + midY - curveR)} l`,
1964
+ `${pad + w / 2 + lw / 2} ${py(pad + midY - curveR * (1 - k))} ${pad + lw / 2 + curveR * k} ${py(pad + midY)} ${pad + lw / 2} ${py(pad + midY)} c`,
1965
+ `${pad + lw / 2 + curveR * k} ${py(pad + midY)} ${pad + w / 2 + lw / 2} ${py(pad + midY + curveR * (1 - k))} ${pad + w / 2 + lw / 2} ${py(pad + midY + curveR)} c`,
1966
+ `${pad + w / 2 + lw / 2} ${py(pad + h - curveR)} l`,
1967
+ `${pad + w / 2 + lw / 2} ${py(pad + h - curveR * (1 - k))} ${rightX - curveR * k} ${py(pad + h)} ${rightX} ${py(pad + h)} c`,
1968
+ // Inner path going back
1969
+ `${leftX} ${py(pad + h)} l`,
1970
+ `${leftX - curveR * k} ${py(pad + h)} ${pad + w / 2 - lw / 2} ${py(pad + h - curveR * (1 - k))} ${pad + w / 2 - lw / 2} ${py(pad + h - curveR)} c`,
1971
+ `${pad + w / 2 - lw / 2} ${py(pad + midY + curveR)} l`,
1972
+ `${pad + w / 2 - lw / 2} ${py(pad + midY + curveR * (1 - k))} ${pad - lw / 2 + curveR * k} ${py(pad + midY)} ${pad - lw / 2} ${py(pad + midY)} c`,
1973
+ `${pad - lw / 2 + curveR * k} ${py(pad + midY)} ${pad + w / 2 - lw / 2} ${py(pad + midY - curveR * (1 - k))} ${pad + w / 2 - lw / 2} ${py(pad + midY - curveR)} c`,
1974
+ `${pad + w / 2 - lw / 2} ${py(pad + curveR)} l`,
1975
+ `${pad + w / 2 - lw / 2} ${py(pad + curveR * (1 - k))} ${leftX - curveR * k} ${py(pad)} ${leftX} ${py(pad)} c`,
1976
+ "f",
1977
+ ].join("\n");
1978
+ }
1979
+ else if (preset === "rightBrace") {
1980
+ // Right brace } — mirror of leftBrace
1981
+ const adj1 = adjustValues?.adj1 ? parseAdjustValue(adjustValues.adj1) : 8333;
1982
+ const adj2 = adjustValues?.adj2 ? parseAdjustValue(adjustValues.adj2) : 50000;
1983
+ const curveR = h * adj1 / 100000;
1984
+ const midY = h * adj2 / 100000;
1985
+ const lw = Math.max(w * 0.15, 1);
1986
+ const k = 0.5522847498;
1987
+ drawCmd = [
1988
+ `${pad} ${py(pad)} m`,
1989
+ `${pad + curveR * k} ${py(pad)} ${pad + w / 2 - lw / 2} ${py(pad + curveR * (1 - k))} ${pad + w / 2 - lw / 2} ${py(pad + curveR)} c`,
1990
+ `${pad + w / 2 - lw / 2} ${py(pad + midY - curveR)} l`,
1991
+ `${pad + w / 2 - lw / 2} ${py(pad + midY - curveR * (1 - k))} ${pad + w - lw / 2 - curveR * k} ${py(pad + midY)} ${pad + w - lw / 2} ${py(pad + midY)} c`,
1992
+ `${pad + w - lw / 2 - curveR * k} ${py(pad + midY)} ${pad + w / 2 - lw / 2} ${py(pad + midY + curveR * (1 - k))} ${pad + w / 2 - lw / 2} ${py(pad + midY + curveR)} c`,
1993
+ `${pad + w / 2 - lw / 2} ${py(pad + h - curveR)} l`,
1994
+ `${pad + w / 2 - lw / 2} ${py(pad + h - curveR * (1 - k))} ${pad + curveR * k} ${py(pad + h)} ${pad} ${py(pad + h)} c`,
1995
+ `${pad + lw} ${py(pad + h)} l`,
1996
+ `${pad + lw + curveR * k} ${py(pad + h)} ${pad + w / 2 + lw / 2} ${py(pad + h - curveR * (1 - k))} ${pad + w / 2 + lw / 2} ${py(pad + h - curveR)} c`,
1997
+ `${pad + w / 2 + lw / 2} ${py(pad + midY + curveR)} l`,
1998
+ `${pad + w / 2 + lw / 2} ${py(pad + midY + curveR * (1 - k))} ${pad + w + lw / 2 - curveR * k} ${py(pad + midY)} ${pad + w + lw / 2} ${py(pad + midY)} c`,
1999
+ `${pad + w + lw / 2 - curveR * k} ${py(pad + midY)} ${pad + w / 2 + lw / 2} ${py(pad + midY - curveR * (1 - k))} ${pad + w / 2 + lw / 2} ${py(pad + midY - curveR)} c`,
2000
+ `${pad + w / 2 + lw / 2} ${py(pad + curveR)} l`,
2001
+ `${pad + w / 2 + lw / 2} ${py(pad + curveR * (1 - k))} ${pad + lw + curveR * k} ${py(pad)} ${pad + lw} ${py(pad)} c`,
2002
+ "f",
2003
+ ].join("\n");
2004
+ }
2005
+ else if (preset === "leftBracket") {
2006
+ // Left bracket [ — vertical line with horizontal caps
2007
+ const lw = Math.max(w * 0.3, 1);
2008
+ const k = 0.5522847498;
2009
+ const curveR = Math.min(w, h * 0.1);
2010
+ drawCmd = [
2011
+ `${pad + w} ${py(pad)} m`,
2012
+ `${pad + curveR} ${py(pad)} l`,
2013
+ `${pad + curveR * (1 - k)} ${py(pad)} ${pad} ${py(pad + curveR * (1 - k))} ${pad} ${py(pad + curveR)} c`,
2014
+ `${pad} ${py(pad + h - curveR)} l`,
2015
+ `${pad} ${py(pad + h - curveR * (1 - k))} ${pad + curveR * (1 - k)} ${py(pad + h)} ${pad + curveR} ${py(pad + h)} c`,
2016
+ `${pad + w} ${py(pad + h)} l`,
2017
+ `${pad + w} ${py(pad + h - lw)} l`,
2018
+ `${pad + lw} ${py(pad + h - lw)} l`,
2019
+ `${pad + lw} ${py(pad + lw)} l`,
2020
+ `${pad + w} ${py(pad + lw)} l`,
2021
+ "f",
2022
+ ].join("\n");
2023
+ }
2024
+ else if (preset === "rightBracket") {
2025
+ // Right bracket ]
2026
+ const lw = Math.max(w * 0.3, 1);
2027
+ const k = 0.5522847498;
2028
+ const curveR = Math.min(w, h * 0.1);
2029
+ drawCmd = [
2030
+ `${pad} ${py(pad)} m`,
2031
+ `${pad + w - curveR} ${py(pad)} l`,
2032
+ `${pad + w - curveR * (1 - k)} ${py(pad)} ${pad + w} ${py(pad + curveR * (1 - k))} ${pad + w} ${py(pad + curveR)} c`,
2033
+ `${pad + w} ${py(pad + h - curveR)} l`,
2034
+ `${pad + w} ${py(pad + h - curveR * (1 - k))} ${pad + w - curveR * (1 - k)} ${py(pad + h)} ${pad + w - curveR} ${py(pad + h)} c`,
2035
+ `${pad} ${py(pad + h)} l`,
2036
+ `${pad} ${py(pad + h - lw)} l`,
2037
+ `${pad + w - lw} ${py(pad + h - lw)} l`,
2038
+ `${pad + w - lw} ${py(pad + lw)} l`,
2039
+ `${pad} ${py(pad + lw)} l`,
2040
+ "f",
2041
+ ].join("\n");
2042
+ }
2043
+ else if (preset === "bracePair") {
2044
+ // Pair of braces { } — left and right brace together
2045
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 8333;
2046
+ const curveR = Math.min(w, h) * adj / 100000;
2047
+ const k = 0.5522847498;
2048
+ const midY = pad + h / 2;
2049
+ // Simplified: render as rounded rectangle with points at middle-left and middle-right
2050
+ drawCmd = [
2051
+ `${pad + curveR} ${py(pad)} m`,
2052
+ `${pad + w - curveR} ${py(pad)} l`,
2053
+ `${pad + w - curveR * (1 - k)} ${py(pad)} ${pad + w} ${py(pad + curveR * (1 - k))} ${pad + w} ${py(pad + curveR)} c`,
2054
+ `${pad + w} ${py(midY - curveR)} l`,
2055
+ `${pad + w} ${py(midY)} l`, // Right point
2056
+ `${pad + w} ${py(midY + curveR)} l`,
2057
+ `${pad + w} ${py(pad + h - curveR)} l`,
2058
+ `${pad + w} ${py(pad + h - curveR * (1 - k))} ${pad + w - curveR * (1 - k)} ${py(pad + h)} ${pad + w - curveR} ${py(pad + h)} c`,
2059
+ `${pad + curveR} ${py(pad + h)} l`,
2060
+ `${pad + curveR * (1 - k)} ${py(pad + h)} ${pad} ${py(pad + h - curveR * (1 - k))} ${pad} ${py(pad + h - curveR)} c`,
2061
+ `${pad} ${py(midY + curveR)} l`,
2062
+ `${pad} ${py(midY)} l`, // Left point
2063
+ `${pad} ${py(midY - curveR)} l`,
2064
+ `${pad} ${py(pad + curveR)} l`,
2065
+ `${pad} ${py(pad + curveR * (1 - k))} ${pad + curveR * (1 - k)} ${py(pad)} ${pad + curveR} ${py(pad)} c`,
2066
+ "f",
2067
+ ].join("\n");
2068
+ }
2069
+ else if (preset === "bracketPair") {
2070
+ // Pair of brackets [ ] — rounded rectangle
2071
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 16667;
2072
+ const radius = Math.min(w, h) * adj / 100000;
2073
+ const x0 = pad, y0 = totalH - pad - h, x1 = pad + w, y1 = totalH - pad;
2074
+ drawCmd = [
2075
+ `${x0 + radius} ${y0} m`, `${x1 - radius} ${y0} l`,
2076
+ `${x1} ${y0} ${x1} ${y0 + radius} v`, `${x1} ${y1 - radius} l`,
2077
+ `${x1} ${y1} ${x1 - radius} ${y1} v`, `${x0 + radius} ${y1} l`,
2078
+ `${x0} ${y1} ${x0} ${y1 - radius} v`, `${x0} ${y0 + radius} l`,
2079
+ `${x0} ${y0} ${x0 + radius} ${y0} v`, "f",
2080
+ ].join("\n");
2081
+ }
2082
+ else {
2083
+ // Fallback: filled rectangle
2084
+ drawCmd = `${pad} ${pad} ${w} ${h} re f`;
2085
+ }
2086
+ return drawCmd;
2087
+ }
2088
+ /** Wrap a PDF content stream into a minimal PDF 1.4 document. */
2089
+ export function buildPdf(totalW, totalH, stream) {
2090
+ const streamBytes = Buffer.from(stream, "ascii");
2091
+ const objects = [
2092
+ `1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj`,
2093
+ `2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj`,
2094
+ `3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${totalW} ${totalH}] /Contents 4 0 R >>\nendobj`,
2095
+ `4 0 obj\n<< /Length ${streamBytes.length} >>\nstream\n${stream}\nendstream\nendobj`,
2096
+ ];
2097
+ const header = "%PDF-1.4\n";
2098
+ let body = "";
2099
+ const offsets = [];
2100
+ for (const obj of objects) {
2101
+ offsets.push(header.length + body.length);
2102
+ body += obj + "\n";
2103
+ }
2104
+ const xrefOffset = header.length + body.length;
2105
+ let xref = `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`;
2106
+ for (const off of offsets) {
2107
+ xref += `${String(off).padStart(10, "0")} 00000 n \n`;
2108
+ }
2109
+ const trailer = `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF`;
2110
+ return Buffer.from(header + body + xref + trailer, "ascii");
2111
+ }
2112
+ export function mapConnector(conn, attachments, ctx) {
2113
+ const b = conn.bounds;
2114
+ if (!b)
2115
+ return "";
2116
+ const x = emuToPx(b.x);
2117
+ const y = emuToPx(b.y);
2118
+ let w = emuToPx(b.cx);
2119
+ let h = emuToPx(b.cy);
2120
+ // OfficeImport accounts for stroke when computing connector PDF bounds:
2121
+ // - Y position is offset by floor(strokeWidth/2) for the stroke extending above origin
2122
+ // - Zero-extent dimensions get minimum 1px (for the stroke), with the other axis reduced by 1
2123
+ const strokePx = emuToPx(conn.stroke?.width ?? 0);
2124
+ const strokeYOffset = Math.floor(strokePx / 2);
2125
+ if (h === 0 && w > 0) {
2126
+ h = 1;
2127
+ w -= 1;
2128
+ }
2129
+ else if (w === 0 && h > 0) {
2130
+ w = 1;
2131
+ h -= 1;
2132
+ }
2133
+ // Connectors are always rendered as PDF images by OfficeImport (like non-rect shapes)
2134
+ const fillColor = fillToColor(conn.fill, ctx) ?? "#000000";
2135
+ const geom = conn.geometry?.preset ?? "line";
2136
+ const pdf = generateShapePdf(w, h, geom, fillColor, conn.geometry?.adjustValues);
2137
+ const imgX = x - PDF_PADDING;
2138
+ const imgY = y - strokeYOffset - PDF_PADDING;
2139
+ const imgW = w + PDF_PADDING * 2;
2140
+ const imgH = h + PDF_PADDING * 2;
2141
+ const idx = nextAttachmentIndex();
2142
+ const name = `Attachment${idx}.pdf`;
2143
+ attachments.set(name, pdf);
2144
+ return `<img src="${name}" style="position:absolute; top:${imgY}; left:${imgX}; width:${imgW}; height:${imgH};">`;
2145
+ }
2146
+ /**
2147
+ * Compute the inscribed text frame for a geometry preset (shapeTextBoxRect in OfficeImport).
2148
+ * OfficeImport computes inset using float px values for radius, then truncates dimensions.
2149
+ * Input/output in EMU, but computation happens in pixel space to match QL's rounding.
2150
+ */
2151
+ export function shapeTextBox(bounds, preset, adjustValues) {
2152
+ const { x, y, cx, cy } = bounds;
2153
+ const EMU = 12700;
2154
+ // Float px values (not truncated) — OfficeImport uses these for radius/inset calc
2155
+ const wF = cx / EMU;
2156
+ const hF = cy / EMU;
2157
+ // Truncated px values — OfficeImport uses these for final dimensions
2158
+ const wT = Math.trunc(wF);
2159
+ const hT = Math.trunc(hF);
2160
+ if (preset === "roundRect") {
2161
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 16667;
2162
+ const radius = Math.min(wF, hF) * adj / 100000;
2163
+ const inset = radius * (1 - Math.cos(Math.PI / 4));
2164
+ const rw = Math.trunc(wT - 2 * inset);
2165
+ const rh = Math.trunc(hT - 2 * inset);
2166
+ return { x: x + inset * EMU, y: y + inset * EMU, cx: rw * EMU, cy: rh * EMU };
2167
+ }
2168
+ if (preset === "ellipse") {
2169
+ const insetX = (wT - wT / Math.SQRT2) / 2;
2170
+ const insetY = (hT - hT / Math.SQRT2) / 2;
2171
+ const rw = Math.trunc(wT - 2 * insetX);
2172
+ const rh = Math.trunc(hT - 2 * insetY);
2173
+ return { x: x + insetX * EMU, y: y + insetY * EMU, cx: rw * EMU, cy: rh * EMU };
2174
+ }
2175
+ if (preset === "diamond") {
2176
+ return { x: x + cx / 4, y: y + cy / 4, cx: cx / 2, cy: cy / 2 };
2177
+ }
2178
+ if (preset === "triangle") {
2179
+ return { x: x + cx / 4, y: y + cy / 2, cx: cx / 2, cy: cy / 2 };
2180
+ }
2181
+ if (preset === "hexagon") {
2182
+ const adj = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : 25000;
2183
+ const insetX = cx * adj / 100000;
2184
+ const insetY = cy * adj / 200000;
2185
+ return { x: x + insetX, y: y + insetY, cx: cx - 2 * insetX, cy: cy - 2 * insetY };
2186
+ }
2187
+ // Star shapes: text frame inscribed within the inner polygon/circle.
2188
+ // Empirically verified against QL output for star4 and star5.
2189
+ const starMatch = preset.match(/^star(\d+)$/);
2190
+ if (starMatch) {
2191
+ const n = parseInt(starMatch[1], 10);
2192
+ const adjMap = { 4: 12500, 5: 19098, 6: 23170, 7: 19098, 8: 17678 };
2193
+ const adjVal = adjustValues?.adj ? parseAdjustValue(adjustValues.adj) : (adjMap[n] ?? 19098);
2194
+ const innerR = Math.min(wF, hF) / 2 * adjVal / 50000;
2195
+ if (n === 4) {
2196
+ // Star4: inner diamond → axis-aligned bbox of 4 inner points at 45° offsets
2197
+ const side = Math.trunc(innerR * Math.SQRT2 * 2);
2198
+ const insetX = Math.trunc(wF / 2 - innerR * Math.cos(Math.PI / 4));
2199
+ const insetY = Math.trunc(hF / 2 - innerR * Math.cos(Math.PI / 4));
2200
+ return { x: x + insetX * EMU, y: y + insetY * EMU, cx: side * EMU, cy: side * EMU };
2201
+ }
2202
+ if (n === 5) {
2203
+ // Star5: inscribed square using inner circle diameter.
2204
+ // textY_rel = textH (text starts at textH from top — empirically confirmed)
2205
+ const side = Math.trunc(2 * innerR);
2206
+ const insetX = Math.trunc(wF / 2 - innerR);
2207
+ return { x: x + insetX * EMU, y: y + side * EMU, cx: side * EMU, cy: side * EMU };
2208
+ }
2209
+ // Other star shapes: use inner circle diameter as inscribed square, centered
2210
+ const side = Math.trunc(2 * innerR);
2211
+ const insetX = Math.trunc(wF / 2 - innerR);
2212
+ const insetY = Math.trunc(hF / 2 - innerR);
2213
+ return { x: x + insetX * EMU, y: y + insetY * EMU, cx: side * EMU, cy: side * EMU };
2214
+ }
2215
+ // Default: use full bounds
2216
+ return { x, y, cx, cy };
2217
+ }
2218
+ function parseAdjustValue(val) {
2219
+ // Adjust values come as "val 4000" — extract the number
2220
+ const m = val.match(/\d+/);
2221
+ return m ? parseInt(m[0], 10) : 16667;
2222
+ }
2223
+ /** Maps OOXML gradient angle (60000ths of a degree) to -webkit-gradient from/to points */
2224
+ function gradientPoints(angleDeg60k) {
2225
+ const a = ((angleDeg60k / 60000) % 360 + 360) % 360;
2226
+ if (a < 22.5 || a >= 337.5)
2227
+ return { from: "left top", to: "right top" };
2228
+ if (a < 67.5)
2229
+ return { from: "left bottom", to: "right top" };
2230
+ if (a < 112.5)
2231
+ return { from: "left bottom", to: "left top" };
2232
+ if (a < 157.5)
2233
+ return { from: "right bottom", to: "left top" };
2234
+ if (a < 202.5)
2235
+ return { from: "right top", to: "left top" };
2236
+ if (a < 247.5)
2237
+ return { from: "right top", to: "left bottom" };
2238
+ if (a < 292.5)
2239
+ return { from: "left top", to: "left bottom" };
2240
+ return { from: "left top", to: "right bottom" };
2241
+ }
2242
+ /** Returns CSS property string (e.g. "background-color:#hex" or "background-image:-webkit-gradient(...)") */
2243
+ function fillToCss(fill, ctx, attachments) {
2244
+ if (!fill)
2245
+ return null;
2246
+ if (fill.type === "solid") {
2247
+ return `background-color:${rgbaToColor(resolveColor(fill.color, ctx.colorMap, ctx.colorScheme))}`;
2248
+ }
2249
+ if (fill.type === "gradient" && fill.stops.length > 0) {
2250
+ if (fill.stops.length === 2) {
2251
+ // 2-stop: -webkit-gradient (matches OfficeImport's old WebKit syntax)
2252
+ const c1 = rgbaToColor(resolveColor(fill.stops[0].color, ctx.colorMap, ctx.colorScheme));
2253
+ const c2 = rgbaToColor(resolveColor(fill.stops[1].color, ctx.colorMap, ctx.colorScheme));
2254
+ const { from, to } = gradientPoints(fill.linear?.angle ?? 5400000);
2255
+ return `background-image:-webkit-gradient(linear, ${from}, ${to}, from(${c1}), to(${c2}))`;
2256
+ }
2257
+ // 3+ stops: average all stop colors to single background-color
2258
+ let r = 0, g = 0, b = 0;
2259
+ for (const stop of fill.stops) {
2260
+ const c = resolveColor(stop.color, ctx.colorMap, ctx.colorScheme);
2261
+ r += c.r;
2262
+ g += c.g;
2263
+ b += c.b;
2264
+ }
2265
+ const n = fill.stops.length;
2266
+ return `background-color:${rgbaToColor({ r: Math.round(r / n), g: Math.round(g / n), b: Math.round(b / n), a: 1 })}`;
2267
+ }
2268
+ if (fill.type === "image" && fill.blipData && attachments) {
2269
+ // Image fill: OfficeImport renders as background-image with background-size:100% 100%
2270
+ const ext = detectFillImageExt(fill.blipData);
2271
+ const idx = nextFillAttachmentIndex();
2272
+ const name = `Attachment${idx}.${ext}`;
2273
+ attachments.set(name, fill.blipData);
2274
+ return `background-image:url(${name}); background-size:100% 100%; background-repeat:no-repeat`;
2275
+ }
2276
+ if (fill.type === "noFill")
2277
+ return null;
2278
+ return null;
2279
+ }
2280
+ let fillAttachmentCounter = 1000; // offset to avoid collision with image attachments
2281
+ function nextFillAttachmentIndex() { return fillAttachmentCounter++; }
2282
+ function detectFillImageExt(buf) {
2283
+ if (buf[0] === 0x89 && buf[1] === 0x50)
2284
+ return "png";
2285
+ if (buf[0] === 0xFF && buf[1] === 0xD8)
2286
+ return "jpeg";
2287
+ if (buf[0] === 0x47 && buf[1] === 0x49)
2288
+ return "gif";
2289
+ return "png";
2290
+ }
2291
+ /**
2292
+ * Resolve stroke info for PDF rendering.
2293
+ * OfficeImport CMDrawingContext always uses fill+stroke (B operator).
2294
+ * Explicit <a:ln><a:solidFill> → declared color + width.
2295
+ * No explicit stroke → undefined (generateShapePdf uses thin black default).
2296
+ */
2297
+ function resolveStrokeForPdf(shape, ctx) {
2298
+ const stroke = shape.stroke;
2299
+ if (!stroke?.fill)
2300
+ return undefined; // no explicit stroke → use default
2301
+ if (stroke.fill.type === "solid") {
2302
+ const color = rgbaToColor(resolveColor(stroke.fill.color, ctx.colorMap, ctx.colorScheme));
2303
+ const width = stroke.width ? emuToPx(stroke.width) : 1;
2304
+ return { color, width: Math.max(width, 0.5) };
2305
+ }
2306
+ return undefined;
2307
+ }
2308
+ /** Returns just the color string (for PDF fill, which only needs a single color) */
2309
+ export function fillToColor(fill, ctx) {
2310
+ if (!fill)
2311
+ return null;
2312
+ if (fill.type === "solid") {
2313
+ return rgbaToColor(resolveColor(fill.color, ctx.colorMap, ctx.colorScheme));
2314
+ }
2315
+ if (fill.type === "gradient" && fill.stops.length > 0) {
2316
+ return rgbaToColor(resolveColor(fill.stops[0].color, ctx.colorMap, ctx.colorScheme));
2317
+ }
2318
+ if (fill.type === "noFill")
2319
+ return null;
2320
+ return null;
2321
+ }
2322
+ /** Resolve stroke from theme style matrix via shapeStyle.lnRef. */
2323
+ function resolveThemeStroke(shape, ctx) {
2324
+ const lnRef = shape.shapeStyle?.lnRef;
2325
+ if (!lnRef || lnRef.idx <= 0 || !ctx.styleMatrix)
2326
+ return undefined;
2327
+ const themeStroke = ctx.styleMatrix.lineStyles[lnRef.idx - 1]; // 1-indexed
2328
+ if (!themeStroke)
2329
+ return undefined;
2330
+ // If theme stroke has a placeholder color (schemeClr phClr), use the lnRef override color
2331
+ if (lnRef.color && themeStroke.fill?.type === "solid") {
2332
+ return { ...themeStroke, fill: { type: "solid", color: lnRef.color } };
2333
+ }
2334
+ return themeStroke;
2335
+ }
2336
+ /** Compute normalized AABB (0..1) for geometry presets that don't fill the full bounding box. */
2337
+ function geometryAABB(preset) {
2338
+ const starMatch = preset.match(/^star(\d+)$/);
2339
+ if (starMatch) {
2340
+ const n = parseInt(starMatch[1], 10);
2341
+ const adjMap = { 4: 12500, 5: 19098, 6: 23170, 7: 19098, 8: 17678 };
2342
+ const adj = adjMap[n] ?? 19098;
2343
+ const outerR = 0.5;
2344
+ const innerR = outerR * adj / 50000;
2345
+ let minX = 1, maxX = 0, minY = 1, maxY = 0;
2346
+ for (let i = 0; i < n; i++) {
2347
+ const outerAngle = (-90 + i * (360 / n)) * Math.PI / 180;
2348
+ const ox = 0.5 + outerR * Math.cos(outerAngle);
2349
+ const oy = 0.5 + outerR * Math.sin(outerAngle);
2350
+ minX = Math.min(minX, ox);
2351
+ maxX = Math.max(maxX, ox);
2352
+ minY = Math.min(minY, oy);
2353
+ maxY = Math.max(maxY, oy);
2354
+ const innerAngle = (-90 + (360 / n / 2) + i * (360 / n)) * Math.PI / 180;
2355
+ const ix = 0.5 + innerR * Math.cos(innerAngle);
2356
+ const iy = 0.5 + innerR * Math.sin(innerAngle);
2357
+ minX = Math.min(minX, ix);
2358
+ maxX = Math.max(maxX, ix);
2359
+ minY = Math.min(minY, iy);
2360
+ maxY = Math.max(maxY, iy);
2361
+ }
2362
+ return { minX, minY, maxX, maxY };
2363
+ }
2364
+ return { minX: 0, minY: 0, maxX: 1, maxY: 1 };
2365
+ }
2366
+ function rgbaToColor(c) {
2367
+ if (c.a < 1)
2368
+ return `rgba(${c.r},${c.g},${c.b},${c.a.toFixed(2)})`;
2369
+ return `#${hex2(c.r)}${hex2(c.g)}${hex2(c.b)}`;
2370
+ }
2371
+ function hex2(n) {
2372
+ return n.toString(16).padStart(2, "0");
2373
+ }
2374
+ /** Map OOXML dash type to CSS border-style. OfficeImport maps these to simple CSS styles. */
2375
+ function dashToCssBorderStyle(dash) {
2376
+ if (!dash || dash === "solid")
2377
+ return "solid";
2378
+ if (dash === "dot" || dash === "sysDot")
2379
+ return "dotted";
2380
+ if (dash === "dash" || dash === "lgDash" || dash === "sysDash")
2381
+ return "dashed";
2382
+ // Compound dash patterns → dashed as closest CSS equivalent
2383
+ return "dashed";
2384
+ }