pptx-angular-viewer 4.6.0 → 4.8.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 (25) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/fesm2022/pptx-angular-viewer-chart-3d-view-scene-CM71j9A9.mjs +1703 -0
  3. package/fesm2022/pptx-angular-viewer-chart-3d-view-scene-CM71j9A9.mjs.map +1 -0
  4. package/fesm2022/{pptx-angular-viewer-chat-history-idb-NpaCpp0C.mjs → pptx-angular-viewer-chat-history-idb-7dyn_ZbD.mjs} +2 -2
  5. package/fesm2022/{pptx-angular-viewer-chat-history-idb-NpaCpp0C.mjs.map → pptx-angular-viewer-chat-history-idb-7dyn_ZbD.mjs.map} +1 -1
  6. package/fesm2022/{pptx-angular-viewer-pptx-angular-viewer-C2z1kMFI.mjs → pptx-angular-viewer-pptx-angular-viewer-Cf08ueRA.mjs} +11005 -2545
  7. package/fesm2022/{pptx-angular-viewer-pptx-angular-viewer-C2z1kMFI.mjs.map → pptx-angular-viewer-pptx-angular-viewer-Cf08ueRA.mjs.map} +1 -1
  8. package/fesm2022/pptx-angular-viewer-view-scene-Bn9SJZk9.mjs +862 -0
  9. package/fesm2022/pptx-angular-viewer-view-scene-Bn9SJZk9.mjs.map +1 -0
  10. package/fesm2022/pptx-angular-viewer.mjs +1 -1
  11. package/i18n/de.cjs +134 -117
  12. package/i18n/de.mjs +134 -117
  13. package/i18n/es.cjs +134 -117
  14. package/i18n/es.mjs +134 -117
  15. package/i18n/fr.cjs +134 -117
  16. package/i18n/fr.mjs +134 -117
  17. package/i18n/zh-CN.cjs +111 -99
  18. package/i18n/zh-CN.mjs +111 -99
  19. package/package.json +2 -2
  20. package/types/pptx-angular-viewer.d.ts +774 -116
  21. package/types/pptx-angular-viewer.d.ts.map +1 -1
  22. package/fesm2022/pptx-angular-viewer-chart-3d-view-scene-BvVx5_I3.mjs +0 -470
  23. package/fesm2022/pptx-angular-viewer-chart-3d-view-scene-BvVx5_I3.mjs.map +0 -1
  24. package/fesm2022/pptx-angular-viewer-view-scene-C5oUG9yl.mjs +0 -235
  25. package/fesm2022/pptx-angular-viewer-view-scene-C5oUG9yl.mjs.map +0 -1
