@sliiu/present-renderer 0.0.0-stage → 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 (126) hide show
  1. package/COPYRIGHT +5 -0
  2. package/LICENSE +661 -0
  3. package/THIRD-PARTY-NOTICES.md +221 -0
  4. package/dist/cjs/arrow-geom.js +38 -0
  5. package/dist/cjs/canvas-backend.js +1368 -0
  6. package/dist/cjs/cell-borders.js +25 -0
  7. package/dist/cjs/chart-canvas.js +1059 -0
  8. package/dist/cjs/chart-ex-render.js +275 -0
  9. package/dist/cjs/chart-geom.js +313 -0
  10. package/dist/cjs/chart-svg.js +1110 -0
  11. package/dist/cjs/compose.js +74 -0
  12. package/dist/cjs/effect-clip.js +652 -0
  13. package/dist/cjs/entrance-hide.js +63 -0
  14. package/dist/cjs/font-stack.js +103 -0
  15. package/dist/cjs/hit-geom.js +50 -0
  16. package/dist/cjs/hybrid-backend.js +93 -0
  17. package/dist/cjs/hybrid-segments.js +54 -0
  18. package/dist/cjs/index.js +70 -0
  19. package/dist/cjs/linked-src.js +116 -0
  20. package/dist/cjs/metafile-preview.js +142 -0
  21. package/dist/cjs/model3d-frame.js +199 -0
  22. package/dist/cjs/model3d-render.js +78 -0
  23. package/dist/cjs/morph-transition.js +57 -0
  24. package/dist/cjs/node-affine.js +79 -0
  25. package/dist/cjs/ole-render.js +291 -0
  26. package/dist/cjs/package.json +1 -0
  27. package/dist/cjs/paint-canvas.js +701 -0
  28. package/dist/cjs/paint-svg.js +674 -0
  29. package/dist/cjs/scene-node.js +762 -0
  30. package/dist/cjs/selection.js +130 -0
  31. package/dist/cjs/shape-effects.js +66 -0
  32. package/dist/cjs/shape-front.js +38 -0
  33. package/dist/cjs/stage-chrome.js +15 -0
  34. package/dist/cjs/svg-backend.js +2172 -0
  35. package/dist/cjs/text-autofit.js +48 -0
  36. package/dist/cjs/text-hit.js +284 -0
  37. package/dist/cjs/text-layout.js +408 -0
  38. package/dist/cjs/text-shared.js +238 -0
  39. package/dist/cjs/text-stacked.js +82 -0
  40. package/dist/cjs/text-warp.js +377 -0
  41. package/dist/cjs/text-wordart.js +33 -0
  42. package/dist/cjs/video-frame.js +113 -0
  43. package/dist/cjs/webgl-model3d-backend.js +285 -0
  44. package/dist/cjs/webgl-shape-backend.js +360 -0
  45. package/dist/esm/arrow-geom.js +33 -0
  46. package/dist/esm/canvas-backend.js +1363 -0
  47. package/dist/esm/cell-borders.js +21 -0
  48. package/dist/esm/chart-canvas.js +1056 -0
  49. package/dist/esm/chart-ex-render.js +261 -0
  50. package/dist/esm/chart-geom.js +296 -0
  51. package/dist/esm/chart-svg.js +1107 -0
  52. package/dist/esm/compose.js +71 -0
  53. package/dist/esm/effect-clip.js +648 -0
  54. package/dist/esm/entrance-hide.js +59 -0
  55. package/dist/esm/font-stack.js +100 -0
  56. package/dist/esm/hit-geom.js +45 -0
  57. package/dist/esm/hybrid-backend.js +89 -0
  58. package/dist/esm/hybrid-segments.js +51 -0
  59. package/dist/esm/index.js +24 -0
  60. package/dist/esm/linked-src.js +109 -0
  61. package/dist/esm/metafile-preview.js +135 -0
  62. package/dist/esm/model3d-frame.js +158 -0
  63. package/dist/esm/model3d-render.js +73 -0
  64. package/dist/esm/morph-transition.js +54 -0
  65. package/dist/esm/node-affine.js +71 -0
  66. package/dist/esm/ole-render.js +286 -0
  67. package/dist/esm/paint-canvas.js +693 -0
  68. package/dist/esm/paint-svg.js +670 -0
  69. package/dist/esm/scene-node.js +750 -0
  70. package/dist/esm/selection.js +122 -0
  71. package/dist/esm/shape-effects.js +60 -0
  72. package/dist/esm/shape-front.js +35 -0
  73. package/dist/esm/stage-chrome.js +12 -0
  74. package/dist/esm/svg-backend.js +2168 -0
  75. package/dist/esm/text-autofit.js +44 -0
  76. package/dist/esm/text-hit.js +280 -0
  77. package/dist/esm/text-layout.js +400 -0
  78. package/dist/esm/text-shared.js +221 -0
  79. package/dist/esm/text-stacked.js +78 -0
  80. package/dist/esm/text-warp.js +371 -0
  81. package/dist/esm/text-wordart.js +30 -0
  82. package/dist/esm/video-frame.js +109 -0
  83. package/dist/esm/webgl-model3d-backend.js +248 -0
  84. package/dist/esm/webgl-shape-backend.js +323 -0
  85. package/dist/types/arrow-geom.d.ts +10 -0
  86. package/dist/types/canvas-backend.d.ts +55 -0
  87. package/dist/types/cell-borders.d.ts +9 -0
  88. package/dist/types/chart-canvas.d.ts +2 -0
  89. package/dist/types/chart-ex-render.d.ts +79 -0
  90. package/dist/types/chart-geom.d.ts +112 -0
  91. package/dist/types/chart-svg.d.ts +2 -0
  92. package/dist/types/compose.d.ts +2 -0
  93. package/dist/types/effect-clip.d.ts +10 -0
  94. package/dist/types/entrance-hide.d.ts +16 -0
  95. package/dist/types/font-stack.d.ts +9 -0
  96. package/dist/types/hit-geom.d.ts +15 -0
  97. package/dist/types/hybrid-backend.d.ts +21 -0
  98. package/dist/types/hybrid-segments.d.ts +11 -0
  99. package/dist/types/index.d.ts +24 -0
  100. package/dist/types/linked-src.d.ts +13 -0
  101. package/dist/types/metafile-preview.d.ts +11 -0
  102. package/dist/types/model3d-frame.d.ts +8 -0
  103. package/dist/types/model3d-render.d.ts +4 -0
  104. package/dist/types/morph-transition.d.ts +15 -0
  105. package/dist/types/node-affine.d.ts +22 -0
  106. package/dist/types/ole-render.d.ts +7 -0
  107. package/dist/types/paint-canvas.d.ts +22 -0
  108. package/dist/types/paint-svg.d.ts +19 -0
  109. package/dist/types/scene-node.d.ts +128 -0
  110. package/dist/types/selection.d.ts +22 -0
  111. package/dist/types/shape-effects.d.ts +13 -0
  112. package/dist/types/shape-front.d.ts +4 -0
  113. package/dist/types/stage-chrome.d.ts +8 -0
  114. package/dist/types/svg-backend.d.ts +88 -0
  115. package/dist/types/text-autofit.d.ts +7 -0
  116. package/dist/types/text-hit.d.ts +7 -0
  117. package/dist/types/text-layout.d.ts +35 -0
  118. package/dist/types/text-shared.d.ts +86 -0
  119. package/dist/types/text-stacked.d.ts +11 -0
  120. package/dist/types/text-warp.d.ts +37 -0
  121. package/dist/types/text-wordart.d.ts +18 -0
  122. package/dist/types/video-frame.d.ts +2 -0
  123. package/dist/types/webgl-model3d-backend.d.ts +25 -0
  124. package/dist/types/webgl-shape-backend.d.ts +33 -0
  125. package/package.json +33 -4
  126. package/README.md +0 -3
