@pascal-app/viewer 1.0.3 → 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 +14 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +84 -26
- 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 +4 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +11 -1
- package/dist/index.d.ts +22 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -14
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +7 -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/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 +55 -17
- 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 +33 -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 +3 -5
- package/dist/systems/level/level-utils.d.ts +7 -4
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +17 -6
- 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 +7 -0
- package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout-cache.js +46 -17
- package/dist/systems/wall/wall-cutout.d.ts +6 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +42 -28
- 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 +9 -4
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +99 -152
- package/dist/systems/wall/wall-system.d.ts +28 -2
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +582 -143
- 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 +41 -22
- package/package.json +5 -5
- 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,9 +129,12 @@ 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
|
|
@@ -192,16 +196,19 @@ function windowSlotDefault(slotId) {
|
|
|
192
196
|
getBaseMaterial(currentShading));
|
|
193
197
|
}
|
|
194
198
|
// Resolve a window's slot to a material: the `node.slots` override (colored mode
|
|
195
|
-
// only) → the
|
|
196
|
-
// 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.
|
|
197
201
|
function resolveWindowSlotMaterial(node, slotId) {
|
|
198
202
|
const fallback = windowSlotDefault(slotId);
|
|
199
203
|
if (!currentTextures)
|
|
200
204
|
return fallback;
|
|
201
205
|
const ref = node.slots?.[slotId];
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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;
|
|
205
212
|
}
|
|
206
213
|
function addBox(parent, material, w, h, d, x, y, z) {
|
|
207
214
|
const geometry = new THREE.BoxGeometry(w, h, d);
|
|
@@ -1526,8 +1533,19 @@ function updateWindowMesh(node, mesh) {
|
|
|
1526
1533
|
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
|
|
1527
1534
|
mesh.material = hitboxMaterial;
|
|
1528
1535
|
// Sync transform from node (React may lag behind the system by a frame during drag)
|
|
1529
|
-
|
|
1530
|
-
|
|
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
|
+
}
|
|
1531
1549
|
// Dispose and remove all old visual children; preserve 'cutout'
|
|
1532
1550
|
for (const child of [...mesh.children]) {
|
|
1533
1551
|
if (child.name === 'cutout')
|
|
@@ -1543,62 +1561,62 @@ function updateWindowMesh(node, mesh) {
|
|
|
1543
1561
|
const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill, sillDepth, sillThickness, openingKind, openingShape, windowType, } = node;
|
|
1544
1562
|
if (openingKind === 'opening') {
|
|
1545
1563
|
syncWindowCutout(node, mesh);
|
|
1546
|
-
return;
|
|
1564
|
+
return true;
|
|
1547
1565
|
}
|
|
1548
1566
|
if (windowType === 'sliding') {
|
|
1549
1567
|
addSlidingWindowVisuals(node, mesh);
|
|
1550
1568
|
syncWindowCutout(node, mesh);
|
|
1551
|
-
return;
|
|
1569
|
+
return true;
|
|
1552
1570
|
}
|
|
1553
1571
|
if (windowType === 'casement') {
|
|
1554
1572
|
addCasementWindowVisuals(node, mesh);
|
|
1555
1573
|
syncWindowCutout(node, mesh);
|
|
1556
|
-
return;
|
|
1574
|
+
return true;
|
|
1557
1575
|
}
|
|
1558
1576
|
if (windowType === 'awning') {
|
|
1559
1577
|
addAwningWindowVisuals(node, mesh);
|
|
1560
1578
|
syncWindowCutout(node, mesh);
|
|
1561
|
-
return;
|
|
1579
|
+
return true;
|
|
1562
1580
|
}
|
|
1563
1581
|
if (windowType === 'hopper') {
|
|
1564
1582
|
addAwningWindowVisuals(node, mesh);
|
|
1565
1583
|
syncWindowCutout(node, mesh);
|
|
1566
|
-
return;
|
|
1584
|
+
return true;
|
|
1567
1585
|
}
|
|
1568
1586
|
if (windowType === 'single-hung') {
|
|
1569
1587
|
addSingleHungWindowVisuals(node, mesh);
|
|
1570
1588
|
syncWindowCutout(node, mesh);
|
|
1571
|
-
return;
|
|
1589
|
+
return true;
|
|
1572
1590
|
}
|
|
1573
1591
|
if (windowType === 'double-hung') {
|
|
1574
1592
|
addDoubleHungWindowVisuals(node, mesh);
|
|
1575
1593
|
syncWindowCutout(node, mesh);
|
|
1576
|
-
return;
|
|
1594
|
+
return true;
|
|
1577
1595
|
}
|
|
1578
1596
|
if (windowType === 'bay') {
|
|
1579
1597
|
addBayWindowVisuals(node, mesh);
|
|
1580
1598
|
syncWindowCutout(node, mesh);
|
|
1581
|
-
return;
|
|
1599
|
+
return true;
|
|
1582
1600
|
}
|
|
1583
1601
|
if (windowType === 'bow') {
|
|
1584
1602
|
addBowWindowVisuals(node, mesh);
|
|
1585
1603
|
syncWindowCutout(node, mesh);
|
|
1586
|
-
return;
|
|
1604
|
+
return true;
|
|
1587
1605
|
}
|
|
1588
1606
|
if (windowType === 'louvered') {
|
|
1589
1607
|
addLouveredWindowVisuals(node, mesh);
|
|
1590
1608
|
syncWindowCutout(node, mesh);
|
|
1591
|
-
return;
|
|
1609
|
+
return true;
|
|
1592
1610
|
}
|
|
1593
1611
|
if (openingShape === 'arch') {
|
|
1594
1612
|
addArchedWindowVisuals(node, mesh);
|
|
1595
1613
|
syncWindowCutout(node, mesh);
|
|
1596
|
-
return;
|
|
1614
|
+
return true;
|
|
1597
1615
|
}
|
|
1598
1616
|
if (openingShape === 'rounded') {
|
|
1599
1617
|
addRoundedWindowVisuals(node, mesh);
|
|
1600
1618
|
syncWindowCutout(node, mesh);
|
|
1601
|
-
return;
|
|
1619
|
+
return true;
|
|
1602
1620
|
}
|
|
1603
1621
|
const innerW = width - 2 * frameThickness;
|
|
1604
1622
|
const innerH = height - 2 * frameThickness;
|
|
@@ -1673,6 +1691,7 @@ function updateWindowMesh(node, mesh) {
|
|
|
1673
1691
|
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
1674
1692
|
}
|
|
1675
1693
|
syncWindowCutout(node, mesh);
|
|
1694
|
+
return true;
|
|
1676
1695
|
}
|
|
1677
1696
|
function syncWindowCutout(node, mesh) {
|
|
1678
1697
|
// ── Cutout: invisible raycast hit target for the whole opening ──
|
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
|
},
|
|
@@ -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
|
-
};
|