@pascal-app/viewer 0.9.2 → 1.0.0-beta.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.
Files changed (77) hide show
  1. package/README.md +20 -5
  2. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  3. package/dist/components/viewer/frame-limiter.js +25 -5
  4. package/dist/components/viewer/glb-scene.js +2 -2
  5. package/dist/components/viewer/glb-walkthrough-controller.d.ts +10 -0
  6. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  7. package/dist/components/viewer/glb-walkthrough-controller.js +96 -8
  8. package/dist/components/viewer/index.d.ts.map +1 -1
  9. package/dist/components/viewer/index.js +9 -0
  10. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  11. package/dist/components/viewer/post-processing.js +1 -0
  12. package/dist/components/viewer/selection-manager.js +5 -2
  13. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  14. package/dist/hooks/use-gltf-ktx2.js +2 -5
  15. package/dist/index.d.ts +8 -5
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +10 -8
  18. package/dist/lib/hero-pose.d.ts +47 -0
  19. package/dist/lib/hero-pose.d.ts.map +1 -0
  20. package/dist/lib/hero-pose.js +227 -0
  21. package/dist/lib/ktx2-loader.d.ts +3 -0
  22. package/dist/lib/ktx2-loader.d.ts.map +1 -1
  23. package/dist/lib/ktx2-loader.js +6 -0
  24. package/dist/lib/materials.d.ts +2 -0
  25. package/dist/lib/materials.d.ts.map +1 -1
  26. package/dist/lib/materials.js +56 -9
  27. package/dist/lib/materials.test.d.ts +2 -0
  28. package/dist/lib/materials.test.d.ts.map +1 -0
  29. package/dist/lib/materials.test.js +25 -0
  30. package/dist/lib/merged-outline-node.test.d.ts +2 -0
  31. package/dist/lib/merged-outline-node.test.d.ts.map +1 -0
  32. package/dist/lib/merged-outline-node.test.js +22 -0
  33. package/dist/lib/snapshot-pipeline.d.ts +42 -0
  34. package/dist/lib/snapshot-pipeline.d.ts.map +1 -0
  35. package/dist/lib/snapshot-pipeline.js +254 -0
  36. package/dist/lib/terrain-perimeter-fill.d.ts +7 -0
  37. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -0
  38. package/dist/lib/terrain-perimeter-fill.js +75 -0
  39. package/dist/lib/texture-node-guard.d.ts +13 -0
  40. package/dist/lib/texture-node-guard.d.ts.map +1 -0
  41. package/dist/lib/texture-node-guard.js +39 -0
  42. package/dist/lib/texture-reference.d.ts +31 -0
  43. package/dist/lib/texture-reference.d.ts.map +1 -0
  44. package/dist/lib/texture-reference.js +139 -0
  45. package/dist/lib/texture-reference.test.d.ts +2 -0
  46. package/dist/lib/texture-reference.test.d.ts.map +1 -0
  47. package/dist/lib/texture-reference.test.js +86 -0
  48. package/dist/store/use-viewer.d.ts +13 -0
  49. package/dist/store/use-viewer.d.ts.map +1 -1
  50. package/dist/store/use-viewer.js +16 -1
  51. package/dist/store/use-viewer.test.js +19 -0
  52. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  53. package/dist/systems/ceiling/ceiling-system.js +8 -4
  54. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  55. package/dist/systems/floor-elevation/floor-elevation-system.js +30 -7
  56. package/dist/systems/level/level-system.d.ts.map +1 -1
  57. package/dist/systems/level/level-system.js +8 -7
  58. package/dist/systems/level/level-utils.d.ts.map +1 -1
  59. package/dist/systems/level/level-utils.js +7 -6
  60. package/dist/systems/slab/slab-system.d.ts +5 -4
  61. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  62. package/dist/systems/slab/slab-system.js +24 -94
  63. package/dist/systems/slab/slab-system.test.js +51 -12
  64. package/dist/systems/stair/stair-system.js +1 -1
  65. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  66. package/dist/systems/wall/wall-cutout.js +5 -2
  67. package/dist/systems/wall/wall-support-extension.test.js +69 -1
  68. package/dist/systems/wall/wall-system.d.ts +3 -7
  69. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  70. package/dist/systems/wall/wall-system.js +87 -22
  71. package/dist/systems/wall/wall-terrain-bottom.test.d.ts +2 -0
  72. package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +1 -0
  73. package/dist/systems/wall/wall-terrain-bottom.test.js +34 -0
  74. package/package.json +2 -2
  75. package/dist/components/viewer/walkthrough-controls.d.ts +0 -3
  76. package/dist/components/viewer/walkthrough-controls.d.ts.map +0 -1
  77. package/dist/components/viewer/walkthrough-controls.js +0 -151