@@ -0,0 +1,158 @@
1
+ import * as THREE from "three";
2
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
3
+ const gltfCache = new Map();
4
+ export function loadGltfScene(src) {
5
+ let hit = gltfCache.get(src);
6
+ if (!hit) {
7
+ const loader = new GLTFLoader();
8
+ hit = new Promise((resolve, reject) => {
9
+ loader.load(src, (gltf) => {
10
+ const root = gltf.scene;
11
+ const box = new THREE.Box3().setFromObject(root);
12
+ const size = new THREE.Vector3();
13
+ const center = new THREE.Vector3();
14
+ box.getSize(size);
15
+ box.getCenter(center);
16
+ root.position.sub(center);
17
+ const maxDim = Math.max(size.x, size.y, size.z, 1e-6);
18
+ root.scale.setScalar(1 / maxDim);
19
+ resolve(root);
20
+ }, undefined, reject);
21
+ });
22
+ gltfCache.set(src, hit);
23
+ }
24
+ return hit;
25
+ }
26
+ function deg(v) {
27
+ return ((v ?? 0) * Math.PI) / 180;
28
+ }
29
+ export function applyModel3DCameraPose(cam, latDeg, lonDeg, dist = 2.8) {
30
+ const lat = deg(latDeg);
31
+ const lon = deg(lonDeg);
32
+ cam.position.set(dist * Math.cos(lat) * Math.sin(lon), dist * Math.sin(lat), dist * Math.cos(lat) * Math.cos(lon));
33
+ cam.lookAt(0, 0, 0);
34
+ cam.updateProjectionMatrix();
35
+ }
36
+ export function applyModel3DCamera(cam, s) {
37
+ applyModel3DCameraPose(cam, s.camera?.rot?.lat ?? 15, s.camera?.rot?.lon ?? 25);
38
+ }
39
+ export function applyModel3DTransform(root, s) {
40
+ const t = s.modelTransform;
41
+ if (!t)
42
+ return;
43
+ const sc = t.scale;
44
+ if (sc)
45
+ root.scale.multiply(new THREE.Vector3(sc.x || 1, sc.y || 1, sc.z || 1));
46
+ const r = t.rot;
47
+ if (r)
48
+ root.rotation.set(deg(r.lat), deg(r.lon), deg(r.rev), "YXZ");
49
+ const o = t.off;
50
+ if (o)
51
+ root.position.add(new THREE.Vector3(o.x, o.y, o.z));
52
+ }
53
+ const previewCache = new Map();
54
+ let previewRenderer = null;
55
+ let previewRendererFailed = false;
56
+ function getPreviewRenderer() {
57
+ if (previewRenderer)
58
+ return previewRenderer;
59
+ if (previewRendererFailed)
60
+ return null;
61
+ if (typeof document === "undefined") {
62
+ previewRendererFailed = true;
63
+ return null;
64
+ }
65
+ try {
66
+ const r = new THREE.WebGLRenderer({ alpha: true, antialias: true, preserveDrawingBuffer: true });
67
+ r.setPixelRatio(1);
68
+ r.setClearColor(0x000000, 0);
69
+ r.outputColorSpace = THREE.SRGBColorSpace;
70
+ previewRenderer = r;
71
+ }
72
+ catch {
73
+ previewRendererFailed = true;
74
+ }
75
+ return previewRenderer;
76
+ }
77
+ function previewKey(s) {
78
+ return JSON.stringify({
79
+ src: s.glbSrc,
80
+ cam: s.camera ?? null,
81
+ xf: s.modelTransform ?? null,
82
+ ar: s.bbox.h > 0 ? Math.round((s.bbox.w / s.bbox.h) * 1000) / 1000 : 1,
83
+ });
84
+ }
85
+ function rasterize(s, root) {
86
+ const renderer = getPreviewRenderer();
87
+ if (!renderer)
88
+ return null;
89
+ const aspect = s.bbox.h > 0 ? s.bbox.w / s.bbox.h : 1;
90
+ const w = 256;
91
+ const h = Math.max(1, Math.round(w / Math.max(aspect, 0.2)));
92
+ const scene = new THREE.Scene();
93
+ const clone = root.clone(true);
94
+ applyModel3DTransform(clone, s);
95
+ scene.add(clone);
96
+ const light = new THREE.DirectionalLight(0xffffff, 1.1);
97
+ light.position.set(2, 3, 4);
98
+ scene.add(light);
99
+ scene.add(new THREE.AmbientLight(0xffffff, 0.45));
100
+ const cam = new THREE.PerspectiveCamera(35, w / h, 0.01, 100);
101
+ applyModel3DCamera(cam, s);
102
+ renderer.setSize(w, h, false);
103
+ renderer.render(scene, cam);
104
+ return renderer.domElement.toDataURL("image/png");
105
+ }
106
+ export function getModel3DPreviewFrame(s, onReady) {
107
+ if (typeof document === "undefined" || !s.glbSrc)
108
+ return null;
109
+ const key = previewKey(s);
110
+ const hit = previewCache.get(key);
111
+ if (hit?.status === "ready")
112
+ return hit.img;
113
+ if (hit?.status === "failed")
114
+ return null;
115
+ if (hit?.status === "pending") {
116
+ if (onReady)
117
+ void hit.wait.then(() => onReady());
118
+ return null;
119
+ }
120
+ const wait = loadGltfScene(s.glbSrc)
121
+ .then((root) => new Promise((resolve) => {
122
+ const url = rasterize(s, root);
123
+ if (!url) {
124
+ previewCache.set(key, { status: "failed" });
125
+ resolve();
126
+ return;
127
+ }
128
+ const img = new Image();
129
+ img.onload = () => {
130
+ previewCache.set(key, { status: "ready", img });
131
+ onReady?.();
132
+ resolve();
133
+ };
134
+ img.onerror = () => {
135
+ previewCache.set(key, { status: "failed" });
136
+ resolve();
137
+ };
138
+ img.src = url;
139
+ }))
140
+ .catch(() => {
141
+ previewCache.set(key, { status: "failed" });
142
+ });
143
+ previewCache.set(key, { status: "pending", wait });
144
+ return null;
145
+ }
146
+ export function waitForModel3DFrames(timeoutMs = 8000) {
147
+ const pending = [];
148
+ for (const e of previewCache.values()) {
149
+ if (e.status === "pending")
150
+ pending.push(e.wait);
151
+ }
152
+ if (!pending.length)
153
+ return Promise.resolve();
154
+ return Promise.race([
155
+ Promise.all(pending).then(() => undefined),
156
+ new Promise((resolve) => setTimeout(resolve, timeoutMs)),
157
+ ]);
158
+ }
@@ -0,0 +1,73 @@
1
+ import { getImage } from "./paint-canvas.js";
2
+ import { getModel3DPreviewFrame } from "./model3d-frame.js";
3
+ export function model3DDisplaySrc(s, onReady) {
4
+ const frame = s.glbSrc ? getModel3DPreviewFrame(s, onReady) : null;
5
+ return frame?.src ?? s.rasterSrc;
6
+ }
7
+ export function drawModel3DCanvas(ctx, s, onReady) {
8
+ const { x, y, w, h } = s.bbox;
9
+ const src = model3DDisplaySrc(s, onReady);
10
+ ctx.save();
11
+ if (src) {
12
+ const img = getImage(src, onReady);
13
+ ctx.fillStyle = "#ffffff";
14
+ ctx.fillRect(x, y, w, h);
15
+ if (img.complete && img.naturalWidth > 0) {
16
+ const scale = Math.max(w / img.naturalWidth, h / img.naturalHeight);
17
+ const dw = img.naturalWidth * scale;
18
+ const dh = img.naturalHeight * scale;
19
+ ctx.drawImage(img, x + (w - dw) / 2, y + (h - dh) / 2, dw, dh);
20
+ }
21
+ }
22
+ else {
23
+ ctx.fillStyle = "#e2e8f0";
24
+ ctx.fillRect(x, y, w, h);
25
+ ctx.fillStyle = "#475569";
26
+ ctx.font = `600 ${Math.max(11, Math.min(16, h * 0.12))}px system-ui, sans-serif`;
27
+ ctx.textAlign = "center";
28
+ ctx.textBaseline = "middle";
29
+ ctx.fillText(s.objectName ?? "3D Model", x + w / 2, y + h / 2);
30
+ }
31
+ ctx.strokeStyle = "#94a3b8";
32
+ ctx.lineWidth = 1;
33
+ ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);
34
+ ctx.restore();
35
+ }
36
+ const SVG_NS = "http://www.w3.org/2000/svg";
37
+ export function renderModel3DSvg(container, s, onReady) {
38
+ while (container.firstChild)
39
+ container.removeChild(container.firstChild);
40
+ const { x, y, w, h } = s.bbox;
41
+ const src = model3DDisplaySrc(s, onReady);
42
+ if (src) {
43
+ const im = document.createElementNS(SVG_NS, "image");
44
+ im.setAttribute("x", String(x));
45
+ im.setAttribute("y", String(y));
46
+ im.setAttribute("width", String(w));
47
+ im.setAttribute("height", String(h));
48
+ im.setAttribute("href", src);
49
+ im.setAttributeNS("http://www.w3.org/1999/xlink", "href", src);
50
+ im.setAttribute("preserveAspectRatio", "xMidYMid slice");
51
+ container.appendChild(im);
52
+ }
53
+ else {
54
+ const bg = document.createElementNS(SVG_NS, "rect");
55
+ bg.setAttribute("x", String(x));
56
+ bg.setAttribute("y", String(y));
57
+ bg.setAttribute("width", String(w));
58
+ bg.setAttribute("height", String(h));
59
+ bg.setAttribute("fill", "#e2e8f0");
60
+ bg.setAttribute("stroke", "#94a3b8");
61
+ container.appendChild(bg);
62
+ const label = document.createElementNS(SVG_NS, "text");
63
+ label.setAttribute("x", String(x + w / 2));
64
+ label.setAttribute("y", String(y + h / 2));
65
+ label.setAttribute("fill", "#475569");
66
+ label.setAttribute("font-size", String(Math.max(11, Math.min(16, h * 0.12))));
67
+ label.setAttribute("font-weight", "600");
68
+ label.setAttribute("text-anchor", "middle");
69
+ label.setAttribute("dominant-baseline", "middle");
70
+ label.textContent = s.objectName ?? "3D Model";
71
+ container.appendChild(label);
72
+ }
73
+ }
@@ -0,0 +1,54 @@
1
+ import { SvgBackend } from "./svg-backend.js";
2
+ function overlayBoxStyle(stage) {
3
+ return [
4
+ "position:absolute",
5
+ `left:${stage.offsetLeft}px`,
6
+ `top:${stage.offsetTop}px`,
7
+ `width:${stage.style.width || `${stage.clientWidth}px`}`,
8
+ `height:${stage.style.height || `${stage.clientHeight}px`}`,
9
+ "pointer-events:none",
10
+ "overflow:hidden",
11
+ ].join(";");
12
+ }
13
+ /**
14
+ * Mount SVG shape layers for morph (FLIP) when the live stage lacks per-shape DOM
15
+ * ids (canvas / hybrid bitmap segments). Pure-SVG stages only need the ghost overlay.
16
+ */
17
+ export function mountMorphTransitionLayers(stageWrap, stage, prevScene, curScene, opts = {}) {
18
+ const ghostZ = opts.ghostZIndex ?? 1000;
19
+ const stageZ = opts.stageZIndex ?? 1001;
20
+ const ghostWrap = document.createElement("div");
21
+ ghostWrap.style.cssText = `${overlayBoxStyle(stage)};z-index:${ghostZ}`;
22
+ if (opts.chromeBg)
23
+ ghostWrap.style.background = opts.chromeBg;
24
+ stageWrap.appendChild(ghostWrap);
25
+ const ghostBe = new SvgBackend(prevScene, ghostWrap, { transparent: true });
26
+ ghostBe.flush();
27
+ const needsStageOverlay = !stage.querySelector("[data-id],[data-shape-id]");
28
+ let stageOverlay = null;
29
+ let stageBe = null;
30
+ let target = stage;
31
+ let hidStage = false;
32
+ if (needsStageOverlay) {
33
+ stageOverlay = document.createElement("div");
34
+ stageOverlay.style.cssText = `${overlayBoxStyle(stage)};z-index:${stageZ}`;
35
+ stageWrap.appendChild(stageOverlay);
36
+ stageBe = new SvgBackend(curScene, stageOverlay, { transparent: true });
37
+ stageBe.flush();
38
+ target = stageOverlay;
39
+ stage.style.visibility = "hidden";
40
+ hidStage = true;
41
+ }
42
+ return {
43
+ ghost: ghostWrap,
44
+ target,
45
+ cleanup() {
46
+ ghostBe.destroy();
47
+ stageBe?.destroy();
48
+ ghostWrap.remove();
49
+ stageOverlay?.remove();
50
+ if (hidStage)
51
+ stage.style.visibility = "";
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,71 @@
1
+ export function affMul(m, n) {
2
+ return {
3
+ a: m.a * n.a + m.c * n.b,
4
+ b: m.b * n.a + m.d * n.b,
5
+ c: m.a * n.c + m.c * n.d,
6
+ d: m.b * n.c + m.d * n.d,
7
+ e: m.a * n.e + m.c * n.f + m.e,
8
+ f: m.b * n.e + m.d * n.f + m.f,
9
+ };
10
+ }
11
+ function affTranslate(x, y) {
12
+ return { a: 1, b: 0, c: 0, d: 1, e: x, f: y };
13
+ }
14
+ /** 单个节点的 SVG 变换:先绕 bbox 中心旋转缩放,再平移 tx/ty。 */
15
+ export function nodeAffine(node) {
16
+ const s = node.shape;
17
+ const cx = s.bbox.x + s.bbox.w / 2;
18
+ const cy = s.bbox.y + s.bbox.h / 2;
19
+ const flipX = s.transform?.scaleX ?? 1;
20
+ const flipY = s.transform?.scaleY ?? 1;
21
+ const sx = node.scale * node.scaleX * node.widthScale * (flipX < 0 ? -1 : 1);
22
+ const sy = node.scale * node.scaleY * node.heightScale * (flipY < 0 ? -1 : 1);
23
+ const rad = ((node.rotation || 0) * Math.PI) / 180;
24
+ const cos = Math.cos(rad);
25
+ const sin = Math.sin(rad);
26
+ const a = cos * sx;
27
+ const b = sin * sx;
28
+ const c = -sin * sy;
29
+ const d = cos * sy;
30
+ const e = cx - (a * cx + c * cy) + node.tx;
31
+ const f = cy - (b * cx + d * cy) + node.ty;
32
+ return { a, b, c, d, e, f };
33
+ }
34
+ /** 从最外层祖先乘到自身。幻灯片根是恒等变换,乘进去不改变结果。 */
35
+ export function accumulatedAffine(node) {
36
+ const chain = [];
37
+ let cur = node;
38
+ while (cur) {
39
+ chain.push(cur);
40
+ cur = cur.parent;
41
+ }
42
+ let m = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
43
+ for (let i = chain.length - 1; i >= 0; i--)
44
+ m = affMul(m, nodeAffine(chain[i]));
45
+ return m;
46
+ }
47
+ /**
48
+ * wrapper 已放在 slide 坐标 (originX, originY)。CSS transform-origin 为 0 0 时,
49
+ * 用这个矩阵才能让视觉位置等于 accumulatedAffine。
50
+ */
51
+ export function cssAffineForBox(m, originX, originY) {
52
+ return affMul(affMul(affTranslate(-originX, -originY), m), affTranslate(originX, originY));
53
+ }
54
+ export function chainOpacity(node) {
55
+ let alpha = 1;
56
+ let cur = node;
57
+ while (cur) {
58
+ alpha *= cur.opacity;
59
+ cur = cur.parent;
60
+ }
61
+ return alpha;
62
+ }
63
+ export function chainHidden(node) {
64
+ let cur = node;
65
+ while (cur) {
66
+ if ((cur.visibleOverride ?? cur.shape.visible) === false)
67
+ return true;
68
+ cur = cur.parent;
69
+ }
70
+ return false;
71
+ }
@@ -0,0 +1,286 @@
1
+ import { getImage } from "./paint-canvas.js";
2
+ const SVG_NS = "http://www.w3.org/2000/svg";
3
+ function truncateLabel(text, maxLen) {
4
+ if (text.length <= maxLen)
5
+ return text;
6
+ return text.slice(0, Math.max(1, maxLen - 1)) + "…";
7
+ }
8
+ /** Full-bleed package preview (not icon mode). */
9
+ export function oleUsesPackagePreview(s) {
10
+ return Boolean(s.previewSrc && s.showAsIcon === false);
11
+ }
12
+ function oleLayout(bbox) {
13
+ const { x, y, w, h } = bbox;
14
+ const pad = Math.max(4, Math.min(w, h) * 0.06);
15
+ const badgeSize = Math.min(w * 0.28, h * 0.55, 72);
16
+ const badgeX = x + pad;
17
+ const badgeY = y + (h - badgeSize) / 2;
18
+ const textX = badgeX + badgeSize + pad;
19
+ const textW = Math.max(0, x + w - pad - textX);
20
+ return { pad, badgeSize, badgeX, badgeY, textX, textW, cx: x + w / 2, cy: y + h / 2 };
21
+ }
22
+ function drawPreviewBadge(ctx, s, x, y) {
23
+ const badgeColor = s.badgeColor ?? "#64748b";
24
+ const badgeText = s.badgeText ?? (s.previewKind === "pdf" ? "PDF" : "OLE");
25
+ const size = 28;
26
+ ctx.save();
27
+ ctx.fillStyle = "rgba(15,23,42,0.55)";
28
+ ctx.fillRect(x, y, size + 8, size + 8);
29
+ ctx.fillStyle = badgeColor;
30
+ if (typeof ctx.roundRect === "function") {
31
+ ctx.beginPath();
32
+ ctx.roundRect(x + 4, y + 4, size, size, 4);
33
+ ctx.fill();
34
+ }
35
+ else {
36
+ ctx.fillRect(x + 4, y + 4, size, size);
37
+ }
38
+ ctx.fillStyle = "#ffffff";
39
+ ctx.font = "bold 10px system-ui, sans-serif";
40
+ ctx.textAlign = "center";
41
+ ctx.textBaseline = "middle";
42
+ ctx.fillText(badgeText.slice(0, 4), x + 4 + size / 2, y + 4 + size / 2);
43
+ ctx.restore();
44
+ }
45
+ function drawOleImagePreview(ctx, s) {
46
+ const { x, y, w, h } = s.bbox;
47
+ const src = s.previewSrc;
48
+ const img = getImage(src);
49
+ ctx.save();
50
+ ctx.fillStyle = "#ffffff";
51
+ ctx.fillRect(x, y, w, h);
52
+ if (img.complete && img.naturalWidth > 0) {
53
+ ctx.drawImage(img, x, y, w, h);
54
+ }
55
+ ctx.strokeStyle = "#94a3b8";
56
+ ctx.lineWidth = 1;
57
+ ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);
58
+ drawPreviewBadge(ctx, s, x + 4, y + 4);
59
+ ctx.restore();
60
+ }
61
+ function drawOlePdfCanvasPlaceholder(ctx, s) {
62
+ const { x, y, w, h } = s.bbox;
63
+ ctx.save();
64
+ ctx.fillStyle = "#f1f5f9";
65
+ ctx.fillRect(x, y, w, h);
66
+ ctx.strokeStyle = "#94a3b8";
67
+ ctx.lineWidth = 1;
68
+ ctx.setLineDash([4, 3]);
69
+ ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);
70
+ ctx.setLineDash([]);
71
+ const title = s.objectName ?? s.embedFileName ?? "PDF Document";
72
+ ctx.fillStyle = "#0f172a";
73
+ ctx.font = `600 ${Math.max(11, Math.min(16, h * 0.12))}px system-ui, sans-serif`;
74
+ ctx.textAlign = "center";
75
+ ctx.textBaseline = "middle";
76
+ ctx.fillText(truncateLabel(title, 40), x + w / 2, y + h / 2 - 8);
77
+ ctx.fillStyle = "#64748b";
78
+ ctx.font = `${Math.max(9, h * 0.08)}px system-ui, sans-serif`;
79
+ ctx.fillText("PDF preview (editor)", x + w / 2, y + h / 2 + 12);
80
+ drawPreviewBadge(ctx, s, x + 4, y + 4);
81
+ ctx.restore();
82
+ }
83
+ /** Canvas / export path: package preview or icon tile + filename. */
84
+ export function drawOleCanvas(ctx, s) {
85
+ if (oleUsesPackagePreview(s)) {
86
+ if (s.previewKind === "image") {
87
+ drawOleImagePreview(ctx, s);
88
+ return;
89
+ }
90
+ if (s.previewKind === "pdf") {
91
+ drawOlePdfCanvasPlaceholder(ctx, s);
92
+ return;
93
+ }
94
+ }
95
+ const { x, y, w, h } = s.bbox;
96
+ const badgeColor = s.badgeColor ?? "#64748b";
97
+ const badgeText = s.badgeText ?? "OLE";
98
+ const title = s.objectName ?? s.embedFileName ?? "Embedded Object";
99
+ const subtitle = s.isLinked ? `${s.typeLabel ?? "Object"} (linked)` : s.typeLabel ?? "Embedded Object";
100
+ ctx.save();
101
+ ctx.fillStyle = "#f8fafc";
102
+ ctx.strokeStyle = "#94a3b8";
103
+ ctx.lineWidth = 1;
104
+ ctx.setLineDash([4, 3]);
105
+ const rr = Math.min(w, h) * 0.08;
106
+ if (typeof ctx.roundRect === "function") {
107
+ ctx.beginPath();
108
+ ctx.roundRect(x, y, w, h, rr);
109
+ ctx.fill();
110
+ ctx.stroke();
111
+ }
112
+ else {
113
+ ctx.fillRect(x, y, w, h);
114
+ ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);
115
+ }
116
+ ctx.setLineDash([]);
117
+ const layout = oleLayout(s.bbox);
118
+ const showIcon = s.showAsIcon !== false && layout.textW > 8;
119
+ if (showIcon) {
120
+ const br = Math.min(layout.badgeSize * 0.18, 8);
121
+ ctx.fillStyle = badgeColor;
122
+ if (typeof ctx.roundRect === "function") {
123
+ ctx.beginPath();
124
+ ctx.roundRect(layout.badgeX, layout.badgeY, layout.badgeSize, layout.badgeSize, br);
125
+ ctx.fill();
126
+ }
127
+ else {
128
+ ctx.fillRect(layout.badgeX, layout.badgeY, layout.badgeSize, layout.badgeSize);
129
+ }
130
+ ctx.fillStyle = "#ffffff";
131
+ const fs = Math.max(9, layout.badgeSize * 0.28);
132
+ ctx.font = `bold ${fs}px system-ui, sans-serif`;
133
+ ctx.textAlign = "center";
134
+ ctx.textBaseline = "middle";
135
+ ctx.fillText(badgeText, layout.badgeX + layout.badgeSize / 2, layout.badgeY + layout.badgeSize / 2);
136
+ }
137
+ const titleFs = Math.max(10, Math.min(16, h * 0.14));
138
+ const subFs = Math.max(8, titleFs * 0.78);
139
+ ctx.textAlign = showIcon ? "left" : "center";
140
+ ctx.textBaseline = "middle";
141
+ ctx.fillStyle = "#0f172a";
142
+ ctx.font = `600 ${titleFs}px system-ui, sans-serif`;
143
+ const titleX = showIcon ? layout.textX : layout.cx;
144
+ const titleMax = showIcon ? Math.floor(layout.textW / (titleFs * 0.55)) : Math.floor(w / (titleFs * 0.55));
145
+ ctx.fillText(truncateLabel(title, Math.max(4, titleMax)), titleX, layout.cy - subFs * 0.55);
146
+ ctx.fillStyle = "#64748b";
147
+ ctx.font = `400 ${subFs}px system-ui, sans-serif`;
148
+ const subMax = showIcon ? Math.floor(layout.textW / (subFs * 0.5)) : Math.floor(w / (subFs * 0.5));
149
+ ctx.fillText(truncateLabel(subtitle, Math.max(6, subMax)), titleX, layout.cy + titleFs * 0.45);
150
+ ctx.restore();
151
+ }
152
+ function appendPreviewBadgeSvg(container, s, x, y) {
153
+ const badgeColor = s.badgeColor ?? "#64748b";
154
+ const badgeText = s.badgeText ?? (s.previewKind === "pdf" ? "PDF" : "OLE");
155
+ const tile = document.createElementNS(SVG_NS, "rect");
156
+ tile.setAttribute("x", String(x + 4));
157
+ tile.setAttribute("y", String(y + 4));
158
+ tile.setAttribute("width", "28");
159
+ tile.setAttribute("height", "28");
160
+ tile.setAttribute("rx", "4");
161
+ tile.setAttribute("fill", badgeColor);
162
+ container.appendChild(tile);
163
+ const badge = document.createElementNS(SVG_NS, "text");
164
+ badge.setAttribute("x", String(x + 18));
165
+ badge.setAttribute("y", String(y + 20));
166
+ badge.setAttribute("fill", "#ffffff");
167
+ badge.setAttribute("font-size", "10");
168
+ badge.setAttribute("font-weight", "700");
169
+ badge.setAttribute("text-anchor", "middle");
170
+ badge.setAttribute("dominant-baseline", "middle");
171
+ badge.textContent = badgeText.slice(0, 4);
172
+ container.appendChild(badge);
173
+ }
174
+ /** SVG preview path — package preview via image / PDF embed, else icon placeholder. */
175
+ export function renderOleSvg(container, s) {
176
+ while (container.firstChild)
177
+ container.removeChild(container.firstChild);
178
+ const { x, y, w, h } = s.bbox;
179
+ if (oleUsesPackagePreview(s) && s.previewSrc) {
180
+ if (s.previewKind === "image") {
181
+ const im = document.createElementNS(SVG_NS, "image");
182
+ im.setAttribute("x", String(x));
183
+ im.setAttribute("y", String(y));
184
+ im.setAttribute("width", String(w));
185
+ im.setAttribute("height", String(h));
186
+ im.setAttribute("href", s.previewSrc);
187
+ im.setAttributeNS("http://www.w3.org/1999/xlink", "href", s.previewSrc);
188
+ im.setAttribute("preserveAspectRatio", "none");
189
+ container.appendChild(im);
190
+ const border = document.createElementNS(SVG_NS, "rect");
191
+ border.setAttribute("x", String(x));
192
+ border.setAttribute("y", String(y));
193
+ border.setAttribute("width", String(w));
194
+ border.setAttribute("height", String(h));
195
+ border.setAttribute("fill", "none");
196
+ border.setAttribute("stroke", "#94a3b8");
197
+ border.setAttribute("stroke-width", "1");
198
+ container.appendChild(border);
199
+ appendPreviewBadgeSvg(container, s, x, y);
200
+ return;
201
+ }
202
+ if (s.previewKind === "pdf") {
203
+ const fo = document.createElementNS(SVG_NS, "foreignObject");
204
+ fo.setAttribute("x", String(x));
205
+ fo.setAttribute("y", String(y));
206
+ fo.setAttribute("width", String(w));
207
+ fo.setAttribute("height", String(h));
208
+ const wrap = document.createElement("div");
209
+ wrap.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
210
+ wrap.style.width = "100%";
211
+ wrap.style.height = "100%";
212
+ wrap.style.background = "#f8fafc";
213
+ wrap.style.overflow = "hidden";
214
+ const embed = document.createElement("embed");
215
+ embed.src = s.previewSrc;
216
+ embed.type = "application/pdf";
217
+ embed.style.width = "100%";
218
+ embed.style.height = "100%";
219
+ embed.style.pointerEvents = "none";
220
+ wrap.appendChild(embed);
221
+ fo.appendChild(wrap);
222
+ container.appendChild(fo);
223
+ appendPreviewBadgeSvg(container, s, x, y);
224
+ return;
225
+ }
226
+ }
227
+ const badgeColor = s.badgeColor ?? "#64748b";
228
+ const badgeText = s.badgeText ?? "OLE";
229
+ const title = s.objectName ?? s.embedFileName ?? "Embedded Object";
230
+ const subtitle = s.isLinked ? `${s.typeLabel ?? "Object"} (linked)` : s.typeLabel ?? "Embedded Object";
231
+ const bg = document.createElementNS(SVG_NS, "rect");
232
+ bg.setAttribute("x", String(x));
233
+ bg.setAttribute("y", String(y));
234
+ bg.setAttribute("width", String(w));
235
+ bg.setAttribute("height", String(h));
236
+ bg.setAttribute("rx", String(Math.min(w, h) * 0.08));
237
+ bg.setAttribute("fill", "#f8fafc");
238
+ bg.setAttribute("stroke", "#94a3b8");
239
+ bg.setAttribute("stroke-width", "1");
240
+ bg.setAttribute("stroke-dasharray", "4 3");
241
+ container.appendChild(bg);
242
+ const layout = oleLayout(s.bbox);
243
+ const showIcon = s.showAsIcon !== false && layout.textW > 8;
244
+ if (showIcon) {
245
+ const tile = document.createElementNS(SVG_NS, "rect");
246
+ tile.setAttribute("x", String(layout.badgeX));
247
+ tile.setAttribute("y", String(layout.badgeY));
248
+ tile.setAttribute("width", String(layout.badgeSize));
249
+ tile.setAttribute("height", String(layout.badgeSize));
250
+ tile.setAttribute("rx", String(Math.min(layout.badgeSize * 0.18, 8)));
251
+ tile.setAttribute("fill", badgeColor);
252
+ container.appendChild(tile);
253
+ const badge = document.createElementNS(SVG_NS, "text");
254
+ badge.setAttribute("x", String(layout.badgeX + layout.badgeSize / 2));
255
+ badge.setAttribute("y", String(layout.badgeY + layout.badgeSize / 2));
256
+ badge.setAttribute("fill", "#ffffff");
257
+ badge.setAttribute("font-size", String(Math.max(9, layout.badgeSize * 0.28)));
258
+ badge.setAttribute("font-weight", "700");
259
+ badge.setAttribute("text-anchor", "middle");
260
+ badge.setAttribute("dominant-baseline", "middle");
261
+ badge.textContent = badgeText;
262
+ container.appendChild(badge);
263
+ }
264
+ const titleFs = Math.max(10, Math.min(16, h * 0.14));
265
+ const subFs = Math.max(8, titleFs * 0.78);
266
+ const titleX = showIcon ? layout.textX : layout.cx;
267
+ const titleEl = document.createElementNS(SVG_NS, "text");
268
+ titleEl.setAttribute("x", String(titleX));
269
+ titleEl.setAttribute("y", String(layout.cy - subFs * 0.55));
270
+ titleEl.setAttribute("fill", "#0f172a");
271
+ titleEl.setAttribute("font-size", String(titleFs));
272
+ titleEl.setAttribute("font-weight", "600");
273
+ titleEl.setAttribute("text-anchor", showIcon ? "start" : "middle");
274
+ titleEl.setAttribute("dominant-baseline", "middle");
275
+ titleEl.textContent = truncateLabel(title, 48);
276
+ container.appendChild(titleEl);
277
+ const subEl = document.createElementNS(SVG_NS, "text");
278
+ subEl.setAttribute("x", String(titleX));
279
+ subEl.setAttribute("y", String(layout.cy + titleFs * 0.45));
280
+ subEl.setAttribute("fill", "#64748b");
281
+ subEl.setAttribute("font-size", String(subFs));
282
+ subEl.setAttribute("text-anchor", showIcon ? "start" : "middle");
283
+ subEl.setAttribute("dominant-baseline", "middle");
284
+ subEl.textContent = truncateLabel(subtitle, 56);
285
+ container.appendChild(subEl);
286
+ }