@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,59 @@
1
+ import { computeInitialEntranceState } from "@sliiu/present-core";
2
+ /**
3
+ * 放映专用:按当前 timing 把 entrance 目标在 scene 上藏起来(opacity=0 / 段落级 hide)。
4
+ *
5
+ * 编辑器会 `forceVisibleForEdit`,让入场图形在编辑态全部可见;分享 / 导出 / 直播快照
6
+ * 打的是这份数据。播放器必须在这里重算 hidden-until-entrance,不能依赖 shape.opacity。
7
+ */
8
+ export function applyEntranceHideToScene(scene, slide) {
9
+ const st = computeInitialEntranceState(slide);
10
+ for (const s of st.hiddenShapes) {
11
+ const node = scene.root.findById(s.id);
12
+ if (node)
13
+ node.setProperty("opacity", 0);
14
+ }
15
+ for (const o of st.paraOverrides) {
16
+ const node = scene.root.findById(o.shapeId);
17
+ if (!node)
18
+ continue;
19
+ if (o.opacity !== undefined)
20
+ node.setParaProperty(o.paraIdx, "opacity", o.opacity);
21
+ }
22
+ for (const o of st.charOverrides) {
23
+ const node = scene.root.findById(o.shapeId);
24
+ if (!node)
25
+ continue;
26
+ if (o.opacity !== undefined) {
27
+ node.setCharRangeProperty(o.paraIdx, o.unit, o.start, o.end, "opacity", o.opacity);
28
+ }
29
+ }
30
+ }
31
+ /**
32
+ * 放映缩略图 / 下一张预览:画出设计稿,而不是「点击前隐藏」的初态。
33
+ *
34
+ * 进入放映时编辑器会把 entrance 目标的 opacity 写回 0。播放舞台要这个初态,
35
+ * 缩略图如果跟着藏,演讲者看到的是空白页。只改 scene,不改 slide 数据。
36
+ */
37
+ export function revealDesignedAppearance(scene, slide) {
38
+ const st = computeInitialEntranceState(slide);
39
+ for (const s of st.hiddenShapes) {
40
+ const node = scene.root.findById(s.id);
41
+ if (node)
42
+ node.setProperty("opacity", 1);
43
+ }
44
+ for (const o of st.paraOverrides) {
45
+ const node = scene.root.findById(o.shapeId);
46
+ if (!node)
47
+ continue;
48
+ if (o.opacity === 0)
49
+ node.setParaProperty(o.paraIdx, "opacity", 1);
50
+ }
51
+ for (const o of st.charOverrides) {
52
+ const node = scene.root.findById(o.shapeId);
53
+ if (!node)
54
+ continue;
55
+ if (o.opacity === 0) {
56
+ node.setCharRangeProperty(o.paraIdx, o.unit, o.start, o.end, "opacity", 1);
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Font fallback for the browser. OOXML names one typeface per run ("Inter", "Songti SC"); when the
3
+ * machine lacks it PowerPoint substitutes a face of the same class (sans / serif / mono), but a
4
+ * browser given a bare unknown family falls back to its default — usually a serif — so a sans deck
5
+ * comes up in Times. Append the class-matched generic (plus CJK faces so Han glyphs never hit a
6
+ * Latin-only fallback) to every family that is not already a stack.
7
+ */
8
+ const SERIF = /\b(serif|times|georgia|garamond|cambria|palatino|baskerville|didot|bodoni|playfair|fraunces|merriweather|lora|libre|spectral|songti|simsun|song|ming|mincho|kaiti|kai|fangsong|batang)\b|宋体|楷体|仿宋|书宋|明朝/i;
9
+ const MONO = /\b(mono|monospace|consolas|menlo|courier|code|inconsolata|fira code|source code|jetbrains|plex mono|space mono)\b/i;
10
+ const CJK_FALLBACK = `"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC"`;
11
+ const CJK_SERIF_FALLBACK = `"Songti SC", "SimSun", "Noto Serif CJK SC"`;
12
+ // OOXML 写的是 Windows 字体名。同一种字在 macOS 上登记成另一个 family,
13
+ // 名字对不上时浏览器会跳过它,后面的拉丁字体就把数字和英文抢走。
14
+ // 别名紧跟在原名后面,原名存在时仍优先用原名。
15
+ const CJK_ALIASES = {
16
+ 黑体: ["SimHei", "Heiti SC", "STHeiti", "华文黑体"],
17
+ SimHei: ["黑体", "Heiti SC", "STHeiti"],
18
+ 宋体: ["SimSun", "Songti SC", "STSong", "华文宋体"],
19
+ SimSun: ["宋体", "Songti SC", "STSong"],
20
+ 新宋体: ["NSimSun", "Songti SC", "SimSun"],
21
+ NSimSun: ["新宋体", "Songti SC"],
22
+ 微软雅黑: ["Microsoft YaHei", "PingFang SC", "Hiragino Sans GB"],
23
+ "Microsoft YaHei": ["微软雅黑", "PingFang SC", "Hiragino Sans GB"],
24
+ 楷体: ["KaiTi", "Kaiti SC", "STKaiti", "华文楷体"],
25
+ KaiTi: ["楷体", "Kaiti SC", "STKaiti"],
26
+ 仿宋: ["FangSong", "STFangsong", "华文仿宋"],
27
+ FangSong: ["仿宋", "STFangsong"],
28
+ 等线: ["DengXian", "Microsoft YaHei", "PingFang SC"],
29
+ };
30
+ const cache = new Map();
31
+ /** `family` as a CSS font-family list with a class-matched generic fallback appended. */
32
+ export function withFontFallback(family) {
33
+ if (!family)
34
+ return `${CJK_FALLBACK}, sans-serif`;
35
+ const hit = cache.get(family);
36
+ if (hit)
37
+ return hit;
38
+ const trimmed = expandCjkAliases(family.trim());
39
+ // Ends in a generic already: trust it. A stack of named faces (`Inter, "Noto Sans SC"`) still
40
+ // needs the generic, since with none of them installed the browser falls to its serif default.
41
+ // The class is judged from the lead family; the rest of the stack is kept as authored.
42
+ const lead = trimmed.split(",")[0].trim().replace(/^["']|["']$/g, "");
43
+ const head = trimmed.includes(",") ? trimmed : quote(trimmed);
44
+ const stack = /\b(sans-serif|serif|monospace|system-ui|cursive|fantasy)\s*$/i.test(trimmed)
45
+ ? trimmed
46
+ : MONO.test(lead)
47
+ ? `${head}, "Menlo", "Consolas", monospace`
48
+ : SERIF.test(lead)
49
+ ? `${head}, ${CJK_SERIF_FALLBACK}, serif`
50
+ : `${head}, ${CJK_FALLBACK}, sans-serif`;
51
+ cache.set(family, stack);
52
+ return stack;
53
+ }
54
+ function quote(name) {
55
+ return /^["']/.test(name) || !/[\s\d]/.test(name) ? name : `"${name}"`;
56
+ }
57
+ function unquote(name) {
58
+ return name.trim().replace(/^["']|["']$/g, "");
59
+ }
60
+ function splitFamilies(stack) {
61
+ const out = [];
62
+ let cur = "";
63
+ let quoted = false;
64
+ for (const ch of stack) {
65
+ if (ch === '"') {
66
+ quoted = !quoted;
67
+ cur += ch;
68
+ continue;
69
+ }
70
+ if (ch === "," && !quoted) {
71
+ if (cur.trim())
72
+ out.push(cur.trim());
73
+ cur = "";
74
+ continue;
75
+ }
76
+ cur += ch;
77
+ }
78
+ if (cur.trim())
79
+ out.push(cur.trim());
80
+ return out;
81
+ }
82
+ function expandCjkAliases(stack) {
83
+ const parts = splitFamilies(stack);
84
+ const seen = new Set(parts.map((part) => unquote(part).toLowerCase()));
85
+ const out = [];
86
+ for (const part of parts) {
87
+ out.push(part);
88
+ const aliases = CJK_ALIASES[unquote(part)];
89
+ if (!aliases)
90
+ continue;
91
+ for (const alias of aliases) {
92
+ const key = alias.toLowerCase();
93
+ if (seen.has(key))
94
+ continue;
95
+ seen.add(key);
96
+ out.push(quote(alias));
97
+ }
98
+ }
99
+ return out.join(", ");
100
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 撤销单个 SceneNode 的绘制变换,与 canvas/svg backend 对偶:
3
+ * translate(tx,ty) → rotate around bbox center → scale around center。
4
+ */
5
+ export function inverseSceneNode(n, x, y) {
6
+ const s = n.shape;
7
+ const cx = s.bbox.x + s.bbox.w / 2;
8
+ const cy = s.bbox.y + s.bbox.h / 2;
9
+ let lx = x - n.tx;
10
+ let ly = y - n.ty;
11
+ if (n.rotation) {
12
+ const a = (-n.rotation * Math.PI) / 180;
13
+ const cos = Math.cos(a);
14
+ const sin = Math.sin(a);
15
+ const dx = lx - cx;
16
+ const dy = ly - cy;
17
+ lx = cx + dx * cos - dy * sin;
18
+ ly = cy + dx * sin + dy * cos;
19
+ }
20
+ const flipX = s.transform?.scaleX ?? 1;
21
+ const flipY = s.transform?.scaleY ?? 1;
22
+ const sx = n.scale * n.scaleX * n.widthScale * (flipX < 0 ? -1 : 1);
23
+ const sy = n.scale * n.scaleY * n.heightScale * (flipY < 0 ? -1 : 1);
24
+ if (sx !== 1 && sx !== 0)
25
+ lx = (lx - cx) / sx + cx;
26
+ if (sy !== 1 && sy !== 0)
27
+ ly = (ly - cy) / sy + cy;
28
+ return { x: lx, y: ly };
29
+ }
30
+ /** 世界坐标 → 该节点未旋转 bbox 空间(含祖先 group 变换)。 */
31
+ export function worldToLocal(node, x, y) {
32
+ const chain = [];
33
+ for (let n = node; n; n = n.parent)
34
+ chain.push(n);
35
+ chain.reverse();
36
+ let p = { x, y };
37
+ for (const n of chain)
38
+ p = inverseSceneNode(n, p.x, p.y);
39
+ return p;
40
+ }
41
+ export function hitTestNodeBBox(node, worldX, worldY) {
42
+ const p = worldToLocal(node, worldX, worldY);
43
+ const b = node.shape.bbox;
44
+ return p.x >= b.x && p.x <= b.x + b.w && p.y >= b.y && p.y <= b.y + b.h;
45
+ }
@@ -0,0 +1,89 @@
1
+ import { SvgBackend } from "./svg-backend.js";
2
+ import { CanvasBackend } from "./canvas-backend.js";
3
+ import { WebGLShapeBackend } from "./webgl-shape-backend.js";
4
+ import { WebGLModel3DBackend } from "./webgl-model3d-backend.js";
5
+ import { buildHybridSegments } from "./hybrid-segments.js";
6
+ export class HybridBackend {
7
+ host;
8
+ svg;
9
+ wrapper;
10
+ backends = [];
11
+ constructor(scene, container, opts = {}) {
12
+ this.wrapper = document.createElement("div");
13
+ this.wrapper.style.position = "relative";
14
+ this.wrapper.style.width = "100%";
15
+ this.wrapper.style.height = "100%";
16
+ this.wrapper.style.background = opts.background ?? "transparent";
17
+ container.appendChild(this.wrapper);
18
+ const segments = buildHybridSegments(scene.root.children);
19
+ let firstSvg = null;
20
+ for (const seg of segments) {
21
+ const layer = document.createElement("div");
22
+ layer.style.position = "absolute";
23
+ layer.style.inset = "0";
24
+ layer.style.width = "100%";
25
+ layer.style.height = "100%";
26
+ this.wrapper.appendChild(layer);
27
+ const ids = new Set(seg.ids);
28
+ const filter = (s) => ids.has(s.id);
29
+ if (seg.kind === "canvas") {
30
+ this.backends.push(new CanvasBackend(scene, layer, { shapeFilter: filter, transparent: true }));
31
+ }
32
+ else if (seg.kind === "webgl") {
33
+ layer.style.pointerEvents = "none";
34
+ this.backends.push(new WebGLShapeBackend(scene, layer, { shapeFilter: filter }));
35
+ }
36
+ else if (seg.kind === "webglm3d") {
37
+ layer.style.pointerEvents = "none";
38
+ this.backends.push(new WebGLModel3DBackend(scene, layer, { shapeFilter: filter, interactive: opts.interactive }));
39
+ }
40
+ else {
41
+ // 这一层必须能接到指针。HTML 祖先一旦 pointer-events:none,foreignObject
42
+ // 里的 <video> 控件就点不到,点击会穿透到 stage,被放映的翻页接走。
43
+ const be = new SvgBackend(scene, layer, {
44
+ shapeFilter: filter,
45
+ transparent: true,
46
+ clipToSlide: opts.clipToSlide,
47
+ });
48
+ this.backends.push(be);
49
+ if (!firstSvg)
50
+ firstSvg = be;
51
+ }
52
+ }
53
+ if (!firstSvg) {
54
+ const layer = document.createElement("div");
55
+ layer.style.position = "absolute";
56
+ layer.style.inset = "0";
57
+ layer.style.width = "100%";
58
+ layer.style.height = "100%";
59
+ layer.style.pointerEvents = "none";
60
+ this.wrapper.appendChild(layer);
61
+ firstSvg = new SvgBackend(scene, layer, {
62
+ shapeFilter: () => false,
63
+ transparent: true,
64
+ clipToSlide: opts.clipToSlide,
65
+ });
66
+ this.backends.push(firstSvg);
67
+ }
68
+ this.svg = firstSvg;
69
+ this.host = this.wrapper;
70
+ }
71
+ get svgRoot() {
72
+ return this.svg.root;
73
+ }
74
+ /** 同步刷新所有子 backend;详见 SvgBackend.flush。 */
75
+ flush() {
76
+ for (const b of this.backends)
77
+ b.flush?.();
78
+ }
79
+ /** 等待所有 live 3D(WebGLShapeBackend)首帧就绪——截图 / visual-compare 前调。 */
80
+ async waitForReady() {
81
+ await Promise.all(this.backends.map((b) => b.whenReady?.() ?? Promise.resolve()));
82
+ }
83
+ destroy() {
84
+ for (const b of this.backends)
85
+ b.destroy();
86
+ this.backends = [];
87
+ this.wrapper.remove();
88
+ }
89
+ }
@@ -0,0 +1,51 @@
1
+ import { needs3DRender, shape3DInfo, shapeSupports3D } from "@sliiu/present-three-bake";
2
+ const BITMAP_KINDS = new Set(["image", "chart"]);
3
+ function shapeIsLive3D(s) {
4
+ return shapeSupports3D(s) && needs3DRender(shape3DInfo(s));
5
+ }
6
+ function shapeIsLiveModel3D(s) {
7
+ return s.kind === "model3d" && Boolean(s.glbSrc);
8
+ }
9
+ function classifyBucket(node) {
10
+ if (shapeIsLive3D(node.shape))
11
+ return "webgl";
12
+ if (shapeIsLiveModel3D(node.shape))
13
+ return "webglm3d";
14
+ if (node.shape.kind === "model3d")
15
+ return "canvas";
16
+ return BITMAP_KINDS.has(node.shape.kind) ? "canvas" : "svg";
17
+ }
18
+ /**
19
+ * 按绘制顺序切段。组合不单独成段:非 3D 子形状留在组合被归入的那一类(图片在组合里
20
+ * 仍走 SVG),3D 子形状在它自己的 z 序位置拆成 webgl 段,这样字可以盖在挤出体上面。
21
+ */
22
+ export function buildHybridSegments(children) {
23
+ const segments = [];
24
+ for (const child of children)
25
+ walk(child, segments, classifyBucket(child));
26
+ return segments;
27
+ }
28
+ function walk(node, segments, bucket) {
29
+ if (shapeIsLive3D(node.shape)) {
30
+ push(segments, "webgl", node.id);
31
+ return;
32
+ }
33
+ if (shapeIsLiveModel3D(node.shape)) {
34
+ push(segments, "webglm3d", node.id);
35
+ return;
36
+ }
37
+ if (node.shape.kind === "group") {
38
+ for (const c of node.children)
39
+ walk(c, segments, bucket);
40
+ return;
41
+ }
42
+ push(segments, bucket, node.id);
43
+ }
44
+ function push(segments, kind, id) {
45
+ const prev = segments[segments.length - 1];
46
+ if (prev && prev.kind === kind && kind !== "webgl" && kind !== "webglm3d") {
47
+ prev.ids.push(id);
48
+ return;
49
+ }
50
+ segments.push({ kind, ids: [id] });
51
+ }
@@ -0,0 +1,24 @@
1
+ export * from "./scene-node.js";
2
+ export { applyEntranceHideToScene, revealDesignedAppearance } from "./entrance-hide.js";
3
+ export * from "./compose.js";
4
+ export * from "./svg-backend.js";
5
+ export * from "./canvas-backend.js";
6
+ export * from "./hybrid-backend.js";
7
+ export { WebGLShapeBackend } from "./webgl-shape-backend.js";
8
+ export { WebGLModel3DBackend } from "./webgl-model3d-backend.js";
9
+ export { stageChromeBackground } from "./stage-chrome.js";
10
+ export { mountMorphTransitionLayers } from "./morph-transition.js";
11
+ export * from "./selection.js";
12
+ export { renderPaint, applyStroke as applySvgStroke } from "./paint-svg.js";
13
+ export { paintToCanvasStyle, applyStroke as applyCanvasStroke, getImage, waitForImagesLoaded } from "./paint-canvas.js";
14
+ export { getVideoPosterFrame, waitForVideoFrames } from "./video-frame.js";
15
+ export { getModel3DPreviewFrame, waitForModel3DFrames } from "./model3d-frame.js";
16
+ export { renderChart } from "./chart-svg.js";
17
+ export { drawChartCanvas } from "./chart-canvas.js";
18
+ export { effectClipPathD, effectClipUsesEvenOdd } from "./effect-clip.js";
19
+ export * from "./text-shared.js";
20
+ export * from "./text-stacked.js";
21
+ export { runLinkAt, hitTestTextRunAt } from "./text-hit.js";
22
+ export { worldToLocal, hitTestNodeBBox, inverseSceneNode } from "./hit-geom.js";
23
+ export { shapeFrontImageSrc } from "./shape-front.js";
24
+ export { warpActive, sampleWarpPath, placeGlyphsOnWarp } from "./text-warp.js";
@@ -0,0 +1,109 @@
1
+ /** 浏览器无法加载 `file:` 外链图;画带文件名的占位,避免空白碎图。
2
+ * Node 下若本地文件仍在,读成 data URL 仅供预览,**不改 IR 的 src**(否则 round-trip 会丢掉 r:link)。
3
+ */
4
+ export function isFileLinkedSrc(src) {
5
+ return !!src && /^file:/i.test(src);
6
+ }
7
+ export function linkedImageLabel(src) {
8
+ const stripped = src.replace(/^file:\/\//i, "").replace(/^\/\//, "");
9
+ const name = stripped.split(/[/\\]/).pop()?.trim();
10
+ return name && name.length > 0 && name.length < 80 ? name : "Linked image";
11
+ }
12
+ function escapeXml(s) {
13
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
14
+ }
15
+ const FILE_PREVIEW_CACHE = new Map();
16
+ function fileUrlToFsPath(src) {
17
+ try {
18
+ const u = new URL(src);
19
+ if (u.protocol !== "file:")
20
+ return undefined;
21
+ let p = decodeURIComponent(u.pathname);
22
+ if (/^\/[A-Za-z]:\//.test(p))
23
+ p = p.slice(1);
24
+ return p;
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
30
+ function mimeFromFsPath(p) {
31
+ const ext = p.slice(p.lastIndexOf(".") + 1).toLowerCase();
32
+ if (ext === "jpg" || ext === "jpeg")
33
+ return "image/jpeg";
34
+ if (ext === "gif")
35
+ return "image/gif";
36
+ if (ext === "webp")
37
+ return "image/webp";
38
+ if (ext === "svg")
39
+ return "image/svg+xml";
40
+ if (ext === "bmp")
41
+ return "image/bmp";
42
+ return "image/png";
43
+ }
44
+ function tryReadFileLinkedDataUrl(src) {
45
+ const cached = FILE_PREVIEW_CACHE.get(src);
46
+ if (cached === null)
47
+ return undefined;
48
+ if (cached)
49
+ return cached;
50
+ const proc = globalThis;
51
+ if (!proc.process?.versions?.node) {
52
+ FILE_PREVIEW_CACHE.set(src, null);
53
+ return undefined;
54
+ }
55
+ const fsPath = fileUrlToFsPath(src);
56
+ if (!fsPath) {
57
+ FILE_PREVIEW_CACHE.set(src, null);
58
+ return undefined;
59
+ }
60
+ try {
61
+ const fs = proc.process.getBuiltinModule?.("fs");
62
+ if (!fs?.existsSync || !fs.readFileSync || !fs.existsSync(fsPath)) {
63
+ FILE_PREVIEW_CACHE.set(src, null);
64
+ return undefined;
65
+ }
66
+ const b64 = fs.readFileSync(fsPath).toString("base64");
67
+ const dataUrl = `data:${mimeFromFsPath(fsPath)};base64,${b64}`;
68
+ FILE_PREVIEW_CACHE.set(src, dataUrl);
69
+ return dataUrl;
70
+ }
71
+ catch {
72
+ FILE_PREVIEW_CACHE.set(src, null);
73
+ return undefined;
74
+ }
75
+ }
76
+ export function linkedImagePlaceholderDataUri(src, w = 320, h = 180) {
77
+ const label = escapeXml(linkedImageLabel(src));
78
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${Math.max(1, Math.round(w))}" height="${Math.max(1, Math.round(h))}">` +
79
+ `<rect width="100%" height="100%" fill="#f3f4f6"/>` +
80
+ `<rect x="0.5" y="0.5" width="99%" height="99%" fill="none" stroke="#d1d5db" stroke-dasharray="6 4"/>` +
81
+ `<text x="50%" y="46%" text-anchor="middle" font-size="14" fill="#6b7280" font-family="sans-serif">Linked image</text>` +
82
+ `<text x="50%" y="60%" text-anchor="middle" font-size="12" fill="#6b7280" font-family="sans-serif">${label}</text>` +
83
+ `</svg>`;
84
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
85
+ }
86
+ export function displayImageSrc(src, w = 320, h = 180) {
87
+ if (!isFileLinkedSrc(src))
88
+ return src;
89
+ return tryReadFileLinkedDataUrl(src) ?? linkedImagePlaceholderDataUri(src, w, h);
90
+ }
91
+ export function drawLinkedImagePlaceholder(ctx, bbox, src) {
92
+ ctx.save();
93
+ ctx.fillStyle = "#f3f4f6";
94
+ ctx.fillRect(bbox.x, bbox.y, bbox.w, bbox.h);
95
+ ctx.strokeStyle = "#d1d5db";
96
+ ctx.setLineDash([6, 4]);
97
+ ctx.lineWidth = 1;
98
+ ctx.strokeRect(bbox.x + 0.5, bbox.y + 0.5, bbox.w - 1, bbox.h - 1);
99
+ ctx.setLineDash([]);
100
+ ctx.fillStyle = "#6b7280";
101
+ ctx.font = "12px sans-serif";
102
+ ctx.textAlign = "center";
103
+ ctx.textBaseline = "middle";
104
+ const label = linkedImageLabel(src);
105
+ ctx.fillText("Linked image", bbox.x + bbox.w / 2, bbox.y + bbox.h / 2 - 8);
106
+ ctx.font = "11px sans-serif";
107
+ ctx.fillText(label, bbox.x + bbox.w / 2, bbox.y + bbox.h / 2 + 10, Math.max(8, bbox.w - 12));
108
+ ctx.restore();
109
+ }
@@ -0,0 +1,135 @@
1
+ // EMF / WMF 浏览器画不出来。上屏前转成 SVG,原 src 不动,导出仍写回图元文件。
2
+ // 转出来的 SVG 不放进 shape.svgSrc:那个字段会写成 asvg:svgBlip,文件就会多出一个部件。
3
+ import { convertMetafileToSvgDataUrl } from "emf-converter";
4
+ export const METAFILE_MARKER_SRC = "data:image/svg+xml," +
5
+ encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><rect x="1.5" y="1.5" width="61" height="61" fill="#f3f4f6" stroke="#d1d5db" stroke-width="1" stroke-dasharray="4 3"/></svg>`);
6
+ const cache = new Map();
7
+ const jobs = new Map();
8
+ const watched = new WeakSet();
9
+ export function isMetafileDataUrl(src) {
10
+ return /^data:image\/(?:x-)?(?:emf|wmf)(?:;|,)/i.test(src);
11
+ }
12
+ /** EMF 签名在偏移 40(" EMF")。可放置 WMF 以 0x9AC6CDD7 开头。 */
13
+ export function isMetafileBytes(bytes, mime = "") {
14
+ if (/emf|wmf/i.test(mime))
15
+ return true;
16
+ if (bytes.length >= 4) {
17
+ const key = bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24);
18
+ if (key === 0x9ac6cdd7)
19
+ return true;
20
+ }
21
+ if (bytes.length >= 44) {
22
+ const sig = bytes[40] | (bytes[41] << 8) | (bytes[42] << 16) | (bytes[43] << 24);
23
+ if (sig === 0x464d4520)
24
+ return true;
25
+ }
26
+ return false;
27
+ }
28
+ /**
29
+ * 画布该用的地址。
30
+ * 返回空字符串表示图元文件还在转,调用方先留白,完成后再画。
31
+ * 转失败返回虚线占位。普通图片原样返回。
32
+ */
33
+ export function previewImageSrc(src, img, onReady) {
34
+ const hit = cache.get(src);
35
+ if (hit?.kind === "native")
36
+ return src;
37
+ if (hit?.kind === "svg" || hit?.kind === "marker")
38
+ return hit.url;
39
+ if (jobs.has(src))
40
+ return "";
41
+ if (isMetafileDataUrl(src)) {
42
+ ensure(src, onReady);
43
+ return "";
44
+ }
45
+ if (img && !img.complete && onReady && !watched.has(img)) {
46
+ watched.add(img);
47
+ img.addEventListener("error", () => {
48
+ ensure(src, onReady);
49
+ }, { once: true });
50
+ }
51
+ if (!img || !img.complete)
52
+ return src;
53
+ if (img.naturalWidth > 0) {
54
+ cache.set(src, { kind: "native" });
55
+ return src;
56
+ }
57
+ ensure(src, onReady);
58
+ return "";
59
+ }
60
+ export function waitForMetafilePreviews() {
61
+ return Promise.all([...jobs.values()]).then(() => undefined);
62
+ }
63
+ function ensure(src, onReady) {
64
+ let job = jobs.get(src);
65
+ if (!job) {
66
+ job = run(src);
67
+ jobs.set(src, job);
68
+ void job.finally(() => {
69
+ jobs.delete(src);
70
+ });
71
+ }
72
+ if (onReady)
73
+ void job.then(() => onReady());
74
+ }
75
+ async function run(src) {
76
+ if (cache.has(src))
77
+ return;
78
+ const claimed = isMetafileDataUrl(src);
79
+ try {
80
+ const loaded = await loadBytes(src);
81
+ if (!loaded) {
82
+ cache.set(src, claimed ? { kind: "marker", url: METAFILE_MARKER_SRC } : { kind: "native" });
83
+ return;
84
+ }
85
+ if (!claimed && !isMetafileBytes(loaded.bytes, loaded.mime)) {
86
+ cache.set(src, { kind: "native" });
87
+ return;
88
+ }
89
+ const svg = await convertMetafileToSvgDataUrl(toArrayBuffer(loaded.bytes));
90
+ cache.set(src, svg ? { kind: "svg", url: svg } : { kind: "marker", url: METAFILE_MARKER_SRC });
91
+ }
92
+ catch {
93
+ cache.set(src, claimed ? { kind: "marker", url: METAFILE_MARKER_SRC } : { kind: "native" });
94
+ }
95
+ }
96
+ function toArrayBuffer(bytes) {
97
+ const copy = new ArrayBuffer(bytes.byteLength);
98
+ new Uint8Array(copy).set(bytes);
99
+ return copy;
100
+ }
101
+ async function loadBytes(src) {
102
+ if (src.startsWith("data:")) {
103
+ const bytes = decodeDataUrl(src);
104
+ return bytes ? { bytes, mime: "" } : undefined;
105
+ }
106
+ if (typeof fetch !== "function")
107
+ return undefined;
108
+ const res = await fetch(src);
109
+ if (!res.ok)
110
+ return undefined;
111
+ return {
112
+ bytes: new Uint8Array(await res.arrayBuffer()),
113
+ mime: res.headers.get("content-type") ?? "",
114
+ };
115
+ }
116
+ function decodeDataUrl(src) {
117
+ const comma = src.indexOf(",");
118
+ if (comma < 0)
119
+ return undefined;
120
+ const head = src.slice(0, comma);
121
+ const body = src.slice(comma + 1);
122
+ try {
123
+ if (/;base64/i.test(head)) {
124
+ const bin = atob(body);
125
+ const out = new Uint8Array(bin.length);
126
+ for (let i = 0; i < bin.length; i++)
127
+ out[i] = bin.charCodeAt(i) & 0xff;
128
+ return out;
129
+ }
130
+ return new TextEncoder().encode(decodeURIComponent(body));
131
+ }
132
+ catch {
133
+ return undefined;
134
+ }
135
+ }