@@ -0,0 +1,227 @@
1
+ import { sceneRegistry, useScene } from '@pascal-app/core';
2
+ import { Box3, Vector3 } from 'three';
3
+ export const DEFAULT_FRAMING_EXCLUDED_TYPES = ['site', 'scan', 'guide', 'spawn'];
4
+ export function heroCameraPose({ boxes, aspect, aim, fovDeg = 60, azimuthRad = Math.PI / 4, elevationRad = (13 * Math.PI) / 180, padding = 1.03, minDistance = 4, frameShift = 0, }) {
5
+ const list = Array.isArray(boxes) ? boxes : [boxes];
6
+ const union = new Box3();
7
+ for (const box of list)
8
+ union.union(box);
9
+ const center = aim ? new Vector3(aim[0], aim[1], aim[2]) : union.getCenter(new Vector3());
10
+ const tanVertical = Math.tan(((fovDeg / 2) * Math.PI) / 180);
11
+ const tanHorizontal = tanVertical * aspect;
12
+ // Camera basis for the chosen azimuth/elevation: `dir` points from the
13
+ // target toward the camera, `forward` is the view direction.
14
+ const dir = new Vector3(Math.sin(azimuthRad) * Math.cos(elevationRad), Math.sin(elevationRad), Math.cos(azimuthRad) * Math.cos(elevationRad));
15
+ const forward = dir.clone().negate();
16
+ const right = new Vector3().crossVectors(forward, new Vector3(0, 1, 0)).normalize();
17
+ const up = new Vector3().crossVectors(right, forward);
18
+ // Exact fit: for every corner of every per-node box, the minimal camera
19
+ // distance along `dir` that keeps it inside the frustum. Fitting the corners
20
+ // of the UNION box instead reads far too wide at a diagonal azimuth — the
21
+ // union AABB's extreme corners are the empty diamond tips nothing actually
22
+ // occupies. (A bounding-sphere fit is worse still for flat, spread scenes.)
23
+ const corner = new Vector3();
24
+ const offset = new Vector3();
25
+ let distance = minDistance;
26
+ for (const box of list) {
27
+ for (const x of [box.min.x, box.max.x]) {
28
+ for (const y of [box.min.y, box.max.y]) {
29
+ for (const z of [box.min.z, box.max.z]) {
30
+ corner.set(x, y, z);
31
+ offset.subVectors(corner, center);
32
+ const lateral = offset.dot(right);
33
+ const vertical = offset.dot(up);
34
+ const depth = offset.dot(forward);
35
+ distance = Math.max(distance, (Math.abs(lateral) / tanHorizontal - depth) * padding, (Math.abs(vertical) / tanVertical - depth) * padding);
36
+ }
37
+ }
38
+ }
39
+ }
40
+ // Reframe: slide aim and camera together along the view-up axis — pure
41
+ // composition shift, no perspective change.
42
+ const drop = up.clone().multiplyScalar(-frameShift * distance * tanVertical);
43
+ const target = center.clone().add(drop);
44
+ return {
45
+ position: [
46
+ target.x + dir.x * distance,
47
+ target.y + dir.y * distance,
48
+ target.z + dir.z * distance,
49
+ ],
50
+ target: [target.x, target.y, target.z],
51
+ };
52
+ }
53
+ export function unionRegisteredNodeBounds({ excludeTypes, }) {
54
+ const excluded = new Set(excludeTypes);
55
+ const result = new Box3();
56
+ for (const [type, ids] of Object.entries(sceneRegistry.byType)) {
57
+ if (excluded.has(type))
58
+ continue;
59
+ for (const id of ids) {
60
+ const object = sceneRegistry.nodes.get(id);
61
+ if (!object)
62
+ continue;
63
+ const bounds = new Box3().setFromObject(object);
64
+ if (!bounds.isEmpty())
65
+ result.union(bounds);
66
+ }
67
+ }
68
+ return result.isEmpty() ? null : result;
69
+ }
70
+ function perNodeBoxes(excludeTypes) {
71
+ const excluded = new Set(excludeTypes);
72
+ const boxes = [];
73
+ for (const [type, ids] of Object.entries(sceneRegistry.byType)) {
74
+ if (excluded.has(type))
75
+ continue;
76
+ for (const id of ids) {
77
+ const object = sceneRegistry.nodes.get(id);
78
+ if (!object)
79
+ continue;
80
+ const bounds = new Box3().setFromObject(object);
81
+ if (!bounds.isEmpty())
82
+ boxes.push(bounds);
83
+ }
84
+ }
85
+ return boxes;
86
+ }
87
+ /**
88
+ * Matches the `SiteNode` bootstrap polygon (a 30×30 square at the origin) —
89
+ * same rule as the editor's `computeSceneBoundsXZ`: an untouched default lot
90
+ * says nothing about the user's intent, so it shouldn't drive framing.
91
+ */
92
+ function isDefaultSitePolygon(points) {
93
+ if (points.length !== 4)
94
+ return false;
95
+ const expected = [
96
+ [-15, -15],
97
+ [15, -15],
98
+ [15, 15],
99
+ [-15, 15],
100
+ ];
101
+ for (let i = 0; i < 4; i++) {
102
+ const p = points[i];
103
+ const e = expected[i];
104
+ if (!Array.isArray(p) || p.length < 2)
105
+ return false;
106
+ if (p[0] !== e[0] || p[1] !== e[1])
107
+ return false;
108
+ }
109
+ return true;
110
+ }
111
+ /** Flat boxes for intentionally-shaped site plates, from scene DATA — the
112
+ * site's Object3D can't be measured (it carries the ±400 m horizon disc). */
113
+ function sitePlateBoxes() {
114
+ const boxes = [];
115
+ for (const node of Object.values(useScene.getState().nodes)) {
116
+ if (node.type !== 'site')
117
+ continue;
118
+ const polygon = node.polygon;
119
+ const points = polygon?.points;
120
+ if (!Array.isArray(points) || isDefaultSitePolygon(points))
121
+ continue;
122
+ const box = new Box3();
123
+ for (const point of points) {
124
+ if (Array.isArray(point) && point.length >= 2) {
125
+ box.expandByPoint(new Vector3(Number(point[0]), 0, Number(point[1])));
126
+ }
127
+ }
128
+ if (!box.isEmpty())
129
+ boxes.push(box);
130
+ }
131
+ return boxes;
132
+ }
133
+ /** Dominant wall direction folded to a 90°-periodic axis (length-weighted
134
+ * vector sum of 4θ), so the hero azimuth sits at a true 45° to the building's
135
+ * facades no matter how the user rotated their plan. */
136
+ function dominantWallYaw() {
137
+ let vx = 0;
138
+ let vz = 0;
139
+ for (const node of Object.values(useScene.getState().nodes)) {
140
+ if (node.type !== 'wall')
141
+ continue;
142
+ const { start, end } = node;
143
+ if (!(Array.isArray(start) && Array.isArray(end)))
144
+ continue;
145
+ const dx = Number(end[0]) - Number(start[0]);
146
+ const dz = Number(end[1]) - Number(start[1]);
147
+ const length = Math.hypot(dx, dz);
148
+ if (!(Number.isFinite(length) && length > 0.01))
149
+ continue;
150
+ const theta = Math.atan2(dz, dx);
151
+ vx += length * Math.cos(4 * theta);
152
+ vz += length * Math.sin(4 * theta);
153
+ }
154
+ if (vx === 0 && vz === 0)
155
+ return null;
156
+ return Math.atan2(vz, vx) / 4;
157
+ }
158
+ /**
159
+ * Framing for a scene hero shot. The base set is the built structure (walls,
160
+ * roofs, slabs, …) plus any intentionally-shaped site plate; an item joins
161
+ * only when it sits near that base — one palm at the far lot corner must not
162
+ * shrink the building. `building`/`level` container groups are skipped (their
163
+ * Object3D holds the whole subtree). The aim keeps the building dead-center:
164
+ * XZ from the plate+structure union, Y from the structure alone so the
165
+ * building — not the ground — is vertically centered. Scenes with no
166
+ * structure (pure furniture arrangements) fall back to framing every
167
+ * non-helper node.
168
+ */
169
+ export function computeHeroFraming() {
170
+ const structural = perNodeBoxes([...DEFAULT_FRAMING_EXCLUDED_TYPES, 'item', 'building', 'level']);
171
+ if (structural.length === 0) {
172
+ const all = perNodeBoxes([...DEFAULT_FRAMING_EXCLUDED_TYPES, 'building', 'level']);
173
+ if (all.length === 0)
174
+ return null;
175
+ const union = new Box3();
176
+ for (const box of all)
177
+ union.union(box);
178
+ const center = union.getCenter(new Vector3());
179
+ return { boxes: all, aim: [center.x, center.y, center.z], azimuthRad: Math.PI / 4 };
180
+ }
181
+ const structuralUnion = new Box3();
182
+ for (const box of structural)
183
+ structuralUnion.union(box);
184
+ const plates = sitePlateBoxes();
185
+ const groundUnion = structuralUnion.clone();
186
+ for (const box of plates)
187
+ groundUnion.union(box);
188
+ const nearby = groundUnion
189
+ .clone()
190
+ .expandByVector(groundUnion.getSize(new Vector3()).multiplyScalar(0.15));
191
+ const boxes = [...structural, ...plates];
192
+ const itemIds = sceneRegistry.byType.item;
193
+ for (const id of itemIds) {
194
+ const object = sceneRegistry.nodes.get(id);
195
+ if (!object)
196
+ continue;
197
+ const bounds = new Box3().setFromObject(object);
198
+ if (!bounds.isEmpty() && nearby.intersectsBox(bounds))
199
+ boxes.push(bounds);
200
+ }
201
+ const groundCenter = groundUnion.getCenter(new Vector3());
202
+ const structuralCenter = structuralUnion.getCenter(new Vector3());
203
+ const yaw = dominantWallYaw();
204
+ return {
205
+ boxes,
206
+ aim: [groundCenter.x, structuralCenter.y, groundCenter.z],
207
+ azimuthRad: Math.PI / 4 - (yaw ?? 0),
208
+ };
209
+ }
210
+ export function temporarilyHideNodeTypes(types) {
211
+ const saved = new Map();
212
+ for (const type of types) {
213
+ const ids = sceneRegistry.byType[type];
214
+ ids.forEach((id) => {
215
+ const node = sceneRegistry.nodes.get(id);
216
+ if (node) {
217
+ saved.set(node, node.visible);
218
+ node.visible = false;
219
+ }
220
+ });
221
+ }
222
+ return () => {
223
+ saved.forEach((wasVisible, node) => {
224
+ node.visible = wasVisible;
225
+ });
226
+ };
227
+ }
@@ -38,6 +38,9 @@ export declare const ktx2Loader: AlignmentSafeKTX2Loader;
38
38
  export declare function whenKtx2Ready(): Promise<void>;
