@pascal-app/viewer 0.9.2 → 1.0.0-beta.2
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/README.md +20 -5
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +25 -5
- package/dist/components/viewer/glb-scene.js +2 -2
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +10 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +96 -8
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +9 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +1 -0
- package/dist/components/viewer/selection-manager.js +5 -2
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -5
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -8
- package/dist/lib/hero-pose.d.ts +47 -0
- package/dist/lib/hero-pose.d.ts.map +1 -0
- package/dist/lib/hero-pose.js +227 -0
- package/dist/lib/ktx2-loader.d.ts +3 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -1
- package/dist/lib/ktx2-loader.js +6 -0
- package/dist/lib/materials.d.ts +2 -0
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +56 -9
- package/dist/lib/materials.test.d.ts +2 -0
- package/dist/lib/materials.test.d.ts.map +1 -0
- package/dist/lib/materials.test.js +25 -0
- package/dist/lib/merged-outline-node.test.d.ts +2 -0
- package/dist/lib/merged-outline-node.test.d.ts.map +1 -0
- package/dist/lib/merged-outline-node.test.js +22 -0
- package/dist/lib/snapshot-pipeline.d.ts +42 -0
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -0
- package/dist/lib/snapshot-pipeline.js +254 -0
- package/dist/lib/terrain-perimeter-fill.d.ts +7 -0
- package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -0
- package/dist/lib/terrain-perimeter-fill.js +75 -0
- package/dist/lib/texture-node-guard.d.ts +13 -0
- package/dist/lib/texture-node-guard.d.ts.map +1 -0
- package/dist/lib/texture-node-guard.js +39 -0
- package/dist/lib/texture-reference.d.ts +31 -0
- package/dist/lib/texture-reference.d.ts.map +1 -0
- package/dist/lib/texture-reference.js +139 -0
- package/dist/lib/texture-reference.test.d.ts +2 -0
- package/dist/lib/texture-reference.test.d.ts.map +1 -0
- package/dist/lib/texture-reference.test.js +86 -0
- package/dist/store/use-viewer.d.ts +13 -0
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +16 -1
- package/dist/store/use-viewer.test.js +19 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +8 -4
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
- package/dist/systems/floor-elevation/floor-elevation-system.js +30 -7
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +18 -2
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +8 -7
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +7 -6
- package/dist/systems/slab/slab-system.d.ts +5 -4
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +24 -94
- package/dist/systems/slab/slab-system.test.js +51 -12
- package/dist/systems/stair/stair-system.js +1 -1
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +5 -2
- package/dist/systems/wall/wall-support-extension.test.js +69 -1
- package/dist/systems/wall/wall-system.d.ts +3 -7
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +87 -22
- package/dist/systems/wall/wall-terrain-bottom.test.d.ts +2 -0
- package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-terrain-bottom.test.js +34 -0
- package/package.json +2 -2
- package/dist/components/viewer/walkthrough-controls.d.ts +0 -3
- package/dist/components/viewer/walkthrough-controls.d.ts.map +0 -1
- 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"}
|
package/dist/lib/ktx2-loader.js
CHANGED
|
@@ -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
|
}
|
package/dist/lib/materials.d.ts
CHANGED
|
@@ -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":"AAAA,OAAO,EAGL,KAAK,qBAAqB,EAE1B,KAAK,cAAc,EAGnB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,WAAW,EACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAE,uBAAuB,EAA4B,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../src/lib/materials.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/materials.js
CHANGED
|
@@ -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
|
|
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
|
|
123
|
-
|
|
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
|
|
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
|
|
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]
|
|
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
|
-
|
|
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 @@
|
|
|
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 @@
|
|
|
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"}
|