@sliiu/present-three-bake 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 (42) hide show
  1. package/COPYRIGHT +5 -0
  2. package/LICENSE +661 -0
  3. package/THIRD-PARTY-NOTICES.md +221 -0
  4. package/dist/cjs/bake-sp3d.js +176 -0
  5. package/dist/cjs/camera.js +94 -0
  6. package/dist/cjs/index.js +47 -0
  7. package/dist/cjs/light-rig.js +162 -0
  8. package/dist/cjs/material.js +136 -0
  9. package/dist/cjs/package.json +1 -0
  10. package/dist/cjs/path-to-shape.js +331 -0
  11. package/dist/cjs/presets.js +112 -0
  12. package/dist/cjs/resolve.js +98 -0
  13. package/dist/cjs/runtime.js +146 -0
  14. package/dist/cjs/scene-builder.js +329 -0
  15. package/dist/cjs/shape-path.js +71 -0
  16. package/dist/cjs/types.js +2 -0
  17. package/dist/esm/bake-sp3d.js +140 -0
  18. package/dist/esm/camera.js +58 -0
  19. package/dist/esm/index.js +9 -0
  20. package/dist/esm/light-rig.js +126 -0
  21. package/dist/esm/material.js +98 -0
  22. package/dist/esm/path-to-shape.js +295 -0
  23. package/dist/esm/presets.js +109 -0
  24. package/dist/esm/resolve.js +91 -0
  25. package/dist/esm/runtime.js +141 -0
  26. package/dist/esm/scene-builder.js +290 -0
  27. package/dist/esm/shape-path.js +64 -0
  28. package/dist/esm/types.js +1 -0
  29. package/dist/types/bake-sp3d.d.ts +2 -0
  30. package/dist/types/camera.d.ts +7 -0
  31. package/dist/types/index.d.ts +8 -0
  32. package/dist/types/light-rig.d.ts +10 -0
  33. package/dist/types/material.d.ts +17 -0
  34. package/dist/types/path-to-shape.d.ts +11 -0
  35. package/dist/types/presets.d.ts +23 -0
  36. package/dist/types/resolve.d.ts +67 -0
  37. package/dist/types/runtime.d.ts +22 -0
  38. package/dist/types/scene-builder.d.ts +35 -0
  39. package/dist/types/shape-path.d.ts +15 -0
  40. package/dist/types/types.d.ts +1 -0
  41. package/package.json +30 -4
  42. package/README.md +0 -3