39
39
  /** Returns true once support has been detected for this renderer (KTX2 safe to load). */
40
40
  export declare function ensureKtx2Support(renderer: unknown): boolean;
41
+ export declare function configureKtx2Support<T>(loader: {
42
+ setKTX2Loader: (ktx2: T) => unknown;
43
+ }, renderer: unknown): boolean;
41
44
  export declare function isKtx2Url(url: string): boolean;
42
45
  export {};
43
46
  //# sourceMappingURL=ktx2-loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ktx2-loader.d.ts","sourceRoot":"","sources":["../../src/lib/ktx2-loader.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAA;AAwBrE;;;;;;;;;;GAUG;AACH,cAAM,uBAAwB,SAAQ,UAAU;IAC9C,OAAO,CAAC,YAAY,CAA0B;IAE9C,OAAO,CAAC,cAAc;IAkBtB;kEAC8D;IACxD,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;IAmC7F,OAAO,IAAI,IAAI;CAMhB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,yBAAgC,CAAA;AAWvD;;;;;;GAMG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAE7C;AAED,yFAAyF;AACzF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAkB5D;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE9C"}
1
+ {"version":3,"file":"ktx2-loader.d.ts","sourceRoot":"","sources":["../../src/lib/ktx2-loader.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAA;AAwBrE;;;;;;;;;;GAUG;AACH,cAAM,uBAAwB,SAAQ,UAAU;IAC9C,OAAO,CAAC,YAAY,CAA0B;IAE9C,OAAO,CAAC,cAAc;IAkBtB;kEAC8D;IACxD,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;IAmC7F,OAAO,IAAI,IAAI;CAMhB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,yBAAgC,CAAA;AAWvD;;;;;;GAMG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAE7C;AAED,yFAAyF;AACzF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAkB5D;AAED,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,MAAM,EAAE;IAAE,aAAa,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAA;CAAE,EAC/C,QAAQ,EAAE,OAAO,GAChB,OAAO,CAIT;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE9C"}
@@ -131,6 +131,12 @@ export function ensureKtx2Support(renderer) {
131
131
  return false;
132
132
  }
133
133
  }
