@pascal-app/viewer 1.0.1 → 1.0.4
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/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +8 -3
- package/dist/components/viewer/frame-limiter.d.ts +1 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +22 -3
- package/dist/components/viewer/glb-interactive.d.ts +25 -1
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
- package/dist/components/viewer/glb-interactive.js +252 -85
- package/dist/components/viewer/glb-mechanisms.d.ts +10 -0
- package/dist/components/viewer/glb-mechanisms.d.ts.map +1 -0
- package/dist/components/viewer/glb-mechanisms.js +19 -0
- package/dist/components/viewer/glb-scene.d.ts +45 -27
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +420 -506
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +12 -1
- package/dist/components/viewer/index.d.ts +26 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +108 -29
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +6 -1
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +6 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +8 -3
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +16 -2
- package/dist/components/viewer/render-error.d.ts +18 -0
- package/dist/components/viewer/render-error.d.ts.map +1 -0
- package/dist/components/viewer/render-error.js +29 -0
- package/dist/components/viewer/scene-ground-replacement.d.ts +6 -0
- package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -1
- package/dist/components/viewer/scene-ground-replacement.js +15 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +10 -0
- package/dist/components/viewer/viewer-camera.d.ts +19 -1
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +53 -5
- package/dist/index.d.ts +28 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -18
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +7 -1
- package/dist/lib/csg-utils.d.ts +1 -1
- package/dist/lib/csg-utils.d.ts.map +1 -1
- package/dist/lib/csg-utils.js +1 -1
- package/dist/lib/display-state.d.ts +25 -0
- package/dist/lib/display-state.d.ts.map +1 -0
- package/dist/lib/display-state.js +1 -0
- package/dist/lib/hero-pose.d.ts +12 -2
- package/dist/lib/hero-pose.d.ts.map +1 -1
- package/dist/lib/hero-pose.js +72 -5
- package/dist/lib/ink-edges.d.ts +4 -2
- package/dist/lib/ink-edges.d.ts.map +1 -1
- package/dist/lib/ink-edges.js +17 -7
- package/dist/lib/key-light-override.d.ts +11 -0
- package/dist/lib/key-light-override.d.ts.map +1 -0
- package/dist/lib/key-light-override.js +14 -0
- package/dist/lib/live-frame-hold.d.ts +14 -0
- package/dist/lib/live-frame-hold.d.ts.map +1 -0
- package/dist/lib/live-frame-hold.js +28 -0
- package/dist/lib/materials.d.ts +14 -7
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +31 -5
- package/dist/lib/node-top-surface-height.d.ts +6 -0
- package/dist/lib/node-top-surface-height.d.ts.map +1 -0
- package/dist/lib/node-top-surface-height.js +74 -0
- package/dist/lib/perf-probe-state.d.ts +39 -0
- package/dist/lib/perf-probe-state.d.ts.map +1 -0
- package/dist/lib/perf-probe-state.js +17 -0
- package/dist/lib/pointer-events.d.ts +8 -0
- package/dist/lib/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +27 -4
- package/dist/lib/procedural-emission.d.ts +7 -0
- package/dist/lib/procedural-emission.d.ts.map +1 -0
- package/dist/lib/procedural-emission.js +54 -0
- package/dist/lib/renderer-capability.d.ts +2 -1
- package/dist/lib/renderer-capability.d.ts.map +1 -1
- package/dist/lib/renderer-capability.js +2 -2
- package/dist/lib/scene-support-height.d.ts +4 -0
- package/dist/lib/scene-support-height.d.ts.map +1 -0
- package/dist/lib/scene-support-height.js +34 -0
- package/dist/lib/scene-visibility.d.ts +6 -0
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +15 -3
- package/dist/lib/scripted-opening.d.ts +11 -0
- package/dist/lib/scripted-opening.d.ts.map +1 -0
- package/dist/lib/scripted-opening.js +20 -0
- package/dist/lib/snapshot-pipeline.d.ts +32 -2
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
- package/dist/lib/snapshot-pipeline.js +215 -105
- package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
- package/dist/lib/terrain-perimeter-fill.js +22 -14
- package/dist/lib/zone-geometry.d.ts +11 -0
- package/dist/lib/zone-geometry.d.ts.map +1 -0
- package/dist/lib/zone-geometry.js +46 -0
- package/dist/store/use-item-light-pool.d.ts +14 -10
- package/dist/store/use-item-light-pool.d.ts.map +1 -1
- package/dist/store/use-item-light-pool.js +52 -26
- package/dist/store/use-viewer.d.ts +11 -0
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +13 -6
- package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +86 -6
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +73 -22
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
- package/dist/systems/floor-elevation/floor-elevation-system.js +148 -5
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +71 -11
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +40 -15
- package/dist/systems/interactive/procedural-controls.d.ts +13 -0
- package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
- package/dist/systems/interactive/procedural-controls.js +17 -0
- package/dist/systems/interactive/scripted-clips.d.ts +19 -0
- package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
- package/dist/systems/interactive/scripted-clips.js +134 -0
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +9 -3
- package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
- package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
- package/dist/systems/item/object-cut-invalidation.js +68 -0
- package/dist/systems/item-light/item-light-system.d.ts +1 -1
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
- package/dist/systems/item-light/item-light-system.js +47 -52
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +17 -19
- package/dist/systems/level/level-utils.d.ts +28 -1
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +36 -4
- package/dist/systems/level/shadow-only-levels.d.ts +9 -0
- package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
- package/dist/systems/level/shadow-only-levels.js +7 -0
- package/dist/systems/roof/roof-materials.d.ts +14 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +54 -15
- package/dist/systems/roof/roof-system.d.ts +13 -3
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +188 -13
- package/dist/systems/slab/slab-system.d.ts +1 -1
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +49 -19
- package/dist/systems/stair/stair-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-system.js +133 -354
- package/dist/systems/wall/wall-cutout-cache.d.ts +11 -1
- package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout-cache.js +58 -20
- package/dist/systems/wall/wall-cutout.d.ts +9 -1
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +46 -32
- package/dist/systems/wall/wall-finish-data.d.ts +22 -0
- package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
- package/dist/systems/wall/wall-finish-data.js +17 -0
- package/dist/systems/wall/wall-materials.d.ts +17 -3
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +109 -156
- package/dist/systems/wall/wall-system.d.ts +43 -4
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +601 -163
- package/dist/systems/window/window-system.d.ts +1 -1
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +44 -24
- package/dist/xr/presentation-context.d.ts +7 -0
- package/dist/xr/presentation-context.d.ts.map +1 -0
- package/dist/xr/presentation-context.js +10 -0
- package/package.json +6 -6
- package/dist/systems/fence/fence-system.d.ts +0 -22
- package/dist/systems/fence/fence-system.d.ts.map +0 -1
- package/dist/systems/fence/fence-system.js +0 -357
|
@@ -10,7 +10,7 @@ export declare const DOUBLE_HUNG_BOTTOM_SASH_NAME = "double-hung-bottom-sash";
|
|
|
10
10
|
export declare const LOUVERED_WINDOW_SLATS_NAME = "louvered-window-slats";
|
|
11
11
|
export declare const AWNING_WINDOW_SASH_NAME = "awning-window-sash";
|
|
12
12
|
export declare const HOPPER_WINDOW_SASH_NAME = "hopper-window-sash";
|
|
13
|
-
export declare const pendingWindowAnimationRebuilds: Set<`building_${string}` | `level_${string}` | `elevator_${string}` | `unit_${string}` | `block_${string}` | `ceiling_${string}` | `item_${string}` | `procedural-item_${string}` | `column_${string}` | `construction-dimension_${string}` | `wall_${string}` | `roof_${string}` | `slab_${string}` | `fence_${string}` | `structural-grid_${string}` | `imesh_${string}` | `zone_${string}` | `stair_${string}` | `scan_${string}` | `guide_${string}` | `measurement_${string}` | `spawn_${string}` | `shelf_${string}` | `duct-segment_${string}` | `duct-fitting_${string}` | `duct-terminal_${string}` | `hvac-equipment_${string}` | `lineset_${string}` | `liquid-line_${string}` | `pipe-segment_${string}` | `pipe-fitting_${string}` | `pipe-trap_${string}` | `site_${string}` | `leanto_${string}` | `door_${string}` | `window_${string}` | `cabinet_${string}` | `cabinet-module_${string}` | `rseg_${string}` | `sseg_${string}` | `bvent_${string}` | `rvent_${string}` | `tvent_${string}` | `cupola_${string}` | `eyebrow-vent_${string}` | `gutter_${string}` | `chimney_${string}` | `solarpanel_${string}` | `skylight_${string}` | `dormer_${string}` | `downspout_${string}`>;
|
|
13
|
+
export declare const pendingWindowAnimationRebuilds: Set<`building_${string}` | `level_${string}` | `elevator_${string}` | `unit_${string}` | `block_${string}` | `ceiling_${string}` | `item_${string}` | `procedural-item_${string}` | `column_${string}` | `construction-dimension_${string}` | `wall_${string}` | `roof_${string}` | `slab_${string}` | `separator_${string}` | `floor-opening_${string}` | `fence_${string}` | `structural-grid_${string}` | `imesh_${string}` | `zone_${string}` | `stair_${string}` | `scan_${string}` | `guide_${string}` | `measurement_${string}` | `spawn_${string}` | `shelf_${string}` | `duct-segment_${string}` | `duct-fitting_${string}` | `duct-terminal_${string}` | `hvac-equipment_${string}` | `lineset_${string}` | `liquid-line_${string}` | `pipe-segment_${string}` | `pipe-fitting_${string}` | `pipe-trap_${string}` | `site_${string}` | `leanto_${string}` | `door_${string}` | `window_${string}` | `fence-gate_${string}` | `fence-opening_${string}` | `cabinet_${string}` | `cabinet-module_${string}` | `rseg_${string}` | `sseg_${string}` | `bvent_${string}` | `rvent_${string}` | `tvent_${string}` | `cupola_${string}` | `eyebrow-vent_${string}` | `gutter_${string}` | `chimney_${string}` | `solarpanel_${string}` | `skylight_${string}` | `dormer_${string}` | `downspout_${string}`>;
|
|
14
14
|
export declare const WindowSystem: () => null;
|
|
15
15
|
/**
|
|
16
16
|
* Build a fresh window mesh for preview/ghost rendering.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAcL,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA2B9B,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;AAS3D,eAAO,MAAM,8BAA8B,gvCAAuB,CAAA;AAElE,eAAO,MAAM,YAAY,YAuHxB,CAAA;AA29GD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,KAAK,CAAC,IAAI,CAInE"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { DEFAULT_WALL_THICKNESS, getEffectiveNode, getWallThickness, sceneRegistry, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
1
|
+
import { DEFAULT_WALL_THICKNESS, getEffectiveNode, getOpeningWallPlacement, getWallThickness, sceneRegistry, scriptedSize, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
5
|
import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
|
|
6
|
-
import { createSurfaceRoleMaterial, glassMaterial as defaultGlassMaterial, baseMaterial as getBaseMaterial, resolveMaterialRef, } from '../../lib/materials';
|
|
6
|
+
import { createMaterial, createSurfaceRoleMaterial, glassMaterial as defaultGlassMaterial, baseMaterial as getBaseMaterial, resolveMaterialRef, } from '../../lib/materials';
|
|
7
7
|
import { timeSpan } from '../../lib/perf-tracks';
|
|
8
|
+
import { settleScriptedOpening } from '../../lib/scripted-opening';
|
|
8
9
|
import useViewer from '../../store/use-viewer';
|
|
9
10
|
import { getOpeningCutoutProxyDepth } from '../wall/opening-cutout-geometry';
|
|
10
11
|
// Invisible material for root mesh — used as selection hitbox only
|
|
@@ -128,16 +129,20 @@ export const WindowSystem = () => {
|
|
|
128
129
|
// Merge any live override (width / height / position) so the mesh
|
|
129
130
|
// rebuild reflects the in-flight drag without zustand churn.
|
|
130
131
|
const effectiveNode = getEffectiveNode(node);
|
|
131
|
-
timeSpan('window', () => updateWindowMesh(effectiveNode, mesh), {
|
|
132
|
+
const built = timeSpan('window', () => updateWindowMesh(effectiveNode, mesh), {
|
|
132
133
|
properties: [['node', id]],
|
|
133
134
|
});
|
|
135
|
+
// A scripted opening stays dirty until its artifact has loaded.
|
|
136
|
+
if (!built)
|
|
137
|
+
continue;
|
|
134
138
|
clearDirty(id);
|
|
135
139
|
rebuiltWindowsThisFrame += 1;
|
|
136
140
|
// Rebuild the parent wall so its cutout reflects the updated window geometry
|
|
137
141
|
// Avoid triggering expensive wall CSG rebuilds while the window is being interactively moved/duplicated.
|
|
138
142
|
// The editor tools will request a final wall rebuild on commit.
|
|
139
|
-
const
|
|
140
|
-
|
|
143
|
+
const metadata = effectiveNode.metadata;
|
|
144
|
+
const deferParentRebuild = !!metadata?.isTransient || !!metadata?.deferParentRebuild;
|
|
145
|
+
if (!deferParentRebuild && effectiveNode.parentId) {
|
|
141
146
|
useScene.getState().dirtyNodes.add(effectiveNode.parentId);
|
|
142
147
|
}
|
|
143
148
|
}
|
|
@@ -191,16 +196,19 @@ function windowSlotDefault(slotId) {
|
|
|
191
196
|
getBaseMaterial(currentShading));
|
|
192
197
|
}
|
|
193
198
|
// Resolve a window's slot to a material: the `node.slots` override (colored mode
|
|
194
|
-
// only) → the
|
|
195
|
-
// escape hatch.
|
|
199
|
+
// only) → the window-wide `material` for the frame (glass keeps its own) → the
|
|
200
|
+
// role/base default. Textures-off ignores overrides — the monochrome escape hatch.
|
|
196
201
|
function resolveWindowSlotMaterial(node, slotId) {
|
|
197
202
|
const fallback = windowSlotDefault(slotId);
|
|
198
203
|
if (!currentTextures)
|
|
199
204
|
return fallback;
|
|
200
205
|
const ref = node.slots?.[slotId];
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
206
|
+
const painted = ref ? resolveMaterialRef(ref, currentSceneMaterials, currentShading) : null;
|
|
207
|
+
if (painted)
|
|
208
|
+
return painted;
|
|
209
|
+
if (node.material && slotId === 'frame')
|
|
210
|
+
return createMaterial(node.material, currentShading);
|
|
211
|
+
return fallback;
|
|
204
212
|
}
|
|
205
213
|
function addBox(parent, material, w, h, d, x, y, z) {
|
|
206
214
|
const geometry = new THREE.BoxGeometry(w, h, d);
|
|
@@ -1525,8 +1533,19 @@ function updateWindowMesh(node, mesh) {
|
|
|
1525
1533
|
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
|
|
1526
1534
|
mesh.material = hitboxMaterial;
|
|
1527
1535
|
// Sync transform from node (React may lag behind the system by a frame during drag)
|
|
1528
|
-
|
|
1529
|
-
|
|
1536
|
+
const parent = node.parentId ? useScene.getState().nodes[node.parentId] : undefined;
|
|
1537
|
+
const placement = parent?.type === 'wall' && !node.roofSegmentId
|
|
1538
|
+
? getOpeningWallPlacement(getEffectiveNode(parent), node, useScene.getState().nodes)
|
|
1539
|
+
: node;
|
|
1540
|
+
mesh.position.set(...placement.position);
|
|
1541
|
+
mesh.rotation.set(...placement.rotation);
|
|
1542
|
+
// Built from a script: the renderer shows its artifact, not the parametric
|
|
1543
|
+
// frame, and the hit box is what the script built.
|
|
1544
|
+
if (node.source) {
|
|
1545
|
+
mesh.geometry.dispose();
|
|
1546
|
+
mesh.geometry = new THREE.BoxGeometry(...scriptedSize(node.source.manifest));
|
|
1547
|
+
return settleScriptedOpening(mesh);
|
|
1548
|
+
}
|
|
1530
1549
|
// Dispose and remove all old visual children; preserve 'cutout'
|
|
1531
1550
|
for (const child of [...mesh.children]) {
|
|
1532
1551
|
if (child.name === 'cutout')
|
|
@@ -1542,62 +1561,62 @@ function updateWindowMesh(node, mesh) {
|
|
|
1542
1561
|
const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill, sillDepth, sillThickness, openingKind, openingShape, windowType, } = node;
|
|
1543
1562
|
if (openingKind === 'opening') {
|
|
1544
1563
|
syncWindowCutout(node, mesh);
|
|
1545
|
-
return;
|
|
1564
|
+
return true;
|
|
1546
1565
|
}
|
|
1547
1566
|
if (windowType === 'sliding') {
|
|
1548
1567
|
addSlidingWindowVisuals(node, mesh);
|
|
1549
1568
|
syncWindowCutout(node, mesh);
|
|
1550
|
-
return;
|
|
1569
|
+
return true;
|
|
1551
1570
|
}
|
|
1552
1571
|
if (windowType === 'casement') {
|
|
1553
1572
|
addCasementWindowVisuals(node, mesh);
|
|
1554
1573
|
syncWindowCutout(node, mesh);
|
|
1555
|
-
return;
|
|
1574
|
+
return true;
|
|
1556
1575
|
}
|
|
1557
1576
|
if (windowType === 'awning') {
|
|
1558
1577
|
addAwningWindowVisuals(node, mesh);
|
|
1559
1578
|
syncWindowCutout(node, mesh);
|
|
1560
|
-
return;
|
|
1579
|
+
return true;
|
|
1561
1580
|
}
|
|
1562
1581
|
if (windowType === 'hopper') {
|
|
1563
1582
|
addAwningWindowVisuals(node, mesh);
|
|
1564
1583
|
syncWindowCutout(node, mesh);
|
|
1565
|
-
return;
|
|
1584
|
+
return true;
|
|
1566
1585
|
}
|
|
1567
1586
|
if (windowType === 'single-hung') {
|
|
1568
1587
|
addSingleHungWindowVisuals(node, mesh);
|
|
1569
1588
|
syncWindowCutout(node, mesh);
|
|
1570
|
-
return;
|
|
1589
|
+
return true;
|
|
1571
1590
|
}
|
|
1572
1591
|
if (windowType === 'double-hung') {
|
|
1573
1592
|
addDoubleHungWindowVisuals(node, mesh);
|
|
1574
1593
|
syncWindowCutout(node, mesh);
|
|
1575
|
-
return;
|
|
1594
|
+
return true;
|
|
1576
1595
|
}
|
|
1577
1596
|
if (windowType === 'bay') {
|
|
1578
1597
|
addBayWindowVisuals(node, mesh);
|
|
1579
1598
|
syncWindowCutout(node, mesh);
|
|
1580
|
-
return;
|
|
1599
|
+
return true;
|
|
1581
1600
|
}
|
|
1582
1601
|
if (windowType === 'bow') {
|
|
1583
1602
|
addBowWindowVisuals(node, mesh);
|
|
1584
1603
|
syncWindowCutout(node, mesh);
|
|
1585
|
-
return;
|
|
1604
|
+
return true;
|
|
1586
1605
|
}
|
|
1587
1606
|
if (windowType === 'louvered') {
|
|
1588
1607
|
addLouveredWindowVisuals(node, mesh);
|
|
1589
1608
|
syncWindowCutout(node, mesh);
|
|
1590
|
-
return;
|
|
1609
|
+
return true;
|
|
1591
1610
|
}
|
|
1592
1611
|
if (openingShape === 'arch') {
|
|
1593
1612
|
addArchedWindowVisuals(node, mesh);
|
|
1594
1613
|
syncWindowCutout(node, mesh);
|
|
1595
|
-
return;
|
|
1614
|
+
return true;
|
|
1596
1615
|
}
|
|
1597
1616
|
if (openingShape === 'rounded') {
|
|
1598
1617
|
addRoundedWindowVisuals(node, mesh);
|
|
1599
1618
|
syncWindowCutout(node, mesh);
|
|
1600
|
-
return;
|
|
1619
|
+
return true;
|
|
1601
1620
|
}
|
|
1602
1621
|
const innerW = width - 2 * frameThickness;
|
|
1603
1622
|
const innerH = height - 2 * frameThickness;
|
|
@@ -1672,6 +1691,7 @@ function updateWindowMesh(node, mesh) {
|
|
|
1672
1691
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1673
1692
|
}
|
|
1674
1693
|
syncWindowCutout(node, mesh);
|
|
1694
|
+
return true;
|
|
1675
1695
|
}
|
|
1676
1696
|
function syncWindowCutout(node, mesh) {
|
|
1677
1697
|
// ── Cutout: invisible raycast hit target for the whole opening ──
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export declare function ImmersiveXRPresentationProvider({ children, enabled, }: {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
}): import("react").JSX.Element;
|
|
6
|
+
export declare function useImmersiveXRPresentation(): boolean;
|
|
7
|
+
//# sourceMappingURL=presentation-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presentation-context.d.ts","sourceRoot":"","sources":["../../src/xr/presentation-context.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,SAAS,EAAc,MAAM,OAAO,CAAA;AAIjE,wBAAgB,+BAA+B,CAAC,EAC9C,QAAQ,EACR,OAAO,GACR,EAAE;IACD,QAAQ,EAAE,SAAS,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;CACjB,+BAMA;AAED,wBAAgB,0BAA0B,YAEzC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
const ImmersiveXRPresentationContext = createContext(false);
|
|
5
|
+
export function ImmersiveXRPresentationProvider({ children, enabled, }) {
|
|
6
|
+
return (_jsx(ImmersiveXRPresentationContext.Provider, { value: enabled, children: children }));
|
|
7
|
+
}
|
|
8
|
+
export function useImmersiveXRPresentation() {
|
|
9
|
+
return useContext(ImmersiveXRPresentationContext);
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pascal-app/viewer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "3D viewer component for Pascal building editor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"prepublishOnly": "npm run build"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@pascal-app/core": "^1.0.
|
|
36
|
+
"@pascal-app/core": "^1.0.4",
|
|
37
37
|
"@react-three/drei": "^10",
|
|
38
38
|
"@react-three/fiber": "^9",
|
|
39
39
|
"react": "^18 || ^19",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"zustand": "^5"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@pascal-app/core": "^1.0.
|
|
49
|
+
"@pascal-app/core": "^1.0.4",
|
|
50
50
|
"@pascal/typescript-config": "*",
|
|
51
51
|
"@react-three/test-renderer": "^9.1.0",
|
|
52
52
|
"@types/node": "^22",
|
|
53
|
-
"@types/react": "
|
|
54
|
-
"@types/react-dom": "19.
|
|
53
|
+
"@types/react": "19.3.0",
|
|
54
|
+
"@types/react-dom": "19.3.0",
|
|
55
55
|
"@types/three": "^0.184.0",
|
|
56
56
|
"typescript": "6.0.3"
|
|
57
57
|
},
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
],
|
|
68
68
|
"repository": {
|
|
69
69
|
"type": "git",
|
|
70
|
-
"url": "https://github.com/pascalorg/editor.git",
|
|
70
|
+
"url": "git+https://github.com/pascalorg/editor.git",
|
|
71
71
|
"directory": "packages/viewer"
|
|
72
72
|
},
|
|
73
73
|
"license": "MIT",
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { type FenceNode } from '@pascal-app/core';
|
|
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>;
|
|
19
|
-
export declare function generateFenceGeometry(fence: FenceNode): THREE.BufferGeometry<THREE.NormalBufferAttributes, THREE.BufferGeometryEventMap>;
|
|
20
|
-
export declare const FenceSystem: () => null;
|
|
21
|
-
export {};
|
|
22
|
-
//# sourceMappingURL=fence-system.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
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,357 +0,0 @@
|
|
|
1
|
-
import { getFenceCenterlineFrameAt, getFenceCenterlineLength, sceneRegistry, useScene, } from '@pascal-app/core';
|
|
2
|
-
import { useFrame } from '@react-three/fiber';
|
|
3
|
-
import * as THREE from 'three';
|
|
4
|
-
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
5
|
-
const MIN_CURVE_SEGMENT_LENGTH = 0.18;
|
|
6
|
-
const HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH = 0.2;
|
|
7
|
-
function createFencePartGeometry(part) {
|
|
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);
|
|
14
|
-
geometry.scale(part.scale[0], part.scale[1], part.scale[2]);
|
|
15
|
-
if (part.rotationY) {
|
|
16
|
-
geometry.rotateY(part.rotationY);
|
|
17
|
-
}
|
|
18
|
-
geometry.translate(part.position[0], part.position[1], part.position[2]);
|
|
19
|
-
applyFenceUVs(geometry);
|
|
20
|
-
return geometry;
|
|
21
|
-
}
|
|
22
|
-
function getFencePointAt(fence, t) {
|
|
23
|
-
const frame = getFenceCenterlineFrameAt(fence, t);
|
|
24
|
-
return {
|
|
25
|
-
point: frame.point,
|
|
26
|
-
tangentAngle: Math.atan2(frame.tangent.y, frame.tangent.x),
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
function createFenceCurveBlockPart(fence, startT, endT, centerY, height, depth) {
|
|
30
|
-
if (endT - startT <= 1e-5)
|
|
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]);
|
|
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();
|
|
80
|
-
return {
|
|
81
|
-
geometry,
|
|
82
|
-
position: [0, 0, 0],
|
|
83
|
-
scale: [1, 1, 1],
|
|
84
|
-
};
|
|
85
|
-
}
|
|
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)));
|
|
89
|
-
const parts = [];
|
|
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);
|
|
96
|
-
}
|
|
97
|
-
return parts;
|
|
98
|
-
}
|
|
99
|
-
function applyFenceUVs(geometry) {
|
|
100
|
-
const position = geometry.getAttribute('position');
|
|
101
|
-
const normal = geometry.getAttribute('normal');
|
|
102
|
-
if (!(position && normal))
|
|
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).
|
|
110
|
-
const uvs = new Float32Array(position.count * 2);
|
|
111
|
-
for (let index = 0; index < position.count; index += 1) {
|
|
112
|
-
const px = position.getX(index);
|
|
113
|
-
const py = position.getY(index);
|
|
114
|
-
const pz = position.getZ(index);
|
|
115
|
-
const nx = Math.abs(normal.getX(index));
|
|
116
|
-
const ny = Math.abs(normal.getY(index));
|
|
117
|
-
const nz = Math.abs(normal.getZ(index));
|
|
118
|
-
let u = 0;
|
|
119
|
-
let v = 0;
|
|
120
|
-
if (ny >= nx && ny >= nz) {
|
|
121
|
-
u = px;
|
|
122
|
-
v = pz;
|
|
123
|
-
}
|
|
124
|
-
else if (nx >= nz) {
|
|
125
|
-
u = pz;
|
|
126
|
-
v = py;
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
u = px;
|
|
130
|
-
v = py;
|
|
131
|
-
}
|
|
132
|
-
uvs[index * 2] = u;
|
|
133
|
-
uvs[index * 2 + 1] = v;
|
|
134
|
-
}
|
|
135
|
-
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
|
136
|
-
geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(uvs.slice(), 2));
|
|
137
|
-
}
|
|
138
|
-
function getStyleDefaults(style) {
|
|
139
|
-
if (style === 'privacy') {
|
|
140
|
-
return { spacingFactor: 0.42, postFactor: 1.35, baseFactor: 1.2, topFactor: 1.2 };
|
|
141
|
-
}
|
|
142
|
-
if (style === 'rail') {
|
|
143
|
-
return { spacingFactor: 0.68, postFactor: 0.8, baseFactor: 0.85, topFactor: 0.85 };
|
|
144
|
-
}
|
|
145
|
-
return { spacingFactor: 0.3, postFactor: 0.55, baseFactor: 1, topFactor: 0.75 };
|
|
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
|
-
}
|
|
237
|
-
function createFenceParts(fence) {
|
|
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);
|
|
245
|
-
const panelDepth = Math.max(fence.thickness, 0.03);
|
|
246
|
-
const clearance = Math.max(fence.groundClearance, 0);
|
|
247
|
-
const styleDefaults = getStyleDefaults(fence.style);
|
|
248
|
-
const baseHeight = Math.max(fence.baseHeight * styleDefaults.baseFactor, 0.04);
|
|
249
|
-
const topRailHeight = Math.max(fence.topRailHeight * styleDefaults.topFactor, 0.01);
|
|
250
|
-
const verticalHeight = Math.max(fence.height - baseHeight - topRailHeight, 0.08);
|
|
251
|
-
const postWidth = Math.max(fence.postSize * styleDefaults.postFactor, 0.01);
|
|
252
|
-
const spacing = Math.max(fence.postSpacing * styleDefaults.spacingFactor, postWidth * 1.2);
|
|
253
|
-
const edgeInset = Math.max(fence.edgeInset ?? 0.015, 0.005);
|
|
254
|
-
const isFloating = fence.baseStyle === 'floating';
|
|
255
|
-
const showInfill = fence.showInfill ?? true;
|
|
256
|
-
const baseY = isFloating ? clearance : 0;
|
|
257
|
-
const effectiveBaseHeight = baseHeight;
|
|
258
|
-
const startInsetT = Math.min(0.499, edgeInset / length);
|
|
259
|
-
const endInsetT = Math.max(0.501, 1 - edgeInset / length);
|
|
260
|
-
if (!isFloating) {
|
|
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));
|
|
263
|
-
}
|
|
264
|
-
const count = showInfill ? Math.max(2, Math.floor((length - edgeInset * 2) / spacing) + 1) : 2;
|
|
265
|
-
const verticalY = baseY + effectiveBaseHeight + verticalHeight / 2;
|
|
266
|
-
for (let index = 0; index < count; index += 1) {
|
|
267
|
-
const t = count === 1 ? 0.5 : startInsetT + (endInsetT - startInsetT) * (index / (count - 1));
|
|
268
|
-
const isEdgePost = index === 0 || index === count - 1;
|
|
269
|
-
const fullHeightPost = !showInfill || (isFloating && isEdgePost);
|
|
270
|
-
const postHeight = fullHeightPost
|
|
271
|
-
? effectiveBaseHeight + verticalHeight + topRailHeight + clearance
|
|
272
|
-
: verticalHeight;
|
|
273
|
-
const postY = fullHeightPost ? postHeight / 2 : verticalY;
|
|
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
|
-
}
|
|
284
|
-
}
|
|
285
|
-
rail.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight + verticalHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
|
|
286
|
-
if (isFloating) {
|
|
287
|
-
rail.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
|
|
288
|
-
}
|
|
289
|
-
return { posts, infill, base, rail };
|
|
290
|
-
}
|
|
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();
|
|
297
|
-
const geometries = parts.map(createFencePartGeometry);
|
|
298
|
-
const merged = mergeGeometries(geometries, false) ?? new THREE.BufferGeometry();
|
|
299
|
-
geometries.forEach((geometry) => {
|
|
300
|
-
geometry.dispose();
|
|
301
|
-
});
|
|
302
|
-
const mergedUv = merged.getAttribute('uv');
|
|
303
|
-
if (mergedUv) {
|
|
304
|
-
merged.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(mergedUv.array), 2));
|
|
305
|
-
}
|
|
306
|
-
merged.computeVertexNormals();
|
|
307
|
-
return merged;
|
|
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
|
-
}
|
|
328
|
-
function updateFenceGeometry(fenceId) {
|
|
329
|
-
const node = useScene.getState().nodes[fenceId];
|
|
330
|
-
if (node?.type !== 'fence')
|
|
331
|
-
return;
|
|
332
|
-
const mesh = sceneRegistry.nodes.get(fenceId);
|
|
333
|
-
if (!mesh)
|
|
334
|
-
return;
|
|
335
|
-
const newGeometry = generateFenceGeometry(node);
|
|
336
|
-
mesh.geometry.dispose();
|
|
337
|
-
mesh.geometry = newGeometry;
|
|
338
|
-
mesh.position.set(0, 0, 0);
|
|
339
|
-
mesh.rotation.set(0, 0, 0);
|
|
340
|
-
}
|
|
341
|
-
export const FenceSystem = () => {
|
|
342
|
-
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
343
|
-
const clearDirty = useScene((state) => state.clearDirty);
|
|
344
|
-
useFrame(() => {
|
|
345
|
-
if (dirtyNodes.size === 0)
|
|
346
|
-
return;
|
|
347
|
-
const nodes = useScene.getState().nodes;
|
|
348
|
-
dirtyNodes.forEach((id) => {
|
|
349
|
-
const node = nodes[id];
|
|
350
|
-
if (node?.type !== 'fence')
|
|
351
|
-
return;
|
|
352
|
-
updateFenceGeometry(id);
|
|
353
|
-
clearDirty(id);
|
|
354
|
-
});
|
|
355
|
-
}, 4);
|
|
356
|
-
return null;
|
|
357
|
-
};
|