@@ -0,0 +1,329 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.buildThreeScene = buildThreeScene;
37
+ exports.createSceneCamera = createSceneCamera;
38
+ exports.getCachedImageTexture = getCachedImageTexture;
39
+ exports.loadImageTexture = loadImageTexture;
40
+ const THREE = __importStar(require("three"));
41
+ const camera_1 = require("./camera");
42
+ const light_rig_1 = require("./light-rig");
43
+ const material_1 = require("./material");
44
+ const path_to_shape_1 = require("./path-to-shape");
45
+ // ────────────────────────────────────────────────────────────────────────────
46
+ // 共享 three.js 场景构建器。offscreen 光栅(bakeSp3d,给 thumbnail/snapshot/pdf)和
47
+ // live WebGL backend(player/editor 实时)都用它,保证两条路径几何/材质/光照一致。
48
+ // 这里只负责「已解析参数(ResolvedScene3D) → THREE.Scene + 相机」,不碰 DOM / canvas。
49
+ // ────────────────────────────────────────────────────────────────────────────
50
+ /**
51
+ * "前缘有倒角、背面平整、薄侧壁" 的板形几何——复刻 PowerPoint sp3d 在仅声明
52
+ * `<a:bevelT/>` / `<a:bevelB/>` 没有 extrusionH 时的视觉(如 7.pptx 棋盘)。
53
+ * 返回 front(贴图面,z=+e/2 内缩 bevelW/H)+ rim(chamfer ring + 薄侧壁),让前面用
54
+ * textured material 保色、rim 用 PBR 吃光照显倒角折边。详细几何见原始注释保留。
55
+ */
56
+ function buildFrontBeveledSlab(baseW, baseH, extrusionPx, bevelWpx, bevelHpx) {
57
+ const e = extrusionPx;
58
+ const bw = Math.min(bevelWpx, baseW * 0.4);
59
+ const bh = Math.min(bevelHpx, e * 0.5);
60
+ const zFront = +e / 2;
61
+ const zRim = +e / 2 - bh;
62
+ const zBack = -e / 2;
63
+ const buildBuf = (pushAll) => {
64
+ const positions = [];
65
+ const uvs = [];
66
+ const normals = [];
67
+ const pushTri = (p1, p2, p3, uv1, uv2, uv3, n) => {
68
+ positions.push(...p1, ...p2, ...p3);
69
+ uvs.push(...uv1, ...uv2, ...uv3);
70
+ normals.push(...n, ...n, ...n);
71
+ };
72
+ const pushQuad = (a, b, c, d, uva, uvb, uvc, uvd, n) => {
73
+ pushTri(a, b, c, uva, uvb, uvc, n);
74
+ pushTri(a, c, d, uva, uvc, uvd, n);
75
+ };
76
+ pushAll({ pushTri, pushQuad });
77
+ const geo = new THREE.BufferGeometry();
78
+ geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
79
+ geo.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
80
+ geo.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
81
+ return geo;
82
+ };
83
+ const front = buildBuf(({ pushQuad }) => {
84
+ const u0 = bw / baseW, u1 = 1 - bw / baseW;
85
+ const v0 = bh / baseH, v1 = 1 - bh / baseH;
86
+ pushQuad([bw, bh, zFront], [baseW - bw, bh, zFront], [baseW - bw, baseH - bh, zFront], [bw, baseH - bh, zFront], [u0, v0], [u1, v0], [u1, v1], [u0, v1], [0, 0, 1]);
87
+ });
88
+ // 倒角 / 侧壁的 UV 直接取顶点 (x,y) 归一化,让正面贴图**连续地**裹过倒角折边到外缘
89
+ // (复刻 PowerPoint:image 的 bevel 是「图片本身绕圆边卷过去」,不是一圈灰色画框)。
90
+ // 之前 rim 全用 (0,0) + 灰色材质 → 正面与灰边之间出现「折痕」、顶部朝相机的倒角是
91
+ // 一条灰色折面(见 7.pptx slide2 棋盘)。连续 UV + 同一张贴图后,正面与倒角无色差,
92
+ // 折痕消失,倒角只贡献轮廓上的微弱立体感。
93
+ const uv = (x, y) => [
94
+ Math.min(1, Math.max(0, x / baseW)),
95
+ Math.min(1, Math.max(0, y / baseH)),
96
+ ];
97
+ const rim = buildBuf(({ pushQuad }) => {
98
+ pushQuad([0, 0, zRim], [baseW, 0, zRim], [baseW - bw, bh, zFront], [bw, bh, zFront], uv(0, 0), uv(baseW, 0), uv(baseW - bw, bh), uv(bw, bh), [0, -0.71, 0.71]);
99
+ pushQuad([bw, baseH - bh, zFront], [baseW - bw, baseH - bh, zFront], [baseW, baseH, zRim], [0, baseH, zRim], uv(bw, baseH - bh), uv(baseW - bw, baseH - bh), uv(baseW, baseH), uv(0, baseH), [0, 0.71, 0.71]);
100
+ pushQuad([0, 0, zRim], [bw, bh, zFront], [bw, baseH - bh, zFront], [0, baseH, zRim], uv(0, 0), uv(bw, bh), uv(bw, baseH - bh), uv(0, baseH), [-0.71, 0, 0.71]);
101
+ pushQuad([baseW - bw, bh, zFront], [baseW, 0, zRim], [baseW, baseH, zRim], [baseW - bw, baseH - bh, zFront], uv(baseW - bw, bh), uv(baseW, 0), uv(baseW, baseH), uv(baseW - bw, baseH - bh), [0.71, 0, 0.71]);
102
+ pushQuad([0, 0, zRim], [0, 0, zBack], [baseW, 0, zBack], [baseW, 0, zRim], uv(0, 0), uv(0, 0), uv(baseW, 0), uv(baseW, 0), [0, -1, 0]);
103
+ pushQuad([baseW, 0, zRim], [baseW, 0, zBack], [baseW, baseH, zBack], [baseW, baseH, zRim], uv(baseW, 0), uv(baseW, 0), uv(baseW, baseH), uv(baseW, baseH), [1, 0, 0]);
104
+ pushQuad([baseW, baseH, zRim], [baseW, baseH, zBack], [0, baseH, zBack], [0, baseH, zRim], uv(baseW, baseH), uv(baseW, baseH), uv(0, baseH), uv(0, baseH), [0, 1, 0]);
105
+ pushQuad([0, baseH, zRim], [0, baseH, zBack], [0, 0, zBack], [0, 0, zRim], uv(0, baseH), uv(0, baseH), uv(0, 0), uv(0, 0), [-1, 0, 0]);
106
+ });
107
+ return { front, rim };
108
+ }
109
+ // THREE.ExtrudeGeometry 默认 UVGenerator 会把侧面拉伸成镜像图。给定 baseW/baseH:
110
+ // - 正/背面:vertex.x/y 归一到 [0,1] 让 image 贴满
111
+ // - 侧面 / 倒角:固定 (0,0),等于纹理左上角实色,让 fillColor / rim 主导
112
+ // V 轴:texture.flipY=true → uv.v=1 对应原图最顶行;geom 顶点 y=baseH(顶部)→ uv.v=1。
113
+ function rectFrontUVGenerator(baseW, baseH) {
114
+ const u = (x) => Math.min(1, Math.max(0, x / baseW));
115
+ const v = (y) => Math.min(1, Math.max(0, y / baseH));
116
+ return {
117
+ generateTopUV: (_geo, vs, ia, ib, ic) => {
118
+ const xy = (i) => new THREE.Vector2(u(vs[i * 3]), v(vs[i * 3 + 1]));
119
+ return [xy(ia), xy(ib), xy(ic)];
120
+ },
121
+ generateSideWallUV: () => {
122
+ const z = new THREE.Vector2(0, 0);
123
+ return [z, z, z, z];
124
+ },
125
+ };
126
+ }
127
+ function buildThreeScene(inputs) {
128
+ const { baseW, baseH, pathD, fill, stroke, resolved, texture, isImage } = inputs;
129
+ const scene = new THREE.Scene();
130
+ const r = resolved.sp3d;
131
+ const fillColor = (0, material_1.pickPrimaryColor)(fill, "#7f7f7f");
132
+ const explicitExtH = r.extrusionH;
133
+ const bevelWpx = Math.min(r.bevelT?.w ?? r.bevelB?.w ?? 0, baseW * 0.4);
134
+ const bevelHpx = Math.min(r.bevelT?.h ?? r.bevelB?.h ?? 0, baseH * 0.4);
135
+ const hasBevel = r.hasBevel && bevelWpx > 0 && bevelHpx > 0;
136
+ const useFrontBevelSlab = isImage && hasBevel && !pathD;
137
+ const useBevel = hasBevel && !isImage;
138
+ const extrusionPx = explicitExtH > 0
139
+ ? explicitExtH
140
+ // image:用 2×bevelHpx 当深度,让 buildFrontBeveledSlab 里 bh=min(bevelHpx, e/2)
141
+ // 取满 bevelHpx → 顶/底倒角折面有足够宽度被看见(否则顶光下与正面分不开)。
142
+ : isImage
143
+ ? Math.max(bevelHpx * 2, 8)
144
+ : useBevel
145
+ ? Math.max(bevelHpx * 1.5, 6)
146
+ : 1;
147
+ const pp = pathD ? (0, path_to_shape_1.pathDToShapes)(pathD) : null;
148
+ let geometry = null;
149
+ let frontGeo = null;
150
+ let rimGeo = null;
151
+ let geoCenterX = 0;
152
+ let geoCenterY = 0;
153
+ let geoW = baseW;
154
+ let geoH = baseH;
155
+ const extrudeOpts = {
156
+ depth: Math.max(extrusionPx, 0.001),
157
+ bevelEnabled: useBevel,
158
+ bevelSegments: 4,
159
+ bevelSize: bevelWpx,
160
+ bevelThickness: bevelHpx,
161
+ curveSegments: 24,
162
+ UVGenerator: rectFrontUVGenerator(baseW, baseH),
163
+ };
164
+ if (pp && pp.shapes.length > 0) {
165
+ const bbox = pp.bbox;
166
+ geoW = bbox.maxX - bbox.minX || baseW;
167
+ geoH = bbox.maxY - bbox.minY || baseH;
168
+ geoCenterX = (bbox.minX + bbox.maxX) / 2;
169
+ geoCenterY = (bbox.minY + bbox.maxY) / 2;
170
+ geometry = new THREE.ExtrudeGeometry(pp.shapes, extrudeOpts);
171
+ }
172
+ else if (useFrontBevelSlab) {
173
+ geoCenterX = baseW / 2;
174
+ geoCenterY = baseH / 2;
175
+ const built = buildFrontBeveledSlab(baseW, baseH, extrusionPx, bevelWpx, bevelHpx);
176
+ frontGeo = built.front;
177
+ rimGeo = built.rim;
178
+ }
179
+ else {
180
+ geoCenterX = baseW / 2;
181
+ geoCenterY = baseH / 2;
182
+ const shape = new THREE.Shape();
183
+ shape.moveTo(0, 0);
184
+ shape.lineTo(baseW, 0);
185
+ shape.lineTo(baseW, baseH);
186
+ shape.lineTo(0, baseH);
187
+ shape.lineTo(0, 0);
188
+ geometry = new THREE.ExtrudeGeometry([shape], extrudeOpts);
189
+ }
190
+ const rimColor = r.contourClr ?? fillColor;
191
+ if (frontGeo && rimGeo) {
192
+ frontGeo.translate(-geoCenterX, -geoCenterY, -extrusionPx / 2);
193
+ rimGeo.translate(-geoCenterX, -geoCenterY, -extrusionPx / 2);
194
+ frontGeo.computeBoundingSphere();
195
+ frontGeo.computeBoundingBox();
196
+ rimGeo.computeBoundingSphere();
197
+ rimGeo.computeBoundingBox();
198
+ const frontMat = (0, material_1.buildMaterial)(fill, {
199
+ prst: r.prstMaterial,
200
+ fallbackColor: fillColor,
201
+ imageTexture: texture ?? undefined,
202
+ });
203
+ // image 倒角 / 侧壁与正面共用贴图(连续 UV),让图片裹过折边而不是露出灰色画框;
204
+ // 但 rim 走 litTexture + tint 压暗:正面 basic 满亮、折边受光后再乘灰度 → 顶光下
205
+ // 「上面(倒角折面)」也明显比「前面(正面)」暗,留出区分两个面的折痕。
206
+ // 无贴图(纯色 shape 不会走 slab 路径,但兜底)时仍回退到 contour / fill 灰边。
207
+ const rimMat = texture
208
+ ? (0, material_1.buildMaterial)(fill, {
209
+ prst: r.prstMaterial,
210
+ fallbackColor: rimColor,
211
+ imageTexture: texture,
212
+ litTexture: true,
213
+ tint: 0x888888,
214
+ })
215
+ : (0, material_1.buildMaterial)(undefined, { prst: r.prstMaterial, fallbackColor: rimColor });
216
+ scene.add(new THREE.Mesh(frontGeo, frontMat));
217
+ scene.add(new THREE.Mesh(rimGeo, rimMat));
218
+ }
219
+ else if (geometry) {
220
+ geometry.translate(-geoCenterX, -geoCenterY, -extrusionPx / 2);
221
+ geometry.computeBoundingSphere();
222
+ geometry.computeBoundingBox();
223
+ const material = (0, material_1.buildMaterial)(fill, {
224
+ prst: r.prstMaterial,
225
+ fallbackColor: fillColor,
226
+ imageTexture: texture ?? undefined,
227
+ });
228
+ scene.add(new THREE.Mesh(geometry, material));
229
+ }
230
+ if (stroke && r.contourW && r.contourW > 0) {
231
+ const geomForEdges = frontGeo ?? geometry;
232
+ if (geomForEdges) {
233
+ const edges = new THREE.EdgesGeometry(geomForEdges, 12);
234
+ const lineMat = new THREE.LineBasicMaterial({ color: r.contourClr ?? "#000000", linewidth: 1 });
235
+ scene.add(new THREE.LineSegments(edges, lineMat));
236
+ }
237
+ }
238
+ (0, light_rig_1.applyLightRig)(scene, resolved.lightRig.rig, resolved.lightRig.dir);
239
+ const dispose = () => {
240
+ scene.traverse((obj) => {
241
+ if (obj instanceof THREE.Mesh || obj instanceof THREE.LineSegments) {
242
+ obj.geometry?.dispose();
243
+ const mat = obj.material;
244
+ if (Array.isArray(mat))
245
+ for (const m of mat)
246
+ m.dispose();
247
+ else
248
+ mat?.dispose();
249
+ }
250
+ });
251
+ };
252
+ return { scene, geoW, geoH, dispose };
253
+ }
254
+ /**
255
+ * 把相机摆到「物体在 viewW×viewH 视口里接近 1:1(含 pad 余量)」的距离并构建 THREE 相机。
256
+ * offscreen 与 live 共用:offscreen 传 padded canvas 尺寸;live 传显示像素尺寸。
257
+ */
258
+ function createSceneCamera(cam, geoW, geoH, viewW, viewH, pad, backdrop) {
259
+ const aspect = viewW / viewH;
260
+ const camFit = 1 + pad;
261
+ let distance;
262
+ if (cam.isOrtho) {
263
+ distance = Math.max(Math.max(geoW, geoH) * 4, 200);
264
+ }
265
+ else {
266
+ const fovRad = (cam.fov * Math.PI) / 180;
267
+ const tanHalf = Math.tan(fovRad / 2);
268
+ const dByH = (geoH * camFit) / (2 * tanHalf);
269
+ const dByW = (geoW * camFit) / (2 * tanHalf * aspect);
270
+ distance = Math.max(dByH, dByW, 200);
271
+ }
272
+ const { camera } = (0, camera_1.setupCamera)(cam, new THREE.Vector3(0, 0, 0), distance, backdrop);
273
+ if (camera instanceof THREE.PerspectiveCamera) {
274
+ camera.aspect = aspect;
275
+ camera.updateProjectionMatrix();
276
+ }
277
+ else if (camera instanceof THREE.OrthographicCamera) {
278
+ const halfX = (viewW / 2) * camFit;
279
+ const halfY = (viewH / 2) * camFit;
280
+ camera.left = -halfX;
281
+ camera.right = halfX;
282
+ camera.top = halfY;
283
+ camera.bottom = -halfY;
284
+ camera.updateProjectionMatrix();
285
+ }
286
+ return camera;
287
+ }
288
+ // 已解码的 <img> 缓存(按 src)。解码是 loadImageTexture 里唯一的异步成本;缓存后
289
+ // live WebGLShapeBackend 在每帧重建(编辑器拖动)时可以 **同步** 拿到贴图、当帧首渲,
290
+ // 避免"重建 backend → 异步等贴图 → 3D 空一帧"造成的闪屏。THREE.Texture 不进缓存
291
+ // (它绑定具体 renderer 的 GPU 资源、且会被各自 backend dispose),每次按缓存的
292
+ // HTMLImageElement 新建一个轻量 Texture 即可。
293
+ const decodedImageCache = new Map();
294
+ function textureFromImage(img) {
295
+ const tex = new THREE.Texture(img);
296
+ tex.colorSpace = THREE.SRGBColorSpace;
297
+ tex.needsUpdate = true;
298
+ return tex;
299
+ }
300
+ /**
301
+ * 若 src 对应图片此前已被 loadImageTexture 解码过,则同步返回一个新 Texture;否则
302
+ * 返回 null(调用方需走异步 loadImageTexture)。用于热路径(拖动重建)下零延迟首渲。
303
+ */
304
+ function getCachedImageTexture(src) {
305
+ const img = decodedImageCache.get(src);
306
+ if (!img || !img.complete || img.naturalWidth === 0)
307
+ return null;
308
+ return textureFromImage(img);
309
+ }
310
+ // dataURL / URL 都走 Image → THREE.Texture,避免 TextureLoader 在 headless 对 dataURL 失败。
311
+ async function loadImageTexture(src) {
312
+ if (typeof Image === "undefined")
313
+ return null;
314
+ const cached = decodedImageCache.get(src);
315
+ if (cached && cached.complete && cached.naturalWidth > 0) {
316
+ return textureFromImage(cached);
317
+ }
318
+ const img = await new Promise((resolve) => {
319
+ const i = new Image();
320
+ i.crossOrigin = "anonymous";
321
+ i.onload = () => resolve(i);
322
+ i.onerror = () => resolve(null);
323
+ i.src = src;
324
+ });
325
+ if (!img || !img.complete || img.naturalWidth === 0)
326
+ return null;
327
+ decodedImageCache.set(src, img);
328
+ return textureFromImage(img);
329
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SHAPE_3D_PAD = exports.IMAGE_3D_PAD = void 0;
4
+ exports.shapeSupports3D = shapeSupports3D;
5
+ exports.shape3DPad = shape3DPad;
6
+ exports.shape3DInfo = shape3DInfo;
7
+ exports.shape3DPathD = shape3DPathD;
8
+ // 实时 WebGL 与离屏烘焙共用的形状范围。文字框用正面贴图(字形画进这张图)再挤出。
9
+ // 组合本身不进;组合里的子形状各自判断。
10
+ const SHAPE_3D_KINDS = new Set(["image", "rect", "ellipse", "path", "polygon", "text"]);
11
+ /** 与 bake-sp3d 的 image pad 一致:贴图几何收在 bbox 内。 */
12
+ exports.IMAGE_3D_PAD = 0.05;
13
+ /** 与 bake-sp3d 的非 image pad 一致:倒角会对称外扩。 */
14
+ exports.SHAPE_3D_PAD = 0.15;
15
+ function shapeSupports3D(s) {
16
+ return SHAPE_3D_KINDS.has(s.kind);
17
+ }
18
+ function shape3DPad(kind) {
19
+ // 文字正面是字形贴图,留白和 image slab 一样,避免倒角把字挤出画布。
20
+ return kind === "image" || kind === "text" ? exports.IMAGE_3D_PAD : exports.SHAPE_3D_PAD;
21
+ }
22
+ /** 形状上真正参加 3D 的那份参数。文字只看 bodyPr,避免和形状自己的 spPr 3D 各挤出一次。 */
23
+ function shape3DInfo(s) {
24
+ if (s.kind === "text")
25
+ return s.body3D;
26
+ return s.meta3D;
27
+ }
28
+ /**
29
+ * 非 image 形状的挤出轮廓,坐标系是形状本地的 0..bbox。
30
+ * image 不需要轮廓(正面是贴图 slab)。rect 也不返回:scene-builder 在没有 pathD
31
+ * 时已经按 bbox 挤出一个矩形。path 的 `d` 原样传入,由场景构建器按路径自身包围盒居中。
32
+ */
33
+ function shape3DPathD(s) {
34
+ if (s.kind === "path")
35
+ return s.d || undefined;
36
+ if (s.kind === "ellipse")
37
+ return ellipsePathD(s.bbox.w, s.bbox.h);
38
+ if (s.kind === "polygon" && s.points.length > 0)
39
+ return polygonPathD(s);
40
+ return undefined;
41
+ }
42
+ function ellipsePathD(w, h) {
43
+ const rx = w / 2;
44
+ const ry = h / 2;
45
+ const k = 0.5522847498;
46
+ const cx = rx;
47
+ const cy = ry;
48
+ const kx = rx * k;
49
+ const ky = ry * k;
50
+ return [
51
+ `M ${cx} 0`,
52
+ `C ${cx + kx} 0 ${w} ${cy - ky} ${w} ${cy}`,
53
+ `C ${w} ${cy + ky} ${cx + kx} ${h} ${cx} ${h}`,
54
+ `C ${cx - kx} ${h} 0 ${cy + ky} 0 ${cy}`,
55
+ `C 0 ${cy - ky} ${cx - kx} 0 ${cx} 0`,
56
+ "Z",
57
+ ].join(" ");
58
+ }
59
+ function polygonPathD(s) {
60
+ const minX = Math.min(...s.points.map((p) => p[0]));
61
+ const minY = Math.min(...s.points.map((p) => p[1]));
62
+ const absolute = Math.abs(minX - s.bbox.x) < 2 && Math.abs(minY - s.bbox.y) < 2;
63
+ const pts = absolute
64
+ ? s.points.map(([x, y]) => [x - s.bbox.x, y - s.bbox.y])
65
+ : s.points;
66
+ const [x0, y0] = pts[0];
67
+ let d = `M ${x0} ${y0}`;
68
+ for (let i = 1; i < pts.length; i++)
69
+ d += ` L ${pts[i][0]} ${pts[i][1]}`;
70
+ return `${d} Z`;
71
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,140 @@
1
+ import * as THREE from "three";
2
+ import { applyTextureCrop } from "./material.js";
3
+ import { resolveScene3D } from "./resolve.js";
4
+ import { buildThreeScene, createSceneCamera, loadImageTexture } from "./scene-builder.js";
5
+ import { shape3DPad } from "./shape-path.js";
6
+ // 离屏光栅路径:把 3D 烘成 PNG,给无法挂 live WebGL context 的消费者用
7
+ // (thumbnail=SvgBackend / snapshot+pdf=CanvasBackend / parse-time bake3D 选项)。
8
+ // player/editor 的实时视图改走 packages/renderer 的 WebGLShapeBackend(live three.js)。
9
+ // 几何/材质/光照/相机与 live 完全共用 scene-builder,确保两条路径一致。
10
+ export async function bakeSp3d(req) {
11
+ if (!hasOffscreenCanvas() && typeof document === "undefined")
12
+ return null;
13
+ const dpr = req.dpr ?? Math.min(2, getDevicePixelRatio());
14
+ const isImage = !!req.imageSrc;
15
+ // image bake:slab 几何全收在 bbox 内不外溢,只需 5% pad 吸收 3D 投影 overshoot;
16
+ // 非 image(path/纯色 extrude)倒角对称外扩,留 15%。
17
+ const pad = req.pad ?? shape3DPad(isImage ? "image" : "path");
18
+ const baseW = Math.max(8, req.bbox.w);
19
+ const baseH = Math.max(8, req.bbox.h);
20
+ const padW = Math.ceil(baseW * (1 + pad));
21
+ const padH = Math.ceil(baseH * (1 + pad));
22
+ const pixelW = Math.ceil(padW * dpr);
23
+ const pixelH = Math.ceil(padH * dpr);
24
+ const canvas = createBakeCanvas(pixelW, pixelH);
25
+ if (!canvas)
26
+ return null;
27
+ let renderer = null;
28
+ let dispose;
29
+ try {
30
+ renderer = new THREE.WebGLRenderer({
31
+ canvas: canvas,
32
+ alpha: true,
33
+ antialias: true,
34
+ premultipliedAlpha: true,
35
+ });
36
+ renderer.setPixelRatio(1);
37
+ renderer.setSize(pixelW, pixelH, false);
38
+ renderer.setClearColor(0x000000, 0);
39
+ // 输出端补 SRGB,否则 PBR 在 linear 空间写到 canvas 会让纯白变灰、棋盘对比被压扁。
40
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
41
+ const resolved = resolveScene3D({ sp3d: req.sp3d, scene3d: req.scene3d });
42
+ let texture = null;
43
+ if (req.imageSrc) {
44
+ texture = await loadImageTexture(req.imageSrc);
45
+ if (texture)
46
+ applyTextureCrop(texture, req.cropRect);
47
+ }
48
+ const built = buildThreeScene({
49
+ baseW,
50
+ baseH,
51
+ pathD: req.pathD,
52
+ fill: req.fill,
53
+ stroke: req.stroke,
54
+ resolved,
55
+ texture,
56
+ isImage,
57
+ });
58
+ dispose = built.dispose;
59
+ const camera = createSceneCamera(resolved.camera, built.geoW, built.geoH, padW, padH, pad, resolved.backdrop);
60
+ renderer.render(built.scene, camera);
61
+ const src = await canvasToDataURL(canvas);
62
+ if (!src)
63
+ return null;
64
+ return {
65
+ src,
66
+ natural: { w: pixelW, h: pixelH },
67
+ paddedBBox: {
68
+ x: req.bbox.x - (padW - baseW) / 2,
69
+ y: req.bbox.y - (padH - baseH) / 2,
70
+ w: padW,
71
+ h: padH,
72
+ },
73
+ };
74
+ }
75
+ catch (err) {
76
+ if (typeof console !== "undefined")
77
+ console.warn("[three-bake] bake failed", err);
78
+ return null;
79
+ }
80
+ finally {
81
+ dispose?.();
82
+ if (renderer) {
83
+ renderer.forceContextLoss();
84
+ renderer.dispose();
85
+ }
86
+ }
87
+ }
88
+ function getDevicePixelRatio() {
89
+ if (typeof window !== "undefined" && window.devicePixelRatio)
90
+ return window.devicePixelRatio;
91
+ return 1;
92
+ }
93
+ function hasOffscreenCanvas() {
94
+ return typeof OffscreenCanvas !== "undefined";
95
+ }
96
+ function createBakeCanvas(w, h) {
97
+ if (typeof OffscreenCanvas !== "undefined") {
98
+ try {
99
+ return new OffscreenCanvas(w, h);
100
+ }
101
+ catch {
102
+ // fall through
103
+ }
104
+ }
105
+ if (typeof document !== "undefined") {
106
+ const c = document.createElement("canvas");
107
+ c.width = w;
108
+ c.height = h;
109
+ return c;
110
+ }
111
+ return null;
112
+ }
113
+ async function canvasToDataURL(c) {
114
+ if (typeof OffscreenCanvas !== "undefined" && c instanceof OffscreenCanvas) {
115
+ try {
116
+ const blob = await c.convertToBlob({ type: "image/png" });
117
+ return await blobToDataURL(blob);
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ if (typeof HTMLCanvasElement !== "undefined" && c instanceof HTMLCanvasElement) {
124
+ try {
125
+ return c.toDataURL("image/png");
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+ function blobToDataURL(blob) {
134
+ return new Promise((resolve, reject) => {
135
+ const fr = new FileReader();
136
+ fr.onload = () => resolve(typeof fr.result === "string" ? fr.result : "");
137
+ fr.onerror = () => reject(fr.error);
138
+ fr.readAsDataURL(blob);
139
+ });
140
+ }
@@ -0,0 +1,58 @@
1
+ import * as THREE from "three";
2
+ // 相机方位约定(与几何 / UV 协调):
3
+ // - ExtrudeGeometry / slab 把 Shape 的 (0..baseW, 0..baseH) 作为顶点 (x,y,0),
4
+ // rectFrontUVGenerator 把 Shape.y=0 → uv.v=0、y=baseH → uv.v=1;texture.flipY=true
5
+ // 使 uv.v=1 取图像最顶行 → "图像顶部" 位于 three.js 世界 +Y。
6
+ // - PowerPoint 的 perspectiveAbove / perspectiveRelaxedModerately 等 "Above" 类语义是
7
+ // 相机在物体上方俯视,"图像顶部应远离相机"(梯形上窄下宽)。要让 lat=-15° 这样的
8
+ // 负值产生这种效果,相机必须落在世界 -Y 方向(远离图像顶部、贴近底部),即
9
+ // eye.y = +D·sin(lat)(lat<0 → eye.y<0)。
10
+ // - `<a:backdrop>` 把场景平面从默认 +Z 旋到 norm,并用 up 覆盖相机 up。identity / 全 0
11
+ // 在 resolveBackdrop 已滤掉,这里只处理非默认。
12
+ export function setupCamera(cam, target, distance, backdrop) {
13
+ const { lat, lon, rev, fov, isOrtho } = cam;
14
+ const latRad = THREE.MathUtils.degToRad(lat);
15
+ const lonRad = THREE.MathUtils.degToRad(lon);
16
+ const revRad = THREE.MathUtils.degToRad(rev);
17
+ const lookAt = backdrop
18
+ ? new THREE.Vector3(target.x + backdrop.target.x, target.y + backdrop.target.y, target.z + backdrop.target.z)
19
+ : target.clone();
20
+ const eye = new THREE.Vector3();
21
+ eye.x = distance * Math.cos(latRad) * Math.sin(lonRad);
22
+ eye.y = distance * Math.sin(latRad);
23
+ eye.z = distance * Math.cos(latRad) * Math.cos(lonRad);
24
+ eye.add(lookAt);
25
+ const basisQ = backdrop?.forward &&
26
+ new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), new THREE.Vector3(backdrop.forward.x, backdrop.forward.y, backdrop.forward.z));
27
+ if (basisQ) {
28
+ eye.sub(lookAt).applyQuaternion(basisQ).add(lookAt);
29
+ }
30
+ const up = new THREE.Vector3(0, -1, 0).applyAxisAngle(new THREE.Vector3(0, 0, 1), revRad);
31
+ if (basisQ)
32
+ up.applyQuaternion(basisQ);
33
+ if (backdrop?.up)
34
+ up.set(backdrop.up.x, backdrop.up.y, backdrop.up.z);
35
+ let camera;
36
+ if (isOrtho) {
37
+ const half = distance * 0.6;
38
+ const ortho = new THREE.OrthographicCamera(-half, half, half, -half, 0.1, distance * 10);
39
+ ortho.position.copy(eye);
40
+ ortho.up.copy(up);
41
+ ortho.lookAt(lookAt);
42
+ ortho.updateProjectionMatrix();
43
+ camera = ortho;
44
+ }
45
+ else {
46
+ const persp = new THREE.PerspectiveCamera(fov, 1, 0.1, distance * 10);
47
+ persp.position.copy(eye);
48
+ persp.up.copy(up);
49
+ persp.lookAt(lookAt);
50
+ persp.updateProjectionMatrix();
51
+ camera = persp;
52
+ }
53
+ if (cam.zoom !== undefined && cam.zoom > 0) {
54
+ camera.zoom = cam.zoom;
55
+ camera.updateProjectionMatrix();
56
+ }
57
+ return { camera, isOrtho };
58
+ }
@@ -0,0 +1,9 @@
1
+ export * from "./types.js";
2
+ export { bakeSp3d } from "./bake-sp3d.js";
3
+ export { createBake3DRuntime, needs3DRender, needs3DBake } from "./runtime.js";
4
+ // PowerPoint 效果层 + 共享场景构建器(live WebGLShapeBackend 复用)。
5
+ export { resolveScene3D, resolveCamera, resolveSp3D, resolveLightRig, resolveBackdrop, } from "./resolve.js";
6
+ export { buildThreeScene, createSceneCamera, loadImageTexture, getCachedImageTexture, } from "./scene-builder.js";
7
+ export { applyTextureCrop } from "./material.js";
8
+ export { shapeSupports3D, shape3DInfo, shape3DPathD, shape3DPad, IMAGE_3D_PAD, SHAPE_3D_PAD, } from "./shape-path.js";
9
+ export { CAMERA_PRESETS } from "./presets.js";