134
+ export function configureKtx2Support(loader, renderer) {
135
+ if (!ensureKtx2Support(renderer))
136
+ return false;
137
+ loader.setKTX2Loader(ktx2Loader);
138
+ return true;
139
+ }
134
140
  export function isKtx2Url(url) {
135
141
  return url.toLowerCase().endsWith('.ktx2');
136
142
  }
@@ -10,6 +10,8 @@ export declare const BLUEPRINT_PALETTE: Record<SurfaceRole, string>;
10
10
  export declare const PRESET_PALETTES: Record<ColorPreset, Record<SurfaceRole, string>>;
11
11
  export declare function resolveSurfaceColor(role: SurfaceRole, preset: ColorPreset, sceneThemeId?: string): string;
12
12
  export declare const glassMaterial: MeshLambertNodeMaterial;
13
+ export declare function resolveTextureRepeat(repeat: unknown, scale: unknown): [number, number];
14
+ export declare function getTextureKey(material?: MaterialSchema): string;
13
15
  export declare function applyMaterialPresetToMaterials(materialInput: THREE.Material | THREE.Material[], preset: MaterialPresetPayload | null | undefined): void;
14
16
  export declare function createMaterialFromPreset(preset: MaterialPresetPayload, shading?: RenderShading): THREE.Material;
