@pascal-app/viewer 0.9.1 → 0.9.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/dist/components/error-boundary.d.ts +6 -0
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/error-boundary.js +6 -0
- package/dist/components/renderers/node-renderer.d.ts +5 -1
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +5 -2
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/parametric-node-renderer.js +7 -4
- package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
- package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
- package/dist/components/viewer/bvh-ecctrl.js +552 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +39 -4
- package/dist/components/viewer/glb-interactive.d.ts +41 -0
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
- package/dist/components/viewer/glb-interactive.js +394 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
- package/dist/components/viewer/glb-reference-nodes.js +80 -0
- package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
- package/dist/components/viewer/glb-replace-instances.js +61 -0
- package/dist/components/viewer/glb-scene.d.ts +54 -0
- package/dist/components/viewer/glb-scene.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene.js +920 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +12 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/glb-walkthrough-controller.js +357 -0
- package/dist/components/viewer/index.d.ts +19 -2
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +104 -12
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +36 -5
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +17 -2
- package/dist/components/viewer/post-processing.d.ts +7 -1
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +179 -24
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +6 -4
- package/dist/components/viewer/scene-bvh.d.ts +2 -1
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
- package/dist/components/viewer/scene-bvh.js +3 -0
- package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
- package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
- package/dist/components/viewer/scene-bvh.test.js +13 -0
- package/dist/components/viewer/scene-environment.d.ts +2 -0
- package/dist/components/viewer/scene-environment.d.ts.map +1 -0
- package/dist/components/viewer/scene-environment.js +79 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +18 -10
- package/dist/components/viewer/walkthrough-controls.d.ts +1 -0
- package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
- package/dist/components/viewer/walkthrough-controls.js +20 -0
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +3 -22
- package/dist/hooks/use-node-events.d.ts +1 -1
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +22 -14
- package/dist/index.d.ts +19 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -8
- package/dist/lib/backdrop.d.ts +33 -0
- package/dist/lib/backdrop.d.ts.map +1 -0
- package/dist/lib/backdrop.js +92 -0
- package/dist/lib/box-uv.d.ts +13 -0
- package/dist/lib/box-uv.d.ts.map +1 -0
- package/dist/lib/box-uv.js +32 -0
- package/dist/lib/csg-utils.d.ts +3 -9
- package/dist/lib/csg-utils.d.ts.map +1 -1
- package/dist/lib/csg-utils.js +65 -5
- package/dist/lib/csg-utils.test.d.ts +2 -0
- package/dist/lib/csg-utils.test.d.ts.map +1 -0
- package/dist/lib/csg-utils.test.js +45 -0
- package/dist/lib/edge-style.d.ts +1 -0
- package/dist/lib/edge-style.d.ts.map +1 -1
- package/dist/lib/edge-style.js +26 -6
- package/dist/lib/ink-edges.d.ts +1 -1
- package/dist/lib/ink-edges.d.ts.map +1 -1
- package/dist/lib/ink-edges.js +13 -7
- package/dist/lib/ktx2-loader.d.ts +43 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -0
- package/dist/lib/ktx2-loader.js +136 -0
- package/dist/lib/layers.d.ts +10 -0
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +10 -0
- package/dist/lib/materials.d.ts +14 -1
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +103 -26
- package/dist/lib/scene-themes.d.ts +1 -0
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +25 -16
- package/dist/lib/shadow-only.d.ts +4 -0
- package/dist/lib/shadow-only.d.ts.map +1 -0
- package/dist/lib/shadow-only.js +36 -0
- package/dist/lib/tsl-compat.d.ts +12 -0
- package/dist/lib/tsl-compat.d.ts.map +1 -0
- package/dist/lib/tsl-compat.js +13 -0
- package/dist/store/use-viewer.d.ts +60 -18
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +175 -3
- package/dist/store/use-viewer.test.d.ts +2 -0
- package/dist/store/use-viewer.test.d.ts.map +1 -0
- package/dist/store/use-viewer.test.js +42 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +4 -1
- package/dist/systems/door/door-animation.test.d.ts +2 -0
- package/dist/systems/door/door-animation.test.d.ts.map +1 -0
- package/dist/systems/door/door-animation.test.js +51 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
- package/dist/systems/door/door-floor-alignment.test.js +50 -0
- package/dist/systems/door/door-system.d.ts +24 -0
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +494 -161
- package/dist/systems/fence/fence-system.d.ts +17 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -1
- package/dist/systems/fence/fence-system.js +221 -53
- package/dist/systems/geometry/geometry-system.d.ts +7 -0
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +95 -26
- package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
- package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-system.test.js +30 -0
- package/dist/systems/interactive/control-widget.d.ts +10 -0
- package/dist/systems/interactive/control-widget.d.ts.map +1 -0
- package/dist/systems/interactive/control-widget.js +35 -0
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +55 -56
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +20 -6
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +28 -4
- package/dist/systems/level/level-utils.d.ts +0 -3
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +2 -41
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +29 -8
- package/dist/systems/roof/roof-system.d.ts +21 -8
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +1079 -221
- package/dist/systems/slab/slab-system.d.ts +5 -3
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +41 -23
- package/dist/systems/slab/slab-system.test.js +3 -2
- package/dist/systems/stair/stair-system.js +6 -3
- package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.js +167 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +46 -17
- package/dist/systems/wall/wall-materials.d.ts +11 -7
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +244 -35
- package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
- package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-support-extension.test.js +48 -0
- package/dist/systems/wall/wall-system.d.ts +2 -2
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +329 -174
- package/dist/systems/window/window-animation-system.d.ts +12 -0
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +16 -5
- package/dist/systems/window/window-system.d.ts +7 -0
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +240 -14
- package/package.json +4 -4
- package/dist/components/viewer/ground-occluder.d.ts +0 -2
- package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
- package/dist/components/viewer/ground-occluder.js +0 -67
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { type FenceNode } from '@pascal-app/core';
|
|
2
2
|
import * as THREE from 'three';
|
|
3
|
+
type FencePart = {
|
|
4
|
+
geometry?: THREE.BufferGeometry;
|
|
5
|
+
position: [number, number, number];
|
|
6
|
+
rotationY?: number;
|
|
7
|
+
scale: [number, number, number];
|
|
8
|
+
shape?: 'box' | 'pyramid';
|
|
9
|
+
};
|
|
10
|
+
export type FenceSlotId = 'posts' | 'infill' | 'base' | 'rail';
|
|
11
|
+
export type FenceSlotParts = Record<FenceSlotId, FencePart[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Geometry split by paint slot — posts, infill, base, rail — each a separate
|
|
14
|
+
* merged BufferGeometry (empty ones included) so the fence renderer can give
|
|
15
|
+
* each its own material + `userData.slotId`. Slots match the panel's build
|
|
16
|
+
* options 1:1.
|
|
17
|
+
*/
|
|
18
|
+
export declare function generateFenceSlotGeometries(fence: FenceNode): Record<FenceSlotId, THREE.BufferGeometry>;
|
|
3
19
|
export declare function generateFenceGeometry(fence: FenceNode): THREE.BufferGeometry<THREE.NormalBufferAttributes, THREE.BufferGeometryEventMap>;
|
|
4
20
|
export declare const FenceSystem: () => null;
|
|
21
|
+
export {};
|
|
5
22
|
//# sourceMappingURL=fence-system.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fence-system.d.ts","sourceRoot":"","sources":["../../../src/systems/fence/fence-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EAKf,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"fence-system.d.ts","sourceRoot":"","sources":["../../../src/systems/fence/fence-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EAKf,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,KAAK,SAAS,GAAG;IACf,QAAQ,CAAC,EAAE,KAAK,CAAC,cAAc,CAAA;IAC/B,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAG/B,KAAK,CAAC,EAAE,KAAK,GAAG,SAAS,CAAA;CAC1B,CAAA;AAqPD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAA;AAE9D,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,WAAW,EAAE,SAAS,EAAE,CAAC,CAAA;AAsQ7D;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,SAAS,GACf,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,cAAc,CAAC,CAQ3C;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,oFAGrD;AAgBD,eAAO,MAAM,WAAW,YAiBvB,CAAA"}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getFenceCenterlineFrameAt, getFenceCenterlineLength, sceneRegistry, useScene, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
3
|
import * as THREE from 'three';
|
|
4
4
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
5
5
|
const MIN_CURVE_SEGMENT_LENGTH = 0.18;
|
|
6
|
+
const HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH = 0.2;
|
|
6
7
|
function createFencePartGeometry(part) {
|
|
7
|
-
|
|
8
|
+
if (part.geometry) {
|
|
9
|
+
return part.geometry;
|
|
10
|
+
}
|
|
11
|
+
const geometry = part.shape === 'pyramid'
|
|
12
|
+
? new THREE.ConeGeometry(0.5, 1, 4, 1, false, Math.PI / 4)
|
|
13
|
+
: new THREE.BoxGeometry(1, 1, 1);
|
|
8
14
|
geometry.scale(part.scale[0], part.scale[1], part.scale[2]);
|
|
9
15
|
if (part.rotationY) {
|
|
10
16
|
geometry.rotateY(part.rotationY);
|
|
@@ -14,37 +20,79 @@ function createFencePartGeometry(part) {
|
|
|
14
20
|
return geometry;
|
|
15
21
|
}
|
|
16
22
|
function getFencePointAt(fence, t) {
|
|
17
|
-
const frame =
|
|
23
|
+
const frame = getFenceCenterlineFrameAt(fence, t);
|
|
18
24
|
return {
|
|
19
25
|
point: frame.point,
|
|
20
26
|
tangentAngle: Math.atan2(frame.tangent.y, frame.tangent.x),
|
|
21
27
|
};
|
|
22
28
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
const dz = end[1] - start[1];
|
|
26
|
-
const length = Math.hypot(dx, dz);
|
|
27
|
-
if (length <= 1e-4) {
|
|
29
|
+
function createFenceCurveBlockPart(fence, startT, endT, centerY, height, depth) {
|
|
30
|
+
if (endT - startT <= 1e-5)
|
|
28
31
|
return null;
|
|
32
|
+
const halfHeight = height / 2;
|
|
33
|
+
const halfDepth = depth / 2;
|
|
34
|
+
const centerlineLength = getFenceCenterlineLength(fence);
|
|
35
|
+
const startDistance = startT * centerlineLength;
|
|
36
|
+
const endDistance = endT * centerlineLength;
|
|
37
|
+
const bottomY = centerY - halfHeight;
|
|
38
|
+
const topY = centerY + halfHeight;
|
|
39
|
+
const corners = [];
|
|
40
|
+
for (const t of [startT, endT]) {
|
|
41
|
+
const frame = getFencePointAt(fence, t);
|
|
42
|
+
const normalX = -Math.sin(frame.tangentAngle);
|
|
43
|
+
const normalZ = Math.cos(frame.tangentAngle);
|
|
44
|
+
const outerX = frame.point.x + normalX * halfDepth;
|
|
45
|
+
const outerZ = frame.point.y + normalZ * halfDepth;
|
|
46
|
+
const innerX = frame.point.x - normalX * halfDepth;
|
|
47
|
+
const innerZ = frame.point.y - normalZ * halfDepth;
|
|
48
|
+
corners.push([outerX, bottomY, outerZ], [innerX, bottomY, innerZ], [outerX, topY, outerZ], [innerX, topY, innerZ]);
|
|
29
49
|
}
|
|
50
|
+
const positions = [];
|
|
51
|
+
const uvs = [];
|
|
52
|
+
const pushVertex = (index, uv) => {
|
|
53
|
+
positions.push(...corners[index]);
|
|
54
|
+
uvs.push(...uv);
|
|
55
|
+
};
|
|
56
|
+
const pushQuad = (a, b, c, d, uvA, uvB, uvC, uvD) => {
|
|
57
|
+
pushVertex(a, uvA);
|
|
58
|
+
pushVertex(b, uvB);
|
|
59
|
+
pushVertex(c, uvC);
|
|
60
|
+
pushVertex(a, uvA);
|
|
61
|
+
pushVertex(c, uvC);
|
|
62
|
+
pushVertex(d, uvD);
|
|
63
|
+
};
|
|
64
|
+
const topOuterV = topY;
|
|
65
|
+
const topInnerV = topY + depth;
|
|
66
|
+
const innerTopV = topInnerV;
|
|
67
|
+
const innerBottomV = topInnerV + height;
|
|
68
|
+
const bottomInnerV = bottomY - depth;
|
|
69
|
+
pushQuad(0, 4, 6, 2, [startDistance, bottomY], [endDistance, bottomY], [endDistance, topY], [startDistance, topY]);
|
|
70
|
+
pushQuad(1, 3, 7, 5, [startDistance, innerBottomV], [startDistance, innerTopV], [endDistance, innerTopV], [endDistance, innerBottomV]);
|
|
71
|
+
pushQuad(2, 6, 7, 3, [startDistance, topOuterV], [endDistance, topOuterV], [endDistance, topInnerV], [startDistance, topInnerV]);
|
|
72
|
+
pushQuad(0, 1, 5, 4, [startDistance, bottomY], [startDistance, bottomInnerV], [endDistance, bottomInnerV], [endDistance, bottomY]);
|
|
73
|
+
pushQuad(0, 2, 3, 1, [0, bottomY], [0, topY], [depth, innerTopV], [depth, innerBottomV]);
|
|
74
|
+
pushQuad(4, 5, 7, 6, [0, bottomY], [depth, innerBottomV], [depth, innerTopV], [0, topY]);
|
|
75
|
+
const geometry = new THREE.BufferGeometry();
|
|
76
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(positions), 3));
|
|
77
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(new Float32Array(uvs), 2));
|
|
78
|
+
geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(new Float32Array(uvs), 2));
|
|
79
|
+
geometry.computeVertexNormals();
|
|
30
80
|
return {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
scale: [
|
|
81
|
+
geometry,
|
|
82
|
+
position: [0, 0, 0],
|
|
83
|
+
scale: [1, 1, 1],
|
|
34
84
|
};
|
|
35
85
|
}
|
|
36
|
-
function
|
|
86
|
+
function createFenceCurveBlockParts(fence, startT, endT, centerY, height, depth, maxSegmentLength = MIN_CURVE_SEGMENT_LENGTH) {
|
|
87
|
+
const length = getFenceCenterlineLength(fence) * Math.max(1e-4, endT - startT);
|
|
88
|
+
const segmentCount = Math.max(1, Math.ceil(length / Math.max(1e-4, maxSegmentLength)));
|
|
37
89
|
const parts = [];
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (segment) {
|
|
45
|
-
parts.push(segment);
|
|
46
|
-
}
|
|
47
|
-
previous = current;
|
|
90
|
+
for (let index = 0; index < segmentCount; index += 1) {
|
|
91
|
+
const segmentStartT = startT + (endT - startT) * (index / segmentCount);
|
|
92
|
+
const segmentEndT = startT + (endT - startT) * ((index + 1) / segmentCount);
|
|
93
|
+
const part = createFenceCurveBlockPart(fence, segmentStartT, segmentEndT, centerY, height, depth);
|
|
94
|
+
if (part)
|
|
95
|
+
parts.push(part);
|
|
48
96
|
}
|
|
49
97
|
return parts;
|
|
50
98
|
}
|
|
@@ -53,15 +101,13 @@ function applyFenceUVs(geometry) {
|
|
|
53
101
|
const normal = geometry.getAttribute('normal');
|
|
54
102
|
if (!(position && normal))
|
|
55
103
|
return;
|
|
104
|
+
// World-scale triplanar UVs: 1 UV unit = 1 metre, sampled from the part's
|
|
105
|
+
// local-space (already translated into fence space) coordinates with NO
|
|
106
|
+
// per-part origin shift. A shared origin keeps a tiled finish continuous
|
|
107
|
+
// across posts, rails, and infill instead of restarting the tile at each
|
|
108
|
+
// part's own min corner (the previous behaviour, which broke the 1 m
|
|
109
|
+
// contract and made adjacent parts mistile).
|
|
56
110
|
const uvs = new Float32Array(position.count * 2);
|
|
57
|
-
let minX = Number.POSITIVE_INFINITY;
|
|
58
|
-
let minY = Number.POSITIVE_INFINITY;
|
|
59
|
-
let minZ = Number.POSITIVE_INFINITY;
|
|
60
|
-
for (let index = 0; index < position.count; index += 1) {
|
|
61
|
-
minX = Math.min(minX, position.getX(index));
|
|
62
|
-
minY = Math.min(minY, position.getY(index));
|
|
63
|
-
minZ = Math.min(minZ, position.getZ(index));
|
|
64
|
-
}
|
|
65
111
|
for (let index = 0; index < position.count; index += 1) {
|
|
66
112
|
const px = position.getX(index);
|
|
67
113
|
const py = position.getY(index);
|
|
@@ -72,16 +118,16 @@ function applyFenceUVs(geometry) {
|
|
|
72
118
|
let u = 0;
|
|
73
119
|
let v = 0;
|
|
74
120
|
if (ny >= nx && ny >= nz) {
|
|
75
|
-
u = px
|
|
76
|
-
v = pz
|
|
121
|
+
u = px;
|
|
122
|
+
v = pz;
|
|
77
123
|
}
|
|
78
124
|
else if (nx >= nz) {
|
|
79
|
-
u = pz
|
|
80
|
-
v = py
|
|
125
|
+
u = pz;
|
|
126
|
+
v = py;
|
|
81
127
|
}
|
|
82
128
|
else {
|
|
83
|
-
u = px
|
|
84
|
-
v = py
|
|
129
|
+
u = px;
|
|
130
|
+
v = py;
|
|
85
131
|
}
|
|
86
132
|
uvs[index * 2] = u;
|
|
87
133
|
uvs[index * 2 + 1] = v;
|
|
@@ -98,9 +144,104 @@ function getStyleDefaults(style) {
|
|
|
98
144
|
}
|
|
99
145
|
return { spacingFactor: 0.3, postFactor: 0.55, baseFactor: 1, topFactor: 0.75 };
|
|
100
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Horizontal-board fence — composite cladding boards stacked between square
|
|
149
|
+
* intermediate posts (each capped), instead of the vertical pickets the other
|
|
150
|
+
* styles draw. Posts march along the whole span at `postSpacing` (not just the
|
|
151
|
+
* two ends), the boards run full-length so they curve with the fence, and a
|
|
152
|
+
* thin reveal between boards leaves the groove shadow that reads as cladding.
|
|
153
|
+
*/
|
|
154
|
+
function createHorizontalFenceParts(fence) {
|
|
155
|
+
const posts = [];
|
|
156
|
+
const infill = [];
|
|
157
|
+
const base = [];
|
|
158
|
+
const rail = [];
|
|
159
|
+
const length = Math.max(getFenceCenterlineLength(fence), 0.01);
|
|
160
|
+
const panelDepth = Math.max(fence.thickness, 0.03);
|
|
161
|
+
const clearance = Math.max(fence.groundClearance, 0);
|
|
162
|
+
const isFloating = fence.baseStyle === 'floating';
|
|
163
|
+
const showInfill = fence.showInfill ?? true;
|
|
164
|
+
const baseHeight = Math.max(fence.baseHeight, 0.04);
|
|
165
|
+
const topRailHeight = Math.max(fence.topRailHeight, 0.01);
|
|
166
|
+
const verticalHeight = Math.max(fence.height - baseHeight - topRailHeight, 0.08);
|
|
167
|
+
const baseY = isFloating ? clearance : 0;
|
|
168
|
+
// Square posts stand proud of the recessed boards on both faces.
|
|
169
|
+
const postWidth = Math.max(fence.postSize * 1.4, 0.04);
|
|
170
|
+
const postDepth = postWidth;
|
|
171
|
+
const boardDepth = Math.min(panelDepth, postDepth - 0.012);
|
|
172
|
+
// Stop the horizontal boards / base / rail at the inner faces of the
|
|
173
|
+
// end posts. Letting curved spans run all the way to t=0/1 makes them
|
|
174
|
+
// overlap the terminal post mesh and creates the broken seam/notch seen
|
|
175
|
+
// at curve ends.
|
|
176
|
+
const edgeInset = Math.max(fence.edgeInset ?? 0.015, postWidth * 0.5);
|
|
177
|
+
const startInsetT = Math.min(0.499, edgeInset / length);
|
|
178
|
+
const endInsetT = Math.max(0.501, 1 - edgeInset / length);
|
|
179
|
+
// Grounded fences get a kickboard along the bottom; floating ones don't.
|
|
180
|
+
if (!isFloating) {
|
|
181
|
+
base.push(...createFenceCurveBlockParts(fence, startInsetT, endInsetT, baseY + baseHeight / 2, baseHeight, postDepth * 0.92, HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH));
|
|
182
|
+
}
|
|
183
|
+
// Stack full-length boards between the kickboard and the top rail. The board
|
|
184
|
+
// height is derived to evenly fill the panel around a ~0.145 m target, with a
|
|
185
|
+
// constant reveal between each so the count adapts to any fence height.
|
|
186
|
+
if (showInfill) {
|
|
187
|
+
const reveal = Math.max(fence.slatGap ?? 0.01, 0);
|
|
188
|
+
const infillBottom = baseY + baseHeight;
|
|
189
|
+
if (reveal < 0.002) {
|
|
190
|
+
// No reveal → one flush panel, so the stacked-board edge seams don't
|
|
191
|
+
// read as faint lines where the user asked for a smooth surface.
|
|
192
|
+
infill.push(...createFenceCurveBlockParts(fence, startInsetT, endInsetT, infillBottom + verticalHeight / 2, verticalHeight, boardDepth, HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH));
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
const boardCount = Math.max(1, Math.round(verticalHeight / (0.145 + reveal)));
|
|
196
|
+
const slabHeight = Math.max((verticalHeight - reveal * (boardCount - 1)) / boardCount, 0.02);
|
|
197
|
+
for (let index = 0; index < boardCount; index += 1) {
|
|
198
|
+
const centerY = infillBottom + slabHeight / 2 + index * (slabHeight + reveal);
|
|
199
|
+
infill.push(...createFenceCurveBlockParts(fence, startInsetT, endInsetT, centerY, slabHeight, boardDepth, HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Top rail caps the boards.
|
|
204
|
+
rail.push(...createFenceCurveBlockParts(fence, startInsetT, endInsetT, baseY + baseHeight + verticalHeight + topRailHeight / 2, topRailHeight, Math.max(postDepth * 0.78, 0.02), HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH));
|
|
205
|
+
// Posts at every `postSpacing`, anchored at both ends, each with a flat cap.
|
|
206
|
+
const spacing = Math.max(fence.postSpacing, postWidth * 1.4);
|
|
207
|
+
const postCount = Math.max(2, Math.floor(length / spacing) + 1);
|
|
208
|
+
const postHeight = baseHeight + verticalHeight + topRailHeight + clearance;
|
|
209
|
+
const capHeight = Math.max(postWidth * 0.32, 0.03);
|
|
210
|
+
const cap = fence.postCap ?? 'pyramid';
|
|
211
|
+
for (let index = 0; index < postCount; index += 1) {
|
|
212
|
+
const t = postCount === 1 ? 0.5 : index / (postCount - 1);
|
|
213
|
+
const frame = getFencePointAt(fence, t);
|
|
214
|
+
posts.push({
|
|
215
|
+
position: [frame.point.x, postHeight / 2, frame.point.y],
|
|
216
|
+
rotationY: -frame.tangentAngle,
|
|
217
|
+
scale: [postWidth, postHeight, postDepth],
|
|
218
|
+
});
|
|
219
|
+
if (cap === 'flat') {
|
|
220
|
+
posts.push({
|
|
221
|
+
position: [frame.point.x, postHeight + capHeight / 2, frame.point.y],
|
|
222
|
+
rotationY: -frame.tangentAngle,
|
|
223
|
+
scale: [postWidth * 1.22, capHeight, postDepth * 1.22],
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else if (cap === 'pyramid') {
|
|
227
|
+
posts.push({
|
|
228
|
+
position: [frame.point.x, postHeight + capHeight * 0.9, frame.point.y],
|
|
229
|
+
rotationY: -frame.tangentAngle,
|
|
230
|
+
scale: [postWidth * 1.18, capHeight * 1.8, postDepth * 1.18],
|
|
231
|
+
shape: 'pyramid',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { posts, infill, base, rail };
|
|
236
|
+
}
|
|
101
237
|
function createFenceParts(fence) {
|
|
102
|
-
|
|
103
|
-
|
|
238
|
+
if (fence.style === 'horizontal')
|
|
239
|
+
return createHorizontalFenceParts(fence);
|
|
240
|
+
const posts = [];
|
|
241
|
+
const infill = [];
|
|
242
|
+
const base = [];
|
|
243
|
+
const rail = [];
|
|
244
|
+
const length = Math.max(getFenceCenterlineLength(fence), 0.01);
|
|
104
245
|
const panelDepth = Math.max(fence.thickness, 0.03);
|
|
105
246
|
const clearance = Math.max(fence.groundClearance, 0);
|
|
106
247
|
const styleDefaults = getStyleDefaults(fence.style);
|
|
@@ -117,34 +258,42 @@ function createFenceParts(fence) {
|
|
|
117
258
|
const startInsetT = Math.min(0.499, edgeInset / length);
|
|
118
259
|
const endInsetT = Math.max(0.501, 1 - edgeInset / length);
|
|
119
260
|
if (!isFloating) {
|
|
120
|
-
|
|
121
|
-
|
|
261
|
+
base.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight / 2, effectiveBaseHeight, panelDepth * 1.05));
|
|
262
|
+
base.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight + verticalHeight * 0.15, topRailHeight * 0.8, panelDepth * 0.35));
|
|
122
263
|
}
|
|
123
264
|
const count = showInfill ? Math.max(2, Math.floor((length - edgeInset * 2) / spacing) + 1) : 2;
|
|
124
265
|
const verticalY = baseY + effectiveBaseHeight + verticalHeight / 2;
|
|
125
266
|
for (let index = 0; index < count; index += 1) {
|
|
126
267
|
const t = count === 1 ? 0.5 : startInsetT + (endInsetT - startInsetT) * (index / (count - 1));
|
|
127
|
-
const frame = getFencePointAt(fence, t);
|
|
128
268
|
const isEdgePost = index === 0 || index === count - 1;
|
|
129
269
|
const fullHeightPost = !showInfill || (isFloating && isEdgePost);
|
|
130
270
|
const postHeight = fullHeightPost
|
|
131
271
|
? effectiveBaseHeight + verticalHeight + topRailHeight + clearance
|
|
132
272
|
: verticalHeight;
|
|
133
273
|
const postY = fullHeightPost ? postHeight / 2 : verticalY;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
274
|
+
// End posts are the structural `posts` slot; the intermediate verticals are
|
|
275
|
+
// the `infill` slats (only present when showInfill adds them).
|
|
276
|
+
const slatHalfT = Math.max(0.0005, postWidth / (2 * length));
|
|
277
|
+
const slatStartT = Math.max(0, t - slatHalfT);
|
|
278
|
+
const slatEndT = Math.min(1, t + slatHalfT);
|
|
279
|
+
const slat = createFenceCurveBlockPart(fence, slatStartT, slatEndT, postY, postHeight, Math.max(panelDepth * 0.35 - 0.001, 0.011));
|
|
280
|
+
if (slat) {
|
|
281
|
+
;
|
|
282
|
+
(isEdgePost ? posts : infill).push(slat);
|
|
283
|
+
}
|
|
139
284
|
}
|
|
140
|
-
|
|
285
|
+
rail.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight + verticalHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
|
|
141
286
|
if (isFloating) {
|
|
142
|
-
|
|
287
|
+
rail.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
|
|
143
288
|
}
|
|
144
|
-
return
|
|
289
|
+
return { posts, infill, base, rail };
|
|
145
290
|
}
|
|
146
|
-
|
|
147
|
-
|
|
291
|
+
function mergeFenceParts(parts) {
|
|
292
|
+
// An empty slot group (e.g. infill with showInfill off, or base on a floating
|
|
293
|
+
// fence) must not reach mergeGeometries — it throws on an empty array. The
|
|
294
|
+
// empty geometry has no position attribute, so the renderer skips its mesh.
|
|
295
|
+
if (parts.length === 0)
|
|
296
|
+
return new THREE.BufferGeometry();
|
|
148
297
|
const geometries = parts.map(createFencePartGeometry);
|
|
149
298
|
const merged = mergeGeometries(geometries, false) ?? new THREE.BufferGeometry();
|
|
150
299
|
geometries.forEach((geometry) => {
|
|
@@ -157,9 +306,28 @@ export function generateFenceGeometry(fence) {
|
|
|
157
306
|
merged.computeVertexNormals();
|
|
158
307
|
return merged;
|
|
159
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Geometry split by paint slot — posts, infill, base, rail — each a separate
|
|
311
|
+
* merged BufferGeometry (empty ones included) so the fence renderer can give
|
|
312
|
+
* each its own material + `userData.slotId`. Slots match the panel's build
|
|
313
|
+
* options 1:1.
|
|
314
|
+
*/
|
|
315
|
+
export function generateFenceSlotGeometries(fence) {
|
|
316
|
+
const parts = createFenceParts(fence);
|
|
317
|
+
return {
|
|
318
|
+
posts: mergeFenceParts(parts.posts),
|
|
319
|
+
infill: mergeFenceParts(parts.infill),
|
|
320
|
+
base: mergeFenceParts(parts.base),
|
|
321
|
+
rail: mergeFenceParts(parts.rail),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
export function generateFenceGeometry(fence) {
|
|
325
|
+
const { posts, infill, base, rail } = createFenceParts(fence);
|
|
326
|
+
return mergeFenceParts([...posts, ...infill, ...base, ...rail]);
|
|
327
|
+
}
|
|
160
328
|
function updateFenceGeometry(fenceId) {
|
|
161
329
|
const node = useScene.getState().nodes[fenceId];
|
|
162
|
-
if (
|
|
330
|
+
if (node?.type !== 'fence')
|
|
163
331
|
return;
|
|
164
332
|
const mesh = sceneRegistry.nodes.get(fenceId);
|
|
165
333
|
if (!mesh)
|
|
@@ -179,7 +347,7 @@ export const FenceSystem = () => {
|
|
|
179
347
|
const nodes = useScene.getState().nodes;
|
|
180
348
|
dirtyNodes.forEach((id) => {
|
|
181
349
|
const node = nodes[id];
|
|
182
|
-
if (
|
|
350
|
+
if (node?.type !== 'fence')
|
|
183
351
|
return;
|
|
184
352
|
updateFenceGeometry(id);
|
|
185
353
|
clearDirty(id);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Group, type Object3D } from 'three';
|
|
1
2
|
/**
|
|
2
3
|
* Generic geometry system.
|
|
3
4
|
*
|
|
@@ -31,4 +32,10 @@
|
|
|
31
32
|
*/
|
|
32
33
|
export declare const GeometrySystem: () => null;
|
|
33
34
|
export default GeometrySystem;
|
|
35
|
+
export type GeometryBuildCacheEntry = {
|
|
36
|
+
group: Group;
|
|
37
|
+
key: string;
|
|
38
|
+
};
|
|
39
|
+
export declare function markGeometryBuildOutput(child: Object3D): void;
|
|
40
|
+
export declare function shouldReuseGeometryBuild(cache: Map<string, GeometryBuildCacheEntry>, id: string, group: Group, key: string): boolean;
|
|
34
41
|
//# sourceMappingURL=geometry-system.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"geometry-system.d.ts","sourceRoot":"","sources":["../../../src/systems/geometry/geometry-system.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"geometry-system.d.ts","sourceRoot":"","sources":["../../../src/systems/geometry/geometry-system.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAa,KAAK,KAAK,EAA4B,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAQtF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,cAAc,YA8L1B,CAAA;AAmJD,eAAe,cAAc,CAAA;AAE7B,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,KAAK,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAU7D;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,uBAAuB,CAAC,EAC3C,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAKT"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { getEffectiveNode, nodeRegistry, sceneRegistry, useScene, } from '@pascal-app/core';
|
|
2
|
+
import { getEffectiveNode, nodeRegistry, sceneRegistry, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
3
3
|
import { useFrame } from '@react-three/fiber';
|
|
4
4
|
import { useEffect, useRef } from 'react';
|
|
5
5
|
import { FrontSide } from 'three';
|
|
@@ -43,11 +43,24 @@ export const GeometrySystem = () => {
|
|
|
43
43
|
const textures = useViewer((s) => s.textures);
|
|
44
44
|
const colorPreset = useViewer((s) => s.colorPreset);
|
|
45
45
|
const sceneTheme = useViewer((s) => s.sceneTheme);
|
|
46
|
+
const bumpGeometryRevision = useViewer((s) => s.bumpGeometryRevision);
|
|
47
|
+
// The shared scene-material library, threaded into each builder's ctx so
|
|
48
|
+
// pure geometry builders can resolve `scene:<id>` slot refs without
|
|
49
|
+
// importing `useScene`.
|
|
50
|
+
const sceneMaterials = useScene((s) => s.materials);
|
|
46
51
|
// Per-node cache of the last-built geometry key (for kinds that declare
|
|
47
52
|
// `def.geometryKey`). Lets us skip a dispose+rebuild when a node is dirty
|
|
48
53
|
// but its geometry inputs are unchanged — e.g. an item reparenting onto a
|
|
49
54
|
// shelf dirties the shelf without altering its boards.
|
|
50
55
|
const builtGeometryKeyRef = useRef(new Map());
|
|
56
|
+
// Re-mark every geometry-backed node dirty whenever a viewer appearance
|
|
57
|
+
// value changes, so `def.geometry` builders re-run and pick up the new
|
|
58
|
+
// shading / texture / preset / theme. These four are deliberate re-run
|
|
59
|
+
// TRIGGERS, not values read in the body — the effect re-fires on any
|
|
60
|
+
// change. They're primitives (stable by value), so listing them is safe;
|
|
61
|
+
// biome flags them as "unnecessary" because the body doesn't reference
|
|
62
|
+
// them, but dropping them silently breaks appearance-mode switching.
|
|
63
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: shading/textures/colorPreset/sceneTheme are intentional re-run triggers; removing them stops geometry from rebuilding on appearance change.
|
|
51
64
|
useEffect(() => {
|
|
52
65
|
const nodes = useScene.getState().nodes;
|
|
53
66
|
for (const node of Object.values(nodes)) {
|
|
@@ -57,10 +70,30 @@ export const GeometrySystem = () => {
|
|
|
57
70
|
}
|
|
58
71
|
}
|
|
59
72
|
}, [shading, textures, colorPreset, sceneTheme]);
|
|
73
|
+
// Editing a scene material must re-colour every geometry node that
|
|
74
|
+
// references it through a `scene:<id>` slot ref. Such a node's
|
|
75
|
+
// `geometryKey` is unchanged (only the referenced material's contents
|
|
76
|
+
// moved), so clear its cached key to defeat the skip in the rebuild loop,
|
|
77
|
+
// then mark it dirty. Scoped to nodes carrying a `scene:` ref so an
|
|
78
|
+
// unrelated material edit doesn't churn the whole scene.
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
void sceneMaterials;
|
|
81
|
+
const nodes = useScene.getState().nodes;
|
|
82
|
+
for (const node of Object.values(nodes)) {
|
|
83
|
+
const def = nodeRegistry.get(node.type);
|
|
84
|
+
if (!def?.geometry)
|
|
85
|
+
continue;
|
|
86
|
+
if (!nodeReferencesSceneMaterial(node))
|
|
87
|
+
continue;
|
|
88
|
+
builtGeometryKeyRef.current.delete(node.id);
|
|
89
|
+
useScene.getState().markDirty(node.id);
|
|
90
|
+
}
|
|
91
|
+
}, [sceneMaterials]);
|
|
60
92
|
useFrame(() => {
|
|
61
93
|
if (dirtyNodes.size === 0)
|
|
62
94
|
return;
|
|
63
95
|
const nodes = useScene.getState().nodes;
|
|
96
|
+
let rebuiltGeometry = false;
|
|
64
97
|
const batches = new Map();
|
|
65
98
|
const dirtyIds = [];
|
|
66
99
|
dirtyNodes.forEach((id) => {
|
|
@@ -132,17 +165,17 @@ export const GeometrySystem = () => {
|
|
|
132
165
|
// never skipped. This kills the board remount + pointer enter/leave
|
|
133
166
|
// churn when an item reparents onto a shelf.
|
|
134
167
|
if (def.geometryKey) {
|
|
135
|
-
const
|
|
136
|
-
|
|
168
|
+
const childLiveOverrideKey = liveChildOverrideKey(node);
|
|
169
|
+
const builtKey = `${shading}|${textures}|${colorPreset}|${sceneTheme}|${def.geometryKey(effectiveNode)}|${childLiveOverrideKey}`;
|
|
170
|
+
if (shouldReuseGeometryBuild(builtGeometryKeyRef.current, id, group, builtKey)) {
|
|
137
171
|
clearDirty(id);
|
|
138
172
|
continue;
|
|
139
173
|
}
|
|
140
|
-
builtGeometryKeyRef.current.set(id, builtKey);
|
|
141
174
|
}
|
|
142
175
|
const parentId = (node.parentId ?? null);
|
|
143
176
|
const key = `${node.type}::${parentId ?? ''}`;
|
|
144
177
|
const levelData = levelDataByBatch.get(key);
|
|
145
|
-
const ctx = buildGeometryContext(effectiveNode, nodes, levelData);
|
|
178
|
+
const ctx = buildGeometryContext(effectiveNode, nodes, levelData, sceneMaterials);
|
|
146
179
|
// The builder is typed against the kind's specific node — at the
|
|
147
180
|
// generic system level we lose that refinement, so the cast lands
|
|
148
181
|
// here. Builders are responsible for trusting their schema.
|
|
@@ -152,22 +185,10 @@ export const GeometrySystem = () => {
|
|
|
152
185
|
}
|
|
153
186
|
disposeChildren(group);
|
|
154
187
|
for (const child of [...built.children]) {
|
|
155
|
-
|
|
156
|
-
// can dispose only THIS rebuild's outputs and leave React-
|
|
157
|
-
// mounted siblings (hosted items inside a shelf / slab / etc.)
|
|
158
|
-
// alone. Without this, a parent rebuild triggered by a child
|
|
159
|
-
// event (e.g. an item reparenting onto a shelf calls
|
|
160
|
-
// `dirtyNodes.add(parent)` in `ItemRenderer`'s effect) would
|
|
161
|
-
// wipe ALL of the parent group's children — including the
|
|
162
|
-
// freshly-mounted item — leaving the item in scene state but
|
|
163
|
-
// invisible.
|
|
164
|
-
;
|
|
165
|
-
child.userData = {
|
|
166
|
-
...child.userData,
|
|
167
|
-
__fromGeometry: true,
|
|
168
|
-
};
|
|
188
|
+
markGeometryBuildOutput(child);
|
|
169
189
|
group.add(child);
|
|
170
190
|
}
|
|
191
|
+
rebuiltGeometry = true;
|
|
171
192
|
// NOTE: we intentionally do NOT reset `group.position` / `group.rotation`
|
|
172
193
|
// here. The `ParametricNodeRenderer` binds them via JSX (`position={...}`
|
|
173
194
|
// / `rotation={...}`) driven by `useLiveTransforms` during drag and
|
|
@@ -178,17 +199,45 @@ export const GeometrySystem = () => {
|
|
|
178
199
|
// origin. Geometry builders are expected to emit local-space children.
|
|
179
200
|
clearDirty(id);
|
|
180
201
|
}
|
|
202
|
+
if (rebuiltGeometry)
|
|
203
|
+
bumpGeometryRevision();
|
|
181
204
|
}, 2);
|
|
182
205
|
return null;
|
|
183
206
|
};
|
|
184
|
-
function
|
|
185
|
-
const
|
|
207
|
+
function liveChildOverrideKey(node) {
|
|
208
|
+
const childIds = node.children;
|
|
209
|
+
if (!Array.isArray(childIds) || childIds.length === 0)
|
|
210
|
+
return '';
|
|
211
|
+
const overrides = useLiveNodeOverrides.getState().overrides;
|
|
212
|
+
const entries = [];
|
|
213
|
+
for (const childId of childIds) {
|
|
214
|
+
const override = overrides.get(childId);
|
|
215
|
+
if (override)
|
|
216
|
+
entries.push([childId, override]);
|
|
217
|
+
}
|
|
218
|
+
return entries.length === 0 ? '' : JSON.stringify(entries);
|
|
219
|
+
}
|
|
220
|
+
function nodeReferencesSceneMaterial(node) {
|
|
221
|
+
const slots = node.slots;
|
|
222
|
+
if (!slots)
|
|
223
|
+
return false;
|
|
224
|
+
for (const ref of Object.values(slots)) {
|
|
225
|
+
if (typeof ref === 'string' && ref.startsWith('scene:'))
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
function buildGeometryContext(node, nodes, levelData, materials) {
|
|
231
|
+
const resolve = (id) => {
|
|
232
|
+
const resolved = nodes[id];
|
|
233
|
+
return resolved ? getEffectiveNode(resolved) : undefined;
|
|
234
|
+
};
|
|
186
235
|
const childIds = node.children;
|
|
187
236
|
const children = Array.isArray(childIds)
|
|
188
|
-
? childIds.map((cid) =>
|
|
237
|
+
? childIds.map((cid) => resolve(cid)).filter((n) => n !== undefined)
|
|
189
238
|
: [];
|
|
190
239
|
const parentId = node.parentId;
|
|
191
|
-
const parent = parentId ? (
|
|
240
|
+
const parent = parentId ? (resolve(parentId) ?? null) : null;
|
|
192
241
|
// Siblings = same kind, same parent, excluding self. Walks the parent's
|
|
193
242
|
// children array; falls back to scanning the whole scene if the parent
|
|
194
243
|
// doesn't carry a `children` list (rare — most parents do).
|
|
@@ -199,16 +248,18 @@ function buildGeometryContext(node, nodes, levelData) {
|
|
|
199
248
|
for (const sid of parentChildIds) {
|
|
200
249
|
if (sid === node.id)
|
|
201
250
|
continue;
|
|
202
|
-
const s =
|
|
251
|
+
const s = resolve(sid);
|
|
203
252
|
if (s && s.type === node.type)
|
|
204
253
|
siblings.push(s);
|
|
205
254
|
}
|
|
206
255
|
}
|
|
207
256
|
else {
|
|
208
|
-
siblings = Object.values(nodes)
|
|
257
|
+
siblings = Object.values(nodes)
|
|
258
|
+
.filter((n) => n !== node && n.type === node.type && n.parentId === parentId)
|
|
259
|
+
.map((n) => getEffectiveNode(n));
|
|
209
260
|
}
|
|
210
261
|
}
|
|
211
|
-
return { resolve, children, siblings, parent, levelData };
|
|
262
|
+
return { resolve, children, siblings, parent, levelData, materials };
|
|
212
263
|
}
|
|
213
264
|
function disposeChildren(group) {
|
|
214
265
|
// Only dispose meshes the geometry builder produced on the previous
|
|
@@ -277,3 +328,21 @@ function isCachedMaterial(value) {
|
|
|
277
328
|
?.__pascalCachedMaterial);
|
|
278
329
|
}
|
|
279
330
|
export default GeometrySystem;
|
|
331
|
+
export function markGeometryBuildOutput(child) {
|
|
332
|
+
// Tag the builder subtree so paint previews can reach nested meshes (for
|
|
333
|
+
// example a cabinet sink faucet handle), while disposal still removes only
|
|
334
|
+
// top-level builder children from the registered group.
|
|
335
|
+
child.traverse((object) => {
|
|
336
|
+
object.userData = {
|
|
337
|
+
...object.userData,
|
|
338
|
+
__fromGeometry: true,
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
export function shouldReuseGeometryBuild(cache, id, group, key) {
|
|
343
|
+
const cached = cache.get(id);
|
|
344
|
+
if (cached?.group === group && cached.key === key)
|
|
345
|
+
return true;
|
|
346
|
+
cache.set(id, { group, key });
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geometry-system.test.d.ts","sourceRoot":"","sources":["../../../src/systems/geometry/geometry-system.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,30 @@
|
|
|
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 { BoxGeometry, Group, Mesh, MeshBasicMaterial } from 'three';
|
|
5
|
+
import { markGeometryBuildOutput, shouldReuseGeometryBuild, } from './geometry-system';
|
|
6
|
+
describe('markGeometryBuildOutput', () => {
|
|
7
|
+
test('marks nested builder meshes for slot paint preview', () => {
|
|
8
|
+
const root = new Group();
|
|
9
|
+
const nested = new Group();
|
|
10
|
+
const mesh = new Mesh(new BoxGeometry(1, 1, 1), new MeshBasicMaterial());
|
|
11
|
+
mesh.userData.slotId = 'appliance';
|
|
12
|
+
nested.add(mesh);
|
|
13
|
+
root.add(nested);
|
|
14
|
+
markGeometryBuildOutput(root);
|
|
15
|
+
expect(root.userData.__fromGeometry).toBe(true);
|
|
16
|
+
expect(nested.userData.__fromGeometry).toBe(true);
|
|
17
|
+
expect(mesh.userData.__fromGeometry).toBe(true);
|
|
18
|
+
expect(mesh.userData.slotId).toBe('appliance');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('shouldReuseGeometryBuild', () => {
|
|
22
|
+
test('rebuilds when the same node id remounts into a new group with the same key', () => {
|
|
23
|
+
const cache = new Map();
|
|
24
|
+
const firstGroup = new Group();
|
|
25
|
+
const remountedGroup = new Group();
|
|
26
|
+
expect(shouldReuseGeometryBuild(cache, 'duct_1', firstGroup, 'same-key')).toBe(false);
|
|
27
|
+
expect(shouldReuseGeometryBuild(cache, 'duct_1', firstGroup, 'same-key')).toBe(true);
|
|
28
|
+
expect(shouldReuseGeometryBuild(cache, 'duct_1', remountedGroup, 'same-key')).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|