@pascal-app/nodes 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/block/definition.d.ts.map +1 -1
- package/dist/block/definition.js +18 -3
- package/dist/block/floorplan.d.ts +11 -1
- package/dist/block/floorplan.d.ts.map +1 -1
- package/dist/block/floorplan.js +81 -13
- package/dist/block/geometry.d.ts +2 -2
- package/dist/block/geometry.d.ts.map +1 -1
- package/dist/block/geometry.js +39 -24
- package/dist/block/hosted-edit.d.ts.map +1 -1
- package/dist/block/hosted-edit.js +25 -0
- package/dist/block/material-slots.d.ts +1 -3
- package/dist/block/material-slots.d.ts.map +1 -1
- package/dist/block/material-slots.js +1 -11
- package/dist/block/paint.d.ts.map +1 -1
- package/dist/block/paint.js +4 -10
- package/dist/block/panel.d.ts.map +1 -1
- package/dist/block/panel.js +8 -3
- package/dist/block/selection.d.ts.map +1 -1
- package/dist/block/selection.js +2 -2
- package/dist/box-vent/definition.d.ts.map +1 -1
- package/dist/box-vent/definition.js +2 -5
- package/dist/box-vent/panel.d.ts.map +1 -1
- package/dist/box-vent/panel.js +5 -28
- package/dist/cabinet/countertop-layout.d.ts +13 -2
- package/dist/cabinet/countertop-layout.d.ts.map +1 -1
- package/dist/cabinet/definition.d.ts.map +1 -1
- package/dist/cabinet/definition.js +1 -2
- package/dist/cabinet/floorplan.d.ts.map +1 -1
- package/dist/cabinet/floorplan.js +125 -28
- package/dist/cabinet/geometry/shared.d.ts +1 -2
- package/dist/cabinet/geometry/shared.d.ts.map +1 -1
- package/dist/cabinet/geometry/shared.js +1 -2
- package/dist/cabinet/run-layout.d.ts.map +1 -1
- package/dist/cabinet/run-layout.js +8 -5
- package/dist/cabinet/run-ops.d.ts +13 -2
- package/dist/cabinet/run-ops.d.ts.map +1 -1
- package/dist/cabinet/tool.d.ts.map +1 -1
- package/dist/cabinet/tool.js +22 -24
- package/dist/cabinet/wall-snap.d.ts.map +1 -1
- package/dist/cabinet/wall-snap.js +8 -8
- package/dist/ceiling/boundary-editor.d.ts.map +1 -1
- package/dist/ceiling/boundary-editor.js +5 -3
- package/dist/ceiling/definition.d.ts +4 -4
- package/dist/ceiling/definition.d.ts.map +1 -1
- package/dist/ceiling/definition.js +15 -15
- package/dist/ceiling/floorplan-affordances.d.ts +84 -4
- package/dist/ceiling/floorplan-affordances.d.ts.map +1 -1
- package/dist/ceiling/floorplan-affordances.js +0 -1
- package/dist/ceiling/floorplan-move.d.ts.map +1 -1
- package/dist/ceiling/floorplan-move.js +0 -1
- package/dist/ceiling/floorplan.d.ts +1 -1
- package/dist/ceiling/floorplan.d.ts.map +1 -1
- package/dist/ceiling/floorplan.js +2 -1
- package/dist/ceiling/height-bounds.d.ts +19 -0
- package/dist/ceiling/height-bounds.d.ts.map +1 -0
- package/dist/ceiling/height-bounds.js +24 -0
- package/dist/ceiling/hole-editor.d.ts.map +1 -1
- package/dist/ceiling/hole-editor.js +3 -4
- package/dist/ceiling/materials.d.ts +1 -1
- package/dist/ceiling/materials.d.ts.map +1 -1
- package/dist/ceiling/materials.js +10 -3
- package/dist/ceiling/paint.d.ts +2 -8
- package/dist/ceiling/paint.d.ts.map +1 -1
- package/dist/ceiling/paint.js +121 -24
- package/dist/ceiling/panel.d.ts.map +1 -1
- package/dist/ceiling/panel.js +40 -23
- package/dist/ceiling/renderer.d.ts.map +1 -1
- package/dist/ceiling/renderer.js +41 -5
- package/dist/chimney/move-tool.d.ts.map +1 -1
- package/dist/chimney/move-tool.js +8 -2
- package/dist/chimney/paint.d.ts.map +1 -1
- package/dist/chimney/paint.js +3 -10
- package/dist/column/definition.d.ts.map +1 -1
- package/dist/column/definition.js +24 -2
- package/dist/column/floorplan-affordances.d.ts.map +1 -1
- package/dist/column/floorplan-affordances.js +5 -0
- package/dist/column/floorplan.d.ts +1 -1
- package/dist/column/floorplan.d.ts.map +1 -1
- package/dist/column/floorplan.js +47 -2
- package/dist/column/hosted-resize.d.ts +101 -2
- package/dist/column/hosted-resize.d.ts.map +1 -1
- package/dist/column/panel.d.ts.map +1 -1
- package/dist/column/panel.js +147 -146
- package/dist/column/renderer.d.ts.map +1 -1
- package/dist/column/renderer.js +6 -4
- package/dist/column/surface.d.ts.map +1 -1
- package/dist/column/surface.js +7 -0
- package/dist/construction-dimension/floorplan.js +7 -3
- package/dist/cupola/definition.d.ts.map +1 -1
- package/dist/cupola/definition.js +2 -8
- package/dist/cupola/panel.d.ts.map +1 -1
- package/dist/cupola/panel.js +5 -23
- package/dist/door/definition.d.ts.map +1 -1
- package/dist/door/definition.js +12 -3
- package/dist/door/door-math.d.ts +5 -10
- package/dist/door/door-math.d.ts.map +1 -1
- package/dist/door/door-math.js +14 -15
- package/dist/door/floorplan-move.d.ts.map +1 -1
- package/dist/door/floorplan-move.js +3 -1
- package/dist/door/floorplan.d.ts.map +1 -1
- package/dist/door/floorplan.js +55 -21
- package/dist/door/mechanism.d.ts +4 -0
- package/dist/door/mechanism.d.ts.map +1 -0
- package/dist/door/mechanism.js +24 -0
- package/dist/door/move-tool.d.ts.map +1 -1
- package/dist/door/move-tool.js +72 -48
- package/dist/door/paint.d.ts.map +1 -1
- package/dist/door/paint.js +3 -0
- package/dist/door/panel.d.ts.map +1 -1
- package/dist/door/panel.js +34 -259
- package/dist/door/placement.d.ts +45 -0
- package/dist/door/placement.d.ts.map +1 -0
- package/dist/door/placement.js +51 -0
- package/dist/door/preview.d.ts.map +1 -1
- package/dist/door/preview.js +2 -0
- package/dist/door/renderer.d.ts.map +1 -1
- package/dist/door/renderer.js +3 -2
- package/dist/door/tool.d.ts.map +1 -1
- package/dist/door/tool.js +92 -21
- package/dist/dormer/csg-geometry.d.ts.map +1 -1
- package/dist/dormer/csg-geometry.js +2 -0
- package/dist/dormer/paint.d.ts.map +1 -1
- package/dist/dormer/paint.js +2 -5
- package/dist/dormer/panel.d.ts.map +1 -1
- package/dist/dormer/panel.js +4 -14
- package/dist/downspout/definition.d.ts.map +1 -1
- package/dist/downspout/definition.js +2 -2
- package/dist/duct-fitting/definition.d.ts.map +1 -1
- package/dist/duct-fitting/definition.js +2 -1
- package/dist/duct-fitting/geometry.d.ts.map +1 -1
- package/dist/duct-fitting/geometry.js +1 -1
- package/dist/duct-fitting/move-tool.d.ts.map +1 -1
- package/dist/duct-fitting/move-tool.js +8 -2
- package/dist/duct-segment/definition.d.ts.map +1 -1
- package/dist/duct-segment/definition.js +2 -2
- package/dist/duct-segment/geometry.d.ts.map +1 -1
- package/dist/duct-segment/geometry.js +1 -1
- package/dist/duct-segment/move-tool.d.ts.map +1 -1
- package/dist/duct-segment/move-tool.js +8 -2
- package/dist/elevator/definition.d.ts.map +1 -1
- package/dist/elevator/definition.js +1 -2
- package/dist/elevator/panel.d.ts.map +1 -1
- package/dist/elevator/panel.js +5 -27
- package/dist/elevator/parametrics.js +1 -1
- package/dist/elevator/renderer.d.ts.map +1 -1
- package/dist/elevator/renderer.js +3 -4
- package/dist/eyebrow-vent/definition.d.ts.map +1 -1
- package/dist/eyebrow-vent/definition.js +2 -5
- package/dist/eyebrow-vent/paint.d.ts.map +1 -1
- package/dist/eyebrow-vent/paint.js +3 -6
- package/dist/eyebrow-vent/panel.d.ts.map +1 -1
- package/dist/eyebrow-vent/panel.js +5 -23
- package/dist/fence/actions/move-endpoint.d.ts +1 -1
- package/dist/fence/actions/move-endpoint.d.ts.map +1 -1
- package/dist/fence/actions/move-endpoint.js +2 -1
- package/dist/fence/definition.d.ts +2 -1
- package/dist/fence/definition.d.ts.map +1 -1
- package/dist/fence/definition.js +83 -28
- package/dist/fence/drafting.d.ts +36 -0
- package/dist/fence/drafting.d.ts.map +1 -0
- package/dist/fence/drafting.js +210 -0
- package/dist/fence/feature-tool.d.ts +4 -0
- package/dist/fence/feature-tool.d.ts.map +1 -0
- package/dist/fence/feature-tool.js +140 -0
- package/dist/fence/features.d.ts +2 -0
- package/dist/fence/features.d.ts.map +1 -0
- package/dist/fence/features.js +12 -0
- package/dist/fence/floorplan-affordances.d.ts.map +1 -1
- package/dist/fence/floorplan-affordances.js +2 -1
- package/dist/fence/floorplan.d.ts +2 -3
- package/dist/fence/floorplan.d.ts.map +1 -1
- package/dist/fence/floorplan.js +131 -71
- package/dist/fence/geometry-parts.d.ts +50 -0
- package/dist/fence/geometry-parts.d.ts.map +1 -0
- package/dist/fence/geometry-parts.js +1144 -0
- package/dist/fence/geometry.d.ts +2 -3
- package/dist/fence/geometry.d.ts.map +1 -1
- package/dist/fence/geometry.js +84 -7
- package/dist/fence/index.d.ts +1 -0
- package/dist/fence/index.d.ts.map +1 -1
- package/dist/fence/index.js +1 -0
- package/dist/fence/inspector-editors.d.ts +15 -0
- package/dist/fence/inspector-editors.d.ts.map +1 -1
- package/dist/fence/inspector-editors.js +128 -3
- package/dist/fence/lift.d.ts.map +1 -1
- package/dist/fence/lift.js +5 -2
- package/dist/fence/move-tool.js +2 -1
- package/dist/fence/panel-model.d.ts +4 -0
- package/dist/fence/panel-model.d.ts.map +1 -0
- package/dist/fence/panel-model.js +12 -0
- package/dist/fence/parametrics.d.ts +0 -12
- package/dist/fence/parametrics.d.ts.map +1 -1
- package/dist/fence/parametrics.js +185 -18
- package/dist/fence/system.d.ts.map +1 -1
- package/dist/fence/system.js +141 -31
- package/dist/fence/tool.d.ts.map +1 -1
- package/dist/fence/tool.js +256 -47
- package/dist/fence-feature/actions.d.ts +7 -0
- package/dist/fence-feature/actions.d.ts.map +1 -0
- package/dist/fence-feature/actions.js +26 -0
- package/dist/fence-feature/definition.d.ts +4 -0
- package/dist/fence-feature/definition.d.ts.map +1 -0
- package/dist/fence-feature/definition.js +112 -0
- package/dist/fence-feature/floorplan-affordances.d.ts +3 -0
- package/dist/fence-feature/floorplan-affordances.d.ts.map +1 -0
- package/dist/fence-feature/floorplan-affordances.js +37 -0
- package/dist/fence-feature/floorplan.d.ts +4 -0
- package/dist/fence-feature/floorplan.d.ts.map +1 -0
- package/dist/fence-feature/floorplan.js +120 -0
- package/dist/fence-feature/geometry.d.ts +5 -0
- package/dist/fence-feature/geometry.d.ts.map +1 -0
- package/dist/fence-feature/geometry.js +43 -0
- package/dist/fence-feature/handles.d.ts +5 -0
- package/dist/fence-feature/handles.d.ts.map +1 -0
- package/dist/fence-feature/handles.js +84 -0
- package/dist/fence-feature/inspector.d.ts +6 -0
- package/dist/fence-feature/inspector.d.ts.map +1 -0
- package/dist/fence-feature/inspector.js +214 -0
- package/dist/fence-feature/interaction.d.ts +3 -0
- package/dist/fence-feature/interaction.d.ts.map +1 -0
- package/dist/fence-feature/interaction.js +41 -0
- package/dist/fence-feature/move-session.d.ts +9 -0
- package/dist/fence-feature/move-session.d.ts.map +1 -0
- package/dist/fence-feature/move-session.js +48 -0
- package/dist/fence-feature/move-tool.d.ts +5 -0
- package/dist/fence-feature/move-tool.d.ts.map +1 -0
- package/dist/fence-feature/move-tool.js +58 -0
- package/dist/fence-feature/pick-target.d.ts +7 -0
- package/dist/fence-feature/pick-target.d.ts.map +1 -0
- package/dist/fence-feature/pick-target.js +52 -0
- package/dist/floor-opening/definition.d.ts +3 -0
- package/dist/floor-opening/definition.d.ts.map +1 -0
- package/dist/floor-opening/definition.js +41 -0
- package/dist/floor-opening/floorplan.d.ts +156 -0
- package/dist/floor-opening/floorplan.d.ts.map +1 -0
- package/dist/floor-opening/floorplan.js +63 -0
- package/dist/gutter/definition.d.ts.map +1 -1
- package/dist/gutter/definition.js +2 -2
- package/dist/imported-mesh/definition.d.ts.map +1 -1
- package/dist/imported-mesh/definition.js +2 -0
- package/dist/imported-mesh/geometry.d.ts +1 -1
- package/dist/imported-mesh/geometry.d.ts.map +1 -1
- package/dist/imported-mesh/geometry.js +40 -12
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -2
- package/dist/item/authored-face-host.d.ts +10 -0
- package/dist/item/authored-face-host.d.ts.map +1 -0
- package/dist/item/authored-face-host.js +87 -0
- package/dist/item/authored-params.d.ts +15 -0
- package/dist/item/authored-params.d.ts.map +1 -0
- package/dist/item/authored-params.js +72 -0
- package/dist/item/definition.d.ts +2 -2
- package/dist/item/definition.d.ts.map +1 -1
- package/dist/item/definition.js +33 -4
- package/dist/item/floorplan.d.ts +1 -1
- package/dist/item/floorplan.d.ts.map +1 -1
- package/dist/item/floorplan.js +35 -5
- package/dist/item/index.d.ts +1 -0
- package/dist/item/index.d.ts.map +1 -1
- package/dist/item/index.js +1 -0
- package/dist/item/model-loader.d.ts +2 -0
- package/dist/item/model-loader.d.ts.map +1 -1
- package/dist/item/model-loader.js +55 -23
- package/dist/item/move-tool.js +4 -4
- package/dist/item/paint.d.ts.map +1 -1
- package/dist/item/paint.js +5 -24
- package/dist/item/panel.d.ts +1 -3
- package/dist/item/panel.d.ts.map +1 -1
- package/dist/item/panel.js +20 -24
- package/dist/item/placement-notice.d.ts +14 -0
- package/dist/item/placement-notice.d.ts.map +1 -0
- package/dist/item/placement-notice.js +38 -0
- package/dist/item/plan-symbols.d.ts +67 -0
- package/dist/item/plan-symbols.d.ts.map +1 -0
- package/dist/item/plan-symbols.js +407 -0
- package/dist/item/renderer.d.ts +10 -0
- package/dist/item/renderer.d.ts.map +1 -1
- package/dist/item/renderer.js +64 -38
- package/dist/item/script-param-defaults.d.ts +12 -0
- package/dist/item/script-param-defaults.d.ts.map +1 -0
- package/dist/item/script-param-defaults.js +34 -0
- package/dist/lean-to-extension/definition.d.ts.map +1 -1
- package/dist/lean-to-extension/definition.js +1 -2
- package/dist/lean-to-extension/geometry.d.ts.map +1 -1
- package/dist/lean-to-extension/geometry.js +1 -1
- package/dist/lean-to-extension/layout.d.ts.map +1 -1
- package/dist/lean-to-extension/layout.js +2 -3
- package/dist/lean-to-extension/placement-validation.d.ts.map +1 -1
- package/dist/lean-to-extension/placement-validation.js +5 -5
- package/dist/lean-to-extension/tool.d.ts.map +1 -1
- package/dist/lean-to-extension/tool.js +5 -5
- package/dist/lineset/move-tool.d.ts.map +1 -1
- package/dist/lineset/move-tool.js +8 -2
- package/dist/liquid-line/move-tool.d.ts.map +1 -1
- package/dist/liquid-line/move-tool.js +8 -2
- package/dist/measurement/definition.js +1 -1
- package/dist/pipe-fitting/move-tool.d.ts.map +1 -1
- package/dist/pipe-fitting/move-tool.js +8 -2
- package/dist/pipe-segment/move-tool.d.ts.map +1 -1
- package/dist/pipe-segment/move-tool.js +8 -2
- package/dist/procedural-item/animation.d.ts +8 -0
- package/dist/procedural-item/animation.d.ts.map +1 -0
- package/dist/procedural-item/animation.js +116 -0
- package/dist/procedural-item/definition.d.ts.map +1 -1
- package/dist/procedural-item/definition.js +74 -8
- package/dist/procedural-item/geometry.d.ts +5 -1
- package/dist/procedural-item/geometry.d.ts.map +1 -1
- package/dist/procedural-item/geometry.js +239 -15
- package/dist/procedural-item/renderer.d.ts.map +1 -1
- package/dist/procedural-item/renderer.js +272 -23
- package/dist/ridge-vent/panel.d.ts.map +1 -1
- package/dist/ridge-vent/panel.js +5 -23
- package/dist/ridge-vent/renderer.d.ts.map +1 -1
- package/dist/ridge-vent/renderer.js +9 -8
- package/dist/roof/definition.d.ts.map +1 -1
- package/dist/roof/definition.js +4 -1
- package/dist/roof/floorplan.d.ts +8 -0
- package/dist/roof/floorplan.d.ts.map +1 -1
- package/dist/roof/floorplan.js +194 -18
- package/dist/roof/panel-model.d.ts.map +1 -1
- package/dist/roof/panel-model.js +10 -6
- package/dist/roof/panel.d.ts.map +1 -1
- package/dist/roof/panel.js +3 -10
- package/dist/roof/renderer.d.ts.map +1 -1
- package/dist/roof/renderer.js +5 -2
- package/dist/roof-segment/panel.d.ts.map +1 -1
- package/dist/roof-segment/panel.js +5 -24
- package/dist/roof-segment/renderer.d.ts.map +1 -1
- package/dist/roof-segment/renderer.js +17 -8
- package/dist/separator/definition.d.ts +3 -0
- package/dist/separator/definition.d.ts.map +1 -0
- package/dist/separator/definition.js +39 -0
- package/dist/separator/renderer.d.ts +5 -0
- package/dist/separator/renderer.d.ts.map +1 -0
- package/dist/separator/renderer.js +36 -0
- package/dist/shared/duct-body-paint.d.ts +0 -4
- package/dist/shared/duct-body-paint.d.ts.map +1 -1
- package/dist/shared/duct-body-paint.js +1 -5
- package/dist/shared/item-interaction-actions.d.ts +6 -0
- package/dist/shared/item-interaction-actions.d.ts.map +1 -0
- package/dist/shared/item-interaction-actions.js +27 -0
- package/dist/shared/item-interactions.d.ts +9 -0
- package/dist/shared/item-interactions.d.ts.map +1 -0
- package/dist/shared/item-interactions.js +74 -0
- package/dist/shared/item-light-placement.d.ts +2 -0
- package/dist/shared/item-light-placement.d.ts.map +1 -0
- package/dist/shared/item-light-placement.js +3 -0
- package/dist/shared/move-roof-tool.js +2 -1
- package/dist/shared/node-batch/batchable.d.ts +12 -0
- package/dist/shared/node-batch/batchable.d.ts.map +1 -0
- package/dist/shared/node-batch/batchable.js +41 -0
- package/dist/shared/node-batch/candidates.d.ts +7 -5
- package/dist/shared/node-batch/candidates.d.ts.map +1 -1
- package/dist/shared/node-batch/candidates.js +43 -59
- package/dist/shared/node-batch/release.d.ts +8 -0
- package/dist/shared/node-batch/release.d.ts.map +1 -0
- package/dist/shared/node-batch/release.js +16 -0
- package/dist/shared/node-batch/system.d.ts.map +1 -1
- package/dist/shared/node-batch/system.js +17 -12
- package/dist/shared/node-batch/types.d.ts +4 -1
- package/dist/shared/node-batch/types.d.ts.map +1 -1
- package/dist/shared/opening-documentation.d.ts +13 -1
- package/dist/shared/opening-documentation.d.ts.map +1 -1
- package/dist/shared/opening-documentation.js +245 -59
- package/dist/shared/opening-guides-runtime.d.ts.map +1 -1
- package/dist/shared/opening-guides-runtime.js +17 -14
- package/dist/shared/opening-placement-dimensions.d.ts.map +1 -1
- package/dist/shared/opening-placement-dimensions.js +3 -3
- package/dist/shared/opening-plane-offset.d.ts +23 -0
- package/dist/shared/opening-plane-offset.d.ts.map +1 -0
- package/dist/shared/opening-plane-offset.js +24 -0
- package/dist/shared/polygon-centroid-move.d.ts.map +1 -1
- package/dist/shared/polygon-centroid-move.js +4 -1
- package/dist/shared/polygon-vertex-affordance.d.ts.map +1 -1
- package/dist/shared/polygon-vertex-affordance.js +19 -15
- package/dist/shared/run-hangers.d.ts.map +1 -1
- package/dist/shared/run-hangers.js +5 -4
- package/dist/shared/scripted-opening-handles.d.ts +18 -0
- package/dist/shared/scripted-opening-handles.d.ts.map +1 -0
- package/dist/shared/scripted-opening-handles.js +53 -0
- package/dist/shared/scripted-opening.d.ts +25 -0
- package/dist/shared/scripted-opening.d.ts.map +1 -0
- package/dist/shared/scripted-opening.js +72 -0
- package/dist/shared/slot-paint.d.ts +14 -2
- package/dist/shared/slot-paint.d.ts.map +1 -1
- package/dist/shared/slot-paint.js +29 -29
- package/dist/shared/surface-paint.d.ts.map +1 -1
- package/dist/shared/surface-paint.js +2 -5
- package/dist/shared/swap-preview-material.d.ts +11 -0
- package/dist/shared/swap-preview-material.d.ts.map +1 -0
- package/dist/shared/swap-preview-material.js +16 -0
- package/dist/shared/system-checks.d.ts.map +1 -1
- package/dist/shared/system-checks.js +5 -3
- package/dist/shared/wall-attach-target.d.ts +3 -10
- package/dist/shared/wall-attach-target.d.ts.map +1 -1
- package/dist/shared/wall-attach-target.js +3 -63
- package/dist/shared/wall-opening-ceiling.d.ts +5 -15
- package/dist/shared/wall-opening-ceiling.d.ts.map +1 -1
- package/dist/shared/wall-opening-ceiling.js +10 -18
- package/dist/shelf/definition.d.ts.map +1 -1
- package/dist/shelf/definition.js +1 -1
- package/dist/shelf/geometry.d.ts.map +1 -1
- package/dist/shelf/geometry.js +1 -2
- package/dist/shelf/paint.d.ts.map +1 -1
- package/dist/shelf/paint.js +4 -10
- package/dist/site/line-ribbon.d.ts +31 -0
- package/dist/site/line-ribbon.d.ts.map +1 -0
- package/dist/site/line-ribbon.js +117 -0
- package/dist/site/lot-mask.d.ts +18 -0
- package/dist/site/lot-mask.d.ts.map +1 -0
- package/dist/site/lot-mask.js +35 -0
- package/dist/site/panel.d.ts +10 -0
- package/dist/site/panel.d.ts.map +1 -0
- package/dist/site/panel.js +155 -0
- package/dist/site/parametrics.d.ts +7 -0
- package/dist/site/parametrics.d.ts.map +1 -1
- package/dist/site/parametrics.js +8 -0
- package/dist/site/recessed-slab-ground-holes.d.ts.map +1 -1
- package/dist/site/recessed-slab-ground-holes.js +5 -2
- package/dist/site/renderer.d.ts.map +1 -1
- package/dist/site/renderer.js +150 -19
- package/dist/site/setbacks.d.ts +33 -0
- package/dist/site/setbacks.d.ts.map +1 -0
- package/dist/site/setbacks.js +204 -0
- package/dist/site/terrain-geometry.d.ts +28 -1
- package/dist/site/terrain-geometry.d.ts.map +1 -1
- package/dist/site/terrain-geometry.js +78 -4
- package/dist/site/terrain-holes.d.ts +15 -0
- package/dist/site/terrain-holes.d.ts.map +1 -0
- package/dist/site/terrain-holes.js +74 -0
- package/dist/site/terrain-mesh.d.ts +9 -2
- package/dist/site/terrain-mesh.d.ts.map +1 -1
- package/dist/site/terrain-mesh.js +36 -9
- package/dist/site/terrain-renderer.d.ts +9 -2
- package/dist/site/terrain-renderer.d.ts.map +1 -1
- package/dist/site/terrain-renderer.js +14 -4
- package/dist/slab/boundary-editor.d.ts +1 -1
- package/dist/slab/boundary-editor.d.ts.map +1 -1
- package/dist/slab/boundary-editor.js +13 -13
- package/dist/slab/definition.d.ts.map +1 -1
- package/dist/slab/definition.js +19 -3
- package/dist/slab/dependency-tracker.d.ts.map +1 -1
- package/dist/slab/dependency-tracker.js +67 -7
- package/dist/slab/elevation-limit.d.ts.map +1 -1
- package/dist/slab/elevation-limit.js +2 -2
- package/dist/slab/floorplan-affordances.d.ts +176 -4
- package/dist/slab/floorplan-affordances.d.ts.map +1 -1
- package/dist/slab/floorplan-affordances.js +0 -1
- package/dist/slab/floorplan-move.d.ts.map +1 -1
- package/dist/slab/floorplan-move.js +3 -4
- package/dist/slab/floorplan.d.ts +7 -0
- package/dist/slab/floorplan.d.ts.map +1 -1
- package/dist/slab/floorplan.js +22 -5
- package/dist/slab/geometry.d.ts.map +1 -1
- package/dist/slab/geometry.js +186 -76
- package/dist/slab/move-tool.js +7 -6
- package/dist/slab/paint.d.ts +18 -5
- package/dist/slab/paint.d.ts.map +1 -1
- package/dist/slab/paint.js +458 -9
- package/dist/slab/panel.d.ts.map +1 -1
- package/dist/slab/panel.js +28 -9
- package/dist/slab/placement-ownership.d.ts +2 -1
- package/dist/slab/placement-ownership.d.ts.map +1 -1
- package/dist/slab/placement-ownership.js +2 -2
- package/dist/slab/surface-split.d.ts +24 -0
- package/dist/slab/surface-split.d.ts.map +1 -0
- package/dist/slab/surface-split.js +389 -0
- package/dist/slab/tool.d.ts.map +1 -1
- package/dist/slab/tool.js +3 -3
- package/dist/stair/baluster-geometry.d.ts +21 -0
- package/dist/stair/baluster-geometry.d.ts.map +1 -0
- package/dist/stair/baluster-geometry.js +74 -0
- package/dist/stair/baluster-guard.d.ts +50 -0
- package/dist/stair/baluster-guard.d.ts.map +1 -0
- package/dist/stair/baluster-guard.js +71 -0
- package/dist/stair/boards-guard.d.ts +37 -0
- package/dist/stair/boards-guard.d.ts.map +1 -0
- package/dist/stair/boards-guard.js +61 -0
- package/dist/stair/cable-guard.d.ts +39 -0
- package/dist/stair/cable-guard.d.ts.map +1 -0
- package/dist/stair/cable-guard.js +105 -0
- package/dist/stair/construction-controls.d.ts +15 -0
- package/dist/stair/construction-controls.d.ts.map +1 -0
- package/dist/stair/construction-controls.js +29 -0
- package/dist/stair/continuous-railings.d.ts +10 -0
- package/dist/stair/continuous-railings.d.ts.map +1 -0
- package/dist/stair/continuous-railings.js +159 -0
- package/dist/stair/definition.d.ts.map +1 -1
- package/dist/stair/definition.js +16 -37
- package/dist/stair/documentation.d.ts +1 -1
- package/dist/stair/documentation.d.ts.map +1 -1
- package/dist/stair/documentation.js +10 -17
- package/dist/stair/floorplan-affordances.d.ts.map +1 -1
- package/dist/stair/floorplan-affordances.js +9 -25
- package/dist/stair/floorplan.d.ts +1 -23
- package/dist/stair/floorplan.d.ts.map +1 -1
- package/dist/stair/floorplan.js +102 -86
- package/dist/stair/glass-guard.d.ts +72 -0
- package/dist/stair/glass-guard.d.ts.map +1 -0
- package/dist/stair/glass-guard.js +167 -0
- package/dist/stair/guard-path.d.ts +119 -0
- package/dist/stair/guard-path.d.ts.map +1 -0
- package/dist/stair/guard-path.js +208 -0
- package/dist/stair/materials.d.ts +11 -0
- package/dist/stair/materials.d.ts.map +1 -0
- package/dist/stair/materials.js +51 -0
- package/dist/stair/metal-guard.d.ts +38 -0
- package/dist/stair/metal-guard.d.ts.map +1 -0
- package/dist/stair/metal-guard.js +79 -0
- package/dist/stair/paint.d.ts +32 -0
- package/dist/stair/paint.d.ts.map +1 -1
- package/dist/stair/paint.js +75 -36
- package/dist/stair/panel.d.ts.map +1 -1
- package/dist/stair/panel.js +154 -17
- package/dist/stair/parametrics.d.ts +1 -1
- package/dist/stair/parametrics.d.ts.map +1 -1
- package/dist/stair/parametrics.js +32 -15
- package/dist/stair/plan-entry.d.ts +31 -0
- package/dist/stair/plan-entry.d.ts.map +1 -0
- package/dist/stair/plan-entry.js +325 -0
- package/dist/stair/post-and-rail-guard.d.ts +31 -0
- package/dist/stair/post-and-rail-guard.d.ts.map +1 -0
- package/dist/stair/post-and-rail-guard.js +63 -0
- package/dist/stair/railing-controls.d.ts +9 -0
- package/dist/stair/railing-controls.d.ts.map +1 -0
- package/dist/stair/railing-controls.js +49 -0
- package/dist/stair/renderer.d.ts.map +1 -1
- package/dist/stair/renderer.js +259 -259
- package/dist/stair/use-stair-render-data.d.ts +317 -0
- package/dist/stair/use-stair-render-data.d.ts.map +1 -0
- package/dist/stair/use-stair-render-data.js +73 -0
- package/dist/stair-segment/definition.d.ts.map +1 -1
- package/dist/stair-segment/definition.js +51 -10
- package/dist/stair-segment/panel.d.ts.map +1 -1
- package/dist/stair-segment/panel.js +36 -42
- package/dist/stair-segment/parametrics.d.ts +1 -1
- package/dist/stair-segment/parametrics.d.ts.map +1 -1
- package/dist/stair-segment/parametrics.js +21 -8
- package/dist/stair-segment/renderer.d.ts.map +1 -1
- package/dist/stair-segment/renderer.js +16 -28
- package/dist/turbine-vent/definition.d.ts.map +1 -1
- package/dist/turbine-vent/definition.js +2 -5
- package/dist/turbine-vent/paint.d.ts.map +1 -1
- package/dist/turbine-vent/paint.js +2 -5
- package/dist/turbine-vent/panel.d.ts.map +1 -1
- package/dist/turbine-vent/panel.js +5 -23
- package/dist/unit/unit-panel.d.ts.map +1 -1
- package/dist/unit/unit-panel.js +6 -1
- package/dist/wall/actions.d.ts.map +1 -1
- package/dist/wall/actions.js +7 -2
- package/dist/wall/construction-dimensions.js +8 -5
- package/dist/wall/contextual-dimensions.js +4 -4
- package/dist/wall/curtain-wall-materials.d.ts +1 -1
- package/dist/wall/curtain-wall-materials.d.ts.map +1 -1
- package/dist/wall/curtain-wall-materials.js +2 -2
- package/dist/wall/curve-tool.d.ts.map +1 -1
- package/dist/wall/curve-tool.js +4 -0
- package/dist/wall/definition.d.ts.map +1 -1
- package/dist/wall/definition.js +41 -17
- package/dist/wall/floorplan-move.d.ts +1 -1
- package/dist/wall/floorplan-move.d.ts.map +1 -1
- package/dist/wall/floorplan-move.js +20 -83
- package/dist/wall/floorplan-tool.d.ts.map +1 -1
- package/dist/wall/floorplan-tool.js +10 -11
- package/dist/wall/floorplan.d.ts +3 -1
- package/dist/wall/floorplan.d.ts.map +1 -1
- package/dist/wall/floorplan.js +184 -21
- package/dist/wall/measurement.d.ts.map +1 -1
- package/dist/wall/measurement.js +5 -5
- package/dist/wall/move-endpoint-tool.d.ts.map +1 -1
- package/dist/wall/move-endpoint-tool.js +20 -7
- package/dist/wall/move-shared.d.ts.map +1 -1
- package/dist/wall/move-shared.js +11 -7
- package/dist/wall/move-tool.d.ts.map +1 -1
- package/dist/wall/move-tool.js +118 -148
- package/dist/wall/paint.d.ts +26 -15
- package/dist/wall/paint.d.ts.map +1 -1
- package/dist/wall/paint.js +306 -117
- package/dist/wall/panel-model.d.ts +18 -0
- package/dist/wall/panel-model.d.ts.map +1 -1
- package/dist/wall/panel-model.js +77 -19
- package/dist/wall/panel.d.ts.map +1 -1
- package/dist/wall/panel.js +217 -41
- package/dist/wall/parametrics.d.ts.map +1 -1
- package/dist/wall/parametrics.js +3 -0
- package/dist/wall/rectangle-command.d.ts +111 -12
- package/dist/wall/rectangle-command.d.ts.map +1 -1
- package/dist/wall/rectangle-tool.d.ts.map +1 -1
- package/dist/wall/rectangle-tool.js +26 -8
- package/dist/wall/renderer.d.ts.map +1 -1
- package/dist/wall/renderer.js +4 -2
- package/dist/wall/split-floorplan-layer.d.ts.map +1 -1
- package/dist/wall/split-floorplan-layer.js +7 -7
- package/dist/wall/split-session.d.ts.map +1 -1
- package/dist/wall/split-session.js +7 -1
- package/dist/wall/system.d.ts.map +1 -1
- package/dist/wall/system.js +36 -4
- package/dist/wall/tool.d.ts.map +1 -1
- package/dist/wall/tool.js +105 -49
- package/dist/wall/treatment-level-data.d.ts +3 -2
- package/dist/wall/treatment-level-data.d.ts.map +1 -1
- package/dist/wall/treatment-level-data.js +17 -13
- package/dist/wall/treatments.d.ts +10 -10
- package/dist/wall/treatments.d.ts.map +1 -1
- package/dist/wall/treatments.js +67 -54
- package/dist/wall/wall-batch-system.d.ts +7 -0
- package/dist/wall/wall-batch-system.d.ts.map +1 -1
- package/dist/wall/wall-batch-system.js +35 -3
- package/dist/wall/wall-batch.d.ts +6 -0
- package/dist/wall/wall-batch.d.ts.map +1 -1
- package/dist/wall/wall-batch.js +7 -1
- package/dist/window/definition.d.ts.map +1 -1
- package/dist/window/definition.js +12 -4
- package/dist/window/floorplan-move.d.ts.map +1 -1
- package/dist/window/floorplan-move.js +3 -1
- package/dist/window/floorplan.d.ts.map +1 -1
- package/dist/window/floorplan.js +145 -60
- package/dist/window/mechanism.d.ts +4 -0
- package/dist/window/mechanism.d.ts.map +1 -0
- package/dist/window/mechanism.js +20 -0
- package/dist/window/move-tool.d.ts.map +1 -1
- package/dist/window/move-tool.js +79 -48
- package/dist/window/paint.d.ts.map +1 -1
- package/dist/window/paint.js +3 -0
- package/dist/window/panel.d.ts.map +1 -1
- package/dist/window/panel.js +32 -88
- package/dist/window/placement.d.ts +23 -0
- package/dist/window/placement.d.ts.map +1 -0
- package/dist/window/placement.js +66 -0
- package/dist/window/preview.d.ts.map +1 -1
- package/dist/window/preview.js +2 -0
- package/dist/window/renderer.d.ts.map +1 -1
- package/dist/window/renderer.js +3 -2
- package/dist/window/tool.d.ts.map +1 -1
- package/dist/window/tool.js +96 -24
- package/dist/window/window-math.d.ts +6 -14
- package/dist/window/window-math.d.ts.map +1 -1
- package/dist/window/window-math.js +14 -20
- package/dist/zone/contextual-dimensions.d.ts.map +1 -1
- package/dist/zone/contextual-dimensions.js +6 -28
- package/dist/zone/definition.d.ts.map +1 -1
- package/dist/zone/definition.js +3 -3
- package/dist/zone/floorplan-affordances.d.ts +200 -0
- package/dist/zone/floorplan-affordances.d.ts.map +1 -1
- package/dist/zone/floorplan.d.ts +1 -1
- package/dist/zone/floorplan.d.ts.map +1 -1
- package/dist/zone/floorplan.js +93 -53
- package/dist/zone/quantities-panel.d.ts.map +1 -1
- package/dist/zone/quantities-panel.js +3 -14
- package/dist/zone/quick-measurement.d.ts +2 -2
- package/dist/zone/quick-measurement.d.ts.map +1 -1
- package/dist/zone/quick-measurement.js +6 -5
- package/dist/zone/renderer.d.ts +1 -1
- package/dist/zone/renderer.d.ts.map +1 -1
- package/dist/zone/renderer.js +58 -146
- package/dist/zone/room-clear-dimensions.js +3 -3
- package/dist/zone/room-documentation.d.ts.map +1 -1
- package/dist/zone/room-documentation.js +2 -6
- package/package.json +11 -11
- package/dist/block/slots.d.ts +0 -4
- package/dist/block/slots.d.ts.map +0 -1
- package/dist/block/slots.js +0 -17
- package/dist/cabinet/slots.d.ts +0 -4
- package/dist/cabinet/slots.d.ts.map +0 -1
- package/dist/cabinet/slots.js +0 -24
- package/dist/ceiling/slots.d.ts +0 -6
- package/dist/ceiling/slots.d.ts.map +0 -1
- package/dist/ceiling/slots.js +0 -7
- package/dist/column/slots.d.ts +0 -9
- package/dist/column/slots.d.ts.map +0 -1
- package/dist/column/slots.js +0 -19
- package/dist/door/slots.d.ts +0 -9
- package/dist/door/slots.d.ts.map +0 -1
- package/dist/door/slots.js +0 -20
- package/dist/elevator/slots.d.ts +0 -8
- package/dist/elevator/slots.d.ts.map +0 -1
- package/dist/elevator/slots.js +0 -17
- package/dist/fence/slots.d.ts +0 -10
- package/dist/fence/slots.d.ts.map +0 -1
- package/dist/fence/slots.js +0 -23
- package/dist/item/__fixtures__/main-floorplan.d.ts +0 -26
- package/dist/item/__fixtures__/main-floorplan.d.ts.map +0 -1
- package/dist/item/__fixtures__/main-floorplan.js +0 -297
- package/dist/lean-to-extension/slots.d.ts +0 -5
- package/dist/lean-to-extension/slots.d.ts.map +0 -1
- package/dist/lean-to-extension/slots.js +0 -15
- package/dist/shelf/slots.d.ts +0 -9
- package/dist/shelf/slots.d.ts.map +0 -1
- package/dist/shelf/slots.js +0 -30
- package/dist/slab/slots.d.ts +0 -10
- package/dist/slab/slots.d.ts.map +0 -1
- package/dist/slab/slots.js +0 -20
- package/dist/stair/floor-stack.d.ts +0 -10
- package/dist/stair/floor-stack.d.ts.map +0 -1
- package/dist/stair/floor-stack.js +0 -115
- package/dist/stair/slots.d.ts +0 -7
- package/dist/stair/slots.d.ts.map +0 -1
- package/dist/stair/slots.js +0 -13
- package/dist/wall/drawing-mode.d.ts +0 -12
- package/dist/wall/drawing-mode.d.ts.map +0 -1
- package/dist/wall/drawing-mode.js +0 -38
- package/dist/wall/slots.d.ts +0 -10
- package/dist/wall/slots.d.ts.map +0 -1
- package/dist/wall/slots.js +0 -92
- package/dist/window/slots.d.ts +0 -5
- package/dist/window/slots.d.ts.map +0 -1
- package/dist/window/slots.js +0 -11
|
@@ -0,0 +1,1144 @@
|
|
|
1
|
+
import { clampFencePicketRailProjection, getFenceCenterlineFrameAt, getFenceCenterlineLength, getFenceGateLeaves, resolveFenceFeatures, } from '@pascal-app/core';
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
4
|
+
const MIN_CURVE_SEGMENT_LENGTH = 0.18;
|
|
5
|
+
const HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH = 0.2;
|
|
6
|
+
function createFencePartGeometry(part) {
|
|
7
|
+
if (part.geometry) {
|
|
8
|
+
return part.geometry;
|
|
9
|
+
}
|
|
10
|
+
const geometry = part.shape === 'pyramid'
|
|
11
|
+
? new THREE.ConeGeometry(0.5, 1, 4, 1, false, Math.PI / 4)
|
|
12
|
+
: part.shape === 'picket'
|
|
13
|
+
? createPicketGeometry(part.scale[0], part.scale[1], part.scale[2], part.picketTop)
|
|
14
|
+
: new THREE.BoxGeometry(1, 1, 1);
|
|
15
|
+
if (part.shape !== 'picket')
|
|
16
|
+
geometry.scale(part.scale[0], part.scale[1], part.scale[2]);
|
|
17
|
+
if (part.rotationZ)
|
|
18
|
+
geometry.rotateZ(part.rotationZ);
|
|
19
|
+
if (part.rotationY) {
|
|
20
|
+
geometry.rotateY(part.rotationY);
|
|
21
|
+
}
|
|
22
|
+
geometry.translate(part.position[0], part.position[1], part.position[2]);
|
|
23
|
+
applyFenceUVs(geometry);
|
|
24
|
+
return geometry;
|
|
25
|
+
}
|
|
26
|
+
function createPicketGeometry(width, height, depth, top = 'dog-ear') {
|
|
27
|
+
const shape = new THREE.Shape();
|
|
28
|
+
const halfWidth = width / 2;
|
|
29
|
+
const tip = height / 2;
|
|
30
|
+
const shoulder = tip - Math.min(halfWidth, height * 0.2);
|
|
31
|
+
shape.moveTo(-halfWidth, -tip);
|
|
32
|
+
shape.lineTo(halfWidth, -tip);
|
|
33
|
+
if (top === 'flat') {
|
|
34
|
+
shape.lineTo(halfWidth, tip);
|
|
35
|
+
shape.lineTo(-halfWidth, tip);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
shape.lineTo(halfWidth, shoulder);
|
|
39
|
+
if (top === 'pointed') {
|
|
40
|
+
shape.lineTo(0, tip);
|
|
41
|
+
}
|
|
42
|
+
else if (top === 'rounded') {
|
|
43
|
+
shape.absellipse(0, shoulder, halfWidth, tip - shoulder, 0, Math.PI, false, 0);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
shape.lineTo(width * 0.28, tip);
|
|
47
|
+
shape.lineTo(-width * 0.28, tip);
|
|
48
|
+
}
|
|
49
|
+
shape.lineTo(-halfWidth, shoulder);
|
|
50
|
+
}
|
|
51
|
+
shape.closePath();
|
|
52
|
+
const geometry = new THREE.ExtrudeGeometry(shape, {
|
|
53
|
+
bevelEnabled: false,
|
|
54
|
+
curveSegments: 8,
|
|
55
|
+
depth,
|
|
56
|
+
steps: 1,
|
|
57
|
+
});
|
|
58
|
+
geometry.translate(0, 0, -depth / 2);
|
|
59
|
+
return geometry;
|
|
60
|
+
}
|
|
61
|
+
function getInfillOffset(fence, supportDepth, infillDepth) {
|
|
62
|
+
const direction = fence.infillPlacement === 'front' ? 1 : fence.infillPlacement === 'back' ? -1 : 0;
|
|
63
|
+
// A small overlap avoids coincident faces at the attachment surface.
|
|
64
|
+
return direction * Math.max(0, (supportDepth + infillDepth) / 2 - 0.002);
|
|
65
|
+
}
|
|
66
|
+
function getFencePointAt(fence, t) {
|
|
67
|
+
const frame = getFenceCenterlineFrameAt(fence, t);
|
|
68
|
+
return {
|
|
69
|
+
point: frame.point,
|
|
70
|
+
tangentAngle: Math.atan2(frame.tangent.y, frame.tangent.x),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function createFenceCurveBlockPart(fence, startT, endT, centerY, height, depth, lateralOffset = 0) {
|
|
74
|
+
if (endT - startT <= 1e-5)
|
|
75
|
+
return null;
|
|
76
|
+
const halfHeight = height / 2;
|
|
77
|
+
const halfDepth = depth / 2;
|
|
78
|
+
const centerlineLength = getFenceCenterlineLength(fence);
|
|
79
|
+
const startDistance = startT * centerlineLength;
|
|
80
|
+
const endDistance = endT * centerlineLength;
|
|
81
|
+
const bottomY = centerY - halfHeight;
|
|
82
|
+
const topY = centerY + halfHeight;
|
|
83
|
+
const corners = [];
|
|
84
|
+
for (const t of [startT, endT]) {
|
|
85
|
+
const frame = getFencePointAt(fence, t);
|
|
86
|
+
const normalX = -Math.sin(frame.tangentAngle);
|
|
87
|
+
const normalZ = Math.cos(frame.tangentAngle);
|
|
88
|
+
const outerX = frame.point.x + normalX * (halfDepth + lateralOffset);
|
|
89
|
+
const outerZ = frame.point.y + normalZ * (halfDepth + lateralOffset);
|
|
90
|
+
const innerX = frame.point.x - normalX * (halfDepth - lateralOffset);
|
|
91
|
+
const innerZ = frame.point.y - normalZ * (halfDepth - lateralOffset);
|
|
92
|
+
corners.push([outerX, bottomY, outerZ], [innerX, bottomY, innerZ], [outerX, topY, outerZ], [innerX, topY, innerZ]);
|
|
93
|
+
}
|
|
94
|
+
const positions = [];
|
|
95
|
+
const uvs = [];
|
|
96
|
+
const pushVertex = (index, uv) => {
|
|
97
|
+
positions.push(...corners[index]);
|
|
98
|
+
uvs.push(...uv);
|
|
99
|
+
};
|
|
100
|
+
const pushQuad = (a, b, c, d, uvA, uvB, uvC, uvD) => {
|
|
101
|
+
pushVertex(a, uvA);
|
|
102
|
+
pushVertex(b, uvB);
|
|
103
|
+
pushVertex(c, uvC);
|
|
104
|
+
pushVertex(a, uvA);
|
|
105
|
+
pushVertex(c, uvC);
|
|
106
|
+
pushVertex(d, uvD);
|
|
107
|
+
};
|
|
108
|
+
const topOuterV = topY;
|
|
109
|
+
const topInnerV = topY + depth;
|
|
110
|
+
const innerTopV = topInnerV;
|
|
111
|
+
const innerBottomV = topInnerV + height;
|
|
112
|
+
const bottomInnerV = bottomY - depth;
|
|
113
|
+
pushQuad(0, 4, 6, 2, [startDistance, bottomY], [endDistance, bottomY], [endDistance, topY], [startDistance, topY]);
|
|
114
|
+
pushQuad(1, 3, 7, 5, [startDistance, innerBottomV], [startDistance, innerTopV], [endDistance, innerTopV], [endDistance, innerBottomV]);
|
|
115
|
+
pushQuad(2, 6, 7, 3, [startDistance, topOuterV], [endDistance, topOuterV], [endDistance, topInnerV], [startDistance, topInnerV]);
|
|
116
|
+
pushQuad(0, 1, 5, 4, [startDistance, bottomY], [startDistance, bottomInnerV], [endDistance, bottomInnerV], [endDistance, bottomY]);
|
|
117
|
+
pushQuad(0, 2, 3, 1, [0, bottomY], [0, topY], [depth, innerTopV], [depth, innerBottomV]);
|
|
118
|
+
pushQuad(4, 5, 7, 6, [0, bottomY], [depth, innerBottomV], [depth, innerTopV], [0, topY]);
|
|
119
|
+
const geometry = new THREE.BufferGeometry();
|
|
120
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(positions), 3));
|
|
121
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(new Float32Array(uvs), 2));
|
|
122
|
+
geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(new Float32Array(uvs), 2));
|
|
123
|
+
geometry.computeVertexNormals();
|
|
124
|
+
return {
|
|
125
|
+
geometry,
|
|
126
|
+
position: [0, 0, 0],
|
|
127
|
+
scale: [1, 1, 1],
|
|
128
|
+
startT,
|
|
129
|
+
endT,
|
|
130
|
+
curveBlock: { centerY, height, depth, lateralOffset },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function createFenceCurveBlockParts(fence, startT, endT, centerY, height, depth, maxSegmentLength = MIN_CURVE_SEGMENT_LENGTH, lateralOffset = 0) {
|
|
134
|
+
const length = getFenceCenterlineLength(fence) * Math.max(1e-4, endT - startT);
|
|
135
|
+
const segmentCount = Math.max(1, Math.ceil(length / Math.max(1e-4, maxSegmentLength)));
|
|
136
|
+
const parts = [];
|
|
137
|
+
for (let index = 0; index < segmentCount; index += 1) {
|
|
138
|
+
const segmentStartT = startT + (endT - startT) * (index / segmentCount);
|
|
139
|
+
const segmentEndT = startT + (endT - startT) * ((index + 1) / segmentCount);
|
|
140
|
+
const part = createFenceCurveBlockPart(fence, segmentStartT, segmentEndT, centerY, height, depth, lateralOffset);
|
|
141
|
+
if (part)
|
|
142
|
+
parts.push(part);
|
|
143
|
+
}
|
|
144
|
+
return parts;
|
|
145
|
+
}
|
|
146
|
+
function applyFenceUVs(geometry) {
|
|
147
|
+
const position = geometry.getAttribute('position');
|
|
148
|
+
const normal = geometry.getAttribute('normal');
|
|
149
|
+
if (!(position && normal))
|
|
150
|
+
return;
|
|
151
|
+
// World-scale triplanar UVs: 1 UV unit = 1 metre, sampled from the part's
|
|
152
|
+
// local-space (already translated into fence space) coordinates with NO
|
|
153
|
+
// per-part origin shift. A shared origin keeps a tiled finish continuous
|
|
154
|
+
// across posts, rails, and infill instead of restarting the tile at each
|
|
155
|
+
// part's own min corner (the previous behaviour, which broke the 1 m
|
|
156
|
+
// contract and made adjacent parts mistile).
|
|
157
|
+
const uvs = new Float32Array(position.count * 2);
|
|
158
|
+
for (let index = 0; index < position.count; index += 1) {
|
|
159
|
+
const px = position.getX(index);
|
|
160
|
+
const py = position.getY(index);
|
|
161
|
+
const pz = position.getZ(index);
|
|
162
|
+
const nx = Math.abs(normal.getX(index));
|
|
163
|
+
const ny = Math.abs(normal.getY(index));
|
|
164
|
+
const nz = Math.abs(normal.getZ(index));
|
|
165
|
+
let u = 0;
|
|
166
|
+
let v = 0;
|
|
167
|
+
if (ny >= nx && ny >= nz) {
|
|
168
|
+
u = px;
|
|
169
|
+
v = pz;
|
|
170
|
+
}
|
|
171
|
+
else if (nx >= nz) {
|
|
172
|
+
u = pz;
|
|
173
|
+
v = py;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
u = px;
|
|
177
|
+
v = py;
|
|
178
|
+
}
|
|
179
|
+
uvs[index * 2] = u;
|
|
180
|
+
uvs[index * 2 + 1] = v;
|
|
181
|
+
}
|
|
182
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
|
183
|
+
geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(uvs.slice(), 2));
|
|
184
|
+
}
|
|
185
|
+
function getStyleDefaults(style) {
|
|
186
|
+
if (style === 'privacy') {
|
|
187
|
+
return { spacingFactor: 0.42, postFactor: 1.35, baseFactor: 1.2, topFactor: 1.2 };
|
|
188
|
+
}
|
|
189
|
+
if (style === 'rail') {
|
|
190
|
+
return { spacingFactor: 0.68, postFactor: 0.8, baseFactor: 0.85, topFactor: 0.85 };
|
|
191
|
+
}
|
|
192
|
+
return { spacingFactor: 0.3, postFactor: 0.55, baseFactor: 1, topFactor: 0.75 };
|
|
193
|
+
}
|
|
194
|
+
function getFenceInfillVerticalBounds(fence) {
|
|
195
|
+
const height = fence.style === 'picket' ? Math.max(fence.height, 0.3) : fence.height;
|
|
196
|
+
if (fence.style === 'picket') {
|
|
197
|
+
const baseHeight = fence.baseStyle === 'floating' ? 0 : Math.min(Math.max(fence.baseHeight, 0.04), height * 0.5);
|
|
198
|
+
const bottom = Math.min(baseHeight + Math.max(fence.groundClearance, 0), height * 0.75);
|
|
199
|
+
const topClearance = Math.min(Math.max(fence.picketTopClearance ?? 0, 0), height - bottom - 0.02);
|
|
200
|
+
return { bottom, top: height - topClearance };
|
|
201
|
+
}
|
|
202
|
+
const styleDefaults = getStyleDefaults(fence.style);
|
|
203
|
+
const baseY = fence.baseStyle === 'floating' ? Math.max(fence.groundClearance, 0) : 0;
|
|
204
|
+
const baseHeight = Math.max(fence.baseHeight * (fence.style === 'horizontal' ? 1 : styleDefaults.baseFactor), 0.04);
|
|
205
|
+
const topRailHeight = Math.max(fence.topRailHeight * (fence.style === 'horizontal' ? 1 : styleDefaults.topFactor), 0.01);
|
|
206
|
+
const verticalHeight = Math.max(height - baseHeight - topRailHeight, 0.08);
|
|
207
|
+
const bottom = baseY + baseHeight;
|
|
208
|
+
return { bottom, top: bottom + verticalHeight };
|
|
209
|
+
}
|
|
210
|
+
export function getFenceFeatureDimensions(fence, feature) {
|
|
211
|
+
const bounds = getFenceInfillVerticalBounds(fence);
|
|
212
|
+
const fullPost = fence.style === 'picket' ||
|
|
213
|
+
fence.style === 'horizontal' ||
|
|
214
|
+
fence.baseStyle === 'floating' ||
|
|
215
|
+
!fence.showInfill;
|
|
216
|
+
const postBottom = fullPost ? 0 : bounds.bottom;
|
|
217
|
+
const defaults = getStyleDefaults(fence.style);
|
|
218
|
+
const railHeight = Math.max(fence.topRailHeight * (fence.style === 'horizontal' ? 1 : defaults.topFactor), 0.01);
|
|
219
|
+
const postTop = fence.style === 'picket'
|
|
220
|
+
? Math.max(fence.height, 0.3)
|
|
221
|
+
: fullPost
|
|
222
|
+
? bounds.top -
|
|
223
|
+
(fence.baseStyle === 'floating' ? Math.max(fence.groundClearance, 0) : 0) +
|
|
224
|
+
railHeight +
|
|
225
|
+
Math.max(fence.groundClearance, 0)
|
|
226
|
+
: bounds.top;
|
|
227
|
+
const matchesHeight = feature.matchFenceHeight ?? feature.matchFenceStyle !== false;
|
|
228
|
+
const bottom = feature.kind === 'opening'
|
|
229
|
+
? postBottom
|
|
230
|
+
: matchesHeight
|
|
231
|
+
? bounds.bottom
|
|
232
|
+
: (feature.clearance ?? bounds.bottom);
|
|
233
|
+
const defaultHeight = feature.kind === 'opening' ? postTop - postBottom : bounds.top - bounds.bottom;
|
|
234
|
+
const height = matchesHeight ? defaultHeight : (feature.height ?? defaultHeight);
|
|
235
|
+
const postHeight = Math.max(0.02, feature.kind === 'opening' ? height : postTop - postBottom);
|
|
236
|
+
return { bottom, height, postBottom, postHeight };
|
|
237
|
+
}
|
|
238
|
+
function distributePattern(fence, start, end, targetSpacing, minimumSpacing, minimumCount = 1) {
|
|
239
|
+
const length = Math.max(0, end - start);
|
|
240
|
+
if (length < 1e-5)
|
|
241
|
+
return minimumCount > 1 ? [start, end] : [(start + end) / 2];
|
|
242
|
+
const target = Math.max(targetSpacing, minimumSpacing);
|
|
243
|
+
const maxCount = Math.max(minimumCount, Math.floor(length / minimumSpacing) + 1);
|
|
244
|
+
const mode = fence.patternDistribution;
|
|
245
|
+
let count;
|
|
246
|
+
if (mode === 'fixed-count') {
|
|
247
|
+
count = THREE.MathUtils.clamp(Math.round(fence.patternCount), minimumCount, maxCount);
|
|
248
|
+
}
|
|
249
|
+
else if (mode === 'maximum-spacing') {
|
|
250
|
+
count = THREE.MathUtils.clamp(Math.ceil(length / target) + 1, minimumCount, maxCount);
|
|
251
|
+
}
|
|
252
|
+
else if (mode === 'equal-fit') {
|
|
253
|
+
count = THREE.MathUtils.clamp(Math.round(length / target) + 1, minimumCount, maxCount);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
count = THREE.MathUtils.clamp(Math.floor(length / target) + 1, minimumCount, maxCount);
|
|
257
|
+
}
|
|
258
|
+
if (count === 1) {
|
|
259
|
+
return [
|
|
260
|
+
mode === 'fixed-spacing' && fence.patternAlignment === 'start'
|
|
261
|
+
? start
|
|
262
|
+
: mode === 'fixed-spacing' && fence.patternAlignment === 'end'
|
|
263
|
+
? end
|
|
264
|
+
: (start + end) / 2,
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
const fixed = mode === 'fixed-spacing' && fence.patternRemainder === 'leave';
|
|
268
|
+
const spacing = fixed ? target : length / (count - 1);
|
|
269
|
+
const extra = Math.max(0, length - spacing * (count - 1));
|
|
270
|
+
const origin = fixed
|
|
271
|
+
? start +
|
|
272
|
+
(fence.patternAlignment === 'end'
|
|
273
|
+
? extra
|
|
274
|
+
: fence.patternAlignment === 'center'
|
|
275
|
+
? extra / 2
|
|
276
|
+
: 0)
|
|
277
|
+
: start;
|
|
278
|
+
return Array.from({ length: count }, (_, index) => origin + index * spacing);
|
|
279
|
+
}
|
|
280
|
+
function miterFencePartEnd(fence, part, endpoint, neighbor) {
|
|
281
|
+
if (!part.geometry || !part.curveBlock)
|
|
282
|
+
return;
|
|
283
|
+
const t = endpoint === 'start' ? 0 : 1;
|
|
284
|
+
if (Math.abs(((endpoint === 'start' ? part.startT : part.endT) ?? -1) - t) > 1e-5)
|
|
285
|
+
return;
|
|
286
|
+
const point = fence[endpoint];
|
|
287
|
+
const frame = getFenceCenterlineFrameAt(fence, t);
|
|
288
|
+
const sign = endpoint === 'start' ? 1 : -1;
|
|
289
|
+
const ux = frame.tangent.x * sign;
|
|
290
|
+
const uz = frame.tangent.y * sign;
|
|
291
|
+
const neighborAtStart = Math.hypot(neighbor.start[0] - point[0], neighbor.start[1] - point[1]) < 0.001;
|
|
292
|
+
const other = getFenceCenterlineFrameAt(neighbor, neighborAtStart ? 0 : 1);
|
|
293
|
+
const vx = other.tangent.x * (neighborAtStart ? 1 : -1);
|
|
294
|
+
const vz = other.tangent.y * (neighborAtStart ? 1 : -1);
|
|
295
|
+
const cross = ux * vz - uz * vx;
|
|
296
|
+
if (Math.abs(cross) < 0.15)
|
|
297
|
+
return;
|
|
298
|
+
const { depth, lateralOffset } = part.curveBlock;
|
|
299
|
+
const ratio = Math.max(neighbor.thickness, 0.03) / Math.max(fence.thickness, 0.03);
|
|
300
|
+
const position = part.geometry.getAttribute('position');
|
|
301
|
+
for (let index = 0; index < position.count; index += 1) {
|
|
302
|
+
const x = position.getX(index);
|
|
303
|
+
const z = position.getZ(index);
|
|
304
|
+
for (const side of [-1, 1]) {
|
|
305
|
+
const offset = lateralOffset + (side * depth) / 2;
|
|
306
|
+
const originalX = point[0] - uz * offset * sign;
|
|
307
|
+
const originalZ = point[1] + ux * offset * sign;
|
|
308
|
+
if (Math.hypot(x - originalX, z - originalZ) > 1e-4)
|
|
309
|
+
continue;
|
|
310
|
+
const neighborOffset = -offset * sign * ratio;
|
|
311
|
+
const nx = -uz * offset * sign;
|
|
312
|
+
const nz = ux * offset * sign;
|
|
313
|
+
const mx = -vz * neighborOffset;
|
|
314
|
+
const mz = vx * neighborOffset;
|
|
315
|
+
const reach = ((mx - nx) * vz - (mz - nz) * vx) / cross;
|
|
316
|
+
if (Math.abs(reach) > Math.max(depth, depth * ratio) * 4)
|
|
317
|
+
break;
|
|
318
|
+
position.setX(index, originalX + ux * reach);
|
|
319
|
+
position.setZ(index, originalZ + uz * reach);
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
position.needsUpdate = true;
|
|
324
|
+
part.geometry.computeVertexNormals();
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Horizontal-board fence — composite cladding boards stacked between square
|
|
328
|
+
* intermediate posts (each capped), instead of the vertical pickets the other
|
|
329
|
+
* styles draw. Posts march along the whole span at `postSpacing` (not just the
|
|
330
|
+
* two ends), the boards run full-length so they curve with the fence, and a
|
|
331
|
+
* thin reveal between boards leaves the groove shadow that reads as cladding.
|
|
332
|
+
*/
|
|
333
|
+
function createHorizontalFenceParts(fence, sharedEndpoints) {
|
|
334
|
+
const posts = [];
|
|
335
|
+
const infill = [];
|
|
336
|
+
const base = [];
|
|
337
|
+
const rail = [];
|
|
338
|
+
const length = Math.max(getFenceCenterlineLength(fence), 0.01);
|
|
339
|
+
const panelDepth = Math.max(fence.thickness, 0.03);
|
|
340
|
+
const clearance = Math.max(fence.groundClearance, 0);
|
|
341
|
+
const isFloating = fence.baseStyle === 'floating';
|
|
342
|
+
// 'raised': the kickboard becomes a bottom rail `clearance` up; the posts still reach the ground
|
|
343
|
+
const isRaised = fence.baseStyle === 'raised';
|
|
344
|
+
const showInfill = fence.showInfill ?? true;
|
|
345
|
+
const baseHeight = Math.max(fence.baseHeight, 0.04);
|
|
346
|
+
const topRailHeight = Math.max(fence.topRailHeight, 0.01);
|
|
347
|
+
const verticalHeight = Math.max(fence.height - baseHeight - topRailHeight, 0.08);
|
|
348
|
+
const baseY = isFloating || isRaised ? clearance : 0;
|
|
349
|
+
// Square posts stand proud of the recessed boards on both faces.
|
|
350
|
+
const postWidth = Math.max(fence.postSize * 1.4, 0.04);
|
|
351
|
+
const postDepth = postWidth;
|
|
352
|
+
const boardDepth = Math.min(panelDepth, postDepth - 0.012);
|
|
353
|
+
const infillOffset = getInfillOffset(fence, postDepth, boardDepth);
|
|
354
|
+
// Stop the horizontal boards / base / rail at the inner faces of the
|
|
355
|
+
// end posts. Letting curved spans run all the way to t=0/1 makes them
|
|
356
|
+
// overlap the terminal post mesh and creates the broken seam/notch seen
|
|
357
|
+
// at curve ends.
|
|
358
|
+
const edgeInset = Math.max(fence.edgeInset ?? 0.015, postWidth * 0.5);
|
|
359
|
+
const startInsetT = sharedEndpoints?.has('start') ? 0 : Math.min(0.499, edgeInset / length);
|
|
360
|
+
const endInsetT = sharedEndpoints?.has('end') ? 1 : Math.max(0.501, 1 - edgeInset / length);
|
|
361
|
+
// Grounded fences get a kickboard along the bottom; floating ones don't.
|
|
362
|
+
if (!isFloating) {
|
|
363
|
+
base.push(...createFenceCurveBlockParts(fence, startInsetT, endInsetT, baseY + baseHeight / 2, baseHeight, postDepth * 0.92, HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH));
|
|
364
|
+
}
|
|
365
|
+
// Stack full-length boards between the kickboard and the top rail. The board
|
|
366
|
+
// height is derived to evenly fill the panel around a ~0.145 m target, with a
|
|
367
|
+
// constant reveal between each so the count adapts to any fence height.
|
|
368
|
+
if (showInfill) {
|
|
369
|
+
const reveal = Math.max(fence.slatGap ?? 0.01, 0);
|
|
370
|
+
const infillBottom = baseY + baseHeight;
|
|
371
|
+
if (reveal < 0.002) {
|
|
372
|
+
// No reveal → one flush panel, so the stacked-board edge seams don't
|
|
373
|
+
// read as faint lines where the user asked for a smooth surface.
|
|
374
|
+
infill.push(...createFenceCurveBlockParts(fence, startInsetT, endInsetT, infillBottom + verticalHeight / 2, verticalHeight, boardDepth, HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH, infillOffset));
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
const boardCount = Math.max(1, Math.round(verticalHeight / (0.145 + reveal)));
|
|
378
|
+
const slabHeight = Math.max((verticalHeight - reveal * (boardCount - 1)) / boardCount, 0.02);
|
|
379
|
+
for (let index = 0; index < boardCount; index += 1) {
|
|
380
|
+
const centerY = infillBottom + slabHeight / 2 + index * (slabHeight + reveal);
|
|
381
|
+
infill.push(...createFenceCurveBlockParts(fence, startInsetT, endInsetT, centerY, slabHeight, boardDepth, HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH, infillOffset));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// Top rail caps the boards.
|
|
386
|
+
rail.push(...createFenceCurveBlockParts(fence, startInsetT, endInsetT, baseY + baseHeight + verticalHeight + topRailHeight / 2, topRailHeight, Math.max(postDepth * 0.78, 0.02), HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH));
|
|
387
|
+
// Posts at every `postSpacing`, anchored at both ends, each with a flat cap.
|
|
388
|
+
const spacing = Math.max(fence.postSpacing, postWidth * 1.4);
|
|
389
|
+
const postCount = Math.max(2, Math.floor(length / spacing) + 1);
|
|
390
|
+
const postDistances = fence.patternDistribution === 'automatic'
|
|
391
|
+
? Array.from({ length: postCount }, (_, index) => (length * index) / (postCount - 1))
|
|
392
|
+
: distributePattern(fence, 0, length, spacing, postWidth * 1.4, 2);
|
|
393
|
+
const anchoredPostDistances = fence.patternDistribution === 'fixed-spacing'
|
|
394
|
+
? [
|
|
395
|
+
0,
|
|
396
|
+
...postDistances.filter((distance) => distance > postWidth * 1.4 && length - distance > postWidth * 1.4),
|
|
397
|
+
length,
|
|
398
|
+
]
|
|
399
|
+
: postDistances;
|
|
400
|
+
const postHeight = baseHeight + verticalHeight + topRailHeight + clearance;
|
|
401
|
+
const capHeight = Math.max(postWidth * 0.32, 0.03);
|
|
402
|
+
const cap = fence.postCap ?? 'pyramid';
|
|
403
|
+
for (const distance of anchoredPostDistances) {
|
|
404
|
+
const t = distance / length;
|
|
405
|
+
const frame = getFencePointAt(fence, t);
|
|
406
|
+
posts.push({
|
|
407
|
+
endpoint: distance === 0 ? 'start' : distance === length ? 'end' : undefined,
|
|
408
|
+
position: [frame.point.x, postHeight / 2, frame.point.y],
|
|
409
|
+
rotationY: -frame.tangentAngle,
|
|
410
|
+
scale: [postWidth, postHeight, postDepth],
|
|
411
|
+
});
|
|
412
|
+
if (cap === 'flat') {
|
|
413
|
+
posts.push({
|
|
414
|
+
endpoint: distance === 0 ? 'start' : distance === length ? 'end' : undefined,
|
|
415
|
+
position: [frame.point.x, postHeight + capHeight / 2, frame.point.y],
|
|
416
|
+
rotationY: -frame.tangentAngle,
|
|
417
|
+
scale: [postWidth * 1.22, capHeight, postDepth * 1.22],
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
else if (cap === 'pyramid') {
|
|
421
|
+
posts.push({
|
|
422
|
+
endpoint: distance === 0 ? 'start' : distance === length ? 'end' : undefined,
|
|
423
|
+
position: [frame.point.x, postHeight + capHeight * 0.9, frame.point.y],
|
|
424
|
+
rotationY: -frame.tangentAngle,
|
|
425
|
+
scale: [postWidth * 1.18, capHeight * 1.8, postDepth * 1.18],
|
|
426
|
+
shape: 'pyramid',
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return { posts, infill, base, rail };
|
|
431
|
+
}
|
|
432
|
+
/** Dressed lumber the guard is built from, metres. */
|
|
433
|
+
const IN = 0.0254;
|
|
434
|
+
const GUARD_CAP_T = 1.5 * IN;
|
|
435
|
+
const GUARD_CAP_W = 5.5 * IN;
|
|
436
|
+
const GUARD_RAIL_T = 1.5 * IN;
|
|
437
|
+
const GUARD_RAIL_D = 3.5 * IN;
|
|
438
|
+
const GUARD_BALUSTER = 1.5 * IN;
|
|
439
|
+
const GUARD_BOARD_D = 5.5 * IN;
|
|
440
|
+
const GUARD_CABLE = 0.5 * IN;
|
|
441
|
+
const GUARD_POST_ABOVE_CAP = 3 * IN;
|
|
442
|
+
const GUARD_POST_CAP_T = 1 * IN;
|
|
443
|
+
/**
|
|
444
|
+
* A deck guard the way the AWC Deck Construction Guide (DCA 6) draws one:
|
|
445
|
+
* 4x4 posts (`postSize`) no more than `postSpacing` apart — the two ends
|
|
446
|
+
* counted as posts whether drawn or not (`startPost` / `endPost` false:
|
|
447
|
+
* the rails die into a post already standing there, a porch's 6x6) — a 2x6
|
|
448
|
+
* cap rail flat over the posts, a 2x4 top rail on edge under it, and the
|
|
449
|
+
* infill: balusters (2x2 on a 2x4 bottom rail `groundClearance` over the
|
|
450
|
+
* deck, `slatGap` clear), cable (½ in runs `slatGap` apart from
|
|
451
|
+
* `groundClearance` up), or boards (`thickness` × 5½ in, `slatGap` apart
|
|
452
|
+
* from `groundClearance` up). `postThrough` runs the posts 3 in past the
|
|
453
|
+
* cap under a cap of their own (`postCap`). Slots: posts / infill / base
|
|
454
|
+
* (the bottom rail) / rail (cap + top rail).
|
|
455
|
+
*/
|
|
456
|
+
function createGuardFenceParts(fence) {
|
|
457
|
+
const posts = [];
|
|
458
|
+
const infill = [];
|
|
459
|
+
const base = [];
|
|
460
|
+
const rail = [];
|
|
461
|
+
const length = Math.max(getFenceCenterlineLength(fence), 0.01);
|
|
462
|
+
const H = Math.max(fence.height, 0.4);
|
|
463
|
+
const postW = Math.max(fence.postSize, 0.02);
|
|
464
|
+
const clearance = Math.max(fence.groundClearance, 0);
|
|
465
|
+
const gap = Math.max(fence.slatGap ?? 0.09, 0.01);
|
|
466
|
+
const infillKind = fence.guardInfill ?? 'balusters';
|
|
467
|
+
const through = fence.postThrough === true;
|
|
468
|
+
const capTop = H;
|
|
469
|
+
const capBottom = H - GUARD_CAP_T;
|
|
470
|
+
const topRailBottom = capBottom - GUARD_RAIL_D;
|
|
471
|
+
const bays = Math.max(1, Math.ceil(length / Math.max(fence.postSpacing, postW * 2)));
|
|
472
|
+
const stations = [];
|
|
473
|
+
for (let i = 0; i <= bays; i++)
|
|
474
|
+
stations.push(i / bays);
|
|
475
|
+
// posts: to the cap's underside, or through it with a cap
|
|
476
|
+
const postH = through ? capTop + GUARD_POST_ABOVE_CAP : capBottom;
|
|
477
|
+
for (const [i, t] of stations.entries()) {
|
|
478
|
+
if (i === 0 && fence.startPost === false)
|
|
479
|
+
continue;
|
|
480
|
+
if (i === stations.length - 1 && fence.endPost === false)
|
|
481
|
+
continue;
|
|
482
|
+
const frame = getFencePointAt(fence, t);
|
|
483
|
+
posts.push({
|
|
484
|
+
position: [frame.point.x, postH / 2, frame.point.y],
|
|
485
|
+
rotationY: -frame.tangentAngle,
|
|
486
|
+
scale: [postW, postH, postW],
|
|
487
|
+
});
|
|
488
|
+
if (through && (fence.postCap ?? 'flat') !== 'none') {
|
|
489
|
+
const cap = fence.postCap ?? 'flat';
|
|
490
|
+
if (cap === 'pyramid') {
|
|
491
|
+
posts.push({
|
|
492
|
+
position: [frame.point.x, postH + postW * 0.45, frame.point.y],
|
|
493
|
+
rotationY: -frame.tangentAngle,
|
|
494
|
+
scale: [postW * 1.18, postW * 0.9, postW * 1.18],
|
|
495
|
+
shape: 'pyramid',
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
posts.push({
|
|
500
|
+
position: [frame.point.x, postH + GUARD_POST_CAP_T / 2, frame.point.y],
|
|
501
|
+
rotationY: -frame.tangentAngle,
|
|
502
|
+
scale: [postW + 2 * IN, GUARD_POST_CAP_T, postW + 2 * IN],
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
// the cap rail flat over the posts and the 2x4 top rail on edge under it
|
|
508
|
+
rail.push(...createFenceCurveBlockParts(fence, 0, 1, capBottom + GUARD_CAP_T / 2, GUARD_CAP_T, GUARD_CAP_W));
|
|
509
|
+
rail.push(...createFenceCurveBlockParts(fence, 0, 1, topRailBottom + GUARD_RAIL_D / 2, GUARD_RAIL_D, GUARD_RAIL_T));
|
|
510
|
+
const postHalfT = postW / 2 / length;
|
|
511
|
+
const clearOfPosts = (t, halfT) => !stations.some((s) => Math.abs(s - t) < postHalfT + halfT);
|
|
512
|
+
if (infillKind === 'balusters') {
|
|
513
|
+
// the 2x4 bottom rail `clearance` over the deck, the balusters on it
|
|
514
|
+
const bottomRailTop = clearance + GUARD_RAIL_D;
|
|
515
|
+
base.push(...createFenceCurveBlockParts(fence, 0, 1, clearance + GUARD_RAIL_D / 2, GUARD_RAIL_D, GUARD_RAIL_T));
|
|
516
|
+
const balH = Math.max(topRailBottom - bottomRailTop, 0.05);
|
|
517
|
+
const pitch = GUARD_BALUSTER + gap;
|
|
518
|
+
const halfT = GUARD_BALUSTER / 2 / length;
|
|
519
|
+
for (let s = pitch; s < length - GUARD_BALUSTER; s += pitch) {
|
|
520
|
+
const t = s / length;
|
|
521
|
+
if (!clearOfPosts(t, halfT))
|
|
522
|
+
continue;
|
|
523
|
+
const part = createFenceCurveBlockPart(fence, t - halfT, t + halfT, bottomRailTop + balH / 2, balH, GUARD_BALUSTER);
|
|
524
|
+
if (part)
|
|
525
|
+
infill.push(part);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
else if (infillKind === 'cable') {
|
|
529
|
+
// ½ in cables `gap` apart from `clearance` up to under the top rail
|
|
530
|
+
for (let y = clearance; y < topRailBottom - GUARD_CABLE; y += gap) {
|
|
531
|
+
infill.push(...createFenceCurveBlockParts(fence, 0, 1, y + GUARD_CABLE / 2, GUARD_CABLE, GUARD_CABLE));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
// boards `gap` apart from `clearance` up to under the top rail
|
|
536
|
+
const boardT = Math.max(fence.thickness, 0.012);
|
|
537
|
+
for (let y = clearance; y + GUARD_BOARD_D <= topRailBottom + 1e-6; y += GUARD_BOARD_D + gap) {
|
|
538
|
+
infill.push(...createFenceCurveBlockParts(fence, 0, 1, y + GUARD_BOARD_D / 2, GUARD_BOARD_D, boardT));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return { posts, infill, base, rail };
|
|
542
|
+
}
|
|
543
|
+
function createFenceParts(fence, sharedEndpoints) {
|
|
544
|
+
if (fence.style === 'guard')
|
|
545
|
+
return createGuardFenceParts(fence);
|
|
546
|
+
if (fence.style === 'horizontal')
|
|
547
|
+
return createHorizontalFenceParts(fence, sharedEndpoints);
|
|
548
|
+
if (fence.style === 'picket')
|
|
549
|
+
return createPicketFenceParts(fence, sharedEndpoints);
|
|
550
|
+
const posts = [];
|
|
551
|
+
const infill = [];
|
|
552
|
+
const base = [];
|
|
553
|
+
const rail = [];
|
|
554
|
+
const length = Math.max(getFenceCenterlineLength(fence), 0.01);
|
|
555
|
+
const panelDepth = Math.max(fence.thickness, 0.03);
|
|
556
|
+
const clearance = Math.max(fence.groundClearance, 0);
|
|
557
|
+
const styleDefaults = getStyleDefaults(fence.style);
|
|
558
|
+
const baseHeight = Math.max(fence.baseHeight * styleDefaults.baseFactor, 0.04);
|
|
559
|
+
const topRailHeight = Math.max(fence.topRailHeight * styleDefaults.topFactor, 0.01);
|
|
560
|
+
const verticalHeight = Math.max(fence.height - baseHeight - topRailHeight, 0.08);
|
|
561
|
+
const postWidth = Math.max(fence.postSize * styleDefaults.postFactor, 0.01);
|
|
562
|
+
const spacing = Math.max(fence.postSpacing *
|
|
563
|
+
(fence.patternDistribution === 'automatic' ? styleDefaults.spacingFactor : 1), postWidth * 1.2);
|
|
564
|
+
const edgeInset = Math.max(fence.edgeInset ?? 0.015, 0.005);
|
|
565
|
+
const isFloating = fence.baseStyle === 'floating';
|
|
566
|
+
// 'raised': the base is a guard's bottom rail held `clearance` above the
|
|
567
|
+
// ground, the pickets ending on it; the end posts run to the ground
|
|
568
|
+
const isRaised = fence.baseStyle === 'raised';
|
|
569
|
+
const showInfill = fence.showInfill ?? true;
|
|
570
|
+
const baseY = isFloating || isRaised ? clearance : 0;
|
|
571
|
+
const effectiveBaseHeight = baseHeight;
|
|
572
|
+
const startInsetT = Math.min(0.499, edgeInset / length);
|
|
573
|
+
const endInsetT = Math.max(0.501, 1 - edgeInset / length);
|
|
574
|
+
if (!isFloating) {
|
|
575
|
+
base.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight / 2, effectiveBaseHeight, panelDepth * 1.05));
|
|
576
|
+
// the grounded kickboard's thin band above it — not on a bottom rail
|
|
577
|
+
if (!isRaised) {
|
|
578
|
+
base.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight + verticalHeight * 0.15, topRailHeight * 0.8, panelDepth * 0.35));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const count = showInfill && fence.style !== 'privacy'
|
|
582
|
+
? Math.max(2, Math.floor((length - edgeInset * 2) / spacing) + 1)
|
|
583
|
+
: 2;
|
|
584
|
+
const interiorStart = edgeInset + postWidth * 1.5;
|
|
585
|
+
const interiorEnd = length - edgeInset - postWidth * 1.5;
|
|
586
|
+
const positions = !showInfill || fence.patternDistribution === 'automatic'
|
|
587
|
+
? Array.from({ length: count }, (_, index) => count === 1 ? 0.5 : startInsetT + (endInsetT - startInsetT) * (index / (count - 1)))
|
|
588
|
+
: [
|
|
589
|
+
startInsetT,
|
|
590
|
+
...(interiorEnd >= interiorStart
|
|
591
|
+
? distributePattern(fence, interiorStart, interiorEnd, spacing, postWidth * 1.2).map((distance) => distance / length)
|
|
592
|
+
: []),
|
|
593
|
+
endInsetT,
|
|
594
|
+
];
|
|
595
|
+
const verticalY = baseY + effectiveBaseHeight + verticalHeight / 2;
|
|
596
|
+
for (let index = 0; index < positions.length; index += 1) {
|
|
597
|
+
const t = positions[index];
|
|
598
|
+
const isEdgePost = index === 0 || index === positions.length - 1;
|
|
599
|
+
const fullHeightPost = !showInfill || ((isFloating || isRaised) && isEdgePost);
|
|
600
|
+
const postHeight = fullHeightPost
|
|
601
|
+
? effectiveBaseHeight + verticalHeight + topRailHeight + clearance
|
|
602
|
+
: verticalHeight;
|
|
603
|
+
const postY = fullHeightPost ? postHeight / 2 : verticalY;
|
|
604
|
+
// End posts are the structural `posts` slot; the intermediate verticals are
|
|
605
|
+
// the `infill` slats (only present when showInfill adds them).
|
|
606
|
+
const slatHalfT = Math.max(0.0005, postWidth / (2 * length));
|
|
607
|
+
const slatStartT = Math.max(0, t - slatHalfT);
|
|
608
|
+
const slatEndT = Math.min(1, t + slatHalfT);
|
|
609
|
+
const slat = createFenceCurveBlockPart(fence, slatStartT, slatEndT, postY, postHeight, Math.max(panelDepth * 0.35 - 0.001, 0.011), isEdgePost
|
|
610
|
+
? 0
|
|
611
|
+
: getInfillOffset(fence, Math.max(panelDepth * 0.55, 0.018), Math.max(panelDepth * 0.35 - 0.001, 0.011)));
|
|
612
|
+
if (slat) {
|
|
613
|
+
if (isEdgePost)
|
|
614
|
+
slat.endpoint = index === 0 ? 'start' : 'end';
|
|
615
|
+
(isEdgePost ? posts : infill).push(slat);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (showInfill && fence.style === 'privacy') {
|
|
619
|
+
infill.push(...createFenceCurveBlockParts(fence, startInsetT, endInsetT, verticalY, verticalHeight, panelDepth * 0.7, MIN_CURVE_SEGMENT_LENGTH, getInfillOffset(fence, Math.max(panelDepth * 0.55, 0.018), panelDepth * 0.7)));
|
|
620
|
+
}
|
|
621
|
+
rail.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight + verticalHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
|
|
622
|
+
if (isFloating) {
|
|
623
|
+
rail.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
|
|
624
|
+
}
|
|
625
|
+
return { posts, infill, base, rail };
|
|
626
|
+
}
|
|
627
|
+
function createPicketFenceParts(fence, sharedEndpoints) {
|
|
628
|
+
const posts = [];
|
|
629
|
+
const infill = [];
|
|
630
|
+
const base = [];
|
|
631
|
+
const rail = [];
|
|
632
|
+
const length = Math.max(getFenceCenterlineLength(fence), 0.01);
|
|
633
|
+
const height = Math.max(fence.height, 0.3);
|
|
634
|
+
const postWidth = Math.max(fence.postSize, 0.02);
|
|
635
|
+
const postDepth = Math.max(fence.thickness, postWidth);
|
|
636
|
+
const baseHeight = fence.baseStyle === 'floating' ? 0 : Math.min(Math.max(fence.baseHeight, 0.04), height * 0.5);
|
|
637
|
+
const picketBottom = Math.min(baseHeight + Math.max(fence.groundClearance, 0), height * 0.75);
|
|
638
|
+
const topClearance = Math.min(Math.max(fence.picketTopClearance ?? 0, 0), height - picketBottom - 0.02);
|
|
639
|
+
const picketHeight = height - picketBottom - topClearance;
|
|
640
|
+
const railHeight = Math.min(Math.max(fence.topRailHeight, 0.01), picketHeight * 0.15);
|
|
641
|
+
// Separate the exposed faces at post/base and post/rail intersections.
|
|
642
|
+
const baseDepth = postDepth * 0.8;
|
|
643
|
+
const railDepth = postDepth + 2 * clampFencePicketRailProjection(fence.picketRailProjection, fence.postSize);
|
|
644
|
+
const variation = fence.picketProfile === 'level'
|
|
645
|
+
? 0
|
|
646
|
+
: Math.min(Math.max(fence.picketVariation, 0), picketHeight * 0.3);
|
|
647
|
+
const shortestPicketHeight = picketHeight - variation;
|
|
648
|
+
const picketDepth = Math.max(Math.min(fence.thickness * 0.5, 0.052), 0.01);
|
|
649
|
+
const picketWidth = Math.max(fence.picketWidth, 0.02);
|
|
650
|
+
const isCurved = (fence.path?.length ?? 0) >= 2 || Math.abs(fence.curveOffset ?? 0) > 0.0001;
|
|
651
|
+
// Curved runs need more clearance as neighboring boards turn toward each other.
|
|
652
|
+
const curveClearance = isCurved ? 0.03 : 0;
|
|
653
|
+
const spacing = Math.max(fence.picketSpacing, picketWidth + 0.01) + curveClearance;
|
|
654
|
+
const endInset = Math.max(fence.edgeInset, 0) + postWidth / 2;
|
|
655
|
+
const postCount = Math.max(1, Math.ceil(length / Math.max(fence.postSpacing, postWidth * 2)));
|
|
656
|
+
for (let index = 0; index <= postCount; index += 1) {
|
|
657
|
+
const frame = getFencePointAt(fence, index / postCount);
|
|
658
|
+
posts.push({
|
|
659
|
+
endpoint: index === 0 ? 'start' : index === postCount ? 'end' : undefined,
|
|
660
|
+
position: [frame.point.x, height / 2, frame.point.y],
|
|
661
|
+
rotationY: -frame.tangentAngle,
|
|
662
|
+
scale: [postWidth, height, postDepth],
|
|
663
|
+
});
|
|
664
|
+
if (fence.postCap !== 'none') {
|
|
665
|
+
const capHeight = postWidth * (fence.postCap === 'flat' ? 0.2 : 0.5);
|
|
666
|
+
posts.push({
|
|
667
|
+
endpoint: index === 0 ? 'start' : index === postCount ? 'end' : undefined,
|
|
668
|
+
position: [frame.point.x, height + capHeight / 2, frame.point.y],
|
|
669
|
+
rotationY: -frame.tangentAngle,
|
|
670
|
+
scale: [postWidth * 1.5, capHeight, postDepth * 1.5],
|
|
671
|
+
shape: fence.postCap === 'flat' ? 'box' : 'pyramid',
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const startInsetT = sharedEndpoints?.has('start') ? 0 : Math.min(0.499, endInset / length);
|
|
676
|
+
const endInsetT = sharedEndpoints?.has('end') ? 1 : Math.max(0.501, 1 - endInset / length);
|
|
677
|
+
const railEndInset = Math.max(0, postWidth / 2 - Math.min(postWidth * 0.2, 0.015));
|
|
678
|
+
const railStartT = sharedEndpoints?.has('start') ? 0 : Math.min(0.499, railEndInset / length);
|
|
679
|
+
const railEndT = sharedEndpoints?.has('end') ? 1 : Math.max(0.501, 1 - railEndInset / length);
|
|
680
|
+
if (baseHeight > 0) {
|
|
681
|
+
base.push(...createFenceCurveBlockParts(fence, startInsetT, endInsetT, baseHeight / 2, baseHeight, baseDepth));
|
|
682
|
+
}
|
|
683
|
+
const railCount = Math.min(3, Math.max(2, Math.round(fence.picketRailCount)));
|
|
684
|
+
for (let index = 0; index < railCount; index += 1) {
|
|
685
|
+
const centerY = picketBottom + shortestPicketHeight * (0.2 + (0.55 * index) / (railCount - 1));
|
|
686
|
+
rail.push(...createFenceCurveBlockParts(fence, railStartT, railEndT, centerY, railHeight, railDepth, 0.22));
|
|
687
|
+
}
|
|
688
|
+
// Lay out each bay independently so boards never intersect the structural posts.
|
|
689
|
+
const bayLength = length / postCount;
|
|
690
|
+
const usableLength = bayLength - 2 * endInset - picketWidth;
|
|
691
|
+
if (fence.showInfill && usableLength >= 0) {
|
|
692
|
+
const faceOffset = getInfillOffset(fence, railDepth, picketDepth);
|
|
693
|
+
for (let bay = 0; bay < postCount; bay += 1) {
|
|
694
|
+
const distances = fence.patternDistribution === 'automatic'
|
|
695
|
+
? (() => {
|
|
696
|
+
const count = Math.max(1, Math.floor(usableLength / spacing) + 1);
|
|
697
|
+
const occupied = (count - 1) * spacing;
|
|
698
|
+
return Array.from({ length: count }, (_, index) => bay * bayLength + (bayLength - occupied) / 2 + index * spacing);
|
|
699
|
+
})()
|
|
700
|
+
: distributePattern(fence, bay * bayLength + endInset + picketWidth / 2, (bay + 1) * bayLength - endInset - picketWidth / 2, fence.picketSpacing, picketWidth + curveClearance + 0.01);
|
|
701
|
+
for (let index = 0; index < distances.length; index += 1) {
|
|
702
|
+
const distance = distances[index];
|
|
703
|
+
const frame = getFenceCenterlineFrameAt(fence, distance / length);
|
|
704
|
+
const u = distances.length === 1 ? 0.5 : index / (distances.length - 1);
|
|
705
|
+
const arch = Math.sin(Math.PI * u);
|
|
706
|
+
const reduction = fence.picketProfile === 'arched'
|
|
707
|
+
? 1 - arch
|
|
708
|
+
: fence.picketProfile === 'scalloped'
|
|
709
|
+
? arch
|
|
710
|
+
: fence.picketProfile === 'alternating'
|
|
711
|
+
? index % 2
|
|
712
|
+
: 0;
|
|
713
|
+
const boardHeight = picketHeight - variation * reduction;
|
|
714
|
+
infill.push({
|
|
715
|
+
position: [
|
|
716
|
+
frame.point.x + frame.normal.x * faceOffset,
|
|
717
|
+
picketBottom + boardHeight / 2,
|
|
718
|
+
frame.point.y + frame.normal.y * faceOffset,
|
|
719
|
+
],
|
|
720
|
+
rotationY: -Math.atan2(frame.tangent.y, frame.tangent.x),
|
|
721
|
+
scale: [picketWidth, boardHeight, picketDepth],
|
|
722
|
+
shape: 'picket',
|
|
723
|
+
picketTop: fence.picketTop,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return { posts, infill, base, rail };
|
|
729
|
+
}
|
|
730
|
+
function fenceHeightTransitions(fence, heightAt) {
|
|
731
|
+
const length = getFenceCenterlineLength(fence);
|
|
732
|
+
const count = Math.max(2, Math.ceil(length / 0.06));
|
|
733
|
+
const transitions = [];
|
|
734
|
+
let previous = getFencePointAt(fence, 0).point;
|
|
735
|
+
let previousHeight = heightAt(previous.x, previous.y);
|
|
736
|
+
for (let index = 1; index <= count; index += 1) {
|
|
737
|
+
const point = getFencePointAt(fence, index / count).point;
|
|
738
|
+
const height = heightAt(point.x, point.y);
|
|
739
|
+
if (Math.abs(height - previousHeight) >= 0.12) {
|
|
740
|
+
const t = (index - 0.5) / count;
|
|
741
|
+
const last = transitions.at(-1);
|
|
742
|
+
if (last && (t - last.t) * length < 0.18) {
|
|
743
|
+
last.after = height;
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
transitions.push({ t, before: previousHeight, after: height });
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
previous = point;
|
|
750
|
+
previousHeight = height;
|
|
751
|
+
}
|
|
752
|
+
return transitions;
|
|
753
|
+
}
|
|
754
|
+
function createFencePathProgress(fence) {
|
|
755
|
+
const length = getFenceCenterlineLength(fence);
|
|
756
|
+
const count = Math.max(2, Math.ceil(length / 0.15));
|
|
757
|
+
const points = Array.from({ length: count + 1 }, (_, index) => getFencePointAt(fence, index / count).point);
|
|
758
|
+
return (x, z) => {
|
|
759
|
+
let bestT = 0;
|
|
760
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
761
|
+
for (let index = 1; index <= count; index += 1) {
|
|
762
|
+
const previous = points[index - 1];
|
|
763
|
+
const next = points[index];
|
|
764
|
+
const dx = next.x - previous.x;
|
|
765
|
+
const dz = next.y - previous.y;
|
|
766
|
+
const squared = dx * dx + dz * dz;
|
|
767
|
+
const fraction = squared > 1e-8
|
|
768
|
+
? THREE.MathUtils.clamp(((x - previous.x) * dx + (z - previous.y) * dz) / squared, 0, 1)
|
|
769
|
+
: 0;
|
|
770
|
+
const px = previous.x + dx * fraction;
|
|
771
|
+
const pz = previous.y + dz * fraction;
|
|
772
|
+
const distance = (x - px) ** 2 + (z - pz) ** 2;
|
|
773
|
+
if (distance < bestDistance) {
|
|
774
|
+
bestDistance = distance;
|
|
775
|
+
bestT = (index - 1 + fraction) / count;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return bestT;
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function transitionRailHeight(fence, rawHeight, transitions) {
|
|
782
|
+
const length = getFenceCenterlineLength(fence);
|
|
783
|
+
const halfSpan = Math.max(0.1, fence.transitionWidth / 2) / Math.max(length, 0.01);
|
|
784
|
+
const progress = createFencePathProgress(fence);
|
|
785
|
+
const ramps = transitions.map((transition) => {
|
|
786
|
+
const left = Math.max(0, transition.t - halfSpan);
|
|
787
|
+
const right = Math.min(1, transition.t + halfSpan);
|
|
788
|
+
const beforePoint = getFencePointAt(fence, left).point;
|
|
789
|
+
const afterPoint = getFencePointAt(fence, right).point;
|
|
790
|
+
return {
|
|
791
|
+
left,
|
|
792
|
+
right,
|
|
793
|
+
before: rawHeight(beforePoint.x, beforePoint.y),
|
|
794
|
+
after: rawHeight(afterPoint.x, afterPoint.y),
|
|
795
|
+
};
|
|
796
|
+
});
|
|
797
|
+
return (x, z) => {
|
|
798
|
+
const t = progress(x, z);
|
|
799
|
+
for (const ramp of ramps) {
|
|
800
|
+
if (t < ramp.left || t > ramp.right)
|
|
801
|
+
continue;
|
|
802
|
+
return THREE.MathUtils.lerp(ramp.before, ramp.after, (t - ramp.left) / Math.max(ramp.right - ramp.left, 1e-6));
|
|
803
|
+
}
|
|
804
|
+
return rawHeight(x, z);
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
export function createFenceRailHeightSampler(fence, heightAt) {
|
|
808
|
+
if (fence.surfaceMode === 'level' || fence.transitionMode !== 'slope')
|
|
809
|
+
return heightAt;
|
|
810
|
+
const transitions = fenceHeightTransitions(fence, heightAt);
|
|
811
|
+
return transitions.length > 0 ? transitionRailHeight(fence, heightAt, transitions) : heightAt;
|
|
812
|
+
}
|
|
813
|
+
function addTransitionPosts(fence, parts, transitions) {
|
|
814
|
+
const length = getFenceCenterlineLength(fence);
|
|
815
|
+
const width = Math.max(fence.postSize, fence.transitionMode === 'step' ? 0.22 : 0.07);
|
|
816
|
+
const depth = Math.max(fence.thickness, width);
|
|
817
|
+
for (const transition of transitions) {
|
|
818
|
+
if (fence.transitionMode === 'break') {
|
|
819
|
+
const offset = Math.min(0.08 / Math.max(length, 0.01), 0.2);
|
|
820
|
+
for (const t of [Math.max(0, transition.t - offset), Math.min(1, transition.t + offset)]) {
|
|
821
|
+
const frame = getFencePointAt(fence, t);
|
|
822
|
+
parts.posts.push({
|
|
823
|
+
position: [frame.point.x, fence.height / 2, frame.point.y],
|
|
824
|
+
rotationY: -frame.tangentAngle,
|
|
825
|
+
scale: [width, fence.height, depth],
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
const frame = getFencePointAt(fence, transition.t);
|
|
831
|
+
const rise = Math.abs(transition.after - transition.before);
|
|
832
|
+
parts.posts.push({
|
|
833
|
+
position: [frame.point.x, (fence.height + rise) / 2, frame.point.y],
|
|
834
|
+
rotationY: -frame.tangentAngle,
|
|
835
|
+
scale: [width, fence.height + rise, depth],
|
|
836
|
+
heightOverride: Math.min(transition.before, transition.after),
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function omitCrossingParts(parts, transitions) {
|
|
842
|
+
return parts.filter((part) => part.startT === undefined ||
|
|
843
|
+
part.endT === undefined ||
|
|
844
|
+
!transitions.some((transition) => transition.t > part.startT && transition.t < part.endT));
|
|
845
|
+
}
|
|
846
|
+
function cutFenceParts(fence, parts, features, heightAt, renderFeatures = true, gateLeafGeometries) {
|
|
847
|
+
const progress = createFencePathProgress(fence);
|
|
848
|
+
for (const slot of ['posts', 'infill', 'base', 'rail']) {
|
|
849
|
+
parts[slot] = parts[slot].flatMap((part) => {
|
|
850
|
+
if (part.startT === undefined || part.endT === undefined || !part.curveBlock) {
|
|
851
|
+
const t = progress(part.position[0], part.position[2]);
|
|
852
|
+
return features.some((feature) => t >= feature.startT && t <= feature.endT) ? [] : [part];
|
|
853
|
+
}
|
|
854
|
+
const ranges = [];
|
|
855
|
+
let cursor = part.startT;
|
|
856
|
+
for (const feature of features) {
|
|
857
|
+
if (feature.endT <= cursor || feature.startT >= part.endT)
|
|
858
|
+
continue;
|
|
859
|
+
if (feature.startT > cursor)
|
|
860
|
+
ranges.push([cursor, Math.min(feature.startT, part.endT)]);
|
|
861
|
+
cursor = Math.max(cursor, feature.endT);
|
|
862
|
+
if (cursor >= part.endT)
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
if (cursor < part.endT)
|
|
866
|
+
ranges.push([cursor, part.endT]);
|
|
867
|
+
if (ranges.length === 1 && ranges[0][0] === part.startT && ranges[0][1] === part.endT)
|
|
868
|
+
return [part];
|
|
869
|
+
part.geometry?.dispose();
|
|
870
|
+
return ranges.flatMap(([start, end]) => {
|
|
871
|
+
const block = part.curveBlock;
|
|
872
|
+
const clipped = createFenceCurveBlockPart(fence, start, end, block.centerY, block.height, block.depth, block.lateralOffset);
|
|
873
|
+
return clipped ? [clipped] : [];
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
if (!renderFeatures)
|
|
878
|
+
return;
|
|
879
|
+
for (const feature of features) {
|
|
880
|
+
const matchesFence = feature.matchFenceStyle !== false;
|
|
881
|
+
const styleDefaults = getStyleDefaults(fence.style);
|
|
882
|
+
const postWidth = matchesFence
|
|
883
|
+
? fence.style === 'picket'
|
|
884
|
+
? Math.max(fence.postSize, 0.02)
|
|
885
|
+
: fence.style === 'horizontal'
|
|
886
|
+
? Math.max(fence.postSize * 1.4, 0.04)
|
|
887
|
+
: Math.max(fence.postSize * styleDefaults.postFactor, 0.01)
|
|
888
|
+
: Math.max(fence.postSize, 0.05);
|
|
889
|
+
const postDepth = matchesFence && fence.style === 'horizontal'
|
|
890
|
+
? postWidth
|
|
891
|
+
: matchesFence && fence.style !== 'picket'
|
|
892
|
+
? Math.max(Math.max(fence.thickness, 0.03) * 0.35 - 0.001, 0.011)
|
|
893
|
+
: Math.max(fence.thickness, postWidth);
|
|
894
|
+
const { bottom, height, postBottom, postHeight } = getFenceFeatureDimensions(fence, feature);
|
|
895
|
+
const top = bottom + height;
|
|
896
|
+
if (feature.showPosts !== false)
|
|
897
|
+
for (const t of [feature.startT, feature.endT]) {
|
|
898
|
+
const frame = getFencePointAt(fence, t);
|
|
899
|
+
parts.posts.push({
|
|
900
|
+
position: [frame.point.x, postBottom + postHeight / 2, frame.point.y],
|
|
901
|
+
rotationY: -frame.tangentAngle,
|
|
902
|
+
scale: [postWidth, postHeight, postDepth],
|
|
903
|
+
});
|
|
904
|
+
if (matchesFence &&
|
|
905
|
+
fence.postCap !== 'none' &&
|
|
906
|
+
(fence.style === 'picket' || fence.style === 'horizontal')) {
|
|
907
|
+
const horizontal = fence.style === 'horizontal';
|
|
908
|
+
const capHeight = horizontal
|
|
909
|
+
? Math.max(postWidth * 0.32, 0.03)
|
|
910
|
+
: postWidth * (fence.postCap === 'flat' ? 0.2 : 0.5);
|
|
911
|
+
const capScale = horizontal ? 1.22 : 1.5;
|
|
912
|
+
parts.posts.push({
|
|
913
|
+
position: [
|
|
914
|
+
frame.point.x,
|
|
915
|
+
postBottom +
|
|
916
|
+
postHeight +
|
|
917
|
+
(horizontal && fence.postCap === 'pyramid' ? capHeight * 0.9 : capHeight / 2),
|
|
918
|
+
frame.point.y,
|
|
919
|
+
],
|
|
920
|
+
rotationY: -frame.tangentAngle,
|
|
921
|
+
scale: [
|
|
922
|
+
postWidth * (horizontal && fence.postCap === 'pyramid' ? 1.18 : capScale),
|
|
923
|
+
horizontal && fence.postCap === 'pyramid' ? capHeight * 1.8 : capHeight,
|
|
924
|
+
postDepth * (horizontal && fence.postCap === 'pyramid' ? 1.18 : capScale),
|
|
925
|
+
],
|
|
926
|
+
shape: fence.postCap === 'pyramid' ? 'pyramid' : 'box',
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (feature.kind !== 'gate')
|
|
931
|
+
continue;
|
|
932
|
+
const style = matchesFence
|
|
933
|
+
? fence.style
|
|
934
|
+
: !feature.style || feature.style === 'match'
|
|
935
|
+
? 'picket'
|
|
936
|
+
: feature.style;
|
|
937
|
+
const hostPostWidth = fence.style === 'picket'
|
|
938
|
+
? Math.max(fence.postSize, 0.02)
|
|
939
|
+
: fence.style === 'horizontal'
|
|
940
|
+
? Math.max(fence.postSize * 1.4, 0.04)
|
|
941
|
+
: Math.max(fence.postSize * styleDefaults.postFactor, 0.01);
|
|
942
|
+
const member = Math.min(matchesFence
|
|
943
|
+
? fence.style === 'rail'
|
|
944
|
+
? Math.max(fence.topRailHeight * styleDefaults.topFactor, 0.01)
|
|
945
|
+
: Math.max(fence.picketWidth, hostPostWidth * 0.55)
|
|
946
|
+
: (feature.frameWidth ?? 0.055), height / 4);
|
|
947
|
+
const matchedDepth = fence.style === 'picket'
|
|
948
|
+
? Math.max(Math.min(fence.thickness * 0.5, 0.052), 0.01)
|
|
949
|
+
: fence.style === 'slat'
|
|
950
|
+
? Math.max(fence.thickness * 0.35, 0.011)
|
|
951
|
+
: Math.max(fence.thickness, 0.03);
|
|
952
|
+
const depth = matchesFence ? matchedDepth : (feature.thickness ?? 0.06);
|
|
953
|
+
const animatedLeaves = getFenceGateLeaves(fence, feature);
|
|
954
|
+
const fullyOpenLeaves = getFenceGateLeaves(fence, { ...feature, openAngle: 90 });
|
|
955
|
+
const leaves = gateLeafGeometries
|
|
956
|
+
? getFenceGateLeaves(fence, { ...feature, openAngle: 0 })
|
|
957
|
+
: animatedLeaves;
|
|
958
|
+
// A swinging leaf is rigid: both leaves share the higher jamb elevation.
|
|
959
|
+
const support = Math.max(...[feature.startT, feature.endT].map((t) => {
|
|
960
|
+
const point = getFencePointAt(fence, t).point;
|
|
961
|
+
return heightAt?.(point.x, point.y) ?? 0;
|
|
962
|
+
}));
|
|
963
|
+
for (const [leafIndex, leaf] of leaves.entries()) {
|
|
964
|
+
const leafParts = [];
|
|
965
|
+
const width = leaf.width;
|
|
966
|
+
const frame = Math.min(member, width / 4);
|
|
967
|
+
const innerWidth = width - frame * 2;
|
|
968
|
+
const innerHeight = height - frame * 2;
|
|
969
|
+
const cos = Math.cos(leaf.rotation);
|
|
970
|
+
const sin = Math.sin(leaf.rotation);
|
|
971
|
+
const add = (x, y, z, w, h, d, rotationZ = 0, picket = false) => {
|
|
972
|
+
leafParts.push({
|
|
973
|
+
position: [leaf.hinge.x + cos * x - sin * z, y, leaf.hinge.y + sin * x + cos * z],
|
|
974
|
+
rotationY: -leaf.rotation,
|
|
975
|
+
rotationZ,
|
|
976
|
+
scale: [w, h, d],
|
|
977
|
+
heightOverride: support,
|
|
978
|
+
shape: picket ? 'picket' : 'box',
|
|
979
|
+
picketTop: fence.picketTop,
|
|
980
|
+
});
|
|
981
|
+
};
|
|
982
|
+
for (const x of [frame / 2, width - frame / 2])
|
|
983
|
+
add(x, bottom + height / 2, 0, frame, height, depth);
|
|
984
|
+
const railThickness = matchesFence && style === 'rail'
|
|
985
|
+
? Math.max(fence.topRailHeight * styleDefaults.topFactor, 0.01)
|
|
986
|
+
: frame;
|
|
987
|
+
for (const y of [
|
|
988
|
+
bottom + railThickness / 2,
|
|
989
|
+
top + (style === 'rail' && matchesFence ? railThickness / 2 : -railThickness / 2),
|
|
990
|
+
])
|
|
991
|
+
add(width / 2, y, 0, innerWidth, railThickness, depth);
|
|
992
|
+
const matchedSpacing = fence.style === 'picket'
|
|
993
|
+
? fence.picketSpacing
|
|
994
|
+
: fence.style === 'slat'
|
|
995
|
+
? fence.postSpacing * styleDefaults.spacingFactor
|
|
996
|
+
: fence.style === 'horizontal'
|
|
997
|
+
? 0.155 + Math.max(fence.slatGap, 0)
|
|
998
|
+
: 0.15;
|
|
999
|
+
const spacing = Math.max(matchesFence ? matchedSpacing : (feature.spacing ?? 0.15), 0.04);
|
|
1000
|
+
const matchedBoardWidth = fence.style === 'picket'
|
|
1001
|
+
? fence.picketWidth
|
|
1002
|
+
: fence.style === 'slat'
|
|
1003
|
+
? hostPostWidth
|
|
1004
|
+
: fence.style === 'horizontal'
|
|
1005
|
+
? 0.14
|
|
1006
|
+
: 0.055;
|
|
1007
|
+
const board = Math.min(matchesFence ? matchedBoardWidth : (feature.boardWidth ?? 0.055), innerWidth);
|
|
1008
|
+
if (style === 'privacy')
|
|
1009
|
+
add(width / 2, bottom + height / 2, 0, innerWidth, innerHeight, depth * 0.6);
|
|
1010
|
+
else if (style === 'horizontal') {
|
|
1011
|
+
const count = Math.max(1, Math.floor(innerHeight / spacing));
|
|
1012
|
+
const boardHeight = Math.min(board, (innerHeight / count) * 0.8);
|
|
1013
|
+
for (let i = 0; i < count; i++)
|
|
1014
|
+
add(width / 2, bottom + frame + (innerHeight * (i + 0.5)) / count, 0, innerWidth, boardHeight, depth * 0.65);
|
|
1015
|
+
}
|
|
1016
|
+
else if (style !== 'rail') {
|
|
1017
|
+
const count = Math.max(1, Math.floor(innerWidth / Math.max(spacing, board + 0.015)));
|
|
1018
|
+
for (let i = 0; i < count; i++)
|
|
1019
|
+
add(frame + (innerWidth * (i + 0.5)) / count, bottom + height / 2, 0, Math.min(board, (innerWidth / count) * 0.8), innerHeight, depth * 0.65, 0, style === 'picket');
|
|
1020
|
+
}
|
|
1021
|
+
if (feature.brace && feature.brace !== 'none') {
|
|
1022
|
+
const angle = Math.atan2(innerHeight, innerWidth);
|
|
1023
|
+
add(width / 2, bottom + height / 2, depth * 0.65, Math.hypot(innerWidth, innerHeight), frame * 0.7, depth * 0.35, angle);
|
|
1024
|
+
if (feature.brace === 'cross')
|
|
1025
|
+
add(width / 2, bottom + height / 2, -depth * 0.65, Math.hypot(innerWidth, innerHeight), frame * 0.7, depth * 0.35, -angle);
|
|
1026
|
+
}
|
|
1027
|
+
if (feature.showHardware !== false) {
|
|
1028
|
+
for (const y of [bottom + height * 0.25, bottom + height * 0.75])
|
|
1029
|
+
add(0, y, depth * 0.7, frame * 1.3, 0.075, depth * 0.6);
|
|
1030
|
+
add(width - frame, bottom + height * 0.55, depth * 0.8, Math.min(0.12, width / 4), 0.025, depth * 0.6);
|
|
1031
|
+
}
|
|
1032
|
+
if (gateLeafGeometries) {
|
|
1033
|
+
gateLeafGeometries.push({
|
|
1034
|
+
geometry: mergeFenceParts(leafParts, heightAt),
|
|
1035
|
+
hinge: { x: leaf.hinge.x, z: leaf.hinge.y },
|
|
1036
|
+
rotationY: -(animatedLeaves[leafIndex].rotation - leaf.rotation),
|
|
1037
|
+
openRotationY: -(fullyOpenLeaves[leafIndex].rotation - leaf.rotation),
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
parts.infill.push(...leafParts);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function mergeFenceParts(parts, heightAt) {
|
|
1047
|
+
// An empty slot group (e.g. infill with showInfill off, or base on a floating
|
|
1048
|
+
// fence) must not reach mergeGeometries — it throws on an empty array. The
|
|
1049
|
+
// empty geometry has no position attribute, so the renderer skips its mesh.
|
|
1050
|
+
if (parts.length === 0)
|
|
1051
|
+
return new THREE.BufferGeometry();
|
|
1052
|
+
const geometries = parts.map((part) => {
|
|
1053
|
+
const geometry = createFencePartGeometry(part);
|
|
1054
|
+
if (heightAt) {
|
|
1055
|
+
const positions = geometry.getAttribute('position');
|
|
1056
|
+
if (positions) {
|
|
1057
|
+
for (let index = 0; index < positions.count; index += 1) {
|
|
1058
|
+
const x = positions.getX(index);
|
|
1059
|
+
const z = positions.getZ(index);
|
|
1060
|
+
const lift = part.heightOverride !== undefined
|
|
1061
|
+
? part.heightOverride
|
|
1062
|
+
: part.geometry
|
|
1063
|
+
? heightAt(x, z)
|
|
1064
|
+
: heightAt(part.position[0], part.position[2]);
|
|
1065
|
+
positions.setY(index, positions.getY(index) + lift);
|
|
1066
|
+
}
|
|
1067
|
+
positions.needsUpdate = true;
|
|
1068
|
+
geometry.computeVertexNormals();
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
if (!geometry.index)
|
|
1072
|
+
return geometry;
|
|
1073
|
+
const nonIndexed = geometry.toNonIndexed();
|
|
1074
|
+
geometry.dispose();
|
|
1075
|
+
return nonIndexed;
|
|
1076
|
+
});
|
|
1077
|
+
const merged = mergeGeometries(geometries, false) ?? new THREE.BufferGeometry();
|
|
1078
|
+
geometries.forEach((geometry) => {
|
|
1079
|
+
geometry.dispose();
|
|
1080
|
+
});
|
|
1081
|
+
const mergedUv = merged.getAttribute('uv');
|
|
1082
|
+
if (mergedUv) {
|
|
1083
|
+
merged.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(mergedUv.array), 2));
|
|
1084
|
+
}
|
|
1085
|
+
merged.computeVertexNormals();
|
|
1086
|
+
return merged;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Geometry split by paint slot — posts, infill, base, rail — each a separate
|
|
1090
|
+
* merged BufferGeometry (empty ones included) so the fence renderer can give
|
|
1091
|
+
* each its own material + `userData.slotId`. Slots match the panel's build
|
|
1092
|
+
* options 1:1.
|
|
1093
|
+
*/
|
|
1094
|
+
export function generateFenceSlotGeometries(fence, heightAt, mode = 'all', omitEndpointPosts, cornerNeighbors, gateLeafGeometries) {
|
|
1095
|
+
const parts = mode === 'features'
|
|
1096
|
+
? { posts: [], infill: [], base: [], rail: [] }
|
|
1097
|
+
: createFenceParts(fence, omitEndpointPosts);
|
|
1098
|
+
if (omitEndpointPosts?.size) {
|
|
1099
|
+
parts.posts = parts.posts.filter((part) => !part.endpoint || !omitEndpointPosts.has(part.endpoint));
|
|
1100
|
+
}
|
|
1101
|
+
if (cornerNeighbors) {
|
|
1102
|
+
for (const slot of ['base', 'rail', 'infill']) {
|
|
1103
|
+
if (slot === 'infill' && fence.style !== 'horizontal')
|
|
1104
|
+
continue;
|
|
1105
|
+
for (const part of parts[slot]) {
|
|
1106
|
+
for (const endpoint of ['start', 'end']) {
|
|
1107
|
+
const neighbor = cornerNeighbors[endpoint];
|
|
1108
|
+
if (neighbor)
|
|
1109
|
+
miterFencePartEnd(fence, part, endpoint, neighbor);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
const features = resolveFenceFeatures(fence);
|
|
1115
|
+
if (features.length > 0)
|
|
1116
|
+
cutFenceParts(fence, parts, features, heightAt, mode !== 'body', gateLeafGeometries);
|
|
1117
|
+
const transitions = mode !== 'features' && heightAt && fence.surfaceMode !== 'level'
|
|
1118
|
+
? fenceHeightTransitions(fence, heightAt)
|
|
1119
|
+
: [];
|
|
1120
|
+
if (heightAt && transitions.length > 0 && fence.transitionMode !== 'slope') {
|
|
1121
|
+
addTransitionPosts(fence, parts, transitions);
|
|
1122
|
+
parts.rail = omitCrossingParts(parts.rail, transitions);
|
|
1123
|
+
parts.base = omitCrossingParts(parts.base, transitions);
|
|
1124
|
+
if (fence.style === 'horizontal')
|
|
1125
|
+
parts.infill = omitCrossingParts(parts.infill, transitions);
|
|
1126
|
+
}
|
|
1127
|
+
const railHeightAt = heightAt && transitions.length > 0 && fence.transitionMode === 'slope'
|
|
1128
|
+
? transitionRailHeight(fence, heightAt, transitions)
|
|
1129
|
+
: heightAt;
|
|
1130
|
+
return {
|
|
1131
|
+
posts: mergeFenceParts(parts.posts, heightAt),
|
|
1132
|
+
infill: mergeFenceParts(parts.infill, fence.style === 'horizontal' ? railHeightAt : heightAt),
|
|
1133
|
+
base: mergeFenceParts(parts.base, railHeightAt),
|
|
1134
|
+
rail: mergeFenceParts(parts.rail, railHeightAt),
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
export function generateFenceGeometry(fence, heightAt) {
|
|
1138
|
+
const slots = generateFenceSlotGeometries(fence, heightAt);
|
|
1139
|
+
const geometries = Object.values(slots).filter((geometry) => geometry.getAttribute('position'));
|
|
1140
|
+
const merged = mergeGeometries(geometries, false) ?? new THREE.BufferGeometry();
|
|
1141
|
+
for (const geometry of Object.values(slots))
|
|
1142
|
+
geometry.dispose();
|
|
1143
|
+
return merged;
|
|
1144
|
+
}
|