15
17
  export declare function createMaterialFromPresetRef(materialPreset?: string, shading?: RenderShading): THREE.Material | null;
@@ -1 +1 @@
1
- {"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../src/lib/materials.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../src/lib/materials.ts"],"names":[],"mappings":"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"}
@@ -5,6 +5,7 @@ import { MeshLambertNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu'
5
5
  import { resolveCdnUrl } from './asset-url';
6
6
  import { isKtx2Url, ktx2Loader, whenKtx2Ready } from './ktx2-loader';
7
7
  import { getSceneTheme } from './scene-themes';
8
+ import { stampPascalTextureRef } from './texture-reference';
8
9
  export const CLAY_PALETTE = {
9
10
  wall: '#dcd6c7',
10
11
  floor: '#cfc8b6',
@@ -115,13 +116,29 @@ function getTextureChannel(slot) {
115
116
  function getCacheKey(props, shading) {
116
117
  return `${shading}-${props.color}-${props.roughness}-${props.metalness}-${props.opacity}-${props.transparent}-${props.side}`;
117
118
  }
118
- function getTextureKey(material) {
119
+ function isFiniteNumber(value) {
120
+ return typeof value === 'number' && Number.isFinite(value);
121
+ }
122
+ export function resolveTextureRepeat(repeat, scale) {
123
+ const fallback = isFiniteNumber(scale) ? scale : 1;
124
+ if (Array.isArray(repeat) && isFiniteNumber(repeat[0]) && isFiniteNumber(repeat[1])) {
125
+ return [repeat[0], repeat[1]];
126
+ }
127
+ if (isFiniteNumber(repeat))
128
+ return [repeat, repeat];
129
+ if (repeat && typeof repeat === 'object' && 'x' in repeat && 'y' in repeat) {
130
+ const { x, y } = repeat;
131
+ if (isFiniteNumber(x) && isFiniteNumber(y))
132
+ return [x, y];
133
+ }
134
+ return [fallback, fallback];
135
+ }
136
+ export function getTextureKey(material) {
119
137
  const texture = material?.texture;
120
138
  if (!texture)
121
139
  return 'none';
122
- const repeat = texture.repeat?.join('x') ?? 'default';
123
- const scale = texture.scale ?? 'default';
124
- return `${texture.url}-${repeat}-${scale}`;
140
+ const [repeatX, repeatY] = resolveTextureRepeat(texture.repeat, texture.scale);
141
+ return `${texture.url}-${repeatX}x${repeatY}`;
125
142
  }
126
143
  function getTexture(material) {
127
144
  const textureConfig = material?.texture;
@@ -131,14 +148,21 @@ function getTexture(material) {
131
148
  const cached = textureCache.get(cacheKey);
132
149
  if (cached)
133
150
  return cached;
134
- const texture = pickTextureLoader(textureConfig.url).load(textureConfig.url);
151
+ const resolvedUrl = /^(?:asset|blob|data):/.test(textureConfig.url)
152
+ ? textureConfig.url
153
+ : (resolveCdnUrl(textureConfig.url) ?? textureConfig.url);
154
+ const texture = pickTextureLoader(resolvedUrl).load(resolvedUrl);
135
155
  texture.wrapS = THREE.RepeatWrapping;
136
156
  texture.wrapT = THREE.RepeatWrapping;
137
- const repeatX = textureConfig.repeat?.[0] ?? textureConfig.scale ?? 1;
138
- const repeatY = textureConfig.repeat?.[1] ?? textureConfig.scale ?? 1;
157
+ const [repeatX, repeatY] = resolveTextureRepeat(textureConfig.repeat, textureConfig.scale);
139
158
  texture.repeat.set(repeatX, repeatY);
140
159
  texture.updateMatrix();
141
160
  texture.colorSpace = THREE.SRGBColorSpace;
161
+ stampPascalTextureRef(texture, {
162
+ kind: 'project-asset',
163
+ src: resolvedUrl,
164
+ slot: 'map',
165
+ });
142
166
  textureCache.set(cacheKey, texture);
143
167
  return texture;
144
168
  }
@@ -179,6 +203,11 @@ function getPresetTexture(path, props, slot) {
179
203
  return cached;
180
204
  const texture = pickTextureLoader(resolvedPath).load(resolvedPath);
181
205
  applyTextureProperties(texture, props, slot);
206
+ stampPascalTextureRef(texture, {
207
+ kind: 'material',
208
+ src: resolvedPath,
209
+ slot: slot ?? 'map',
210
+ });
182
211
  setTextureCacheKey(texture, cacheKey);
183
212
  textureCache.set(cacheKey, texture);
184
213
  return texture;
@@ -220,6 +249,11 @@ async function loadPresetTexture(path, props, slot) {
220
249
  const promise = load
221
250
  .then((texture) => {
222
251
  applyTextureProperties(texture, props, slot);
252
+ stampPascalTextureRef(texture, {
253
+ kind: 'material',
254
+ src: resolvedPath,
255
+ slot: slot ?? 'map',
256
+ });
223
257
  setTextureCacheKey(texture, cacheKey);
224
258
  textureCache.set(cacheKey, texture);
225
259
  textureLoadPromises.delete(cacheKey);
@@ -236,7 +270,13 @@ async function loadPresetTexture(path, props, slot) {
236
270
  function queueTextureAssignment(material, slot, path, props) {
237
271
  const textureMaterial = material;
238
272
  if (!path) {
239
- textureMaterial[slot] = null;
273
+ if (textureMaterial[slot] != null) {
274
+ // Rebuild the node graph: a cached WebGPU material keeps a TextureNode
275
+ // for the slot, whose per-frame material reference would pull the null
276
+ // and crash in TextureNode.update ("null (reading 'matrix')").
277
+ textureMaterial[slot] = null;
278
+ material.needsUpdate = true;
279
+ }
240
280
  return;
241
281
  }
242
282
  const resolvedPath = resolveCdnUrl(path) ?? path;
@@ -251,7 +291,14 @@ function queueTextureAssignment(material, slot, path, props) {
251
291
  material.needsUpdate = true;
252
292
  return;
253
293
  }
254
- textureMaterial[slot] = null;
294
+ // Cold load: clear the slot for the fetch window, and rebuild the node
295
+ // graph if it previously held a texture — reused cached materials otherwise
296
+ // keep a TextureNode whose reference pulls the null and crashes the render
297
+ // pass. Cold loads are the norm for freshly generated library materials.
298
+ if (textureMaterial[slot] != null) {
299
+ textureMaterial[slot] = null;
300
+ material.needsUpdate = true;
301
+ }
255
302
  loadPresetTexture(path, props, slot).then((texture) => {
256
303
  if (!texture)
257
304
  return;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=materials.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"materials.test.d.ts","sourceRoot":"","sources":["../../src/lib/materials.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,25 @@
1
+ // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
+ // depend on @types/bun so the import type is unresolved at compile time.
3
+ import { describe, expect, test } from 'bun:test';
4
+ import { getTextureKey, resolveTextureRepeat } from './materials';
5
+ function materialWithRepeat(repeat) {
6
+ return {
7
+ texture: {
8
+ url: 'https://example.com/texture.png',
9
+ repeat,
10
+ },
11
+ };
12
+ }
13
+ describe('legacy texture repeat values', () => {
14
+ test('normalizes tuple, scalar, and Vector2-shaped repeats', () => {
15
+ expect(resolveTextureRepeat([2, 3], undefined)).toEqual([2, 3]);
16
+ expect(resolveTextureRepeat(2, undefined)).toEqual([2, 2]);
17
+ expect(resolveTextureRepeat({ x: 2, y: 3 }, undefined)).toEqual([2, 3]);
18
+ });
19
+ test('falls back to scale for malformed repeats', () => {
20
+ expect(resolveTextureRepeat({ width: 2 }, 4)).toEqual([4, 4]);
21
+ });
22
+ test('keeps distinct Vector2-shaped repeats in distinct cache entries', () => {
23
+ expect(getTextureKey(materialWithRepeat({ x: 2, y: 3 }))).not.toBe(getTextureKey(materialWithRepeat({ x: 4, y: 5 })));
24
+ });
25
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=merged-outline-node.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merged-outline-node.test.d.ts","sourceRoot":"","sources":["../../src/lib/merged-outline-node.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,22 @@
1
+ // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
+ // depend on @types/bun so the import type is unresolved at compile time.
3
+ import { describe, expect, test } from 'bun:test';
4
+ import { Object3D, PerspectiveCamera, Scene } from 'three';
5
+ import { mergedOutline } from './merged-outline-node';
6
+ describe('merged outline rendering', () => {
7
+ test('keeps selected outlines active during camera interaction', () => {
8
+ const cameraInteractionActive = true;
9
+ const params = {
10
+ enabled: () => !cameraInteractionActive,
11
+ primaryObjects: [new Object3D()],
12
+ };
13
+ const outline = mergedOutline(new Scene(), new PerspectiveCamera(), params);
14
+ const frame = {
15
+ get renderer() {
16
+ throw new Error('outline renderer was reached');
17
+ },
18
+ };
19
+ expect(() => outline.updateBefore(frame)).toThrow('outline renderer was reached');
20
+ outline.dispose();
21
+ });
22
+ });
@@ -0,0 +1,42 @@
1
+ import { type Camera, type Scene } from 'three';
2
+ import { type WebGPURenderer } from 'three/webgpu';
3
+ import { type EdgeMode } from './edge-style';
4
+ export declare const THUMBNAIL_WIDTH = 1920;
5
+ export declare const THUMBNAIL_HEIGHT = 1080;
6
+ export type SnapshotCaptureMode = 'standard' | 'viewport' | 'area';
7
+ export type SnapshotCropRegion = {
8
+ x: number;
9
+ y: number;
10
+ width: number;
11
+ height: number;
12
+ };
13
+ export type SnapshotSize = {
14
+ w: number;
15
+ h: number;
16
+ };
17
+ export type SnapshotCaptureResult = {
18
+ blob: Blob;
19
+ outW: number;
20
+ outH: number;
21
+ };
22
+ export type SnapshotPipeline = {
23
+ applyEnvironment: ({ theme, transparent, grade, edges, camera, }: {
24
+ theme: string;
25
+ transparent: boolean;
26
+ grade: boolean;
27
+ edges: EdgeMode;
28
+ camera: Camera;
29
+ }) => void;
30
+ capture: ({ captureMode, cropRegion, standardSize, }: {
31
+ captureMode?: SnapshotCaptureMode;
32
+ cropRegion?: SnapshotCropRegion;
33
+ standardSize?: SnapshotSize;
34
+ }) => Promise<SnapshotCaptureResult>;
35
+ dispose: () => void;
36
+ };
37
+ export declare function createSnapshotPipeline({ renderer, scene, camera, }: {
38
+ renderer: WebGPURenderer;
39
+ scene: Scene;
40
+ camera: Camera;
41
+ }): Promise<SnapshotPipeline | null>;
42
+ //# sourceMappingURL=snapshot-pipeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-pipeline.d.ts","sourceRoot":"","sources":["../../src/lib/snapshot-pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAkB,KAAK,KAAK,EAAoB,MAAM,OAAO,CAAA;AAqBjF,OAAO,EAAgC,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAGhF,OAAO,EAAE,KAAK,QAAQ,EAAqC,MAAM,cAAc,CAAA;AAK/E,eAAO,MAAM,eAAe,OAAO,CAAA;AACnC,eAAO,MAAM,gBAAgB,OAAO,CAAA;AAEpC,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,UAAU,GAAG,MAAM,CAAA;AAElE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,IAAI,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,gBAAgB,EAAE,CAAC,EACjB,KAAK,EACL,WAAW,EACX,KAAK,EACL,KAAK,EACL,MAAM,GACP,EAAE;QACD,KAAK,EAAE,MAAM,CAAA;QACb,WAAW,EAAE,OAAO,CAAA;QACpB,KAAK,EAAE,OAAO,CAAA;QACd,KAAK,EAAE,QAAQ,CAAA;QACf,MAAM,EAAE,MAAM,CAAA;KACf,KAAK,IAAI,CAAA;IACV,OAAO,EAAE,CAAC,EACR,WAAW,EACX,UAAU,EACV,YAAY,GACb,EAAE;QACD,WAAW,CAAC,EAAE,mBAAmB,CAAA;QACjC,UAAU,CAAC,EAAE,kBAAkB,CAAA;QAC/B,YAAY,CAAC,EAAE,YAAY,CAAA;KAC5B,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACpC,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,CAAA;AAED,wBAAsB,sBAAsB,CAAC,EAC3C,QAAQ,EACR,KAAK,EACL,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,cAAc,CAAA;IACxB,KAAK,EAAE,KAAK,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAoSnC"}