@pascal-app/viewer 1.0.5 → 1.0.7
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/viewer/daylight-atmosphere.d.ts +9 -0
- package/dist/components/viewer/daylight-atmosphere.d.ts.map +1 -0
- package/dist/components/viewer/daylight-atmosphere.js +315 -0
- package/dist/components/viewer/drone-controller.d.ts +15 -0
- package/dist/components/viewer/drone-controller.d.ts.map +1 -0
- package/dist/components/viewer/drone-controller.js +194 -0
- package/dist/components/viewer/glb-interactive.d.ts +4 -18
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
- package/dist/components/viewer/glb-interactive.js +32 -268
- package/dist/components/viewer/glb-scene-motion.d.ts +8 -0
- package/dist/components/viewer/glb-scene-motion.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene-motion.js +57 -0
- package/dist/components/viewer/glb-scene.d.ts +3 -1
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +289 -49
- package/dist/components/viewer/glb-selection-callout.d.ts +17 -0
- package/dist/components/viewer/glb-selection-callout.d.ts.map +1 -0
- package/dist/components/viewer/glb-selection-callout.js +54 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +6 -3
- package/dist/components/viewer/index.d.ts +5 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +30 -6
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +192 -5
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +150 -94
- package/dist/components/viewer/scene-environment.js +4 -5
- package/dist/components/viewer/selection-callout.d.ts +51 -0
- package/dist/components/viewer/selection-callout.d.ts.map +1 -0
- package/dist/components/viewer/selection-callout.js +197 -0
- package/dist/components/viewer/use-daylight-rooms.d.ts +13 -0
- package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -0
- package/dist/components/viewer/use-daylight-rooms.js +236 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +12 -1
- package/dist/index.d.ts +21 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -7
- package/dist/lib/daylight/ground.d.ts +21 -0
- package/dist/lib/daylight/ground.d.ts.map +1 -0
- package/dist/lib/daylight/ground.js +53 -0
- package/dist/lib/daylight/location.d.ts +32 -0
- package/dist/lib/daylight/location.d.ts.map +1 -0
- package/dist/lib/daylight/location.js +74 -0
- package/dist/lib/daylight/look.d.ts +33 -0
- package/dist/lib/daylight/look.d.ts.map +1 -0
- package/dist/lib/daylight/look.js +13 -0
- package/dist/lib/daylight/material-hooks.d.ts +2 -0
- package/dist/lib/daylight/material-hooks.d.ts.map +1 -0
- package/dist/lib/daylight/material-hooks.js +150 -0
- package/dist/lib/daylight/room-context.d.ts +12 -0
- package/dist/lib/daylight/room-context.d.ts.map +1 -0
- package/dist/lib/daylight/room-context.js +53 -0
- package/dist/lib/daylight/room-scene.d.ts +54 -0
- package/dist/lib/daylight/room-scene.d.ts.map +1 -0
- package/dist/lib/daylight/room-scene.js +356 -0
- package/dist/lib/daylight/rooms.d.ts +262 -0
- package/dist/lib/daylight/rooms.d.ts.map +1 -0
- package/dist/lib/daylight/rooms.js +1223 -0
- package/dist/lib/daylight/runtime.d.ts +33 -0
- package/dist/lib/daylight/runtime.d.ts.map +1 -0
- package/dist/lib/daylight/runtime.js +42 -0
- package/dist/lib/daylight/sky.d.ts +26 -0
- package/dist/lib/daylight/sky.d.ts.map +1 -0
- package/dist/lib/daylight/sky.js +217 -0
- package/dist/lib/daylight/solar.d.ts +46 -0
- package/dist/lib/daylight/solar.d.ts.map +1 -0
- package/dist/lib/daylight/solar.js +156 -0
- package/dist/lib/device-tier.d.ts +29 -0
- package/dist/lib/device-tier.d.ts.map +1 -0
- package/dist/lib/device-tier.js +71 -0
- package/dist/lib/dispose-object3d.d.ts.map +1 -1
- package/dist/lib/dispose-object3d.js +4 -2
- package/dist/lib/drone-flight.d.ts +44 -0
- package/dist/lib/drone-flight.d.ts.map +1 -0
- package/dist/lib/drone-flight.js +121 -0
- package/dist/lib/editor-highlight.d.ts +16 -0
- package/dist/lib/editor-highlight.d.ts.map +1 -0
- package/dist/lib/editor-highlight.js +33 -0
- package/dist/lib/foundation-materials.d.ts.map +1 -1
- package/dist/lib/foundation-materials.js +2 -4
- package/dist/lib/glb-merge.d.ts +23 -0
- package/dist/lib/glb-merge.d.ts.map +1 -0
- package/dist/lib/glb-merge.js +184 -0
- package/dist/lib/hardware-geometry.d.ts +21 -0
- package/dist/lib/hardware-geometry.d.ts.map +1 -0
- package/dist/lib/hardware-geometry.js +283 -0
- package/dist/lib/layer-pass.d.ts +2 -0
- package/dist/lib/layer-pass.d.ts.map +1 -1
- package/dist/lib/layer-pass.js +20 -0
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +50 -23
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +7 -1
- package/dist/lib/occlusion-culler.d.ts +34 -0
- package/dist/lib/occlusion-culler.d.ts.map +1 -0
- package/dist/lib/occlusion-culler.js +698 -0
- package/dist/lib/post-processing-resources.d.ts +4 -0
- package/dist/lib/post-processing-resources.d.ts.map +1 -1
- package/dist/lib/post-processing-resources.js +5 -0
- package/dist/lib/readback-rows.d.ts +14 -0
- package/dist/lib/readback-rows.d.ts.map +1 -0
- package/dist/lib/readback-rows.js +31 -0
- package/dist/lib/reveal-pose.d.ts.map +1 -1
- package/dist/lib/reveal-pose.js +3 -0
- package/dist/lib/scene-matrices.d.ts +12 -0
- package/dist/lib/scene-matrices.d.ts.map +1 -0
- package/dist/lib/scene-matrices.js +287 -0
- package/dist/lib/scene-pass-mrt.d.ts +5 -2
- package/dist/lib/scene-pass-mrt.d.ts.map +1 -1
- package/dist/lib/scene-pass-mrt.js +7 -2
- package/dist/lib/scene-themes.d.ts +0 -2
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +0 -2
- package/dist/lib/scene-visibility.d.ts +2 -0
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +12 -3
- package/dist/lib/site-surfaces.d.ts +1 -8
- package/dist/lib/site-surfaces.d.ts.map +1 -1
- package/dist/lib/site-surfaces.js +0 -19
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
- package/dist/lib/snapshot-pipeline.js +24 -40
- package/dist/lib/texture-reference.d.ts.map +1 -1
- package/dist/lib/texture-reference.js +10 -8
- package/dist/lib/underground-ambiance.d.ts +0 -2
- package/dist/lib/underground-ambiance.d.ts.map +1 -1
- package/dist/lib/underground-ambiance.js +0 -2
- package/dist/store/use-item-light-pool.d.ts +3 -6
- package/dist/store/use-item-light-pool.d.ts.map +1 -1
- package/dist/store/use-item-light-pool.js +12 -0
- package/dist/store/use-viewer.d.ts +72 -15
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +103 -18
- package/dist/systems/ceiling/ceiling-system.d.ts +5 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +11 -1
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +501 -145
- package/dist/systems/door/leaf-profile.d.ts +11 -0
- package/dist/systems/door/leaf-profile.d.ts.map +1 -0
- package/dist/systems/door/leaf-profile.js +140 -0
- package/dist/systems/interactive/callout-actions.d.ts +72 -0
- package/dist/systems/interactive/callout-actions.d.ts.map +1 -0
- package/dist/systems/interactive/callout-actions.js +212 -0
- package/dist/systems/interactive/interactive-system.d.ts +5 -1
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +44 -121
- package/dist/systems/interactive/opening-settled.d.ts +16 -0
- package/dist/systems/interactive/opening-settled.d.ts.map +1 -0
- package/dist/systems/interactive/opening-settled.js +53 -0
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
- package/dist/systems/item-light/item-light-system.js +22 -214
- package/dist/systems/item-light/pooled-lights.d.ts +70 -0
- package/dist/systems/item-light/pooled-lights.d.ts.map +1 -0
- package/dist/systems/item-light/pooled-lights.js +449 -0
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +19 -1
- package/dist/systems/roof/roof-material-slots.d.ts +14 -0
- package/dist/systems/roof/roof-material-slots.d.ts.map +1 -0
- package/dist/systems/roof/roof-material-slots.js +13 -0
- package/dist/systems/roof/roof-materials.d.ts +25 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +85 -26
- package/dist/systems/roof/roof-system.d.ts +34 -1
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +477 -105
- package/dist/systems/wall/opening-casing-geometry.d.ts +14 -0
- package/dist/systems/wall/opening-casing-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-casing-geometry.js +102 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts +9 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -1
- package/dist/systems/wall/opening-cutout-geometry.js +7 -3
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +28 -35
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +34 -19
- package/dist/systems/wall/wall-t-junctions.d.ts +15 -0
- package/dist/systems/wall/wall-t-junctions.d.ts.map +1 -0
- package/dist/systems/wall/wall-t-junctions.js +149 -0
- package/dist/systems/window/window-animation-system.d.ts +3 -12
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +37 -122
- package/dist/systems/window/window-pose.d.ts +31 -0
- package/dist/systems/window/window-pose.d.ts.map +1 -0
- package/dist/systems/window/window-pose.js +152 -0
- package/dist/systems/window/window-roller-shutter.d.ts +8 -0
- package/dist/systems/window/window-roller-shutter.d.ts.map +1 -0
- package/dist/systems/window/window-roller-shutter.js +64 -0
- package/dist/systems/window/window-system.d.ts +2 -11
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +470 -303
- package/package.json +6 -6
- package/dist/systems/interactive/control-widget.d.ts +0 -10
- package/dist/systems/interactive/control-widget.d.ts.map +0 -1
- package/dist/systems/interactive/control-widget.js +0 -35
- package/dist/systems/interactive/procedural-controls.d.ts +0 -13
- package/dist/systems/interactive/procedural-controls.d.ts.map +0 -1
- package/dist/systems/interactive/procedural-controls.js +0 -17
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
const T_JUNCTION_EPSILON = 1e-5;
|
|
3
|
+
// CSG T-junction vertices sit within Float32 rounding (≲3e-7 m) of the edge
|
|
4
|
+
// they split; finely tessellated arch vertices can sit ~1e-5 m off a chord
|
|
5
|
+
// and must not be pulled onto it.
|
|
6
|
+
const ON_EDGE_EPSILON = 1e-6;
|
|
7
|
+
const WELD_GRID = 1e-6;
|
|
8
|
+
/**
|
|
9
|
+
* Splits every triangle whose edge carries another triangle's vertex, so the
|
|
10
|
+
* neighbours share that vertex instead of meeting at a T-junction. CSG cuts and
|
|
11
|
+
* finish-band splits leave T-junctions on wall faces: they look closed at full
|
|
12
|
+
* float precision, but crack open into dotted light leaks once a GLB bake
|
|
13
|
+
* quantizes positions.
|
|
14
|
+
*
|
|
15
|
+
* Each piece keeps its source triangle's material group, so the repair never
|
|
16
|
+
* reclassifies a face. Geometry without T-junctions is returned untouched;
|
|
17
|
+
* otherwise the result is non-indexed and position-only (callers recompute
|
|
18
|
+
* normals) and the input is disposed.
|
|
19
|
+
*/
|
|
20
|
+
export function repairWallTJunctions(geometry) {
|
|
21
|
+
const position = geometry.getAttribute('position');
|
|
22
|
+
const index = geometry.getIndex();
|
|
23
|
+
const cornerCount = index ? index.count : (position?.count ?? 0);
|
|
24
|
+
if (!position || cornerCount < 3)
|
|
25
|
+
return geometry;
|
|
26
|
+
// Welded vertex table: every corner snaps to one representative so that
|
|
27
|
+
// edges sharing a vertex share its exact coordinates.
|
|
28
|
+
const welded = new Map();
|
|
29
|
+
const points = [];
|
|
30
|
+
const corners = new Uint32Array(cornerCount);
|
|
31
|
+
for (let corner = 0; corner < cornerCount; corner += 1) {
|
|
32
|
+
const vertex = index ? index.getX(corner) : corner;
|
|
33
|
+
const x = position.getX(vertex);
|
|
34
|
+
const y = position.getY(vertex);
|
|
35
|
+
const z = position.getZ(vertex);
|
|
36
|
+
const key = `${Math.round(x / WELD_GRID)},${Math.round(y / WELD_GRID)},${Math.round(z / WELD_GRID)}`;
|
|
37
|
+
let id = welded.get(key);
|
|
38
|
+
if (id === undefined) {
|
|
39
|
+
id = points.length;
|
|
40
|
+
points.push([x, y, z]);
|
|
41
|
+
welded.set(key, id);
|
|
42
|
+
}
|
|
43
|
+
corners[corner] = id;
|
|
44
|
+
}
|
|
45
|
+
const byX = points.map((_, id) => id).sort((a, b) => points[a][0] - points[b][0]);
|
|
46
|
+
const xs = byX.map((id) => points[id][0]);
|
|
47
|
+
const pointsOnEdge = (from, to) => {
|
|
48
|
+
const p = points[from];
|
|
49
|
+
const q = points[to];
|
|
50
|
+
const dx = q[0] - p[0];
|
|
51
|
+
const dy = q[1] - p[1];
|
|
52
|
+
const dz = q[2] - p[2];
|
|
53
|
+
const lengthSq = dx * dx + dy * dy + dz * dz;
|
|
54
|
+
if (lengthSq < T_JUNCTION_EPSILON * T_JUNCTION_EPSILON)
|
|
55
|
+
return [];
|
|
56
|
+
const minT = T_JUNCTION_EPSILON / Math.sqrt(lengthSq);
|
|
57
|
+
const maxX = Math.max(p[0], q[0]) + T_JUNCTION_EPSILON;
|
|
58
|
+
const hits = [];
|
|
59
|
+
for (let k = lowerBound(xs, Math.min(p[0], q[0]) - T_JUNCTION_EPSILON); k < xs.length; k += 1) {
|
|
60
|
+
if (xs[k] > maxX)
|
|
61
|
+
break;
|
|
62
|
+
const id = byX[k];
|
|
63
|
+
if (id === from || id === to)
|
|
64
|
+
continue;
|
|
65
|
+
const v = points[id];
|
|
66
|
+
const t = ((v[0] - p[0]) * dx + (v[1] - p[1]) * dy + (v[2] - p[2]) * dz) / lengthSq;
|
|
67
|
+
if (t <= minT || t >= 1 - minT)
|
|
68
|
+
continue;
|
|
69
|
+
const ox = p[0] + dx * t - v[0];
|
|
70
|
+
const oy = p[1] + dy * t - v[1];
|
|
71
|
+
const oz = p[2] + dz * t - v[2];
|
|
72
|
+
if (ox * ox + oy * oy + oz * oz > ON_EDGE_EPSILON * ON_EDGE_EPSILON)
|
|
73
|
+
continue;
|
|
74
|
+
hits.push({ id, t });
|
|
75
|
+
}
|
|
76
|
+
return hits.sort((a, b) => a.t - b.t).map((hit) => hit.id);
|
|
77
|
+
};
|
|
78
|
+
const edgePoints = [];
|
|
79
|
+
let found = false;
|
|
80
|
+
for (let corner = 0; corner + 2 < cornerCount; corner += 3) {
|
|
81
|
+
const a = corners[corner];
|
|
82
|
+
const b = corners[corner + 1];
|
|
83
|
+
const c = corners[corner + 2];
|
|
84
|
+
const ab = pointsOnEdge(a, b);
|
|
85
|
+
const bc = pointsOnEdge(b, c);
|
|
86
|
+
const ca = pointsOnEdge(c, a);
|
|
87
|
+
const split = ab.length > 0 || bc.length > 0 || ca.length > 0;
|
|
88
|
+
edgePoints.push(split ? [ab, bc, ca] : null);
|
|
89
|
+
found ||= split;
|
|
90
|
+
}
|
|
91
|
+
if (!found)
|
|
92
|
+
return geometry;
|
|
93
|
+
const out = [];
|
|
94
|
+
const push = (id) => {
|
|
95
|
+
const point = points[id];
|
|
96
|
+
out.push(point[0], point[1], point[2]);
|
|
97
|
+
};
|
|
98
|
+
// Splits at an edge point by joining it to the opposite corner. The new edge
|
|
99
|
+
// is interior, so it carries no points, and no vertex is ever invented.
|
|
100
|
+
const emit = (a, b, c, ab, bc, ca) => {
|
|
101
|
+
if (ab.length === 0 && bc.length === 0 && ca.length === 0) {
|
|
102
|
+
push(a);
|
|
103
|
+
push(b);
|
|
104
|
+
push(c);
|
|
105
|
+
}
|
|
106
|
+
else if (ab.length === 0) {
|
|
107
|
+
if (bc.length > 0)
|
|
108
|
+
emit(b, c, a, bc, ca, ab);
|
|
109
|
+
else
|
|
110
|
+
emit(c, a, b, ca, ab, bc);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const mid = ab.length >> 1;
|
|
114
|
+
const p = ab[mid];
|
|
115
|
+
emit(a, p, c, ab.slice(0, mid), [], ca);
|
|
116
|
+
emit(p, b, c, ab.slice(mid + 1), bc, []);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const repaired = new THREE.BufferGeometry();
|
|
120
|
+
const groups = geometry.groups.length
|
|
121
|
+
? geometry.groups
|
|
122
|
+
: [{ start: 0, count: cornerCount, materialIndex: undefined }];
|
|
123
|
+
for (const group of groups) {
|
|
124
|
+
const start = out.length / 3;
|
|
125
|
+
const end = Math.min(group.start + group.count, cornerCount);
|
|
126
|
+
for (let corner = group.start; corner + 2 < end; corner += 3) {
|
|
127
|
+
const [ab, bc, ca] = edgePoints[corner / 3] ?? [[], [], []];
|
|
128
|
+
emit(corners[corner], corners[corner + 1], corners[corner + 2], ab, bc, ca);
|
|
129
|
+
}
|
|
130
|
+
if (geometry.groups.length) {
|
|
131
|
+
repaired.addGroup(start, out.length / 3 - start, group.materialIndex);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
repaired.setAttribute('position', new THREE.Float32BufferAttribute(out, 3));
|
|
135
|
+
geometry.dispose();
|
|
136
|
+
return repaired;
|
|
137
|
+
}
|
|
138
|
+
function lowerBound(values, target) {
|
|
139
|
+
let low = 0;
|
|
140
|
+
let high = values.length;
|
|
141
|
+
while (low < high) {
|
|
142
|
+
const mid = (low + high) >> 1;
|
|
143
|
+
if (values[mid] < target)
|
|
144
|
+
low = mid + 1;
|
|
145
|
+
else
|
|
146
|
+
high = mid;
|
|
147
|
+
}
|
|
148
|
+
return low;
|
|
149
|
+
}
|
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Pose a window's moving parts (sash/panel/slats) at `value` (0 = closed,
|
|
5
|
-
* 1 = open) by mutating the named child groups under `mesh`. Returns true when
|
|
6
|
-
* the window type has a direct pose path and the named parts were found.
|
|
7
|
-
*
|
|
8
|
-
* This is the single source of truth for window kinematics: the live animation
|
|
9
|
-
* system poses the registered scene mesh, and the GLB exporter poses an export
|
|
10
|
-
* clone to sample the open/close keyframes for a baked animation clip.
|
|
11
|
-
*/
|
|
12
|
-
export declare function poseWindowMovingParts(node: WindowNode, mesh: Object3D | undefined, value: number): boolean;
|
|
1
|
+
export { poseWindowMovingParts } from './window-pose';
|
|
2
|
+
/** Advance the same viewer frame loop with an explicit clock for deterministic hosts and tests. */
|
|
3
|
+
export declare function advanceWindowAnimations(now: number): void;
|
|
13
4
|
export declare const WindowAnimationSystem: () => null;
|
|
14
5
|
//# sourceMappingURL=window-animation-system.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"window-animation-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-animation-system.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"window-animation-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-animation-system.tsx"],"names":[],"mappings":"AAaA,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAiBrD,mGAAmG;AACnG,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CA2CzD;AAED,eAAO,MAAM,qBAAqB,YAGjC,CAAA"}
|
|
@@ -1,147 +1,62 @@
|
|
|
1
1
|
import { emitter, sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
|
-
import {
|
|
3
|
+
import { poseWindowMovingParts, poseWindowShutters } from './window-pose';
|
|
4
|
+
import { pendingWindowAnimationRebuilds } from './window-system';
|
|
5
|
+
export { poseWindowMovingParts } from './window-pose';
|
|
4
6
|
const easeWindowAnimation = (value) => value * value * (3 - 2 * value);
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* the window type has a direct pose path and the named parts were found.
|
|
9
|
-
*
|
|
10
|
-
* This is the single source of truth for window kinematics: the live animation
|
|
11
|
-
* system poses the registered scene mesh, and the GLB exporter poses an export
|
|
12
|
-
* clone to sample the open/close keyframes for a baked animation clip.
|
|
13
|
-
*/
|
|
14
|
-
export function poseWindowMovingParts(node, mesh, value) {
|
|
15
|
-
if (node.windowType === 'sliding') {
|
|
16
|
-
const activePanel = mesh?.getObjectByName(SLIDING_WINDOW_ACTIVE_PANEL_NAME);
|
|
17
|
-
if (!activePanel)
|
|
18
|
-
return false;
|
|
19
|
-
const innerW = node.width - 2 * node.frameThickness;
|
|
20
|
-
const panelOverlap = Math.min(Math.max(node.frameThickness * 0.9, 0.04), innerW * 0.12);
|
|
21
|
-
const travel = Math.max(innerW / 2 - panelOverlap, 0) * value;
|
|
22
|
-
activePanel.position.x = -innerW / 4 - panelOverlap / 4 + travel;
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
if (node.windowType === 'single-hung') {
|
|
26
|
-
const activeSash = mesh?.getObjectByName(SINGLE_HUNG_ACTIVE_SASH_NAME);
|
|
27
|
-
if (!activeSash)
|
|
28
|
-
return false;
|
|
29
|
-
const innerH = node.height - 2 * node.frameThickness;
|
|
30
|
-
const panelOverlap = Math.min(Math.max(node.frameThickness * 0.9, 0.04), innerH * 0.12);
|
|
31
|
-
const travel = Math.max(innerH / 2 - panelOverlap, 0) * value;
|
|
32
|
-
activeSash.position.y = -innerH / 4 - panelOverlap / 4 + travel;
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
if (node.windowType === 'double-hung') {
|
|
36
|
-
const topSash = mesh?.getObjectByName(DOUBLE_HUNG_TOP_SASH_NAME);
|
|
37
|
-
const bottomSash = mesh?.getObjectByName(DOUBLE_HUNG_BOTTOM_SASH_NAME);
|
|
38
|
-
if (!(topSash && bottomSash))
|
|
39
|
-
return false;
|
|
40
|
-
const innerH = node.height - 2 * node.frameThickness;
|
|
41
|
-
const panelOverlap = Math.min(Math.max(node.frameThickness * 0.9, 0.04), innerH * 0.12);
|
|
42
|
-
const travel = Math.max(innerH / 2 - panelOverlap, 0) * value;
|
|
43
|
-
topSash.position.y = innerH / 4 + panelOverlap / 4 - travel;
|
|
44
|
-
bottomSash.position.y = -innerH / 4 - panelOverlap / 4 + travel;
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
if (node.windowType === 'louvered') {
|
|
48
|
-
const slats = mesh?.getObjectByName(LOUVERED_WINDOW_SLATS_NAME);
|
|
49
|
-
if (!slats)
|
|
50
|
-
return false;
|
|
51
|
-
const slatAngle = -value * (Math.PI / 3);
|
|
52
|
-
for (const slat of slats.children) {
|
|
53
|
-
slat.rotation.x = slatAngle;
|
|
54
|
-
}
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
if (node.windowType === 'casement') {
|
|
58
|
-
if ((node.casementStyle ?? 'single') === 'french') {
|
|
59
|
-
const leftSash = mesh?.getObjectByName(FRENCH_CASEMENT_LEFT_SASH_NAME);
|
|
60
|
-
const rightSash = mesh?.getObjectByName(FRENCH_CASEMENT_RIGHT_SASH_NAME);
|
|
61
|
-
if (!(leftSash && rightSash))
|
|
62
|
-
return false;
|
|
63
|
-
leftSash.rotation.y = -value * (Math.PI / 2);
|
|
64
|
-
rightSash.rotation.y = value * (Math.PI / 2);
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
const sash = mesh?.getObjectByName(CASEMENT_WINDOW_SASH_NAME);
|
|
68
|
-
if (!sash)
|
|
69
|
-
return false;
|
|
70
|
-
const hingeSign = (node.hingesSide ?? 'left') === 'left' ? -1 : 1;
|
|
71
|
-
sash.rotation.y = hingeSign * value * (Math.PI / 2);
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
if (node.windowType === 'awning') {
|
|
75
|
-
const sash = mesh?.getObjectByName(AWNING_WINDOW_SASH_NAME);
|
|
76
|
-
if (!sash)
|
|
77
|
-
return false;
|
|
78
|
-
sash.rotation.x = -value * (Math.PI / 3);
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
if (node.windowType === 'hopper') {
|
|
82
|
-
const sash = mesh?.getObjectByName(AWNING_WINDOW_SASH_NAME) ??
|
|
83
|
-
mesh?.getObjectByName(HOPPER_WINDOW_SASH_NAME);
|
|
84
|
-
if (!sash)
|
|
85
|
-
return false;
|
|
86
|
-
sash.rotation.x = -value * (Math.PI / 3);
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
function applyDirectWindowAnimation(windowId, value) {
|
|
92
|
-
const node = useScene.getState().nodes[windowId];
|
|
93
|
-
if (node?.type !== 'window')
|
|
7
|
+
function applyDirectWindowAnimation(node, field, value) {
|
|
8
|
+
const mesh = sceneRegistry.nodes.get(node.id);
|
|
9
|
+
if (field === 'rollerShutterOpen')
|
|
94
10
|
return false;
|
|
95
|
-
|
|
11
|
+
if (field === 'shutterOpen')
|
|
12
|
+
return poseWindowShutters(node, mesh, value);
|
|
13
|
+
const shutterOpen = useInteractive.getState().windows[node.id]?.shutterOpen ?? node.shutterOpen ?? 1;
|
|
14
|
+
return poseWindowMovingParts(node, mesh, value, shutterOpen);
|
|
96
15
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
16
|
+
/** Advance the same viewer frame loop with an explicit clock for deterministic hosts and tests. */
|
|
17
|
+
export function advanceWindowAnimations(now) {
|
|
18
|
+
const interactive = useInteractive.getState();
|
|
19
|
+
for (const [windowId, animations] of Object.entries(interactive.windowAnimations)) {
|
|
20
|
+
const typedWindowId = windowId;
|
|
21
|
+
const scene = useScene.getState();
|
|
22
|
+
const node = scene.nodes[typedWindowId];
|
|
23
|
+
if (node?.type !== 'window') {
|
|
24
|
+
interactive.cancelWindowAnimation(typedWindowId);
|
|
25
|
+
interactive.removeWindowOpenState(typedWindowId);
|
|
26
|
+
interactive.removeMechanismParts(typedWindowId);
|
|
27
|
+
pendingWindowAnimationRebuilds.delete(typedWindowId);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
for (const animation of Object.values(animations)) {
|
|
113
31
|
const startedAt = animation.startedAt ?? now;
|
|
114
32
|
if (animation.startedAt === null) {
|
|
115
33
|
interactive.startWindowAnimation(typedWindowId, { ...animation, startedAt });
|
|
116
34
|
}
|
|
117
|
-
const progress = Math.min(1, (now - startedAt) / animation.durationMs);
|
|
35
|
+
const progress = Math.min(1, Math.max(0, (now - startedAt) / animation.durationMs));
|
|
118
36
|
const value = animation.from + (animation.to - animation.from) * easeWindowAnimation(progress);
|
|
119
37
|
interactive.setWindowOpenState(typedWindowId, { [animation.field]: value });
|
|
120
|
-
const appliedDirectly = applyDirectWindowAnimation(
|
|
121
|
-
// A dirty mark is one-shot work, not a needs-frame signal
|
|
122
|
-
//
|
|
123
|
-
// path get a transient rebuild request instead.
|
|
38
|
+
const appliedDirectly = applyDirectWindowAnimation(node, animation.field, value);
|
|
39
|
+
// A dirty mark is one-shot work, not a needs-frame signal. Variable
|
|
40
|
+
// topology (including roller slats) requests a transient rebuild instead.
|
|
124
41
|
if (!appliedDirectly)
|
|
125
42
|
pendingWindowAnimationRebuilds.add(typedWindowId);
|
|
126
43
|
if (progress < 1)
|
|
127
44
|
continue;
|
|
128
|
-
interactive.cancelWindowAnimation(typedWindowId);
|
|
45
|
+
interactive.cancelWindowAnimation(typedWindowId, animation.field);
|
|
129
46
|
if (animation.persist) {
|
|
130
47
|
scene.updateNode(typedWindowId, { [animation.field]: animation.to });
|
|
131
|
-
interactive.removeWindowOpenState(typedWindowId);
|
|
132
|
-
// One-shot: the rebuild re-derives the pose from the persisted node.
|
|
133
|
-
scene.markDirty(typedWindowId);
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
interactive.setWindowOpenState(typedWindowId, { [animation.field]: animation.to });
|
|
137
|
-
if (!appliedDirectly)
|
|
138
|
-
scene.markDirty(typedWindowId);
|
|
48
|
+
interactive.removeWindowOpenState(typedWindowId, animation.field);
|
|
139
49
|
}
|
|
50
|
+
// Rejoin the static batch at the settled pose, including preview-only runs.
|
|
51
|
+
scene.markDirty(typedWindowId);
|
|
140
52
|
emitter.emit('window:animation-completed', {
|
|
141
53
|
windowId: typedWindowId,
|
|
142
54
|
field: animation.field,
|
|
143
55
|
});
|
|
144
56
|
}
|
|
145
|
-
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export const WindowAnimationSystem = () => {
|
|
60
|
+
useFrame(({ clock }) => advanceWindowAnimations(clock.getElapsedTime() * 1000), 2);
|
|
146
61
|
return null;
|
|
147
62
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type WindowNode } from '@pascal-app/core';
|
|
2
|
+
import type { Object3D } from 'three';
|
|
3
|
+
export declare const WINDOW_LEFT_SHUTTER_NAME = "shutter-left";
|
|
4
|
+
export declare const WINDOW_RIGHT_SHUTTER_NAME = "shutter-right";
|
|
5
|
+
export declare const WINDOW_SHUTTER_OUTER_NAME = "shutter-outer";
|
|
6
|
+
export declare const CASEMENT_WINDOW_SASH_NAME = "casement-window-sash";
|
|
7
|
+
export declare const FRENCH_CASEMENT_LEFT_SASH_NAME = "french-casement-left-sash";
|
|
8
|
+
export declare const FRENCH_CASEMENT_RIGHT_SASH_NAME = "french-casement-right-sash";
|
|
9
|
+
export declare const SLIDING_WINDOW_ACTIVE_PANEL_NAME = "sliding-window-active-panel";
|
|
10
|
+
export declare const SINGLE_HUNG_ACTIVE_SASH_NAME = "single-hung-active-sash";
|
|
11
|
+
export declare const DOUBLE_HUNG_TOP_SASH_NAME = "double-hung-top-sash";
|
|
12
|
+
export declare const DOUBLE_HUNG_BOTTOM_SASH_NAME = "double-hung-bottom-sash";
|
|
13
|
+
export declare const LOUVERED_WINDOW_SLATS_NAME = "louvered-window-slats";
|
|
14
|
+
export declare const AWNING_WINDOW_SASH_NAME = "awning-window-sash";
|
|
15
|
+
export declare const HOPPER_WINDOW_SASH_NAME = "hopper-window-sash";
|
|
16
|
+
export declare const TILT_TURN_WINDOW_SASH_NAME = "tilt-turn-window-sash";
|
|
17
|
+
/** Cached in the built subtree so detached export clones keep the host's exterior direction. */
|
|
18
|
+
export declare function windowExteriorSign(node: WindowNode, mesh?: Object3D): number;
|
|
19
|
+
/** Leaves start along the wall in local X; bifolds stack perpendicular to it when open. */
|
|
20
|
+
export declare function poseWindowShutters(node: WindowNode, mesh: Object3D | undefined, value: number): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Pose a window's moving parts (sash/panel/slats) at `value` (0 = closed,
|
|
23
|
+
* 1 = open) by mutating the named child groups under `mesh`. Returns true when
|
|
24
|
+
* the window type has a direct pose path and the named parts were found.
|
|
25
|
+
*
|
|
26
|
+
* This is the single source of truth for window kinematics: the live animation
|
|
27
|
+
* system poses the registered scene mesh, and the GLB exporter poses an export
|
|
28
|
+
* clone to sample the open/close keyframes for a baked animation clip.
|
|
29
|
+
*/
|
|
30
|
+
export declare function poseWindowMovingParts(node: WindowNode, mesh: Object3D | undefined, value: number, shutterOpen?: number): boolean;
|
|
31
|
+
//# sourceMappingURL=window-pose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"window-pose.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-pose.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoD,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACpG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAErC,eAAO,MAAM,wBAAwB,iBAAiB,CAAA;AACtD,eAAO,MAAM,yBAAyB,kBAAkB,CAAA;AACxD,eAAO,MAAM,yBAAyB,kBAAkB,CAAA;AAExD,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,8BAA8B,8BAA8B,CAAA;AACzE,eAAO,MAAM,+BAA+B,+BAA+B,CAAA;AAC3E,eAAO,MAAM,gCAAgC,gCAAgC,CAAA;AAC7E,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,0BAA0B,0BAA0B,CAAA;AACjE,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAC3D,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAE3D,eAAO,MAAM,0BAA0B,0BAA0B,CAAA;AAEjE,gGAAgG;AAChG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,CAI5E;AAED,2FAA2F;AAC3F,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,QAAQ,GAAG,SAAS,EAC1B,KAAK,EAAE,MAAM,GACZ,OAAO,CAiBT;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,QAAQ,GAAG,SAAS,EAC1B,KAAK,EAAE,MAAM,EACb,WAAW,GAAE,MAA8B,GAC1C,OAAO,CA2GT"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { openingExteriorFace, openingHostFacing, useScene } from '@pascal-app/core';
|
|
2
|
+
export const WINDOW_LEFT_SHUTTER_NAME = 'shutter-left';
|
|
3
|
+
export const WINDOW_RIGHT_SHUTTER_NAME = 'shutter-right';
|
|
4
|
+
export const WINDOW_SHUTTER_OUTER_NAME = 'shutter-outer';
|
|
5
|
+
export const CASEMENT_WINDOW_SASH_NAME = 'casement-window-sash';
|
|
6
|
+
export const FRENCH_CASEMENT_LEFT_SASH_NAME = 'french-casement-left-sash';
|
|
7
|
+
export const FRENCH_CASEMENT_RIGHT_SASH_NAME = 'french-casement-right-sash';
|
|
8
|
+
export const SLIDING_WINDOW_ACTIVE_PANEL_NAME = 'sliding-window-active-panel';
|
|
9
|
+
export const SINGLE_HUNG_ACTIVE_SASH_NAME = 'single-hung-active-sash';
|
|
10
|
+
export const DOUBLE_HUNG_TOP_SASH_NAME = 'double-hung-top-sash';
|
|
11
|
+
export const DOUBLE_HUNG_BOTTOM_SASH_NAME = 'double-hung-bottom-sash';
|
|
12
|
+
export const LOUVERED_WINDOW_SLATS_NAME = 'louvered-window-slats';
|
|
13
|
+
export const AWNING_WINDOW_SASH_NAME = 'awning-window-sash';
|
|
14
|
+
export const HOPPER_WINDOW_SASH_NAME = 'hopper-window-sash';
|
|
15
|
+
export const TILT_TURN_WINDOW_SASH_NAME = 'tilt-turn-window-sash';
|
|
16
|
+
/** Cached in the built subtree so detached export clones keep the host's exterior direction. */
|
|
17
|
+
export function windowExteriorSign(node, mesh) {
|
|
18
|
+
if (typeof mesh?.userData.windowExteriorSign === 'number')
|
|
19
|
+
return mesh.userData.windowExteriorSign;
|
|
20
|
+
const face = openingExteriorFace(node, useScene.getState().nodes);
|
|
21
|
+
return (face === 'a' ? 1 : -1) * openingHostFacing(node);
|
|
22
|
+
}
|
|
23
|
+
/** Leaves start along the wall in local X; bifolds stack perpendicular to it when open. */
|
|
24
|
+
export function poseWindowShutters(node, mesh, value) {
|
|
25
|
+
const exterior = windowExteriorSign(node, mesh);
|
|
26
|
+
let found = false;
|
|
27
|
+
for (const [name, side] of [
|
|
28
|
+
[WINDOW_LEFT_SHUTTER_NAME, -1],
|
|
29
|
+
[WINDOW_RIGHT_SHUTTER_NAME, 1],
|
|
30
|
+
]) {
|
|
31
|
+
const leaf = mesh?.getObjectByName(name);
|
|
32
|
+
if (!leaf)
|
|
33
|
+
continue;
|
|
34
|
+
// Export clones can decompose a half-turn into nonzero X/Z Euler angles.
|
|
35
|
+
const outer = leaf.getObjectByName(WINDOW_SHUTTER_OUTER_NAME);
|
|
36
|
+
const swing = value * (outer ? Math.PI / 2 : Math.PI);
|
|
37
|
+
leaf.rotation.set(0, -side * exterior * (Math.PI - swing), 0);
|
|
38
|
+
if (outer)
|
|
39
|
+
outer.rotation.set(0, -side * exterior * 2 * swing, 0);
|
|
40
|
+
found = true;
|
|
41
|
+
}
|
|
42
|
+
return found;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Pose a window's moving parts (sash/panel/slats) at `value` (0 = closed,
|
|
46
|
+
* 1 = open) by mutating the named child groups under `mesh`. Returns true when
|
|
47
|
+
* the window type has a direct pose path and the named parts were found.
|
|
48
|
+
*
|
|
49
|
+
* This is the single source of truth for window kinematics: the live animation
|
|
50
|
+
* system poses the registered scene mesh, and the GLB exporter poses an export
|
|
51
|
+
* clone to sample the open/close keyframes for a baked animation clip.
|
|
52
|
+
*/
|
|
53
|
+
export function poseWindowMovingParts(node, mesh, value, shutterOpen = node.shutterOpen ?? 1) {
|
|
54
|
+
poseWindowShutters(node, mesh, shutterOpen);
|
|
55
|
+
if (node.windowType === 'sliding') {
|
|
56
|
+
const activePanel = mesh?.getObjectByName(SLIDING_WINDOW_ACTIVE_PANEL_NAME);
|
|
57
|
+
if (!activePanel)
|
|
58
|
+
return false;
|
|
59
|
+
const innerW = node.width - 2 * node.frameThickness;
|
|
60
|
+
const panelOverlap = Math.min(Math.max(node.frameThickness * 0.9, 0.04), innerW * 0.12);
|
|
61
|
+
const travel = Math.max(innerW / 2 - panelOverlap, 0) * value;
|
|
62
|
+
activePanel.position.x = -innerW / 4 - panelOverlap / 4 + travel;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (node.windowType === 'single-hung') {
|
|
66
|
+
const activeSash = mesh?.getObjectByName(SINGLE_HUNG_ACTIVE_SASH_NAME);
|
|
67
|
+
if (!activeSash)
|
|
68
|
+
return false;
|
|
69
|
+
const innerH = node.height - 2 * node.frameThickness;
|
|
70
|
+
const panelOverlap = Math.min(Math.max(node.frameThickness * 0.9, 0.04), innerH * 0.12);
|
|
71
|
+
const travel = Math.max(innerH / 2 - panelOverlap, 0) * value;
|
|
72
|
+
activeSash.position.y = -innerH / 4 - panelOverlap / 4 + travel;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (node.windowType === 'double-hung') {
|
|
76
|
+
const topSash = mesh?.getObjectByName(DOUBLE_HUNG_TOP_SASH_NAME);
|
|
77
|
+
const bottomSash = mesh?.getObjectByName(DOUBLE_HUNG_BOTTOM_SASH_NAME);
|
|
78
|
+
if (!(topSash && bottomSash))
|
|
79
|
+
return false;
|
|
80
|
+
const innerH = node.height - 2 * node.frameThickness;
|
|
81
|
+
const panelOverlap = Math.min(Math.max(node.frameThickness * 0.9, 0.04), innerH * 0.12);
|
|
82
|
+
const travel = Math.max(innerH / 2 - panelOverlap, 0) * value;
|
|
83
|
+
topSash.position.y = innerH / 4 + panelOverlap / 4 - travel;
|
|
84
|
+
bottomSash.position.y = -innerH / 4 - panelOverlap / 4 + travel;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (node.windowType === 'louvered') {
|
|
88
|
+
const slats = mesh?.getObjectByName(LOUVERED_WINDOW_SLATS_NAME);
|
|
89
|
+
if (!slats)
|
|
90
|
+
return false;
|
|
91
|
+
const slatAngle = -value * (Math.PI / 3);
|
|
92
|
+
for (const slat of slats.children) {
|
|
93
|
+
slat.rotation.x = slatAngle;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
const exterior = windowExteriorSign(node, mesh);
|
|
98
|
+
if (node.windowType === 'tilt-turn') {
|
|
99
|
+
const sash = mesh?.getObjectByName(TILT_TURN_WINDOW_SASH_NAME);
|
|
100
|
+
if (!sash)
|
|
101
|
+
return false;
|
|
102
|
+
sash.rotation.set(0, 0, 0);
|
|
103
|
+
if (node.tiltTurnMode === 'turn') {
|
|
104
|
+
sash.rotation.y = ((node.hingesSide === 'right' ? -1 : 1) * exterior * value * Math.PI) / 2;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
sash.rotation.x = (-exterior * value * Math.PI) / 12;
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
const direction = node.openingDirection === undefined
|
|
112
|
+
? 1
|
|
113
|
+
: exterior * (node.openingDirection === 'inward' ? -1 : 1);
|
|
114
|
+
if (node.windowType === 'casement') {
|
|
115
|
+
if ((node.casementStyle ?? 'single') === 'french') {
|
|
116
|
+
const leftSash = mesh?.getObjectByName(FRENCH_CASEMENT_LEFT_SASH_NAME);
|
|
117
|
+
const rightSash = mesh?.getObjectByName(FRENCH_CASEMENT_RIGHT_SASH_NAME);
|
|
118
|
+
if (!(leftSash && rightSash))
|
|
119
|
+
return false;
|
|
120
|
+
leftSash.rotation.y = -direction * value * (Math.PI / 2);
|
|
121
|
+
rightSash.rotation.y = direction * value * (Math.PI / 2);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
const sash = mesh?.getObjectByName(CASEMENT_WINDOW_SASH_NAME);
|
|
125
|
+
if (!sash)
|
|
126
|
+
return false;
|
|
127
|
+
const hingeSign = (node.hingesSide ?? 'left') === 'left' ? -1 : 1;
|
|
128
|
+
sash.rotation.y = direction * hingeSign * value * (Math.PI / 2);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (node.windowType === 'awning') {
|
|
132
|
+
const sash = mesh?.getObjectByName(AWNING_WINDOW_SASH_NAME);
|
|
133
|
+
if (!sash)
|
|
134
|
+
return false;
|
|
135
|
+
sash.rotation.x =
|
|
136
|
+
(node.openingDirection === undefined
|
|
137
|
+
? -1
|
|
138
|
+
: (node.awningDirection === 'down' ? 1 : -1) * direction) *
|
|
139
|
+
value *
|
|
140
|
+
(Math.PI / 3);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
if (node.windowType === 'hopper') {
|
|
144
|
+
const sash = mesh?.getObjectByName(AWNING_WINDOW_SASH_NAME) ??
|
|
145
|
+
mesh?.getObjectByName(HOPPER_WINDOW_SASH_NAME);
|
|
146
|
+
if (!sash)
|
|
147
|
+
return false;
|
|
148
|
+
sash.rotation.x = -exterior * value * (Math.PI / 3);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { WindowNode } from '@pascal-app/core';
|
|
2
|
+
import { Group, type Material, type Object3D } from 'three';
|
|
3
|
+
/** Measure the closed unit in its own coordinates, including a surface-mounted roller box. */
|
|
4
|
+
export declare function getWindowRollerCurtainZ(mesh: Object3D, exterior: number): number;
|
|
5
|
+
export declare function buildWindowRollerCurtain(node: WindowNode, material: Material | Material[], z: number, open: number): Group;
|
|
6
|
+
/** Export clones must use saved topology even when the live curtain is previewing another pose. */
|
|
7
|
+
export declare function restoreWindowRollerShutter(node: WindowNode, mesh: Object3D): void;
|
|
8
|
+
//# sourceMappingURL=window-roller-shutter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"window-roller-shutter.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-roller-shutter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,EAAe,KAAK,EAAE,KAAK,QAAQ,EAAiB,KAAK,QAAQ,EAAW,MAAM,OAAO,CAAA;AAIhG,8FAA8F;AAC9F,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAsBhF;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,QAAQ,GAAG,QAAQ,EAAE,EAC/B,CAAC,EAAE,MAAM,EACT,IAAI,EAAE,MAAM,GACX,KAAK,CAoBP;AAED,mGAAmG;AACnG,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAgBjF"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { BoxGeometry, Group, Matrix4, Mesh, Vector3 } from 'three';
|
|
2
|
+
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
|
|
3
|
+
import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
|
|
4
|
+
/** Measure the closed unit in its own coordinates, including a surface-mounted roller box. */
|
|
5
|
+
export function getWindowRollerCurtainZ(mesh, exterior) {
|
|
6
|
+
mesh.updateWorldMatrix(true, true);
|
|
7
|
+
const toWindow = mesh.matrixWorld.clone().invert();
|
|
8
|
+
const transform = new Matrix4();
|
|
9
|
+
const vertex = new Vector3();
|
|
10
|
+
let extent = 0;
|
|
11
|
+
mesh.traverse((part) => {
|
|
12
|
+
if (!(part instanceof Mesh) ||
|
|
13
|
+
part === mesh ||
|
|
14
|
+
(!['frame', 'glass', 'hardware'].includes(part.userData.slotId) &&
|
|
15
|
+
part.name !== 'window-roller-box'))
|
|
16
|
+
return;
|
|
17
|
+
transform.multiplyMatrices(toWindow, part.matrixWorld);
|
|
18
|
+
const position = part.geometry.getAttribute('position');
|
|
19
|
+
for (let i = 0; i < position.count; i++) {
|
|
20
|
+
vertex.fromBufferAttribute(position, i).applyMatrix4(transform);
|
|
21
|
+
extent = Math.max(extent, vertex.z * exterior);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return exterior * (extent + 0.015);
|
|
25
|
+
}
|
|
26
|
+
export function buildWindowRollerCurtain(node, material, z, open) {
|
|
27
|
+
const curtain = new Group();
|
|
28
|
+
curtain.name = 'window-roller-curtain';
|
|
29
|
+
curtain.position.z = z;
|
|
30
|
+
const width = node.width - (node.rollerShutter === 'concealed' ? 2 * 0.025 : 0);
|
|
31
|
+
const slatCount = Math.floor(((1 - open) * node.height) / 0.04);
|
|
32
|
+
if (slatCount <= 0)
|
|
33
|
+
return curtain;
|
|
34
|
+
const slats = [];
|
|
35
|
+
for (let i = 0; i < slatCount; i++) {
|
|
36
|
+
const slat = new BoxGeometry(width, 0.036, 0.01);
|
|
37
|
+
applyWorldScaleBoxUVs(slat, width, 0.036, 0.01);
|
|
38
|
+
slat.translate(0, node.height / 2 - i * 0.04 - 0.018, 0);
|
|
39
|
+
slats.push(slat);
|
|
40
|
+
}
|
|
41
|
+
const geometry = mergeGeometries(slats);
|
|
42
|
+
for (const slat of slats)
|
|
43
|
+
slat.dispose();
|
|
44
|
+
const panel = new Mesh(geometry, material);
|
|
45
|
+
panel.userData.slotId = 'shutters';
|
|
46
|
+
curtain.add(panel);
|
|
47
|
+
return curtain;
|
|
48
|
+
}
|
|
49
|
+
/** Export clones must use saved topology even when the live curtain is previewing another pose. */
|
|
50
|
+
export function restoreWindowRollerShutter(node, mesh) {
|
|
51
|
+
if (node.rollerShutter !== 'box' && node.rollerShutter !== 'concealed')
|
|
52
|
+
return;
|
|
53
|
+
const curtain = mesh.getObjectByName('window-roller-curtain');
|
|
54
|
+
const guide = mesh.getObjectByName('window-roller-guide');
|
|
55
|
+
if (!curtain?.parent || !guide)
|
|
56
|
+
return;
|
|
57
|
+
const restored = buildWindowRollerCurtain(node, guide.material, curtain.position.z, node.rollerShutterOpen ?? 1);
|
|
58
|
+
curtain.parent.add(restored);
|
|
59
|
+
curtain.traverse((part) => {
|
|
60
|
+
if (part instanceof Mesh)
|
|
61
|
+
part.geometry.dispose();
|
|
62
|
+
});
|
|
63
|
+
curtain.removeFromParent();
|
|
64
|
+
}
|
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
import { type WindowNode } from '@pascal-app/core';
|
|
2
2
|
import * as THREE from 'three';
|
|
3
|
-
export
|
|
4
|
-
export declare const
|
|
5
|
-
export declare const FRENCH_CASEMENT_RIGHT_SASH_NAME = "french-casement-right-sash";
|
|
6
|
-
export declare const SLIDING_WINDOW_ACTIVE_PANEL_NAME = "sliding-window-active-panel";
|
|
7
|
-
export declare const SINGLE_HUNG_ACTIVE_SASH_NAME = "single-hung-active-sash";
|
|
8
|
-
export declare const DOUBLE_HUNG_TOP_SASH_NAME = "double-hung-top-sash";
|
|
9
|
-
export declare const DOUBLE_HUNG_BOTTOM_SASH_NAME = "double-hung-bottom-sash";
|
|
10
|
-
export declare const LOUVERED_WINDOW_SLATS_NAME = "louvered-window-slats";
|
|
11
|
-
export declare const AWNING_WINDOW_SASH_NAME = "awning-window-sash";
|
|
12
|
-
export declare const HOPPER_WINDOW_SASH_NAME = "hopper-window-sash";
|
|
13
|
-
export declare const pendingWindowAnimationRebuilds: Set<`block_${string}` | `building_${string}` | `bvent_${string}` | `cabinet-module_${string}` | `cabinet_${string}` | `ceiling_${string}` | `chimney_${string}` | `column_${string}` | `construction-dimension_${string}` | `cupola_${string}` | `door_${string}` | `dormer_${string}` | `downspout_${string}` | `duct-fitting_${string}` | `duct-segment_${string}` | `duct-terminal_${string}` | `elevator_${string}` | `eyebrow-vent_${string}` | `fence-gate_${string}` | `fence-opening_${string}` | `fence_${string}` | `floor-opening_${string}` | `guide_${string}` | `gutter_${string}` | `hvac-equipment_${string}` | `imesh_${string}` | `item_${string}` | `leanto_${string}` | `level_${string}` | `lineset_${string}` | `liquid-line_${string}` | `measurement_${string}` | `pipe-fitting_${string}` | `pipe-segment_${string}` | `pipe-trap_${string}` | `procedural-item_${string}` | `roof_${string}` | `rseg_${string}` | `rvent_${string}` | `scan_${string}` | `separator_${string}` | `shelf_${string}` | `site_${string}` | `skylight_${string}` | `slab_${string}` | `solarpanel_${string}` | `spawn_${string}` | `sseg_${string}` | `stair_${string}` | `structural-grid_${string}` | `tvent_${string}` | `unit_${string}` | `wall_${string}` | `window_${string}` | `zone_${string}`>;
|
|
3
|
+
export * from './window-pose';
|
|
4
|
+
export declare const pendingWindowAnimationRebuilds: Set<`beam_${string}` | `block_${string}` | `building_${string}` | `bvent_${string}` | `cabinet-module_${string}` | `cabinet_${string}` | `ceiling_${string}` | `chimney_${string}` | `column_${string}` | `construction-dimension_${string}` | `cupola_${string}` | `door_${string}` | `dormer_${string}` | `downspout_${string}` | `duct-fitting_${string}` | `duct-segment_${string}` | `duct-terminal_${string}` | `elevator_${string}` | `eyebrow-vent_${string}` | `fence-gate_${string}` | `fence-opening_${string}` | `fence_${string}` | `floor-opening_${string}` | `guide_${string}` | `gutter_${string}` | `hvac-equipment_${string}` | `imesh_${string}` | `item_${string}` | `leanto_${string}` | `level_${string}` | `lineset_${string}` | `liquid-line_${string}` | `measurement_${string}` | `pipe-fitting_${string}` | `pipe-segment_${string}` | `pipe-trap_${string}` | `procedural-item_${string}` | `roof_${string}` | `rseg_${string}` | `rvent_${string}` | `scan_${string}` | `separator_${string}` | `shelf_${string}` | `site_${string}` | `skylight_${string}` | `slab_${string}` | `solarpanel_${string}` | `spawn_${string}` | `sseg_${string}` | `stair_${string}` | `structural-grid_${string}` | `tvent_${string}` | `unit_${string}` | `wall_${string}` | `window_${string}` | `zone_${string}`>;
|
|
14
5
|
export declare const WindowSystem: () => null;
|
|
15
6
|
/**
|
|
16
7
|
* Build a fresh window mesh for preview/ghost rendering.
|