@pascal-app/core 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/events/bus.d.ts +30 -2
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/hooks/scene-registry/item-clip-registry.d.ts +18 -0
- package/dist/hooks/scene-registry/item-clip-registry.d.ts.map +1 -0
- package/dist/hooks/scene-registry/item-clip-registry.js +10 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts +8 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +27 -1
- package/dist/hooks/scene-registry/scene-registry.test.d.ts +2 -0
- package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +1 -0
- package/dist/hooks/scene-registry/scene-registry.test.js +26 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +93 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +69 -12
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +476 -195
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +19 -1
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +2 -0
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +363 -0
- package/dist/index.d.ts +16 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -5
- package/dist/lib/level-name.d.ts +4 -0
- package/dist/lib/level-name.d.ts.map +1 -0
- package/dist/lib/level-name.js +10 -0
- package/dist/lib/measurement-geometry.d.ts +19 -0
- package/dist/lib/measurement-geometry.d.ts.map +1 -0
- package/dist/lib/measurement-geometry.js +232 -0
- package/dist/lib/measurement-geometry.test.d.ts +2 -0
- package/dist/lib/measurement-geometry.test.d.ts.map +1 -0
- package/dist/lib/measurement-geometry.test.js +102 -0
- package/dist/lib/polygon-geometry.d.ts +0 -1
- package/dist/lib/polygon-geometry.d.ts.map +1 -1
- package/dist/lib/polygon-geometry.js +0 -18
- package/dist/lib/selection-proxy.d.ts +4 -0
- package/dist/lib/selection-proxy.d.ts.map +1 -0
- package/dist/lib/selection-proxy.js +12 -0
- package/dist/lib/slab-polygon.d.ts +31 -2
- package/dist/lib/slab-polygon.d.ts.map +1 -1
- package/dist/lib/slab-polygon.js +525 -31
- package/dist/lib/slab-polygon.test.d.ts +2 -0
- package/dist/lib/slab-polygon.test.d.ts.map +1 -0
- package/dist/lib/slab-polygon.test.js +635 -0
- package/dist/lib/slots.d.ts +14 -0
- package/dist/lib/slots.d.ts.map +1 -0
- package/dist/lib/slots.js +26 -0
- package/dist/lib/space-detection.d.ts +25 -2
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +414 -118
- package/dist/lib/space-detection.test.js +301 -2
- package/dist/lib/wall-distance.d.ts +60 -0
- package/dist/lib/wall-distance.d.ts.map +1 -0
- package/dist/lib/wall-distance.js +85 -0
- package/dist/lib/zone-quantities.d.ts +21 -0
- package/dist/lib/zone-quantities.d.ts.map +1 -0
- package/dist/lib/zone-quantities.js +183 -0
- package/dist/lib/zone-quantities.test.d.ts +2 -0
- package/dist/lib/zone-quantities.test.d.ts.map +1 -0
- package/dist/lib/zone-quantities.test.js +82 -0
- package/dist/material-library.d.ts +20 -1
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +2181 -441
- package/dist/registry/handles.d.ts +75 -2
- package/dist/registry/handles.d.ts.map +1 -1
- package/dist/registry/index.d.ts +3 -3
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/registry/registry.d.ts +46 -3
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +82 -1
- package/dist/registry/registry.test.js +14 -1
- package/dist/registry/subtree.d.ts.map +1 -1
- package/dist/registry/subtree.js +4 -0
- package/dist/registry/subtree.test.js +30 -0
- package/dist/registry/types.d.ts +717 -38
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +21 -5
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +17 -3
- package/dist/schema/material.d.ts +3 -2
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +7 -5
- package/dist/schema/material.test.d.ts +2 -0
- package/dist/schema/material.test.d.ts.map +1 -0
- package/dist/schema/material.test.js +43 -0
- package/dist/schema/nodes/box-vent.d.ts +2 -2
- package/dist/schema/nodes/cabinet.d.ts +534 -0
- package/dist/schema/nodes/cabinet.d.ts.map +1 -0
- package/dist/schema/nodes/cabinet.js +137 -0
- package/dist/schema/nodes/ceiling.d.ts +3 -2
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +4 -0
- package/dist/schema/nodes/chimney.d.ts +4 -4
- package/dist/schema/nodes/column.d.ts +3 -2
- package/dist/schema/nodes/column.d.ts.map +1 -1
- package/dist/schema/nodes/column.js +3 -0
- package/dist/schema/nodes/cupola.d.ts +2 -2
- package/dist/schema/nodes/door.d.ts +13 -5
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/door.js +12 -0
- package/dist/schema/nodes/dormer.d.ts +8 -8
- package/dist/schema/nodes/dormer.js +1 -1
- package/dist/schema/nodes/downspout.d.ts +2 -2
- package/dist/schema/nodes/duct-fitting.d.ts +91 -0
- package/dist/schema/nodes/duct-fitting.d.ts.map +1 -0
- package/dist/schema/nodes/duct-fitting.js +92 -0
- package/dist/schema/nodes/duct-segment.d.ts +61 -0
- package/dist/schema/nodes/duct-segment.d.ts.map +1 -0
- package/dist/schema/nodes/duct-segment.js +73 -0
- package/dist/schema/nodes/duct-terminal.d.ts +57 -0
- package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
- package/dist/schema/nodes/duct-terminal.js +51 -0
- package/dist/schema/nodes/elevator.d.ts +3 -2
- package/dist/schema/nodes/elevator.d.ts.map +1 -1
- package/dist/schema/nodes/elevator.js +3 -0
- package/dist/schema/nodes/eyebrow-vent.d.ts +2 -2
- package/dist/schema/nodes/fence.d.ts +18 -2
- package/dist/schema/nodes/fence.d.ts.map +1 -1
- package/dist/schema/nodes/fence.js +25 -2
- package/dist/schema/nodes/gutter.d.ts +2 -2
- package/dist/schema/nodes/hvac-equipment.d.ts +63 -0
- package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
- package/dist/schema/nodes/hvac-equipment.js +55 -0
- package/dist/schema/nodes/item.d.ts +8 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +11 -0
- package/dist/schema/nodes/level.d.ts +1 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +2 -0
- package/dist/schema/nodes/lineset.d.ts +42 -0
- package/dist/schema/nodes/lineset.d.ts.map +1 -0
- package/dist/schema/nodes/lineset.js +38 -0
- package/dist/schema/nodes/liquid-line.d.ts +34 -0
- package/dist/schema/nodes/liquid-line.d.ts.map +1 -0
- package/dist/schema/nodes/liquid-line.js +24 -0
- package/dist/schema/nodes/measurement.d.ts +306 -0
- package/dist/schema/nodes/measurement.d.ts.map +1 -0
- package/dist/schema/nodes/measurement.js +87 -0
- package/dist/schema/nodes/measurement.test.d.ts +2 -0
- package/dist/schema/nodes/measurement.test.d.ts.map +1 -0
- package/dist/schema/nodes/measurement.test.js +139 -0
- package/dist/schema/nodes/pipe-fitting.d.ts +56 -0
- package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-fitting.js +45 -0
- package/dist/schema/nodes/pipe-segment.d.ts +48 -0
- package/dist/schema/nodes/pipe-segment.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-segment.js +36 -0
- package/dist/schema/nodes/pipe-trap.d.ts +43 -0
- package/dist/schema/nodes/pipe-trap.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-trap.js +36 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +2 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +1 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.js +205 -0
- package/dist/schema/nodes/ridge-vent.d.ts +18 -3
- package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
- package/dist/schema/nodes/ridge-vent.js +221 -3
- package/dist/schema/nodes/roof-segment-shape.d.ts +90 -0
- package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-shape.js +318 -0
- package/dist/schema/nodes/roof-segment-shape.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-shape.test.js +175 -0
- package/dist/schema/nodes/roof-segment-surface.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-surface.test.js +48 -0
- package/dist/schema/nodes/roof-segment-trim.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-trim.test.js +107 -0
- package/dist/schema/nodes/roof-segment-walls.d.ts +101 -0
- package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-walls.js +330 -0
- package/dist/schema/nodes/roof-segment-walls.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-walls.test.js +92 -0
- package/dist/schema/nodes/roof-segment.d.ts +84 -8
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment.js +290 -5
- package/dist/schema/nodes/roof.d.ts +8 -8
- package/dist/schema/nodes/shelf.d.ts +3 -2
- package/dist/schema/nodes/shelf.d.ts.map +1 -1
- package/dist/schema/nodes/shelf.js +6 -0
- package/dist/schema/nodes/skylight.d.ts +10 -10
- package/dist/schema/nodes/slab.d.ts +3 -2
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +4 -0
- package/dist/schema/nodes/solar-panel.d.ts +4 -4
- package/dist/schema/nodes/stair-segment.d.ts +4 -4
- package/dist/schema/nodes/stair.d.ts +13 -12
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +3 -0
- package/dist/schema/nodes/turbine-vent.d.ts +2 -2
- package/dist/schema/nodes/wall.d.ts +245 -6
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +257 -0
- package/dist/schema/nodes/wall.test.d.ts +2 -0
- package/dist/schema/nodes/wall.test.d.ts.map +1 -0
- package/dist/schema/nodes/wall.test.js +208 -0
- package/dist/schema/nodes/window.d.ts +11 -3
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +12 -0
- package/dist/schema/nodes/zone.d.ts +2 -0
- package/dist/schema/nodes/zone.d.ts.map +1 -1
- package/dist/schema/nodes/zone.js +6 -0
- package/dist/schema/scene-material.d.ts +41 -0
- package/dist/schema/scene-material.d.ts.map +1 -0
- package/dist/schema/scene-material.js +9 -0
- package/dist/schema/types.d.ts +1089 -94
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +23 -0
- package/dist/services/alignment-anchors.d.ts.map +1 -1
- package/dist/services/alignment-anchors.js +30 -1
- package/dist/services/alignment.d.ts +5 -0
- package/dist/services/alignment.d.ts.map +1 -1
- package/dist/services/alignment.js +2 -0
- package/dist/services/hosting.d.ts +8 -0
- package/dist/services/hosting.d.ts.map +1 -1
- package/dist/services/hosting.js +13 -4
- package/dist/services/hosting.test.js +32 -1
- package/dist/services/index.d.ts +8 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +8 -2
- package/dist/services/level-height.d.ts +28 -0
- package/dist/services/level-height.d.ts.map +1 -0
- package/dist/services/level-height.js +62 -0
- package/dist/services/opening-guides.d.ts +141 -0
- package/dist/services/opening-guides.d.ts.map +1 -0
- package/dist/services/opening-guides.js +267 -0
- package/dist/services/opening-guides.test.d.ts +2 -0
- package/dist/services/opening-guides.test.d.ts.map +1 -0
- package/dist/services/opening-guides.test.js +208 -0
- package/dist/services/port-connectivity.d.ts +115 -0
- package/dist/services/port-connectivity.d.ts.map +1 -0
- package/dist/services/port-connectivity.js +300 -0
- package/dist/services/port-connectivity.test.d.ts +2 -0
- package/dist/services/port-connectivity.test.d.ts.map +1 -0
- package/dist/services/port-connectivity.test.js +297 -0
- package/dist/services/riser-diagram.d.ts +40 -0
- package/dist/services/riser-diagram.d.ts.map +1 -0
- package/dist/services/riser-diagram.js +110 -0
- package/dist/services/riser-diagram.test.d.ts +2 -0
- package/dist/services/riser-diagram.test.d.ts.map +1 -0
- package/dist/services/riser-diagram.test.js +66 -0
- package/dist/services/snap.d.ts +9 -1
- package/dist/services/snap.d.ts.map +1 -1
- package/dist/services/snap.js +21 -1
- package/dist/services/snap.test.js +43 -1
- package/dist/services/system-graph.d.ts +30 -0
- package/dist/services/system-graph.d.ts.map +1 -0
- package/dist/services/system-graph.js +164 -0
- package/dist/services/system-graph.test.d.ts +2 -0
- package/dist/services/system-graph.test.d.ts.map +1 -0
- package/dist/services/system-graph.test.js +138 -0
- package/dist/services/validate-dwv.d.ts +32 -0
- package/dist/services/validate-dwv.d.ts.map +1 -0
- package/dist/services/validate-dwv.js +118 -0
- package/dist/services/validate-dwv.test.d.ts +2 -0
- package/dist/services/validate-dwv.test.d.ts.map +1 -0
- package/dist/services/validate-dwv.test.js +109 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +94 -1
- package/dist/store/actions/ridge-vent-update.test.d.ts +2 -0
- package/dist/store/actions/ridge-vent-update.test.d.ts.map +1 -0
- package/dist/store/actions/ridge-vent-update.test.js +264 -0
- package/dist/store/history-control.d.ts +11 -0
- package/dist/store/history-control.d.ts.map +1 -1
- package/dist/store/history-control.js +33 -0
- package/dist/store/use-scene-dirty-tracking.test.d.ts +2 -0
- package/dist/store/use-scene-dirty-tracking.test.d.ts.map +1 -0
- package/dist/store/use-scene-dirty-tracking.test.js +68 -0
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts +2 -0
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +1 -0
- package/dist/store/use-scene-measurement-roundtrip.test.js +97 -0
- package/dist/store/use-scene-plugins.test.d.ts +2 -0
- package/dist/store/use-scene-plugins.test.d.ts.map +1 -0
- package/dist/store/use-scene-plugins.test.js +51 -0
- package/dist/store/use-scene-wall-slot-migration.test.d.ts +2 -0
- package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +1 -0
- package/dist/store/use-scene-wall-slot-migration.test.js +210 -0
- package/dist/store/use-scene-window-migration.test.d.ts +2 -0
- package/dist/store/use-scene-window-migration.test.d.ts.map +1 -0
- package/dist/store/use-scene-window-migration.test.js +85 -0
- package/dist/store/use-scene.d.ts +17 -2
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +358 -51
- package/dist/systems/elevator/elevator-service.js +1 -1
- package/dist/systems/fence/fence-centerline.d.ts +12 -0
- package/dist/systems/fence/fence-centerline.d.ts.map +1 -0
- package/dist/systems/fence/fence-centerline.js +37 -0
- package/dist/systems/fence/fence-spline.d.ts +39 -0
- package/dist/systems/fence/fence-spline.d.ts.map +1 -0
- package/dist/systems/fence/fence-spline.js +196 -0
- package/dist/systems/fence/fence-spline.test.d.ts +2 -0
- package/dist/systems/fence/fence-spline.test.d.ts.map +1 -0
- package/dist/systems/fence/fence-spline.test.js +136 -0
- package/dist/systems/stair/stair-footprint.d.ts.map +1 -1
- package/dist/systems/stair/stair-footprint.js +21 -11
- package/dist/systems/stair/stair-opening-preview.d.ts +1139 -469
- package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.js +1 -18
- package/dist/systems/stair/stair-opening-sync.test.js +39 -0
- package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
- package/dist/systems/wall/wall-mitering.js +38 -2
- package/dist/systems/wall/wall-mitering.test.d.ts +2 -0
- package/dist/systems/wall/wall-mitering.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-mitering.test.js +63 -0
- package/dist/utils/clone-scene-graph.d.ts +1 -0
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +23 -3
- package/dist/utils/clone-scene-graph.test.js +3 -0
- package/dist/utils/heal-scene-graph.d.ts +18 -0
- package/dist/utils/heal-scene-graph.d.ts.map +1 -0
- package/dist/utils/heal-scene-graph.js +90 -0
- package/dist/utils/heal-scene-graph.test.d.ts +2 -0
- package/dist/utils/heal-scene-graph.test.d.ts.map +1 -0
- package/dist/utils/heal-scene-graph.test.js +88 -0
- package/dist/validation/validate-build-json.d.ts +1 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -1
- package/dist/validation/validate-build-json.js +31 -2
- package/package.json +3 -3
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { ROOF_SHAPE_DEFAULTS } from './roof-segment';
|
|
2
|
+
export function getRoofShapeRatios(input) {
|
|
3
|
+
return {
|
|
4
|
+
gambrelLowerWidthRatio: input.gambrelLowerWidthRatio ?? ROOF_SHAPE_DEFAULTS.gambrelLowerWidthRatio,
|
|
5
|
+
mansardSteepWidthRatio: input.mansardSteepWidthRatio ?? ROOF_SHAPE_DEFAULTS.mansardSteepWidthRatio,
|
|
6
|
+
dutchHipWidthRatio: input.dutchHipWidthRatio ?? ROOF_SHAPE_DEFAULTS.dutchHipWidthRatio,
|
|
7
|
+
dutchHipHeightRatio: input.dutchHipHeightRatio ?? ROOF_SHAPE_DEFAULTS.dutchHipHeightRatio,
|
|
8
|
+
dutchWaistLengthRatio: input.dutchWaistLengthRatio ?? ROOF_SHAPE_DEFAULTS.dutchWaistLengthRatio,
|
|
9
|
+
dutchGabletRake: input.dutchGabletRake ?? ROOF_SHAPE_DEFAULTS.dutchGabletRake,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function getDutchRoofShapeMetrics(input) {
|
|
13
|
+
const fallbackInset = Math.min(input.baseW, input.baseD) * input.shapeRatios.dutchHipWidthRatio;
|
|
14
|
+
const maxI = Math.max(0, Math.min(input.w, input.d) / 2 - 0.005);
|
|
15
|
+
const inset = Math.min(Math.max(0, input.dutchI ?? fallbackInset), maxI);
|
|
16
|
+
const peakHeight = input.wh + Math.max(0.001, input.rh);
|
|
17
|
+
const middleHeight = input.wh + input.rh * input.shapeRatios.dutchHipHeightRatio;
|
|
18
|
+
if (!(inset > 0.001) || !(peakHeight > middleHeight + 0.001))
|
|
19
|
+
return null;
|
|
20
|
+
if (input.w >= input.d) {
|
|
21
|
+
const innerWaistHalfX = Math.max(0, (input.w / 2 - inset) * input.shapeRatios.dutchWaistLengthRatio);
|
|
22
|
+
const innerWaistHalfZ = Math.max(0, input.d / 2 - inset);
|
|
23
|
+
if (!(innerWaistHalfX > 0.001 && innerWaistHalfZ > 0.001))
|
|
24
|
+
return null;
|
|
25
|
+
const rakeReach = Math.min(Math.max(0, input.shapeRatios.dutchGabletRake), Math.max(0, input.w / 2 - innerWaistHalfX) * 0.98);
|
|
26
|
+
return {
|
|
27
|
+
axis: 'width',
|
|
28
|
+
inset,
|
|
29
|
+
middleHeight,
|
|
30
|
+
peakHeight,
|
|
31
|
+
rakeReach,
|
|
32
|
+
innerWaistHalfX,
|
|
33
|
+
innerWaistHalfZ,
|
|
34
|
+
outerWaistHalfX: innerWaistHalfX + rakeReach,
|
|
35
|
+
outerWaistHalfZ: innerWaistHalfZ,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const innerWaistHalfX = Math.max(0, input.w / 2 - inset);
|
|
39
|
+
const innerWaistHalfZ = Math.max(0, (input.d / 2 - inset) * input.shapeRatios.dutchWaistLengthRatio);
|
|
40
|
+
if (!(innerWaistHalfX > 0.001 && innerWaistHalfZ > 0.001))
|
|
41
|
+
return null;
|
|
42
|
+
const rakeReach = Math.min(Math.max(0, input.shapeRatios.dutchGabletRake), Math.max(0, input.d / 2 - innerWaistHalfZ) * 0.98);
|
|
43
|
+
return {
|
|
44
|
+
axis: 'depth',
|
|
45
|
+
inset,
|
|
46
|
+
middleHeight,
|
|
47
|
+
peakHeight,
|
|
48
|
+
rakeReach,
|
|
49
|
+
innerWaistHalfX,
|
|
50
|
+
innerWaistHalfZ,
|
|
51
|
+
outerWaistHalfX: innerWaistHalfX,
|
|
52
|
+
outerWaistHalfZ: innerWaistHalfZ + rakeReach,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function getRoofShapeInsets(input) {
|
|
56
|
+
let inset = (input.wh - input.baseY) * input.tanTheta;
|
|
57
|
+
const maxSafeInset = Math.min(input.brushW, input.brushD) / 2 - 0.005;
|
|
58
|
+
if (inset > maxSafeInset)
|
|
59
|
+
inset = maxSafeInset;
|
|
60
|
+
let iF = 0;
|
|
61
|
+
let iB = 0;
|
|
62
|
+
let iL = 0;
|
|
63
|
+
let iR = 0;
|
|
64
|
+
if (input.roofType === 'hip' || input.roofType === 'mansard' || input.roofType === 'dutch') {
|
|
65
|
+
iF = inset;
|
|
66
|
+
iB = inset;
|
|
67
|
+
iL = inset;
|
|
68
|
+
iR = inset;
|
|
69
|
+
}
|
|
70
|
+
else if (input.roofType === 'gable' || input.roofType === 'gambrel') {
|
|
71
|
+
iF = inset;
|
|
72
|
+
iB = inset;
|
|
73
|
+
}
|
|
74
|
+
else if (input.roofType === 'shed') {
|
|
75
|
+
iF = inset;
|
|
76
|
+
}
|
|
77
|
+
let dutchI = Math.min(input.width, input.depth) * input.dutchHipWidthRatio;
|
|
78
|
+
if (input.isVoid)
|
|
79
|
+
dutchI += input.shingleThickness;
|
|
80
|
+
return { iF, iB, iL, iR, dutchI };
|
|
81
|
+
}
|
|
82
|
+
export function getDutchEndSlopeFaces(input) {
|
|
83
|
+
const dutch = getDutchRoofShapeMetrics({
|
|
84
|
+
w: input.w,
|
|
85
|
+
d: input.d,
|
|
86
|
+
wh: input.wh,
|
|
87
|
+
rh: input.rh,
|
|
88
|
+
dutchI: input.insets.dutchI,
|
|
89
|
+
baseW: input.baseW,
|
|
90
|
+
baseD: input.baseD,
|
|
91
|
+
shapeRatios: input.shapeRatios,
|
|
92
|
+
});
|
|
93
|
+
if (!dutch)
|
|
94
|
+
return [];
|
|
95
|
+
const v = (x, y, z) => ({ x, y, z });
|
|
96
|
+
const e1 = v(-input.w / 2, input.wh, input.d / 2);
|
|
97
|
+
const e2 = v(input.w / 2, input.wh, input.d / 2);
|
|
98
|
+
const e3 = v(input.w / 2, input.wh, -input.d / 2);
|
|
99
|
+
const e4 = v(-input.w / 2, input.wh, -input.d / 2);
|
|
100
|
+
// The hip end slope is constructed only up to the outer waist (rake) line.
|
|
101
|
+
// Extend its top edge inward along the same hip rulings until it reaches the
|
|
102
|
+
// gablet's inner triangle (the inner waist line), continuing each
|
|
103
|
+
// eave→outer-waist ridge line so the face stays planar and the pitch is
|
|
104
|
+
// unchanged — the edge climbs past middleHeight and meets the gablet face.
|
|
105
|
+
const lerp = (a, b, t) => v(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t);
|
|
106
|
+
const lowerAlongY = (point) => v(point.x, point.y - Math.max(0, input.dutchTopRakeThickness ?? 0), point.z);
|
|
107
|
+
if (dutch.axis === 'width') {
|
|
108
|
+
const o1 = v(-dutch.outerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
109
|
+
const o2 = v(dutch.outerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
110
|
+
const o3 = v(dutch.outerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
111
|
+
const o4 = v(-dutch.outerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
112
|
+
const m1 = v(-dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
113
|
+
const m2 = v(dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
114
|
+
const m3 = v(dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
115
|
+
const m4 = v(-dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
116
|
+
const r1 = v(-dutch.innerWaistHalfX, input.wh + input.rh, 0);
|
|
117
|
+
const r2 = v(dutch.innerWaistHalfX, input.wh + input.rh, 0);
|
|
118
|
+
const projectWidthAxisPointToEndSlope = (point, side) => {
|
|
119
|
+
const denomY = dutch.middleHeight - input.wh;
|
|
120
|
+
const tPlane = Math.abs(denomY) > 1e-9 ? (point.y - input.wh) / denomY : 0;
|
|
121
|
+
const x = side * (input.w / 2 + (dutch.outerWaistHalfX - input.w / 2) * tPlane);
|
|
122
|
+
return v(x, point.y, point.z);
|
|
123
|
+
};
|
|
124
|
+
const top2 = projectWidthAxisPointToEndSlope(lowerAlongY(lerp(m2, r2, 0.5)), 1);
|
|
125
|
+
const top3 = projectWidthAxisPointToEndSlope(lowerAlongY(lerp(m3, r2, 0.5)), 1);
|
|
126
|
+
const top1 = projectWidthAxisPointToEndSlope(lowerAlongY(lerp(m1, r1, 0.5)), -1);
|
|
127
|
+
const top4 = projectWidthAxisPointToEndSlope(lowerAlongY(lerp(m4, r1, 0.5)), -1);
|
|
128
|
+
return [
|
|
129
|
+
[e2, e3, o3, top3, top2, o2],
|
|
130
|
+
[e4, e1, o1, top1, top4, o4],
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
const o1 = v(-dutch.innerWaistHalfX, dutch.middleHeight, dutch.outerWaistHalfZ);
|
|
134
|
+
const o2 = v(dutch.innerWaistHalfX, dutch.middleHeight, dutch.outerWaistHalfZ);
|
|
135
|
+
const o3 = v(dutch.innerWaistHalfX, dutch.middleHeight, -dutch.outerWaistHalfZ);
|
|
136
|
+
const o4 = v(-dutch.innerWaistHalfX, dutch.middleHeight, -dutch.outerWaistHalfZ);
|
|
137
|
+
const m1 = v(-dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
138
|
+
const m2 = v(dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
139
|
+
const m3 = v(dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
140
|
+
const m4 = v(-dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
141
|
+
const r1 = v(0, input.wh + input.rh, dutch.innerWaistHalfZ);
|
|
142
|
+
const r2 = v(0, input.wh + input.rh, -dutch.innerWaistHalfZ);
|
|
143
|
+
const projectDepthAxisPointToEndSlope = (point, side) => {
|
|
144
|
+
const denomY = dutch.middleHeight - input.wh;
|
|
145
|
+
const tPlane = Math.abs(denomY) > 1e-9 ? (point.y - input.wh) / denomY : 0;
|
|
146
|
+
const z = side * (input.d / 2 + (dutch.outerWaistHalfZ - input.d / 2) * tPlane);
|
|
147
|
+
return v(point.x, point.y, z);
|
|
148
|
+
};
|
|
149
|
+
const top2 = projectDepthAxisPointToEndSlope(lowerAlongY(lerp(m2, r1, 0.5)), 1);
|
|
150
|
+
const top1 = projectDepthAxisPointToEndSlope(lowerAlongY(lerp(m1, r1, 0.5)), 1);
|
|
151
|
+
const top4 = projectDepthAxisPointToEndSlope(lowerAlongY(lerp(m4, r2, 0.5)), -1);
|
|
152
|
+
const top3 = projectDepthAxisPointToEndSlope(lowerAlongY(lerp(m3, r2, 0.5)), -1);
|
|
153
|
+
return [
|
|
154
|
+
[e1, e2, o2, top2, top1, o1],
|
|
155
|
+
[e3, e4, o4, top4, top3, o3],
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
export function getRoofModuleFaces(input) {
|
|
159
|
+
const v = (x, y, z) => ({ x, y, z });
|
|
160
|
+
const { iF = 0, iB = 0, iL = 0, iR = 0 } = input.insets;
|
|
161
|
+
const b1 = v(-input.w / 2 + iL, input.baseY, input.d / 2 - iF);
|
|
162
|
+
const b2 = v(input.w / 2 - iR, input.baseY, input.d / 2 - iF);
|
|
163
|
+
const b3 = v(input.w / 2 - iR, input.baseY, -input.d / 2 + iB);
|
|
164
|
+
const b4 = v(-input.w / 2 + iL, input.baseY, -input.d / 2 + iB);
|
|
165
|
+
const bottom = [b4, b3, b2, b1];
|
|
166
|
+
const e1 = v(-input.w / 2, input.wh, input.d / 2);
|
|
167
|
+
const e2 = v(input.w / 2, input.wh, input.d / 2);
|
|
168
|
+
const e3 = v(input.w / 2, input.wh, -input.d / 2);
|
|
169
|
+
const e4 = v(-input.w / 2, input.wh, -input.d / 2);
|
|
170
|
+
const faces = [];
|
|
171
|
+
faces.push([b1, b2, e2, e1], [b2, b3, e3, e2], [b3, b4, e4, e3], [b4, b1, e1, e4], bottom);
|
|
172
|
+
const h = input.wh + Math.max(0.001, input.rh);
|
|
173
|
+
if (input.type === 'flat' || input.rh === 0) {
|
|
174
|
+
faces.push([e1, e2, e3, e4]);
|
|
175
|
+
}
|
|
176
|
+
else if (input.type === 'gable') {
|
|
177
|
+
const r1 = v(-input.w / 2, h, 0);
|
|
178
|
+
const r2 = v(input.w / 2, h, 0);
|
|
179
|
+
faces.push([e4, e1, r1], [e2, e3, r2], [e1, e2, r2, r1], [e3, e4, r1, r2]);
|
|
180
|
+
}
|
|
181
|
+
else if (input.type === 'hip') {
|
|
182
|
+
if (Math.abs(input.w - input.d) < 0.01) {
|
|
183
|
+
const r = v(0, h, 0);
|
|
184
|
+
faces.push([e4, e1, r], [e1, e2, r], [e2, e3, r], [e3, e4, r]);
|
|
185
|
+
}
|
|
186
|
+
else if (input.w >= input.d) {
|
|
187
|
+
const r1 = v(-input.w / 2 + input.d / 2, h, 0);
|
|
188
|
+
const r2 = v(input.w / 2 - input.d / 2, h, 0);
|
|
189
|
+
faces.push([e4, e1, r1], [e2, e3, r2], [e1, e2, r2, r1], [e3, e4, r1, r2]);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const r1 = v(0, h, input.d / 2 - input.w / 2);
|
|
193
|
+
const r2 = v(0, h, -input.d / 2 + input.w / 2);
|
|
194
|
+
faces.push([e1, e2, r1], [e3, e4, r2], [e2, e3, r2, r1], [e4, e1, r1, r2]);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if (input.type === 'shed') {
|
|
198
|
+
const t1 = v(-input.w / 2, h, -input.d / 2);
|
|
199
|
+
const t2 = v(input.w / 2, h, -input.d / 2);
|
|
200
|
+
faces.push([e1, e2, t2, t1], [e2, e3, t2], [e3, e4, t1, t2], [e4, e1, t1]);
|
|
201
|
+
}
|
|
202
|
+
else if (input.type === 'gambrel') {
|
|
203
|
+
const mz = (input.baseD / 2) * input.shapeRatios.gambrelLowerWidthRatio;
|
|
204
|
+
const dist = input.d / 2 - mz;
|
|
205
|
+
const mh = input.wh + dist * (input.tanTheta || 0);
|
|
206
|
+
const m1 = v(-input.w / 2, mh, mz);
|
|
207
|
+
const m2 = v(input.w / 2, mh, mz);
|
|
208
|
+
const m3 = v(input.w / 2, mh, -mz);
|
|
209
|
+
const m4 = v(-input.w / 2, mh, -mz);
|
|
210
|
+
const r1 = v(-input.w / 2, h, 0);
|
|
211
|
+
const r2 = v(input.w / 2, h, 0);
|
|
212
|
+
faces.push([e4, e1, m1, r1, m4], [e2, e3, m3, r2, m2], [e1, e2, m2, m1], [m1, m2, r2, r1], [e3, e4, m4, m3], [m3, m4, r1, r2]);
|
|
213
|
+
}
|
|
214
|
+
else if (input.type === 'mansard') {
|
|
215
|
+
const i = Math.min(input.baseW, input.baseD) * input.shapeRatios.mansardSteepWidthRatio;
|
|
216
|
+
const mh = input.wh + i * (input.tanTheta || 0);
|
|
217
|
+
const m1 = v(-input.w / 2 + i, mh, input.d / 2 - i);
|
|
218
|
+
const m2 = v(input.w / 2 - i, mh, input.d / 2 - i);
|
|
219
|
+
const m3 = v(input.w / 2 - i, mh, -input.d / 2 + i);
|
|
220
|
+
const m4 = v(-input.w / 2 + i, mh, -input.d / 2 + i);
|
|
221
|
+
const topW = input.w - i * 2;
|
|
222
|
+
const topD = input.d - i * 2;
|
|
223
|
+
faces.push([e1, e2, m2, m1], [e2, e3, m3, m2], [e3, e4, m4, m3], [e4, e1, m1, m4]);
|
|
224
|
+
if (Math.abs(topW - topD) < 0.01) {
|
|
225
|
+
const r = v(0, h, 0);
|
|
226
|
+
faces.push([m4, m1, r], [m1, m2, r], [m2, m3, r], [m3, m4, r]);
|
|
227
|
+
}
|
|
228
|
+
else if (topW >= topD) {
|
|
229
|
+
const r1 = v(-topW / 2 + topD / 2, h, 0);
|
|
230
|
+
const r2 = v(topW / 2 - topD / 2, h, 0);
|
|
231
|
+
faces.push([m4, m1, r1], [m2, m3, r2], [m1, m2, r2, r1], [m3, m4, r1, r2]);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const r1 = v(0, h, topD / 2 - topW / 2);
|
|
235
|
+
const r2 = v(0, h, -topD / 2 + topW / 2);
|
|
236
|
+
faces.push([m1, m2, r1], [m3, m4, r2], [m2, m3, r2, r1], [m4, m1, r1, r2]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else if (input.type === 'dutch') {
|
|
240
|
+
const dutch = getDutchRoofShapeMetrics({
|
|
241
|
+
w: input.w,
|
|
242
|
+
d: input.d,
|
|
243
|
+
wh: input.wh,
|
|
244
|
+
rh: input.rh,
|
|
245
|
+
dutchI: input.insets.dutchI,
|
|
246
|
+
baseW: input.baseW,
|
|
247
|
+
baseD: input.baseD,
|
|
248
|
+
shapeRatios: input.shapeRatios,
|
|
249
|
+
});
|
|
250
|
+
if (!dutch)
|
|
251
|
+
return faces;
|
|
252
|
+
const m1 = v(-dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
253
|
+
const m2 = v(dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
254
|
+
const m3 = v(dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
255
|
+
const m4 = v(-dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
256
|
+
if (dutch.axis === 'width') {
|
|
257
|
+
const o1 = v(-dutch.outerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
258
|
+
const o2 = v(dutch.outerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
259
|
+
const o3 = v(dutch.outerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
260
|
+
const o4 = v(-dutch.outerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
261
|
+
const r1 = v(-dutch.innerWaistHalfX, h, 0);
|
|
262
|
+
const r2 = v(dutch.innerWaistHalfX, h, 0);
|
|
263
|
+
const endSlopes = input.excludeDutchEndSlopes
|
|
264
|
+
? []
|
|
265
|
+
: getDutchEndSlopeFaces({
|
|
266
|
+
w: input.w,
|
|
267
|
+
d: input.d,
|
|
268
|
+
wh: input.wh,
|
|
269
|
+
rh: input.rh,
|
|
270
|
+
insets: input.insets,
|
|
271
|
+
baseW: input.baseW,
|
|
272
|
+
baseD: input.baseD,
|
|
273
|
+
shapeRatios: input.shapeRatios,
|
|
274
|
+
dutchTopRakeThickness: input.dutchTopRakeThickness,
|
|
275
|
+
});
|
|
276
|
+
faces.push([e1, e2, o2, m2, m1, o1], [e3, e4, o4, m4, m3, o3]);
|
|
277
|
+
if (endSlopes.length === 2) {
|
|
278
|
+
faces.push(...endSlopes);
|
|
279
|
+
}
|
|
280
|
+
else if (!input.excludeDutchEndSlopes) {
|
|
281
|
+
faces.push([e2, e3, o3, o2], [e4, e1, o1, o4]);
|
|
282
|
+
}
|
|
283
|
+
faces.push([m1, m2, r2, r1], [m3, m4, r1, r2]);
|
|
284
|
+
faces.push([m4, m1, r1], [m2, m3, r2]);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
const o1 = v(-dutch.innerWaistHalfX, dutch.middleHeight, dutch.outerWaistHalfZ);
|
|
288
|
+
const o2 = v(dutch.innerWaistHalfX, dutch.middleHeight, dutch.outerWaistHalfZ);
|
|
289
|
+
const o3 = v(dutch.innerWaistHalfX, dutch.middleHeight, -dutch.outerWaistHalfZ);
|
|
290
|
+
const o4 = v(-dutch.innerWaistHalfX, dutch.middleHeight, -dutch.outerWaistHalfZ);
|
|
291
|
+
const r1 = v(0, h, dutch.innerWaistHalfZ);
|
|
292
|
+
const r2 = v(0, h, -dutch.innerWaistHalfZ);
|
|
293
|
+
const endSlopes = input.excludeDutchEndSlopes
|
|
294
|
+
? []
|
|
295
|
+
: getDutchEndSlopeFaces({
|
|
296
|
+
w: input.w,
|
|
297
|
+
d: input.d,
|
|
298
|
+
wh: input.wh,
|
|
299
|
+
rh: input.rh,
|
|
300
|
+
insets: input.insets,
|
|
301
|
+
baseW: input.baseW,
|
|
302
|
+
baseD: input.baseD,
|
|
303
|
+
shapeRatios: input.shapeRatios,
|
|
304
|
+
dutchTopRakeThickness: input.dutchTopRakeThickness,
|
|
305
|
+
});
|
|
306
|
+
faces.push([e2, e3, o3, m3, m2, o2], [e4, e1, o1, m1, m4, o4]);
|
|
307
|
+
if (endSlopes.length === 2) {
|
|
308
|
+
faces.push(...endSlopes);
|
|
309
|
+
}
|
|
310
|
+
else if (!input.excludeDutchEndSlopes) {
|
|
311
|
+
faces.push([e1, e2, o2, o1], [e3, e4, o4, o3]);
|
|
312
|
+
}
|
|
313
|
+
faces.push([m2, m3, r2, r1], [m4, m1, r1, r2]);
|
|
314
|
+
faces.push([m1, m2, r1], [m3, m4, r2]);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return faces;
|
|
318
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roof-segment-shape.test.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/roof-segment-shape.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { getDutchEndSlopeFaces, getDutchRoofShapeMetrics, getRoofModuleFaces, getRoofShapeRatios, } from './roof-segment-shape';
|
|
3
|
+
describe('roof segment shape', () => {
|
|
4
|
+
test('dutch shell is built as one complete non-duplicated face set', () => {
|
|
5
|
+
const wh = 3;
|
|
6
|
+
const rh = 2;
|
|
7
|
+
const faces = getRoofModuleFaces({
|
|
8
|
+
type: 'dutch',
|
|
9
|
+
w: 8,
|
|
10
|
+
d: 6,
|
|
11
|
+
wh,
|
|
12
|
+
rh,
|
|
13
|
+
baseY: 0,
|
|
14
|
+
insets: { dutchI: 1.5 },
|
|
15
|
+
baseW: 8,
|
|
16
|
+
baseD: 6,
|
|
17
|
+
tanTheta: 1,
|
|
18
|
+
shapeRatios: getRoofShapeRatios({
|
|
19
|
+
dutchHipWidthRatio: 0.25,
|
|
20
|
+
dutchHipHeightRatio: 0.5,
|
|
21
|
+
dutchWaistLengthRatio: 1,
|
|
22
|
+
dutchGabletRake: 0.25,
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
const peakY = wh + rh;
|
|
26
|
+
const peakFaces = faces.filter((face) => face.some((vertex) => Math.abs(vertex.y - peakY) < 1e-6));
|
|
27
|
+
const signatures = new Set(faces.map((face) => face
|
|
28
|
+
.map((vertex) => `${vertex.x.toFixed(3)},${vertex.y.toFixed(3)},${vertex.z.toFixed(3)}`)
|
|
29
|
+
.sort()
|
|
30
|
+
.join('|')));
|
|
31
|
+
expect(faces).toHaveLength(13);
|
|
32
|
+
expect(peakFaces).toHaveLength(4);
|
|
33
|
+
expect(signatures.size).toBe(faces.length);
|
|
34
|
+
const lowerRightHip = faces.find((face) => face.some((vertex) => Math.abs(vertex.x - 2.75) < 1e-6 &&
|
|
35
|
+
Math.abs(vertex.y - 4) < 1e-6 &&
|
|
36
|
+
Math.abs(vertex.z - 1.5) < 1e-6));
|
|
37
|
+
const gableTriangle = faces.find((face) => face.length === 3 &&
|
|
38
|
+
face.some((vertex) => vertex.x === 2.5 && vertex.y === 5) &&
|
|
39
|
+
face.some((vertex) => vertex.x === 2.5 && vertex.z === 1.5));
|
|
40
|
+
expect(lowerRightHip).toBeDefined();
|
|
41
|
+
expect(gableTriangle?.some((vertex) => vertex.x === 2.75)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
test('dutch depth-axis shell rotates the upper gable correctly', () => {
|
|
44
|
+
const faces = getRoofModuleFaces({
|
|
45
|
+
type: 'dutch',
|
|
46
|
+
w: 6,
|
|
47
|
+
d: 8,
|
|
48
|
+
wh: 3,
|
|
49
|
+
rh: 2,
|
|
50
|
+
baseY: 0,
|
|
51
|
+
insets: { dutchI: 1.5 },
|
|
52
|
+
baseW: 6,
|
|
53
|
+
baseD: 8,
|
|
54
|
+
tanTheta: 1,
|
|
55
|
+
shapeRatios: getRoofShapeRatios({
|
|
56
|
+
dutchHipWidthRatio: 0.25,
|
|
57
|
+
dutchHipHeightRatio: 0.5,
|
|
58
|
+
dutchWaistLengthRatio: 1,
|
|
59
|
+
dutchGabletRake: 0.25,
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
const peakFaces = faces.filter((face) => face.some((vertex) => vertex.y === 5));
|
|
63
|
+
const ridgeOnlyOnZAxis = peakFaces
|
|
64
|
+
.flat()
|
|
65
|
+
.filter((vertex) => vertex.y === 5)
|
|
66
|
+
.every((vertex) => vertex.x === 0);
|
|
67
|
+
expect(peakFaces).toHaveLength(4);
|
|
68
|
+
expect(ridgeOnlyOnZAxis).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
test('dutch end hip slope extends inward until it meets the gable triangle', () => {
|
|
71
|
+
const ratios = getRoofShapeRatios({
|
|
72
|
+
dutchHipWidthRatio: 0.25,
|
|
73
|
+
dutchHipHeightRatio: 0.5,
|
|
74
|
+
dutchWaistLengthRatio: 1,
|
|
75
|
+
dutchGabletRake: 0.75,
|
|
76
|
+
});
|
|
77
|
+
const metrics = getDutchRoofShapeMetrics({
|
|
78
|
+
w: 8,
|
|
79
|
+
d: 6,
|
|
80
|
+
wh: 3,
|
|
81
|
+
rh: 2,
|
|
82
|
+
dutchI: 1.5,
|
|
83
|
+
baseW: 8,
|
|
84
|
+
baseD: 6,
|
|
85
|
+
shapeRatios: ratios,
|
|
86
|
+
});
|
|
87
|
+
const faces = getRoofModuleFaces({
|
|
88
|
+
type: 'dutch',
|
|
89
|
+
w: 8,
|
|
90
|
+
d: 6,
|
|
91
|
+
wh: 3,
|
|
92
|
+
rh: 2,
|
|
93
|
+
baseY: 0,
|
|
94
|
+
insets: { dutchI: 1.5 },
|
|
95
|
+
baseW: 8,
|
|
96
|
+
baseD: 6,
|
|
97
|
+
tanTheta: 1,
|
|
98
|
+
shapeRatios: ratios,
|
|
99
|
+
dutchTopRakeThickness: 0.21,
|
|
100
|
+
});
|
|
101
|
+
expect(metrics?.innerWaistHalfX).toBe(2.5);
|
|
102
|
+
expect(metrics?.outerWaistHalfX).toBe(3.25);
|
|
103
|
+
// The end slope now clips against the rake's lower inner edge and is
|
|
104
|
+
// reprojected back onto the end-slope plane, so the shorter top edge stays
|
|
105
|
+
// planar instead of twisting.
|
|
106
|
+
const hipTopFace = faces.find((face) => face.some((vertex) => Math.abs(vertex.x - 4) < 1e-6 &&
|
|
107
|
+
Math.abs(vertex.y - 3) < 1e-6 &&
|
|
108
|
+
Math.abs(vertex.z - 3) < 1e-6) &&
|
|
109
|
+
face.some((vertex) => Math.abs(vertex.x - 3.0325) < 1e-6 &&
|
|
110
|
+
Math.abs(vertex.y - 4.29) < 1e-6 &&
|
|
111
|
+
Math.abs(vertex.z - 0.75) < 1e-6) &&
|
|
112
|
+
face.some((vertex) => Math.abs(vertex.x - 3.0325) < 1e-6 &&
|
|
113
|
+
Math.abs(vertex.y - 4.29) < 1e-6 &&
|
|
114
|
+
Math.abs(vertex.z + 0.75) < 1e-6));
|
|
115
|
+
const gableTriangle = faces.find((face) => face.length === 3 &&
|
|
116
|
+
face.some((vertex) => vertex.x === 2.5 && vertex.y === 5) &&
|
|
117
|
+
face.some((vertex) => vertex.x === 2.5 && vertex.z === 1.5));
|
|
118
|
+
expect(hipTopFace).toBeDefined();
|
|
119
|
+
expect(gableTriangle?.some((vertex) => vertex.x === 3.25)).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
test('excludeDutchEndSlopes pulls the end slopes out into getDutchEndSlopeFaces', () => {
|
|
122
|
+
const ratios = getRoofShapeRatios({
|
|
123
|
+
dutchHipWidthRatio: 0.25,
|
|
124
|
+
dutchHipHeightRatio: 0.5,
|
|
125
|
+
dutchWaistLengthRatio: 1,
|
|
126
|
+
dutchGabletRake: 0.75,
|
|
127
|
+
});
|
|
128
|
+
const args = {
|
|
129
|
+
type: 'dutch',
|
|
130
|
+
w: 8,
|
|
131
|
+
d: 6,
|
|
132
|
+
wh: 3,
|
|
133
|
+
rh: 2,
|
|
134
|
+
baseY: 0,
|
|
135
|
+
insets: { dutchI: 1.5 },
|
|
136
|
+
baseW: 8,
|
|
137
|
+
baseD: 6,
|
|
138
|
+
tanTheta: 1,
|
|
139
|
+
shapeRatios: ratios,
|
|
140
|
+
dutchTopRakeThickness: 0.21,
|
|
141
|
+
};
|
|
142
|
+
const full = getRoofModuleFaces(args);
|
|
143
|
+
const shell = getRoofModuleFaces({ ...args, excludeDutchEndSlopes: true });
|
|
144
|
+
const endSlopes = getDutchEndSlopeFaces({
|
|
145
|
+
w: 8,
|
|
146
|
+
d: 6,
|
|
147
|
+
wh: 3,
|
|
148
|
+
rh: 2,
|
|
149
|
+
insets: { dutchI: 1.5 },
|
|
150
|
+
baseW: 8,
|
|
151
|
+
baseD: 6,
|
|
152
|
+
shapeRatios: ratios,
|
|
153
|
+
dutchTopRakeThickness: 0.21,
|
|
154
|
+
});
|
|
155
|
+
// The two end slopes leave the shell and reappear in the standalone set.
|
|
156
|
+
expect(shell).toHaveLength(full.length - 2);
|
|
157
|
+
expect(endSlopes).toHaveLength(2);
|
|
158
|
+
// The standalone end slopes are 6-point polygons whose shorter top edge is
|
|
159
|
+
// clipped to the rake's lower inner edge and then kept on the end-slope
|
|
160
|
+
// plane.
|
|
161
|
+
const endIsEndSlope = endSlopes.every((face) => face.length === 6 &&
|
|
162
|
+
face.some((vertex) => Math.abs(Math.abs(vertex.x) - 3.25) < 1e-6) &&
|
|
163
|
+
face.some((vertex) => Math.abs(Math.abs(vertex.x) - 3.0325) < 1e-6) &&
|
|
164
|
+
face.some((vertex) => Math.abs(Math.abs(vertex.x) - 4) < 1e-6));
|
|
165
|
+
expect(endIsEndSlope).toBe(true);
|
|
166
|
+
// The removed faces are exactly the end slopes — the shell keeps every
|
|
167
|
+
// other face the full module produced.
|
|
168
|
+
const signature = (face) => face
|
|
169
|
+
.map((vertex) => `${vertex.x.toFixed(3)},${vertex.y.toFixed(3)},${vertex.z.toFixed(3)}`)
|
|
170
|
+
.join('|');
|
|
171
|
+
const shellSigs = new Set(shell.map(signature));
|
|
172
|
+
const removed = full.filter((face) => !shellSigs.has(signature(face)));
|
|
173
|
+
expect(removed).toHaveLength(2);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roof-segment-surface.test.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/roof-segment-surface.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { getRoofSegmentSurfaceY, ROOF_SHAPE_DEFAULTS, RoofSegmentNode } from './roof-segment';
|
|
3
|
+
describe('getRoofSegmentSurfaceY', () => {
|
|
4
|
+
test('keeps the Dutch width-axis rake on the upper gable slope', () => {
|
|
5
|
+
const segment = RoofSegmentNode.parse({
|
|
6
|
+
roofType: 'dutch',
|
|
7
|
+
width: 8,
|
|
8
|
+
depth: 6,
|
|
9
|
+
wallHeight: 3,
|
|
10
|
+
pitch: 40,
|
|
11
|
+
});
|
|
12
|
+
const activeRh = getRoofSegmentSurfaceY(segment, 0, 0) - segment.wallHeight;
|
|
13
|
+
const upperRise = activeRh * (1 - segment.dutchHipHeightRatio);
|
|
14
|
+
const waistHalfZ = segment.depth / 2 - Math.min(segment.width, segment.depth) * segment.dutchHipWidthRatio;
|
|
15
|
+
const availableRake = Math.max(0, segment.width / 2 -
|
|
16
|
+
(segment.width / 2 - Math.min(segment.width, segment.depth) * segment.dutchHipWidthRatio) *
|
|
17
|
+
segment.dutchWaistLengthRatio);
|
|
18
|
+
const rakeReach = Math.min(segment.dutchGabletRake, availableRake * 0.98);
|
|
19
|
+
const localX = (segment.width / 2 - Math.min(segment.width, segment.depth) * segment.dutchHipWidthRatio) *
|
|
20
|
+
segment.dutchWaistLengthRatio +
|
|
21
|
+
rakeReach * 0.5;
|
|
22
|
+
const localZ = waistHalfZ * 0.5;
|
|
23
|
+
const expected = segment.wallHeight + activeRh - localZ * (upperRise / waistHalfZ);
|
|
24
|
+
expect(localX).toBeGreaterThan((segment.width / 2 - Math.min(segment.width, segment.depth) * segment.dutchHipWidthRatio) *
|
|
25
|
+
segment.dutchWaistLengthRatio);
|
|
26
|
+
expect(getRoofSegmentSurfaceY(segment, localX, localZ)).toBeCloseTo(expected, 6);
|
|
27
|
+
});
|
|
28
|
+
test('keeps the Dutch depth-axis rake on the upper gable slope', () => {
|
|
29
|
+
const segment = RoofSegmentNode.parse({
|
|
30
|
+
roofType: 'dutch',
|
|
31
|
+
width: 6,
|
|
32
|
+
depth: 8,
|
|
33
|
+
wallHeight: 3,
|
|
34
|
+
pitch: 40,
|
|
35
|
+
});
|
|
36
|
+
const activeRh = getRoofSegmentSurfaceY(segment, 0, 0) - segment.wallHeight;
|
|
37
|
+
const upperRise = activeRh * (1 - segment.dutchHipHeightRatio);
|
|
38
|
+
const waistHalfX = segment.width / 2 - Math.min(segment.width, segment.depth) * segment.dutchHipWidthRatio;
|
|
39
|
+
const innerWaistHalfZ = (segment.depth / 2 - Math.min(segment.width, segment.depth) * segment.dutchHipWidthRatio) *
|
|
40
|
+
segment.dutchWaistLengthRatio;
|
|
41
|
+
const rakeReach = Math.min(segment.dutchGabletRake ?? ROOF_SHAPE_DEFAULTS.dutchGabletRake, Math.max(0, segment.depth / 2 - innerWaistHalfZ) * 0.98);
|
|
42
|
+
const localX = waistHalfX * 0.5;
|
|
43
|
+
const localZ = innerWaistHalfZ + rakeReach * 0.5;
|
|
44
|
+
const expected = segment.wallHeight + activeRh - localX * (upperRise / waistHalfX);
|
|
45
|
+
expect(localZ).toBeGreaterThan(innerWaistHalfZ);
|
|
46
|
+
expect(getRoofSegmentSurfaceY(segment, localX, localZ)).toBeCloseTo(expected, 6);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roof-segment-trim.test.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/roof-segment-trim.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { getRoofSegmentVisibleTopBounds, normalizeRoofSegmentTrim, RoofSegmentNode, } from './roof-segment';
|
|
3
|
+
describe('roof segment trim', () => {
|
|
4
|
+
test('defaults legacy segments to no trim', () => {
|
|
5
|
+
const segment = RoofSegmentNode.parse({
|
|
6
|
+
id: 'rseg_test',
|
|
7
|
+
type: 'roof-segment',
|
|
8
|
+
});
|
|
9
|
+
expect(segment.trim).toEqual({
|
|
10
|
+
left: 0,
|
|
11
|
+
right: 0,
|
|
12
|
+
front: 0,
|
|
13
|
+
back: 0,
|
|
14
|
+
frontLeft: 0,
|
|
15
|
+
frontRight: 0,
|
|
16
|
+
backLeft: 0,
|
|
17
|
+
backRight: 0,
|
|
18
|
+
frontLeftX: 0,
|
|
19
|
+
frontLeftZ: 0,
|
|
20
|
+
frontRightX: 0,
|
|
21
|
+
frontRightZ: 0,
|
|
22
|
+
backLeftX: 0,
|
|
23
|
+
backLeftZ: 0,
|
|
24
|
+
backRightX: 0,
|
|
25
|
+
backRightZ: 0,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
test('normalizes impossible side totals without inverting the footprint', () => {
|
|
29
|
+
const trim = normalizeRoofSegmentTrim({
|
|
30
|
+
width: 4,
|
|
31
|
+
depth: 3,
|
|
32
|
+
trim: { left: 3, right: 3, front: 2, back: 2 },
|
|
33
|
+
});
|
|
34
|
+
expect(trim.left + trim.right).toBeCloseTo(3.9);
|
|
35
|
+
expect(trim.front + trim.back).toBeCloseTo(2.9);
|
|
36
|
+
});
|
|
37
|
+
test('visible top bounds respect asymmetric trims', () => {
|
|
38
|
+
const segment = RoofSegmentNode.parse({
|
|
39
|
+
id: 'rseg_test',
|
|
40
|
+
type: 'roof-segment',
|
|
41
|
+
width: 8,
|
|
42
|
+
depth: 6,
|
|
43
|
+
overhang: 0,
|
|
44
|
+
wallThickness: 0,
|
|
45
|
+
shingleThickness: 0,
|
|
46
|
+
trim: { left: 1, right: 2, front: 0, back: 0 },
|
|
47
|
+
});
|
|
48
|
+
const bounds = getRoofSegmentVisibleTopBounds(segment);
|
|
49
|
+
expect(bounds.minX).toBeCloseTo(-3);
|
|
50
|
+
expect(bounds.maxX).toBeCloseTo(2);
|
|
51
|
+
expect(bounds.width).toBeCloseTo(5);
|
|
52
|
+
});
|
|
53
|
+
test('visible top bounds stay finite for legacy partial segments', () => {
|
|
54
|
+
const bounds = getRoofSegmentVisibleTopBounds({
|
|
55
|
+
id: 'rseg_legacy',
|
|
56
|
+
type: 'roof-segment',
|
|
57
|
+
roofType: 'gable',
|
|
58
|
+
trim: { left: 1 },
|
|
59
|
+
});
|
|
60
|
+
expect(Number.isFinite(bounds.minX)).toBe(true);
|
|
61
|
+
expect(Number.isFinite(bounds.maxX)).toBe(true);
|
|
62
|
+
expect(Number.isFinite(bounds.minZ)).toBe(true);
|
|
63
|
+
expect(Number.isFinite(bounds.maxZ)).toBe(true);
|
|
64
|
+
expect(Number.isFinite(bounds.width)).toBe(true);
|
|
65
|
+
expect(Number.isFinite(bounds.depth)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
test('diagonal trims can shorten the ridge span', () => {
|
|
68
|
+
const segment = RoofSegmentNode.parse({
|
|
69
|
+
id: 'rseg_test',
|
|
70
|
+
type: 'roof-segment',
|
|
71
|
+
width: 8,
|
|
72
|
+
depth: 6,
|
|
73
|
+
overhang: 0,
|
|
74
|
+
wallThickness: 0,
|
|
75
|
+
shingleThickness: 0,
|
|
76
|
+
trim: { left: 0, right: 0, front: 0, back: 0, frontLeft: 4, backRight: 4 },
|
|
77
|
+
});
|
|
78
|
+
const bounds = getRoofSegmentVisibleTopBounds(segment);
|
|
79
|
+
expect(bounds.minX).toBeCloseTo(-3);
|
|
80
|
+
expect(bounds.maxX).toBeCloseTo(3);
|
|
81
|
+
expect(bounds.width).toBeCloseTo(6);
|
|
82
|
+
});
|
|
83
|
+
test('diagonal trims support independent width and depth endpoints', () => {
|
|
84
|
+
const segment = RoofSegmentNode.parse({
|
|
85
|
+
id: 'rseg_test',
|
|
86
|
+
type: 'roof-segment',
|
|
87
|
+
width: 8,
|
|
88
|
+
depth: 6,
|
|
89
|
+
overhang: 0,
|
|
90
|
+
wallThickness: 0,
|
|
91
|
+
shingleThickness: 0,
|
|
92
|
+
trim: {
|
|
93
|
+
frontLeftX: 5,
|
|
94
|
+
frontLeftZ: 2,
|
|
95
|
+
backRightX: 2,
|
|
96
|
+
backRightZ: 5,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
const trim = normalizeRoofSegmentTrim(segment);
|
|
100
|
+
const bounds = getRoofSegmentVisibleTopBounds(segment);
|
|
101
|
+
expect(trim.frontLeft).toBeCloseTo(2);
|
|
102
|
+
expect(trim.frontLeftX).toBeCloseTo(5);
|
|
103
|
+
expect(trim.frontLeftZ).toBeCloseTo(2);
|
|
104
|
+
expect(bounds.maxZ).toBeCloseTo(2.6);
|
|
105
|
+
expect(bounds.maxX).toBeCloseTo(3.2);
|
|
106
|
+
});
|
|
107
|
+
});
|