@@ -0,0 +1,862 @@
1
+ import { x as hexToRgb, y as smartArt3DGradientColor, z as shadeSmartArt3DVertices, A as smartArt3DGradientCoordinate, B as threeViewOverflowFromNdc, N as NO_THREE_VIEW_OVERFLOW, E as hasThreeViewOverflow, F as overflowViewOffset, G as smartArt3DCameraMatrix, H as smartArt3DEyeInDiagram, I as resolveSmartArt3DLightModel } from './pptx-angular-viewer-pptx-angular-viewer-Cf08ueRA.mjs';
2
+
3
+ /** Supersampling factor for crisp text at any zoom. */
4
+ const SUPERSAMPLE = 3;
5
+ /** Build a canvas texture rendering `block`'s pre-wrapped lines, or `null` off-DOM. */
6
+ function buildTextBlockTexture(three, block) {
7
+ if (typeof document === 'undefined' || block.lines.every((l) => l.text.length === 0)) {
8
+ return null;
9
+ }
10
+ const worldWidth = Math.max(1, block.maxWidth);
11
+ const worldHeight = Math.max(1, block.maxHeight);
12
+ const canvas = document.createElement('canvas');
13
+ canvas.width = Math.max(8, Math.round(worldWidth * SUPERSAMPLE));
14
+ canvas.height = Math.max(8, Math.round(worldHeight * SUPERSAMPLE));
15
+ const ctx2d = canvas.getContext('2d');
16
+ if (!ctx2d) {
17
+ return null;
18
+ }
19
+ const weight = block.fontWeight ?? 400;
20
+ const italic = block.fontStyle === 'italic' ? 'italic ' : '';
21
+ const family = block.fontFamily
22
+ ? `${block.fontFamily}, system-ui, sans-serif`
23
+ : 'system-ui, -apple-system, Segoe UI, Roboto, sans-serif';
24
+ const px = Math.max(6, block.fontSize) * SUPERSAMPLE;
25
+ ctx2d.clearRect(0, 0, canvas.width, canvas.height);
26
+ ctx2d.fillStyle = block.color;
27
+ ctx2d.textAlign = 'center';
28
+ // `dy` places each line's CENTRE (as `centeredSvgTextLines` lays it out,
29
+ // and the 2D renderers draw with `dominant-baseline: central`).
30
+ ctx2d.textBaseline = 'middle';
31
+ ctx2d.font = `${italic}${weight} ${px}px ${family}`;
32
+ const centerX = canvas.width / 2;
33
+ const centerY = canvas.height / 2;
34
+ for (const line of block.lines) {
35
+ if (!line.text) {
36
+ continue;
37
+ }
38
+ // `dy` is a y-up offset from the block centre; canvas y grows downward.
39
+ ctx2d.fillText(line.text, centerX, centerY - line.dy * SUPERSAMPLE);
40
+ }
41
+ const texture = new three.CanvasTexture(canvas);
42
+ texture.colorSpace = three.SRGBColorSpace;
43
+ texture.minFilter = three.LinearFilter;
44
+ texture.magFilter = three.LinearFilter;
45
+ // See `smartart-3d/text-texture.ts` for why flipY must stay false with a
46
+ // compensating UV flip: WebGL2 forbids UNPACK_FLIP_Y_WEBGL for some texture
47
+ // targets, and leaving it enabled pollutes global pixel-store state.
48
+ texture.flipY = false;
49
+ texture.premultiplyAlpha = false;
50
+ texture.wrapT = three.RepeatWrapping;
51
+ texture.repeat.set(1, -1);
52
+ texture.offset.set(0, 1);
53
+ texture.needsUpdate = true;
54
+ return { texture, worldWidth, worldHeight };
55
+ }
56
+
57
+ /** z-lift of the stroke outline above its fill, so the two don't z-fight. */
58
+ const STROKE_Z_LIFT = 0.05;
59
+ function buildShape(three, mesh) {
60
+ const shape = new three.Shape(mesh.outline.map((p) => new three.Vector2(p.x, p.y)));
61
+ for (const hole of mesh.holes ?? []) {
62
+ shape.holes.push(new three.Path(hole.map((p) => new three.Vector2(p.x, p.y))));
63
+ }
64
+ return shape;
65
+ }
66
+ /** Build the fill mesh for a flat (zero-depth) `SmartArt3DMesh`. */
67
+ function buildFlatFill(three, mesh, shape, disposables) {
68
+ if (mesh.fillNone) {
69
+ return null;
70
+ }
71
+ const geometry = new three.ShapeGeometry(shape);
72
+ const material = new three.MeshBasicMaterial({
73
+ color: mesh.fill,
74
+ side: three.DoubleSide,
75
+ transparent: mesh.opacity < 1,
76
+ opacity: mesh.opacity,
77
+ });
78
+ disposables.push(geometry, material);
79
+ return new three.Mesh(geometry, material);
80
+ }
81
+ /** Build the stroke outline (and hole outlines) as line loops. */
82
+ function buildStrokeLines(three, mesh, disposables) {
83
+ if (mesh.strokeWidth <= 0 || mesh.stroke === 'transparent') {
84
+ return [];
85
+ }
86
+ const material = new three.LineBasicMaterial({ color: mesh.stroke });
87
+ disposables.push(material);
88
+ const loops = [mesh.outline, ...(mesh.holes ?? [])];
89
+ return loops.map((loop) => {
90
+ const geometry = new three.BufferGeometry().setFromPoints(loop.map((p) => new three.Vector3(p.x, p.y, STROKE_Z_LIFT)));
91
+ disposables.push(geometry);
92
+ return new three.LineLoop(geometry, material);
93
+ });
94
+ }
95
+ /** Build the text label plane, or `null` when the mesh has no text. */
96
+ function buildTextPlane(three, mesh, group, disposables) {
97
+ if (!mesh.textBlock) {
98
+ return;
99
+ }
100
+ const built = buildTextBlockTexture(three, mesh.textBlock);
101
+ if (!built) {
102
+ return;
103
+ }
104
+ disposables.push(built.texture);
105
+ const geometry = new three.PlaneGeometry(built.worldWidth, built.worldHeight);
106
+ const material = new three.MeshBasicMaterial({
107
+ map: built.texture,
108
+ transparent: true,
109
+ depthWrite: false,
110
+ });
111
+ disposables.push(geometry, material);
112
+ const plane = new three.Mesh(geometry, material);
113
+ // `textBlock` carries its own world position (already includes the
114
+ // shape's rotation applied to its centre); the group itself is placed at
115
+ // the shape's own centre, so offset back to absolute world space here.
116
+ plane.position.set(mesh.textBlock.x - mesh.position.x, mesh.textBlock.y - mesh.position.y, mesh.textBlock.z - mesh.position.z);
117
+ group.add(plane);
118
+ }
119
+ /** Build the complete `Object3D` for one flat mesh. */
120
+ function buildFlatMeshObject(three, mesh) {
121
+ const disposables = [];
122
+ const group = new three.Group();
123
+ group.position.set(mesh.position.x, mesh.position.y, mesh.position.z);
124
+ const shape = buildShape(three, mesh);
125
+ const fill = buildFlatFill(three, mesh, shape, disposables);
126
+ if (fill) {
127
+ group.add(fill);
128
+ }
129
+ for (const line of buildStrokeLines(three, mesh, disposables)) {
130
+ group.add(line);
131
+ }
132
+ buildTextPlane(three, mesh, group, disposables);
133
+ return { group, disposables };
134
+ }
135
+
136
+ /** Points closer than this are treated as the same vertex. */
137
+ const EPSILON = 1e-6;
138
+ /** Longest mitre, as a multiple of the offset distance (sharp spikes are clipped). */
139
+ const MITER_LIMIT = 4;
140
+ /** Drop repeated consecutive points and the closing duplicate of the first point. */
141
+ function cleanRing(points) {
142
+ const out = [];
143
+ for (const p of points) {
144
+ const last = out[out.length - 1];
145
+ if (!last || Math.hypot(p.x - last.x, p.y - last.y) > EPSILON) {
146
+ out.push({ x: p.x, y: p.y });
147
+ }
148
+ }
149
+ while (out.length > 1) {
150
+ const first = out[0];
151
+ const last = out[out.length - 1];
152
+ if (Math.hypot(first.x - last.x, first.y - last.y) > EPSILON) {
153
+ break;
154
+ }
155
+ out.pop();
156
+ }
157
+ return out;
158
+ }
159
+ /** Shoelace signed area: positive for a counter-clockwise ring in y-up space. */
160
+ function ringSignedArea(ring) {
161
+ let area = 0;
162
+ for (let i = 0; i < ring.length; i++) {
163
+ const a = ring[i];
164
+ const b = ring[(i + 1) % ring.length];
165
+ area += a.x * b.y - b.x * a.y;
166
+ }
167
+ return area / 2;
168
+ }
169
+ /** A cleaned, counter-clockwise (y-up) copy of `points`. */
170
+ function toCcwRing(points) {
171
+ const ring = cleanRing(points);
172
+ return ringSignedArea(ring) < 0 ? ring.reverse() : ring;
173
+ }
174
+ /**
175
+ * Unit inward normal of every edge `i -> i + 1` of a counter-clockwise ring
176
+ * (the interior is on the edge's left).
177
+ */
178
+ function ringEdgeNormals(ring) {
179
+ return ring.map((a, i) => {
180
+ const b = ring[(i + 1) % ring.length];
181
+ const dx = b.x - a.x;
182
+ const dy = b.y - a.y;
183
+ const len = Math.hypot(dx, dy) || 1;
184
+ return { x: -dy / len, y: dx / len };
185
+ });
186
+ }
187
+ /**
188
+ * Offset a counter-clockwise ring by `distance` along its inward normals
189
+ * (negative grows it). Each vertex moves along its mitre, clipped at
190
+ * {@link MITER_LIMIT} times the distance so a sharp spike stays bounded.
191
+ */
192
+ function offsetRing(ring, distance) {
193
+ if (distance === 0) {
194
+ return ring.map((p) => ({ x: p.x, y: p.y }));
195
+ }
196
+ const normals = ringEdgeNormals(ring);
197
+ const n = ring.length;
198
+ return ring.map((p, i) => {
199
+ const prev = normals[(i - 1 + n) % n];
200
+ const next = normals[i];
201
+ const denom = Math.max(0.05, 1 + prev.x * next.x + prev.y * next.y);
202
+ let ox = ((prev.x + next.x) / denom) * distance;
203
+ let oy = ((prev.y + next.y) / denom) * distance;
204
+ const len = Math.hypot(ox, oy);
205
+ const max = Math.abs(distance) * MITER_LIMIT;
206
+ if (len > max) {
207
+ ox = (ox / len) * max;
208
+ oy = (oy / len) * max;
209
+ }
210
+ return { x: p.x + ox, y: p.y + oy };
211
+ });
212
+ }
213
+ /**
214
+ * The in-plane inward direction to shade each edge's two ends with.
215
+ *
216
+ * `result[i]` is `[atStart, atEnd]` for edge `i -> i + 1`. Where the ring
217
+ * turns by less than `smoothDeg` (a curve's tessellation) the two edges share
218
+ * the averaged normal, so a circle shades smoothly; at a real corner each
219
+ * edge keeps its own normal, so a rectangle's sides shade flat.
220
+ */
221
+ function ringShadingNormals(ring, smoothDeg = 30) {
222
+ const normals = ringEdgeNormals(ring);
223
+ const n = ring.length;
224
+ const cosLimit = Math.cos((smoothDeg * Math.PI) / 180);
225
+ const vertexNormal = (own, other) => {
226
+ if (own.x * other.x + own.y * other.y < cosLimit) {
227
+ return own;
228
+ }
229
+ const x = own.x + other.x;
230
+ const y = own.y + other.y;
231
+ const len = Math.hypot(x, y) || 1;
232
+ return { x: x / len, y: y / len };
233
+ };
234
+ return normals.map((own, i) => [
235
+ vertexNormal(own, normals[(i - 1 + n) % n]),
236
+ vertexNormal(own, normals[(i + 1) % n]),
237
+ ]);
238
+ }
239
+
240
+ /**
241
+ * `a:bevelT/@prst` cross-sections for the lit SmartArt 3D solids
242
+ * (framework-agnostic, pure).
243
+ *
244
+ * A profile is a polyline from the shape's outline (`s = 0`, `t = 0`) to the
245
+ * inner edge of the bevel (`s = 1`, `t = 1`): `s` is the inset as a fraction
246
+ * of the bevel width, `t` the height as a fraction of the bevel height. The
247
+ * shapes follow the ECMA-376 20.1.10.9 preset silhouettes, tuned against the
248
+ * PowerPoint exports of the SmartArt bevel quick styles
249
+ * (`e2e/fixtures/three-d-parity/gt`): `circle` (Polished) is a quarter round,
250
+ * `relaxedInset` (Inset) dips into a groove before rising to the face, which
251
+ * is what paints its dark outer line and bright inner ridge.
252
+ *
253
+ * @module render/smartart-3d-bevel-profile
254
+ */
255
+ /** Samples used for the curved profiles. */
256
+ const CURVE_SEGMENTS = 8;
257
+ function sampleCurve(fn) {
258
+ const points = [];
259
+ for (let i = 0; i <= CURVE_SEGMENTS; i++) {
260
+ points.push(fn(i / CURVE_SEGMENTS));
261
+ }
262
+ return points;
263
+ }
264
+ const QUARTER = Math.PI / 2;
265
+ const PROFILES = {
266
+ // Quarter round: vertical at the outline, flat where it meets the face.
267
+ circle: () => sampleCurve((u) => ({ s: 1 - Math.cos(u * QUARTER), t: Math.sin(u * QUARTER) })),
268
+ // Round, but reaching the outline at a slant instead of vertically.
269
+ convex: () => sampleCurve((u) => ({ s: u, t: Math.sin(u * QUARTER) })),
270
+ softRound: () => sampleCurve((u) => ({ s: u, t: 1 - (1 - u) * (1 - u) })),
271
+ // A groove next to the outline, then a rounded rise onto the face.
272
+ relaxedInset: () => sampleCurve((u) => ({
273
+ s: u,
274
+ t: u * u * (3 - 2 * u) - 0.3 * Math.sin(Math.PI * u) * (1 - u),
275
+ })),
276
+ divot: () => sampleCurve((u) => ({
277
+ s: u,
278
+ t: u - 0.5 * Math.sin(Math.PI * u),
279
+ })),
280
+ angle: () => [
281
+ { s: 0, t: 0 },
282
+ { s: 1, t: 1 },
283
+ ],
284
+ slope: () => [
285
+ { s: 0, t: 0 },
286
+ { s: 1, t: 1 },
287
+ ],
288
+ coolSlant: () => [
289
+ { s: 0, t: 0 },
290
+ { s: 0.3, t: 0.75 },
291
+ { s: 1, t: 1 },
292
+ ],
293
+ hardEdge: () => [
294
+ { s: 0, t: 0 },
295
+ { s: 0.2, t: 0.9 },
296
+ { s: 1, t: 1 },
297
+ ],
298
+ cross: () => [
299
+ { s: 0, t: 0 },
300
+ { s: 0.5, t: 1 },
301
+ { s: 1, t: 1 },
302
+ ],
303
+ riblet: () => [
304
+ { s: 0, t: 0 },
305
+ { s: 0.33, t: 1 },
306
+ { s: 0.66, t: 0.5 },
307
+ { s: 1, t: 1 },
308
+ ],
309
+ artDeco: () => [
310
+ { s: 0, t: 0 },
311
+ { s: 0.25, t: 0.5 },
312
+ { s: 0.5, t: 0.5 },
313
+ { s: 0.75, t: 1 },
314
+ { s: 1, t: 1 },
315
+ ],
316
+ };
317
+ /**
318
+ * The cross-section for a bevel preset token; unknown tokens fall back to
319
+ * `circle` (the ECMA-376 default for an `a:bevelT` without `@prst`).
320
+ */
321
+ function getSmartArtBevelProfile(profile) {
322
+ const build = (profile && PROFILES[profile]) || PROFILES.circle;
323
+ return build();
324
+ }
325
+
326
+ /** Contour rims sit this far in front of the plane they ring, so they win the depth test. */
327
+ const CONTOUR_Z_LIFT = 0.05;
328
+ /** Slope used for a vertical profile segment (an outward, horizontal normal). */
329
+ const VERTICAL_SLOPE = 1e3;
330
+ function emptyTriangles() {
331
+ return { positions: [], normals: [] };
332
+ }
333
+ function pushVertex(out, p, z, n) {
334
+ out.positions.push(p.x, p.y, z);
335
+ out.normals.push(n[0], n[1], n[2]);
336
+ }
337
+ function normalize(x, y, z) {
338
+ const len = Math.hypot(x, y, z) || 1;
339
+ return [x / len, y / len, z / len];
340
+ }
341
+ /** Push the two triangles of quad `a b c d` (in order around the quad). */
342
+ function pushQuad(out, corners) {
343
+ const [a, b, c, d] = corners;
344
+ for (const v of [a, b, c, a, c, d]) {
345
+ pushVertex(out, v.p, v.z, v.n);
346
+ }
347
+ }
348
+ /**
349
+ * A bevel band from `ring` (at `zOuter`) inward to `ring` inset by the bevel
350
+ * width, rising by the bevel height toward `facing` (+1 front, -1 back).
351
+ * Returns the band's inner ring.
352
+ */
353
+ function addBand(out, ring, bevel, zOuter, facing) {
354
+ const profile = getSmartArtBevelProfile(bevel.profile);
355
+ const rings = profile.map((pt) => offsetRing(ring, bevel.width * pt.s));
356
+ const shading = ringShadingNormals(ring);
357
+ const n = ring.length;
358
+ for (let k = 0; k < profile.length - 1; k++) {
359
+ const ds = profile[k + 1].s - profile[k].s;
360
+ const dt = profile[k + 1].t - profile[k].t;
361
+ const slope = Math.abs(ds) < 1e-6
362
+ ? Math.sign(dt || 1) * VERTICAL_SLOPE
363
+ : (bevel.height * dt) / (bevel.width * ds);
364
+ const z0 = zOuter + facing * bevel.height * profile[k].t;
365
+ const z1 = zOuter + facing * bevel.height * profile[k + 1].t;
366
+ for (let i = 0; i < n; i++) {
367
+ const j = (i + 1) % n;
368
+ const [mi, mj] = shading[i];
369
+ const ni = normalize(-slope * mi.x, -slope * mi.y, facing);
370
+ const nj = normalize(-slope * mj.x, -slope * mj.y, facing);
371
+ pushQuad(out, [
372
+ { p: rings[k][i], z: z0, n: ni },
373
+ { p: rings[k][j], z: z0, n: nj },
374
+ { p: rings[k + 1][j], z: z1, n: nj },
375
+ { p: rings[k + 1][i], z: z1, n: ni },
376
+ ]);
377
+ }
378
+ }
379
+ return rings[rings.length - 1];
380
+ }
381
+ /** Vertical walls around `ring` from `zTop` down to `zBottom`, facing outward. */
382
+ function addWalls(out, ring, zTop, zBottom) {
383
+ if (zTop - zBottom <= 0) {
384
+ return;
385
+ }
386
+ const shading = ringShadingNormals(ring);
387
+ const n = ring.length;
388
+ for (let i = 0; i < n; i++) {
389
+ const j = (i + 1) % n;
390
+ const [mi, mj] = shading[i];
391
+ const ni = normalize(-mi.x, -mi.y, 0);
392
+ const nj = normalize(-mj.x, -mj.y, 0);
393
+ pushQuad(out, [
394
+ { p: ring[i], z: zTop, n: ni },
395
+ { p: ring[j], z: zTop, n: nj },
396
+ { p: ring[j], z: zBottom, n: nj },
397
+ { p: ring[i], z: zBottom, n: ni },
398
+ ]);
399
+ }
400
+ }
401
+ /**
402
+ * Build the geometry for one solid from its mesh-local outline (and holes,
403
+ * which are cut from the caps but not bevelled).
404
+ */
405
+ function buildSmartArt3DSolidGeometry(outline, holes, solid) {
406
+ const ring = toCcwRing(outline);
407
+ const body = emptyTriangles();
408
+ const sides = emptyTriangles();
409
+ const topHeight = solid.bevelTop?.height ?? 0;
410
+ const zShoulder = -topHeight;
411
+ const zBackShoulder = zShoulder - solid.extrusion;
412
+ const backHeight = solid.bevelBottom?.height ?? 0;
413
+ const zBack = zBackShoulder - backHeight;
414
+ const holeRings = holes.map((hole) => toCcwRing(hole)).filter((hole) => hole.length >= 3);
415
+ const frontRing = solid.bevelTop ? addBand(body, ring, solid.bevelTop, zShoulder, 1) : ring;
416
+ addWalls(sides, ring, zShoulder, zBackShoulder);
417
+ const hasDepth = zBack < 0;
418
+ const backRing = hasDepth && solid.bevelBottom
419
+ ? addBand(sides, ring, solid.bevelBottom, zBackShoulder, -1)
420
+ : ring;
421
+ const contourCaps = [];
422
+ if (solid.contourWidth > 0) {
423
+ const rim = offsetRing(ring, -solid.contourWidth);
424
+ const inner = [...ring].reverse();
425
+ contourCaps.push({ ring: rim, holes: [inner], z: zShoulder + CONTOUR_Z_LIFT, facing: 1 });
426
+ if (hasDepth) {
427
+ contourCaps.push({ ring: rim, holes: [inner], z: zBack - CONTOUR_Z_LIFT, facing: -1 });
428
+ }
429
+ }
430
+ return {
431
+ body,
432
+ sides,
433
+ frontCap: { ring: frontRing, holes: holeRings, z: 0, facing: 1 },
434
+ ...(hasDepth ? { backCap: { ring: backRing, holes: holeRings, z: zBack, facing: -1 } } : {}),
435
+ contourCaps,
436
+ };
437
+ }
438
+
439
+ /** Depth between two side layers, layout px (fine enough to read as a wall). */
440
+ const TEXT_LAYER_STEP = 0.4;
441
+ /** Most side layers one label gets. */
442
+ const MAX_TEXT_LAYERS = 32;
443
+ /**
444
+ * sRGB factor on the side colour. Read off the Bird's Eye Scene export
445
+ * (`gt/sa-014.webp`): white letters show walls around (164, 172, 175) under
446
+ * a front face reading (245, 248, 249).
447
+ */
448
+ const TEXT_SIDE_SHADE = 0.69;
449
+ function shadeHex(hex, factor) {
450
+ const match = /^#?([0-9a-f]{6})$/iu.exec(hex.trim());
451
+ if (!match) {
452
+ return hex;
453
+ }
454
+ const n = Number.parseInt(match[1], 16);
455
+ const channel = (shift) => Math.round(((n >> shift) & 255) * factor)
456
+ .toString(16)
457
+ .padStart(2, '0');
458
+ return `#${channel(16)}${channel(8)}${channel(0)}`;
459
+ }
460
+ /**
461
+ * The stacked layers for an extruded label, or `undefined` when the label is
462
+ * flat (no `extrusion`).
463
+ */
464
+ function smartArt3DTextLayers(block) {
465
+ const depth = block.extrusion ?? 0;
466
+ if (!(depth > 0)) {
467
+ return undefined;
468
+ }
469
+ const count = Math.max(1, Math.min(MAX_TEXT_LAYERS, Math.ceil(depth / TEXT_LAYER_STEP)));
470
+ const sideZ = [];
471
+ for (let i = 0; i < count; i++) {
472
+ sideZ.push(block.z + (depth * i) / count);
473
+ }
474
+ return {
475
+ sideZ,
476
+ frontZ: block.z + depth,
477
+ sideColor: shadeHex(block.extrusionColor ?? block.color, TEXT_SIDE_SHADE),
478
+ };
479
+ }
480
+
481
+ /**
482
+ * Planes showing `block` at each of `zs` (sharing one texture, geometry and
483
+ * material).
484
+ */
485
+ function textPlanes(three, mesh, block, zs, light, disposables) {
486
+ const built = zs.length > 0 ? buildTextBlockTexture(three, block) : null;
487
+ if (!built) {
488
+ return [];
489
+ }
490
+ const geometry = new three.PlaneGeometry(built.worldWidth, built.worldHeight);
491
+ const material = new three.MeshBasicMaterial({
492
+ map: built.texture,
493
+ transparent: true,
494
+ depthWrite: false,
495
+ });
496
+ // The rig's tint is a linear factor, like the solids' vertex colours.
497
+ material.color.setRGB(light.tint[0], light.tint[1], light.tint[2]);
498
+ disposables.push(built.texture, geometry, material);
499
+ return zs.map((z) => {
500
+ const plane = new three.Mesh(geometry, material);
501
+ // Text blocks carry world positions; the group sits at the mesh position.
502
+ plane.position.set(block.x - mesh.position.x, block.y - mesh.position.y, z - mesh.position.z);
503
+ return plane;
504
+ });
505
+ }
506
+ /** Add the lit label (flat or extruded) of `mesh` to `group`. */
507
+ function buildLitTextObject(three, mesh, group, light, disposables) {
508
+ const block = mesh.textBlock;
509
+ if (!block) {
510
+ return;
511
+ }
512
+ const layers = smartArt3DTextLayers(block);
513
+ const planes = [
514
+ ...(layers
515
+ ? textPlanes(three, mesh, { ...block, color: layers.sideColor }, layers.sideZ, light, disposables)
516
+ : []),
517
+ ...textPlanes(three, mesh, block, [layers?.frontZ ?? block.z], light, disposables),
518
+ ];
519
+ for (const plane of planes) {
520
+ group.add(plane);
521
+ }
522
+ }
523
+
524
+ /** Width of the 1D gradient lookup texture. */
525
+ const GRADIENT_TEXELS = 256;
526
+ /** Append a triangulated planar cap to a triangle list. */
527
+ function appendCap(three, out, cap) {
528
+ if (cap.ring.length < 3) {
529
+ return;
530
+ }
531
+ const contour = cap.ring.map((p) => new three.Vector2(p.x, p.y));
532
+ const holes = cap.holes.map((hole) => hole.map((p) => new three.Vector2(p.x, p.y)));
533
+ const all = [...contour, ...holes.flat()];
534
+ for (const face of three.ShapeUtils.triangulateShape(contour, holes)) {
535
+ for (const index of face) {
536
+ const p = all[index];
537
+ out.positions.push(p.x, p.y, cap.z);
538
+ out.normals.push(0, 0, cap.facing);
539
+ }
540
+ }
541
+ }
542
+ function buildGradientTexture(three, gradient) {
543
+ if (typeof document === 'undefined') {
544
+ return null;
545
+ }
546
+ const canvas = document.createElement('canvas');
547
+ canvas.width = GRADIENT_TEXELS;
548
+ canvas.height = 1;
549
+ const ctx = canvas.getContext('2d');
550
+ if (!ctx) {
551
+ return null;
552
+ }
553
+ const paint = ctx.createLinearGradient(0, 0, GRADIENT_TEXELS, 0);
554
+ for (const stop of gradient.stops) {
555
+ paint.addColorStop(stop.offset, stop.color);
556
+ }
557
+ ctx.fillStyle = paint;
558
+ ctx.fillRect(0, 0, GRADIENT_TEXELS, 1);
559
+ const texture = new three.CanvasTexture(canvas);
560
+ texture.colorSpace = three.SRGBColorSpace;
561
+ texture.wrapS = three.ClampToEdgeWrapping;
562
+ texture.needsUpdate = true;
563
+ return texture;
564
+ }
565
+ function buildPart(three, part, light, eye, disposables) {
566
+ const { positions, normals } = part.triangles;
567
+ if (positions.length === 0) {
568
+ return null;
569
+ }
570
+ const gradient = part.gradient;
571
+ const flatColor = hexToRgb(part.color);
572
+ const baseAt = (p) => (gradient ? smartArt3DGradientColor(gradient, p) : flatColor);
573
+ const geometry = new three.BufferGeometry();
574
+ geometry.setAttribute('position', new three.Float32BufferAttribute(positions, 3));
575
+ geometry.setAttribute('color', new three.BufferAttribute(shadeSmartArt3DVertices(positions, normals, baseAt, light, eye), 3));
576
+ const texture = gradient ? buildGradientTexture(three, gradient) : null;
577
+ if (gradient && texture) {
578
+ const uv = [];
579
+ for (let i = 0; i < positions.length; i += 3) {
580
+ uv.push(smartArt3DGradientCoordinate({ x: positions[i], y: positions[i + 1] }, gradient), 0.5);
581
+ }
582
+ geometry.setAttribute('uv', new three.Float32BufferAttribute(uv, 2));
583
+ disposables.push(texture);
584
+ }
585
+ const material = new three.MeshBasicMaterial({
586
+ color: texture ? '#ffffff' : part.color,
587
+ map: texture,
588
+ vertexColors: true,
589
+ side: three.DoubleSide,
590
+ transparent: part.opacity < 1,
591
+ opacity: part.opacity,
592
+ });
593
+ disposables.push(geometry, material);
594
+ return new three.Mesh(geometry, material);
595
+ }
596
+ /**
597
+ * Build the complete `Object3D` for one lit mesh (in diagram space; a scene
598
+ * camera's rotation is applied by the caller's parent group).
599
+ *
600
+ * @param eye - a perspective scene camera's position in diagram space.
601
+ */
602
+ function buildLitMeshObject(three, mesh, light, eye) {
603
+ const localEye = eye
604
+ ? { x: eye.x - mesh.position.x, y: eye.y - mesh.position.y, z: eye.z - mesh.position.z }
605
+ : undefined;
606
+ const disposables = [];
607
+ const group = new three.Group();
608
+ group.position.set(mesh.position.x, mesh.position.y, mesh.position.z);
609
+ if (!mesh.fillNone && mesh.outline.length >= 3) {
610
+ const holes = mesh.holes ?? [];
611
+ const geometry = mesh.solid
612
+ ? buildSmartArt3DSolidGeometry(mesh.outline, holes, mesh.solid)
613
+ : undefined;
614
+ const body = geometry?.body ?? { positions: [], normals: [] };
615
+ appendCap(three, body, geometry?.frontCap ?? { ring: toCcwRing(mesh.outline), holes, z: 0, facing: 1 });
616
+ const sides = geometry?.sides ?? { positions: [], normals: [] };
617
+ if (geometry?.backCap) {
618
+ appendCap(three, sides, geometry.backCap);
619
+ }
620
+ const contour = { positions: [], normals: [] };
621
+ for (const cap of geometry?.contourCaps ?? []) {
622
+ appendCap(three, contour, cap);
623
+ }
624
+ const parts = [
625
+ { triangles: body, color: mesh.fill, gradient: mesh.gradient, opacity: mesh.opacity },
626
+ {
627
+ triangles: sides,
628
+ color: mesh.solid?.extrusionColor ?? mesh.fill,
629
+ opacity: mesh.opacity,
630
+ },
631
+ {
632
+ triangles: contour,
633
+ color: mesh.solid?.contourColor ?? mesh.fill,
634
+ opacity: mesh.opacity,
635
+ },
636
+ ];
637
+ for (const part of parts) {
638
+ const built = buildPart(three, part, light, localEye, disposables);
639
+ if (built) {
640
+ if (mesh.opacity < 1) {
641
+ // A translucent fill (Basic Venn): the whole solid shows through.
642
+ const material = built.material;
643
+ material.transparent = true;
644
+ material.opacity = mesh.opacity;
645
+ }
646
+ group.add(built);
647
+ }
648
+ }
649
+ }
650
+ for (const line of buildStrokeLines(three, mesh, disposables)) {
651
+ group.add(line);
652
+ }
653
+ buildLitTextObject(three, mesh, group, light, disposables);
654
+ return { group, disposables };
655
+ }
656
+
657
+ /** Orthographic camera distance from the origin along +z (near/far only). */
658
+ const ORTHO_DISTANCE = 1000;
659
+ /** Half-extents of the view frustum at z = 0, letterboxed to the view's aspect. */
660
+ function frameHalfExtents(bounds, size) {
661
+ const halfW = Math.max(1, bounds.width) / 2;
662
+ const halfH = Math.max(1, bounds.height) / 2;
663
+ const aspect = Math.max(1, size.pixelWidth) / Math.max(1, size.pixelHeight);
664
+ const fitHalfH = Math.max(halfH, halfW / aspect);
665
+ return { halfW: fitHalfH * aspect, halfH: fitHalfH };
666
+ }
667
+ /** The camera for a model, framed for `size`. */
668
+ function buildSmartArtViewCamera(three, bounds, sceneCamera, size) {
669
+ let camera;
670
+ if (sceneCamera?.projection === 'perspective') {
671
+ const distance = Math.max(1, sceneCamera.distance);
672
+ camera = new three.PerspectiveCamera(30, 1, distance / 20, distance * 20);
673
+ camera.position.set(0, 0, distance);
674
+ }
675
+ else {
676
+ camera = new three.OrthographicCamera(-1, 1, 1, -1, -ORTHO_DISTANCE * 2, ORTHO_DISTANCE * 2);
677
+ camera.position.set(0, 0, ORTHO_DISTANCE);
678
+ }
679
+ camera.zoom = sceneCamera?.zoom ?? 1;
680
+ camera.lookAt(0, 0, 0);
681
+ fitSmartArtViewCamera(camera, bounds, size);
682
+ return camera;
683
+ }
684
+ /** Re-frame a camera from {@link buildSmartArtViewCamera} for a new view size. */
685
+ function fitSmartArtViewCamera(camera, bounds, size) {
686
+ const { halfW, halfH } = frameHalfExtents(bounds, size);
687
+ if ('isPerspectiveCamera' in camera && camera.isPerspectiveCamera) {
688
+ camera.aspect = halfW / halfH;
689
+ camera.fov = (2 * Math.atan(halfH / camera.position.length()) * 180) / Math.PI;
690
+ }
691
+ else {
692
+ const ortho = camera;
693
+ ortho.left = -halfW;
694
+ ortho.right = halfW;
695
+ ortho.top = halfH;
696
+ ortho.bottom = -halfH;
697
+ }
698
+ camera.updateProjectionMatrix();
699
+ }
700
+
701
+ /**
702
+ * Project the corners of every mesh's bounding box through `camera` (with no
703
+ * view offset) and measure how far they reach past the element box.
704
+ */
705
+ function measureOverflow(three, root, camera) {
706
+ root.updateMatrixWorld(true);
707
+ camera.updateMatrixWorld(true);
708
+ const points = [];
709
+ const corner = new three.Vector3();
710
+ root.traverse((object) => {
711
+ const geometry = object.geometry;
712
+ if (!geometry?.getAttribute?.('position')) {
713
+ return;
714
+ }
715
+ if (!geometry.boundingBox) {
716
+ geometry.computeBoundingBox();
717
+ }
718
+ const box = geometry.boundingBox;
719
+ if (!box || box.isEmpty()) {
720
+ return;
721
+ }
722
+ for (let i = 0; i < 8; i++) {
723
+ corner
724
+ .set(i & 1 ? box.max.x : box.min.x, i & 2 ? box.max.y : box.min.y, i & 4 ? box.max.z : box.min.z)
725
+ .applyMatrix4(object.matrixWorld)
726
+ .project(camera);
727
+ points.push({ x: corner.x, y: corner.y });
728
+ }
729
+ });
730
+ return threeViewOverflowFromNdc(points);
731
+ }
732
+ /**
733
+ * Frame `camera` for the element box plus the diagram's overflow, and return
734
+ * that overflow. `turned` is false for a face-on diagram, which always fits
735
+ * its box (no overflow is measured).
736
+ */
737
+ function frameSmartArtOverflow(three, root, camera, size, turned) {
738
+ camera.clearViewOffset();
739
+ if (!turned) {
740
+ return NO_THREE_VIEW_OVERFLOW;
741
+ }
742
+ const overflow = measureOverflow(three, root, camera);
743
+ if (hasThreeViewOverflow(overflow)) {
744
+ const offset = overflowViewOffset(size, overflow);
745
+ camera.setViewOffset(offset.fullWidth, offset.fullHeight, offset.x, offset.y, offset.width, offset.height);
746
+ }
747
+ return overflow;
748
+ }
749
+
750
+ /**
751
+ * `<pptx-three-view>` scene module for 3D SmartArt.
752
+ *
753
+ * Flat quick styles (Simple Fill, White Outline, Subtle, Moderate, Intense):
754
+ * every mesh is a zero-depth `ShapeGeometry` face, unlit (`MeshBasicMaterial`),
755
+ * under a fixed orthographic camera framing the diagram's own viewBox
756
+ * (`model.bounds`) - the same geometry/fills/text the 2D SVG renderer draws,
757
+ * built by `render/smartart-3d-drawing-model.ts`.
758
+ *
759
+ * Bevel and scene quick styles (`model.styleCategory` `'bevel'`/`'scene'`):
760
+ * each mesh becomes a lit solid (`lit-mesh-object.ts`: bevel bands,
761
+ * extrusion, contour, per-vertex shading from the model's light rig). A scene
762
+ * style also turns the whole diagram by its quick-style camera
763
+ * (`model.camera`, `view-camera.ts`).
764
+ *
765
+ * Must not import `three` at runtime (use `ctx.three`): this module is
766
+ * reachable from the main barrel through the scene registry.
767
+ *
768
+ * @module smartart-3d/view-scene
769
+ */
770
+ async function mountSmartArt3DView(model, ctx) {
771
+ const { three } = ctx;
772
+ const scene = new three.Scene();
773
+ if (model.background) {
774
+ scene.background = new three.Color(model.background);
775
+ }
776
+ const disposables = [];
777
+ const lit = model.styleCategory === 'bevel' || model.styleCategory === 'scene';
778
+ const view = model.camera ? smartArt3DCameraMatrix(model.camera) : undefined;
779
+ const eye = model.camera ? smartArt3DEyeInDiagram(model.camera) : undefined;
780
+ const root = new three.Group();
781
+ if (view) {
782
+ root.matrixAutoUpdate = false;
783
+ root.matrix.set(view[0], view[1], view[2], 0, view[3], view[4], view[5], 0, view[6], view[7], view[8], 0, 0, 0, 0, 1);
784
+ }
785
+ scene.add(root);
786
+ for (const mesh of model.meshes) {
787
+ const built = lit
788
+ ? buildLitMeshObject(three, mesh, resolveSmartArt3DLightModel(model.lighting, mesh.solid?.material, eye !== undefined), eye)
789
+ : buildFlatMeshObject(three, mesh);
790
+ root.add(built.group);
791
+ disposables.push(...built.disposables);
792
+ }
793
+ const camera = buildSmartArtViewCamera(three, model.bounds, model.camera, ctx.size);
794
+ const authoredPosition = camera.position.clone();
795
+ // A turned diagram draws past its box as PowerPoint does (view-overflow.ts).
796
+ let overflow = frameSmartArtOverflow(three, root, camera, ctx.size, Boolean(model.camera));
797
+ let controls = null;
798
+ let dampingActive = false;
799
+ if (ctx.OrbitControls) {
800
+ controls = new ctx.OrbitControls(camera, ctx.eventTarget);
801
+ controls.enableDamping = true;
802
+ controls.enablePan = false;
803
+ controls.enabled = ctx.interactive;
804
+ controls.enableRotate = ctx.interactive;
805
+ controls.enableZoom = ctx.interactive;
806
+ const onChange = () => {
807
+ dampingActive = true;
808
+ ctx.requestRender();
809
+ };
810
+ controls.addEventListener('change', onChange);
811
+ }
812
+ const resetView = () => {
813
+ camera.position.copy(authoredPosition);
814
+ camera.lookAt(0, 0, 0);
815
+ if (controls) {
816
+ controls.target.set(0, 0, 0);
817
+ controls.update();
818
+ }
819
+ ctx.requestRender();
820
+ };
821
+ const onDoubleClick = () => resetView();
822
+ ctx.eventTarget.addEventListener('dblclick', onDoubleClick);
823
+ return {
824
+ render(renderer) {
825
+ if (controls) {
826
+ dampingActive = controls.update();
827
+ }
828
+ renderer.render(scene, camera);
829
+ },
830
+ resize(size) {
831
+ fitSmartArtViewCamera(camera, model.bounds, size);
832
+ overflow = frameSmartArtOverflow(three, root, camera, size, Boolean(model.camera));
833
+ },
834
+ overflow() {
835
+ return overflow;
836
+ },
837
+ isAnimating() {
838
+ return dampingActive;
839
+ },
840
+ setInteractive(on) {
841
+ if (!controls) {
842
+ return;
843
+ }
844
+ controls.enabled = on;
845
+ controls.enableRotate = on;
846
+ controls.enableZoom = on;
847
+ if (!on) {
848
+ resetView();
849
+ }
850
+ },
851
+ dispose() {
852
+ ctx.eventTarget.removeEventListener('dblclick', onDoubleClick);
853
+ controls?.dispose();
854
+ for (const d of disposables) {
855
+ d.dispose();
856
+ }
857
+ },
858
+ };
859
+ }
860
+
861
+ export { mountSmartArt3DView };
862
+ //# sourceMappingURL=pptx-angular-viewer-view-scene-Bn9SJZk9.mjs.map