@sliiu/present-three-bake 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COPYRIGHT +5 -0
- package/LICENSE +661 -0
- package/README.md +8 -2
- package/THIRD-PARTY-NOTICES.md +221 -0
- package/dist/cjs/bake-sp3d.js +176 -0
- package/dist/cjs/camera.js +94 -0
- package/dist/cjs/index.js +47 -0
- package/dist/cjs/light-rig.js +162 -0
- package/dist/cjs/material.js +136 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/path-to-shape.js +331 -0
- package/dist/cjs/presets.js +112 -0
- package/dist/cjs/resolve.js +98 -0
- package/dist/cjs/runtime.js +146 -0
- package/dist/cjs/scene-builder.js +329 -0
- package/dist/cjs/shape-path.js +71 -0
- package/dist/cjs/types.js +2 -0
- package/dist/esm/bake-sp3d.js +140 -0
- package/dist/esm/camera.js +58 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/light-rig.js +126 -0
- package/dist/esm/material.js +98 -0
- package/dist/esm/path-to-shape.js +295 -0
- package/dist/esm/presets.js +109 -0
- package/dist/esm/resolve.js +91 -0
- package/dist/esm/runtime.js +141 -0
- package/dist/esm/scene-builder.js +290 -0
- package/dist/esm/shape-path.js +64 -0
- package/dist/esm/types.js +1 -0
- package/dist/types/bake-sp3d.d.ts +2 -0
- package/dist/types/camera.d.ts +7 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/light-rig.d.ts +10 -0
- package/dist/types/material.d.ts +17 -0
- package/dist/types/path-to-shape.d.ts +11 -0
- package/dist/types/presets.d.ts +23 -0
- package/dist/types/resolve.d.ts +67 -0
- package/dist/types/runtime.d.ts +22 -0
- package/dist/types/scene-builder.d.ts +35 -0
- package/dist/types/shape-path.d.ts +15 -0
- package/dist/types/types.d.ts +1 -0
- package/package.json +40 -4
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
3
|
+
// PowerPoint 效果层 —— preset 数值表与默认值(唯一来源)
|
|
4
|
+
//
|
|
5
|
+
// ECMA-376 只定义 preset 的**名称**(camera/@prst、sp3d/@prstMaterial、lightRig/@rig),
|
|
6
|
+
// **不**定义数值。下面的相机角度 / fov / 材质参数都是**逆向 PowerPoint / WPS 行为**
|
|
7
|
+
// 得到的 best-effort 值,并以 samples/7.pptx slide2 棋盘(perspectiveRelaxedModerately
|
|
8
|
+
// + fov 55° + 默认 bevel + threePt/dir=t)做视觉校验基准。
|
|
9
|
+
//
|
|
10
|
+
// 这里是整套 3D 的「PowerPoint 知识」集中地:parser 只忠实解析名称,渲染器只消费
|
|
11
|
+
// resolve.ts 解析出来的具体参数。改 preset 视觉只动这一份文件。
|
|
12
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.DEFAULT_FOV = exports.DEFAULT_LIGHT_RIG = exports.DEFAULT_MATERIAL = exports.DEFAULT_BEVEL_PX = exports.FOV_LOCKED_PRESETS = exports.CAMERA_PRESETS = void 0;
|
|
15
|
+
// 相机方位/符号约定与几何/UV 的协调见 resolve.ts + camera.ts 的注释。
|
|
16
|
+
exports.CAMERA_PRESETS = {
|
|
17
|
+
// ── legacy oblique(窄 fov 斜投影)─────────────────────────────────────────
|
|
18
|
+
legacyObliqueTopLeft: { lat: -20, lon: -30, rev: 0, fov: 25 },
|
|
19
|
+
legacyObliqueTop: { lat: -20, lon: 0, rev: 0, fov: 25 },
|
|
20
|
+
legacyObliqueTopRight: { lat: -20, lon: 30, rev: 0, fov: 25 },
|
|
21
|
+
legacyObliqueLeft: { lat: 0, lon: -30, rev: 0, fov: 25 },
|
|
22
|
+
legacyObliqueFront: { lat: 0, lon: 0, rev: 0, fov: 25 },
|
|
23
|
+
legacyObliqueRight: { lat: 0, lon: 30, rev: 0, fov: 25 },
|
|
24
|
+
legacyObliqueBottomLeft: { lat: 20, lon: -30, rev: 0, fov: 25 },
|
|
25
|
+
legacyObliqueBottom: { lat: 20, lon: 0, rev: 0, fov: 25 },
|
|
26
|
+
legacyObliqueBottomRight: { lat: 20, lon: 30, rev: 0, fov: 25 },
|
|
27
|
+
// ── legacy perspective(中等 fov 透视)─────────────────────────────────────
|
|
28
|
+
legacyPerspectiveTopLeft: { lat: -20, lon: -30, rev: 0, fov: 40 },
|
|
29
|
+
legacyPerspectiveTop: { lat: -20, lon: 0, rev: 0, fov: 40 },
|
|
30
|
+
legacyPerspectiveTopRight: { lat: -20, lon: 30, rev: 0, fov: 40 },
|
|
31
|
+
legacyPerspectiveLeft: { lat: 0, lon: -30, rev: 0, fov: 40 },
|
|
32
|
+
legacyPerspectiveFront: { lat: 0, lon: 0, rev: 0, fov: 40 },
|
|
33
|
+
legacyPerspectiveRight: { lat: 0, lon: 30, rev: 0, fov: 40 },
|
|
34
|
+
legacyPerspectiveBottomLeft: { lat: 20, lon: -30, rev: 0, fov: 40 },
|
|
35
|
+
legacyPerspectiveBottom: { lat: 20, lon: 0, rev: 0, fov: 40 },
|
|
36
|
+
legacyPerspectiveBottomRight: { lat: 20, lon: 30, rev: 0, fov: 40 },
|
|
37
|
+
// ── orthographic / isometric(正交)────────────────────────────────────────
|
|
38
|
+
orthographicFront: { lat: 0, lon: 0, rev: 0, fov: 0.01 },
|
|
39
|
+
isometricTopUp: { lat: -35, lon: 45, rev: 0, fov: 0.01 },
|
|
40
|
+
isometricTopDown: { lat: 35, lon: 45, rev: 0, fov: 0.01 },
|
|
41
|
+
isometricBottomUp: { lat: -35, lon: -135, rev: 0, fov: 0.01 },
|
|
42
|
+
isometricBottomDown: { lat: 35, lon: -135, rev: 0, fov: 0.01 },
|
|
43
|
+
isometricLeftUp: { lat: -35, lon: -45, rev: 0, fov: 0.01 },
|
|
44
|
+
isometricLeftDown: { lat: 35, lon: -45, rev: 0, fov: 0.01 },
|
|
45
|
+
isometricRightUp: { lat: -35, lon: 135, rev: 0, fov: 0.01 },
|
|
46
|
+
isometricRightDown: { lat: 35, lon: 135, rev: 0, fov: 0.01 },
|
|
47
|
+
isometricOffAxis1Left: { lat: -25, lon: -50, rev: 0, fov: 0.01 },
|
|
48
|
+
isometricOffAxis1Right: { lat: -25, lon: 50, rev: 0, fov: 0.01 },
|
|
49
|
+
isometricOffAxis1Top: { lat: -45, lon: 0, rev: 0, fov: 0.01 },
|
|
50
|
+
isometricOffAxis2Left: { lat: 25, lon: -50, rev: 0, fov: 0.01 },
|
|
51
|
+
isometricOffAxis2Right: { lat: 25, lon: 50, rev: 0, fov: 0.01 },
|
|
52
|
+
isometricOffAxis2Top: { lat: 45, lon: 0, rev: 0, fov: 0.01 },
|
|
53
|
+
isometricOffAxis3Bottom: { lat: 0, lon: 45, rev: 0, fov: 0.01 },
|
|
54
|
+
isometricOffAxis3Left: { lat: 0, lon: -45, rev: 0, fov: 0.01 },
|
|
55
|
+
isometricOffAxis3Right: { lat: 0, lon: 45, rev: 0, fov: 0.01 },
|
|
56
|
+
isometricOffAxis3Top: { lat: -45, lon: 0, rev: 0, fov: 0.01 },
|
|
57
|
+
isometricOffAxis4Bottom: { lat: 30, lon: -45, rev: 0, fov: 0.01 },
|
|
58
|
+
isometricOffAxis4Left: { lat: 0, lon: -45, rev: 0, fov: 0.01 },
|
|
59
|
+
isometricOffAxis4Right: { lat: 0, lon: 45, rev: 0, fov: 0.01 },
|
|
60
|
+
isometricOffAxis4Top: { lat: -30, lon: 45, rev: 0, fov: 0.01 },
|
|
61
|
+
// ── oblique(正交斜投影)────────────────────────────────────────────────────
|
|
62
|
+
obliqueTopLeft: { lat: -20, lon: -30, rev: 0, fov: 0.01 },
|
|
63
|
+
obliqueTop: { lat: -20, lon: 0, rev: 0, fov: 0.01 },
|
|
64
|
+
obliqueTopRight: { lat: -20, lon: 30, rev: 0, fov: 0.01 },
|
|
65
|
+
obliqueLeft: { lat: 0, lon: -30, rev: 0, fov: 0.01 },
|
|
66
|
+
obliqueRight: { lat: 0, lon: 30, rev: 0, fov: 0.01 },
|
|
67
|
+
obliqueBottomLeft: { lat: 20, lon: -30, rev: 0, fov: 0.01 },
|
|
68
|
+
obliqueBottom: { lat: 20, lon: 0, rev: 0, fov: 0.01 },
|
|
69
|
+
obliqueBottomRight: { lat: 20, lon: 30, rev: 0, fov: 0.01 },
|
|
70
|
+
// ── perspective(透视)──────────────────────────────────────────────────────
|
|
71
|
+
perspectiveFront: { lat: 0, lon: 0, rev: 0, fov: 35 },
|
|
72
|
+
perspectiveLeft: { lat: 0, lon: -25, rev: 0, fov: 35 },
|
|
73
|
+
perspectiveRight: { lat: 0, lon: 25, rev: 0, fov: 35 },
|
|
74
|
+
perspectiveAbove: { lat: -20, lon: 0, rev: 0, fov: 35 },
|
|
75
|
+
perspectiveBelow: { lat: 20, lon: 0, rev: 0, fov: 35 },
|
|
76
|
+
perspectiveAboveLeftFacing: { lat: -20, lon: -25, rev: 0, fov: 35 },
|
|
77
|
+
perspectiveAboveRightFacing: { lat: -20, lon: 25, rev: 0, fov: 35 },
|
|
78
|
+
perspectiveContrastingLeftFacing: { lat: -10, lon: -50, rev: 0, fov: 60 },
|
|
79
|
+
perspectiveContrastingRightFacing: { lat: -10, lon: 50, rev: 0, fov: 60 },
|
|
80
|
+
perspectiveHeroicLeftFacing: { lat: -45, lon: -45, rev: 0, fov: 60 },
|
|
81
|
+
perspectiveHeroicRightFacing: { lat: -45, lon: 45, rev: 0, fov: 60 },
|
|
82
|
+
perspectiveHeroicExtremeLeftFacing: { lat: -55, lon: -55, rev: 0, fov: 80 },
|
|
83
|
+
perspectiveHeroicExtremeRightFacing: { lat: -55, lon: 55, rev: 0, fov: 80 },
|
|
84
|
+
// perspectiveRelaxed / perspectiveRelaxedModerately:PowerPoint 3D 旋转库里的
|
|
85
|
+
// "透视:宽松 / 适度宽松" —— 真·透视相机(不是正交!),相机在物体上方俯视,使顶边
|
|
86
|
+
// 后退、底边前凸,产生"桌面式"透视倾斜。两个关键:
|
|
87
|
+
// 1. 必须用 PerspectiveCamera:正交投影下倾斜只按 cos(angle) 缩放(15° 仅 3%,看不出),
|
|
88
|
+
// 透视收缩才让倾斜可见。
|
|
89
|
+
// 2. "relaxed" = 长焦镜头(窄 fov),透视轻柔、畸变小;配中等俯角形成柔和的梯形。
|
|
90
|
+
// PowerPoint 写进 OOXML 的 `@fov`(棋盘 fov=55°)不是字面相机视角,PPT 内部当
|
|
91
|
+
// zoom/参考用 —— 真正落地是窄 fov + 适度俯视,所以这两个 preset 锁定窄 fov。
|
|
92
|
+
// - lat:俯视下倾角,负值=相机在上方("Above" 语义,见 camera.ts eye.y 约定)。
|
|
93
|
+
perspectiveRelaxed: { lat: -13, lon: 0, rev: 0, fov: 22 },
|
|
94
|
+
perspectiveRelaxedModerately: { lat: -18, lon: 0, rev: 0, fov: 28 },
|
|
95
|
+
};
|
|
96
|
+
// "perspectiveRelaxed*" 锁定预设窄 fov,忽略 OOXML 里偏宽的显式 `@fov`(见上注释)。
|
|
97
|
+
exports.FOV_LOCKED_PRESETS = new Set([
|
|
98
|
+
"perspectiveRelaxed",
|
|
99
|
+
"perspectiveRelaxedModerately",
|
|
100
|
+
]);
|
|
101
|
+
// ── 默认值(OOXML 缺省时的 PowerPoint 默认)───────────────────────────────────
|
|
102
|
+
/**
|
|
103
|
+
* ECMA-376 §20.1.5.4:`<a:bevelT/>` / `<a:bevelB/>` 缺省 w/h = 76200 EMU。
|
|
104
|
+
* 76200 ÷ 9525 EMU/px = 8 px。parser 把缺省留 undefined,这里补默认。
|
|
105
|
+
*/
|
|
106
|
+
exports.DEFAULT_BEVEL_PX = 8;
|
|
107
|
+
/** ECMA-376 §20.1.5.12 sp3d/@prstMaterial 默认 = warmMatte。 */
|
|
108
|
+
exports.DEFAULT_MATERIAL = "warmMatte";
|
|
109
|
+
/** ECMA-376 §20.1.5.9 lightRig 缺省(无 scene3d 时)我们用 threePt 兜底。 */
|
|
110
|
+
exports.DEFAULT_LIGHT_RIG = "threePt";
|
|
111
|
+
/** PerspectiveCamera 缺省 fov(无 preset 也无显式 fov 时)。 */
|
|
112
|
+
exports.DEFAULT_FOV = 35;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveCamera = resolveCamera;
|
|
4
|
+
exports.resolveSp3D = resolveSp3D;
|
|
5
|
+
exports.resolveLightRig = resolveLightRig;
|
|
6
|
+
exports.resolveBackdrop = resolveBackdrop;
|
|
7
|
+
exports.resolveScene3D = resolveScene3D;
|
|
8
|
+
const presets_1 = require("./presets");
|
|
9
|
+
function resolveCamera(cam) {
|
|
10
|
+
let lat = 0;
|
|
11
|
+
let lon = 0;
|
|
12
|
+
let rev = 0;
|
|
13
|
+
let fov = presets_1.DEFAULT_FOV;
|
|
14
|
+
if (cam?.prst) {
|
|
15
|
+
const p = presets_1.CAMERA_PRESETS[cam.prst];
|
|
16
|
+
if (p) {
|
|
17
|
+
lat = p.lat;
|
|
18
|
+
lon = p.lon;
|
|
19
|
+
rev = p.rev;
|
|
20
|
+
fov = p.fov;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// 显式 <a:rot> 覆盖角度(preset 与 rot 是覆盖而非叠加)。
|
|
24
|
+
if (cam?.rot) {
|
|
25
|
+
lat = cam.rot.lat;
|
|
26
|
+
lon = cam.rot.lon;
|
|
27
|
+
rev = cam.rot.rev;
|
|
28
|
+
}
|
|
29
|
+
// 显式 @fov 覆盖 fov——但 relaxed 系 preset 无条件用预设窄 fov(PPT 把显式 fov 当 zoom 参考)。
|
|
30
|
+
if (cam?.fov !== undefined && !(cam.prst && presets_1.FOV_LOCKED_PRESETS.has(cam.prst))) {
|
|
31
|
+
fov = cam.fov;
|
|
32
|
+
}
|
|
33
|
+
return { lat, lon, rev, fov, isOrtho: fov < 1, zoom: cam?.zoom };
|
|
34
|
+
}
|
|
35
|
+
function resolveBevel(b) {
|
|
36
|
+
if (!b)
|
|
37
|
+
return undefined;
|
|
38
|
+
return {
|
|
39
|
+
w: b.w ?? presets_1.DEFAULT_BEVEL_PX,
|
|
40
|
+
h: b.h ?? presets_1.DEFAULT_BEVEL_PX,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function resolveSp3D(sp3d) {
|
|
44
|
+
const bevelT = resolveBevel(sp3d?.bevelT);
|
|
45
|
+
const bevelB = resolveBevel(sp3d?.bevelB);
|
|
46
|
+
return {
|
|
47
|
+
extrusionH: sp3d?.extrusionH ?? 0,
|
|
48
|
+
contourW: sp3d?.contourW,
|
|
49
|
+
contourClr: sp3d?.contourClr,
|
|
50
|
+
extrusionClr: sp3d?.extrusionClr,
|
|
51
|
+
prstMaterial: sp3d?.prstMaterial ?? presets_1.DEFAULT_MATERIAL,
|
|
52
|
+
bevelT,
|
|
53
|
+
bevelB,
|
|
54
|
+
hasBevel: !!(bevelT || bevelB),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function resolveLightRig(lr) {
|
|
58
|
+
return { rig: lr?.rig ?? presets_1.DEFAULT_LIGHT_RIG, dir: lr?.dir };
|
|
59
|
+
}
|
|
60
|
+
function asVec(v) {
|
|
61
|
+
return "dx" in v ? { x: v.dx, y: v.dy, z: v.dz } : v;
|
|
62
|
+
}
|
|
63
|
+
function normalizeVec(v) {
|
|
64
|
+
const len = Math.hypot(v.x, v.y, v.z);
|
|
65
|
+
if (len < 1e-8)
|
|
66
|
+
return undefined;
|
|
67
|
+
return { x: v.x / len, y: v.y / len, z: v.z / len };
|
|
68
|
+
}
|
|
69
|
+
function nearAxis(v, x, y, z, eps = 0.02) {
|
|
70
|
+
return Math.abs(v.x - x) < eps && Math.abs(v.y - y) < eps && Math.abs(v.z - z) < eps;
|
|
71
|
+
}
|
|
72
|
+
/** identity / 全 0 → undefined,setupCamera 保持 preset 朝向。 */
|
|
73
|
+
function resolveBackdrop(bd) {
|
|
74
|
+
if (!bd)
|
|
75
|
+
return undefined;
|
|
76
|
+
const n = bd.norm ? normalizeVec(asVec(bd.norm)) : undefined;
|
|
77
|
+
const u = bd.up ? normalizeVec(asVec(bd.up)) : undefined;
|
|
78
|
+
const identityN = !n || nearAxis(n, 0, 0, 1);
|
|
79
|
+
const identityU = !u || nearAxis(u, 0, 1, 0);
|
|
80
|
+
const a = bd.anchor;
|
|
81
|
+
const hasAnchor = !!(a && (Math.abs(a.x) > 1e-4 || Math.abs(a.y) > 1e-4 || Math.abs(a.z) > 1e-4));
|
|
82
|
+
if (identityN && identityU && !hasAnchor)
|
|
83
|
+
return undefined;
|
|
84
|
+
return {
|
|
85
|
+
target: a ?? { x: 0, y: 0, z: 0 },
|
|
86
|
+
forward: identityN ? undefined : n,
|
|
87
|
+
up: identityU ? undefined : u,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function resolveScene3D(info) {
|
|
91
|
+
const scene = info.scene3d;
|
|
92
|
+
return {
|
|
93
|
+
camera: resolveCamera(scene?.camera),
|
|
94
|
+
sp3d: resolveSp3D(info.sp3d),
|
|
95
|
+
lightRig: resolveLightRig(scene?.lightRig),
|
|
96
|
+
backdrop: resolveBackdrop(scene?.backdrop),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// 高层 runtime helper:把 bakeSp3d 接到任意持有 Scene 的离屏光栅消费者
|
|
3
|
+
// (thumbnail=SvgBackend / snapshot+pdf=CanvasBackend)上。bake 完成后通过 `applyBaked`
|
|
4
|
+
// 回调把结果交给 caller 写入**副通道**(meta3D.bakedSrc 等),由那些后端读取显示。
|
|
5
|
+
// player/editor 的实时视图不走这里,改用 packages/renderer 的 WebGLShapeBackend。
|
|
6
|
+
//
|
|
7
|
+
// 触发条件见 needs3DRender:相机 rot 非平凡 / extrusionH>0 / 声明了 bevel。
|
|
8
|
+
// 性能:bake 是 OffscreenCanvas + WebGLRenderer 的一次性渲染,per-shape 30-100 ms。
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.needs3DRender = needs3DRender;
|
|
11
|
+
exports.needs3DBake = needs3DBake;
|
|
12
|
+
exports.createBake3DRuntime = createBake3DRuntime;
|
|
13
|
+
const bake_sp3d_1 = require("./bake-sp3d");
|
|
14
|
+
const shape_path_1 = require("./shape-path");
|
|
15
|
+
/**
|
|
16
|
+
* 该 3D 信息是否需要 3D 渲染(实时 or 烘焙)。parser 现在对 `<a:bevelT/>` 留空 w/h,
|
|
17
|
+
* 所以用「是否声明了 bevel 元素」判断,而非 h>0。
|
|
18
|
+
*/
|
|
19
|
+
function needs3DRender(meta) {
|
|
20
|
+
if (!meta)
|
|
21
|
+
return false;
|
|
22
|
+
const rot = meta.scene3d?.camera?.rot;
|
|
23
|
+
const hasRot = !!rot && Math.abs(rot.lat) + Math.abs(rot.lon) + Math.abs(rot.rev) > 0.01;
|
|
24
|
+
const ext = meta.sp3d?.extrusionH ?? 0;
|
|
25
|
+
const hasBevel = !!(meta.sp3d?.bevelT || meta.sp3d?.bevelB);
|
|
26
|
+
return hasRot || ext > 0 || hasBevel;
|
|
27
|
+
}
|
|
28
|
+
function readMeta(s) {
|
|
29
|
+
return (0, shape_path_1.shape3DInfo)(s);
|
|
30
|
+
}
|
|
31
|
+
// 烘焙输出的几何/参数签名:bakeSp3d 的全部输入(bbox / fill / stroke / sp3d / scene3d /
|
|
32
|
+
// src / cropRect)。任一变化 → 签名变化 → 需要重烘。id 是身份不入签名。
|
|
33
|
+
function bakeKeyOf(s) {
|
|
34
|
+
const meta = readMeta(s);
|
|
35
|
+
return JSON.stringify({
|
|
36
|
+
k: s.kind,
|
|
37
|
+
b: s.bbox ?? null,
|
|
38
|
+
f: s.fill ?? null,
|
|
39
|
+
st: s.stroke ?? null,
|
|
40
|
+
sp: meta?.sp3d ?? null,
|
|
41
|
+
sc: meta?.scene3d ?? null,
|
|
42
|
+
src: s.kind === "image" ? s.src ?? null : null,
|
|
43
|
+
cr: s.kind === "image" ? s.cropRect ?? null : null,
|
|
44
|
+
d: s.kind === "path" ? s.d : s.kind === "polygon" ? s.points : null,
|
|
45
|
+
tx: s.kind === "text" ? s.paragraphs : null,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// 该 shape 当前是否需要(重新)烘焙:image / 自选形状 / 带 body3D 的文字,
|
|
49
|
+
// 需要 3D 渲染,且"当前几何签名"与上次烘焙记录的 bakedKey 不一致
|
|
50
|
+
// (首次烘焙时 bakedKey 为 undefined,必然不一致)。
|
|
51
|
+
function needs3DBake(s) {
|
|
52
|
+
if (!(0, shape_path_1.shapeSupports3D)(s))
|
|
53
|
+
return false;
|
|
54
|
+
const meta = readMeta(s);
|
|
55
|
+
if (!meta || !needs3DRender(meta))
|
|
56
|
+
return false;
|
|
57
|
+
return meta.bakedKey !== bakeKeyOf(s);
|
|
58
|
+
}
|
|
59
|
+
function shouldBake(s) {
|
|
60
|
+
return needs3DBake(s);
|
|
61
|
+
}
|
|
62
|
+
function walkShapes(slide, fn) {
|
|
63
|
+
for (const layer of slide.layers) {
|
|
64
|
+
const stack = [[layer.shapes, 0]];
|
|
65
|
+
while (stack.length) {
|
|
66
|
+
const top = stack[stack.length - 1];
|
|
67
|
+
const [arr, i] = top;
|
|
68
|
+
if (i >= arr.length) {
|
|
69
|
+
stack.pop();
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const s = arr[i];
|
|
73
|
+
top[1] = i + 1;
|
|
74
|
+
fn(s);
|
|
75
|
+
if (s.kind === "group")
|
|
76
|
+
stack.push([s.children, 0]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function createBake3DRuntime() {
|
|
81
|
+
// markBaked 强制跳过集合(外部显式声明"别再烘这个 id")。
|
|
82
|
+
const forceSkip = new Set();
|
|
83
|
+
const inFlight = new Map();
|
|
84
|
+
const bakeOne = async (s, applyBaked, frontImageSrc) => {
|
|
85
|
+
const meta = readMeta(s);
|
|
86
|
+
if (!meta)
|
|
87
|
+
return;
|
|
88
|
+
// 在烘焙前快照签名:bake 是异步的,期间用户可能又动了 shape;用快照写回 bakedKey
|
|
89
|
+
// 保证"已烘的就是这个签名",若烘焙期间又变了,下一轮 kick 的签名不一致会再烘。
|
|
90
|
+
const key = bakeKeyOf(s);
|
|
91
|
+
const front = s.kind === "text" ? frontImageSrc?.(s) : undefined;
|
|
92
|
+
const isPicture = s.kind === "image" || !!front;
|
|
93
|
+
try {
|
|
94
|
+
const result = await (0, bake_sp3d_1.bakeSp3d)({
|
|
95
|
+
id: s.id,
|
|
96
|
+
bbox: s.bbox,
|
|
97
|
+
pathD: isPicture ? undefined : (0, shape_path_1.shape3DPathD)(s),
|
|
98
|
+
fill: s.fill,
|
|
99
|
+
stroke: s.stroke,
|
|
100
|
+
sp3d: meta.sp3d ?? {},
|
|
101
|
+
scene3d: meta.scene3d,
|
|
102
|
+
imageSrc: s.kind === "image" ? s.src : front,
|
|
103
|
+
cropRect: s.kind === "image" ? s.cropRect : undefined,
|
|
104
|
+
});
|
|
105
|
+
// baked/bakedKey 仅作 dedupe;不改 shape.src/bbox/cropRect,让 live 视图保持原始几何。
|
|
106
|
+
meta.baked = true;
|
|
107
|
+
if (result)
|
|
108
|
+
applyBaked(s, result);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// 烘焙失败不致命,仍能看到 2D 图。
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
// 成功或失败都记录签名(避免失败时无限重试);下次几何变化签名变了再烘。
|
|
115
|
+
meta.bakedKey = key;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
kick(slide, applyBaked, opts) {
|
|
120
|
+
const front = opts?.frontImageSrc;
|
|
121
|
+
walkShapes(slide, (s) => {
|
|
122
|
+
if (forceSkip.has(s.id) || inFlight.has(s.id))
|
|
123
|
+
return;
|
|
124
|
+
if (!shouldBake(s))
|
|
125
|
+
return;
|
|
126
|
+
const p = bakeOne(s, applyBaked, front).then(() => {
|
|
127
|
+
inFlight.delete(s.id);
|
|
128
|
+
});
|
|
129
|
+
inFlight.set(s.id, p);
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
async waitForAll() {
|
|
133
|
+
while (inFlight.size > 0) {
|
|
134
|
+
await Promise.all(Array.from(inFlight.values()));
|
|
135
|
+
}
|
|
136
|
+
if (typeof requestAnimationFrame !== "undefined") {
|
|
137
|
+
await new Promise((r) => {
|
|
138
|
+
requestAnimationFrame(() => requestAnimationFrame(() => r()));
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
markBaked(id) {
|
|
143
|
+
forceSkip.add(id);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|