@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,101 @@
|
|
|
1
|
+
import type { RoofSegmentNode } from './roof-segment';
|
|
2
|
+
/**
|
|
3
|
+
* Wall-face math for roof segments — the vertical surfaces a wall-mounted
|
|
4
|
+
* opening (door / window) can attach to. A segment's generated volume has
|
|
5
|
+
* four vertical faces; on gable-family roofs the end faces extend past the
|
|
6
|
+
* eave line into the gable (rect + triangle/pentagon, coplanar with the
|
|
7
|
+
* base wall). These helpers describe each face as a 2D frame
|
|
8
|
+
* (`u` along the face, `v` height above the segment base) plus the
|
|
9
|
+
* placeable profile polygon, so placement tools, renderers, and CSG cut
|
|
10
|
+
* builders all share one definition of "the wall under the roof".
|
|
11
|
+
*
|
|
12
|
+
* The numbers MUST mirror the outer wall volume built by
|
|
13
|
+
* `getRoofSegmentBrushes` in the viewer's roof system
|
|
14
|
+
* (`getVol(wallThickness / 2, 0, 0, …)`): the volume is the segment
|
|
15
|
+
* footprint extended outward by `wallThickness / 2`, which drops the eave
|
|
16
|
+
* line by `(wallThickness / 2) · tanθ` and raises the ridge by the same
|
|
17
|
+
* amount so the apex stays at `wallHeight + activeRh`.
|
|
18
|
+
*/
|
|
19
|
+
export type RoofWallFaceId = 'front' | 'back' | 'right' | 'left';
|
|
20
|
+
export type RoofSegmentWallFace = {
|
|
21
|
+
id: RoofWallFaceId;
|
|
22
|
+
/** Outward normal in segment-local space. */
|
|
23
|
+
normal: [number, number, number];
|
|
24
|
+
/**
|
|
25
|
+
* Yaw (radians, rotation-y) mapping opening-local +Z to the outward
|
|
26
|
+
* normal and opening-local +X to the face's +U direction — the same
|
|
27
|
+
* frame a wall-hosted door/window uses relative to its wall mesh.
|
|
28
|
+
*/
|
|
29
|
+
yaw: number;
|
|
30
|
+
/** Face length along U. */
|
|
31
|
+
length: number;
|
|
32
|
+
/**
|
|
33
|
+
* Placeable region, CCW polygon in face coords. `u ∈ [0, length]`,
|
|
34
|
+
* `v` is height above the segment base (segment-local Y).
|
|
35
|
+
*/
|
|
36
|
+
profile: [number, number][];
|
|
37
|
+
};
|
|
38
|
+
type SegmentWallInputs = Pick<RoofSegmentNode, 'roofType' | 'width' | 'depth' | 'wallHeight' | 'wallThickness' | 'pitch'> & Partial<Pick<RoofSegmentNode, 'gambrelLowerWidthRatio' | 'gambrelLowerHeightRatio' | 'mansardSteepWidthRatio' | 'mansardSteepHeightRatio' | 'dutchHipWidthRatio' | 'dutchHipHeightRatio' | 'dutchWaistLengthRatio'>>;
|
|
39
|
+
export declare function getRoofSegmentWallFace(node: SegmentWallInputs, id: RoofWallFaceId): RoofSegmentWallFace;
|
|
40
|
+
export declare function getRoofSegmentWallFaces(node: SegmentWallInputs): RoofSegmentWallFace[];
|
|
41
|
+
/**
|
|
42
|
+
* Segment-local point → face coords. `dist` is the signed offset off the
|
|
43
|
+
* outer wall plane along the face normal (0 = on the plane, positive =
|
|
44
|
+
* outside the volume).
|
|
45
|
+
*/
|
|
46
|
+
export declare function segmentPointToRoofWallFace(node: SegmentWallInputs, id: RoofWallFaceId, point: [number, number, number]): {
|
|
47
|
+
u: number;
|
|
48
|
+
v: number;
|
|
49
|
+
dist: number;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The face's render frame in segment-local space: a group placed at
|
|
53
|
+
* `origin` and yawed by `yaw` maps face coords to segment space —
|
|
54
|
+
* frame X = U (along the face), frame Y = V (height), frame Z = the
|
|
55
|
+
* outward normal, with z = 0 on the WALL MID-PLANE. The mid-plane of
|
|
56
|
+
* the generated wall volume lands exactly on the nominal footprint
|
|
57
|
+
* (`±width/2` / `±depth/2`), so hosted children use the same position
|
|
58
|
+
* conventions as wall children (openings at z = 0, wall-side items
|
|
59
|
+
* pushed +thickness/2 at render time). Renderers derive this from the
|
|
60
|
+
* live-override-merged segment, which is what makes hosted children
|
|
61
|
+
* track segment edits live instead of jumping on commit.
|
|
62
|
+
*/
|
|
63
|
+
export declare function getRoofWallFaceFrame(node: SegmentWallInputs, id: RoofWallFaceId): {
|
|
64
|
+
origin: [number, number, number];
|
|
65
|
+
yaw: number;
|
|
66
|
+
};
|
|
67
|
+
/** Face-frame point ([u, v, z-from-mid-plane]) → segment-local point. */
|
|
68
|
+
export declare function roofFacePointToSegment(node: SegmentWallInputs, id: RoofWallFaceId, point: [number, number, number]): [number, number, number];
|
|
69
|
+
/**
|
|
70
|
+
* Max width of a rect growing from an anchored vertical edge (`anchorU`)
|
|
71
|
+
* in direction `growSign` (±1 along U) while staying inside the face
|
|
72
|
+
* profile at the fixed vertical center `vCenter`. Resize-handle limit:
|
|
73
|
+
* the anchored-edge model matches the handles' apply math (opposite
|
|
74
|
+
* edge stays put, center re-derives).
|
|
75
|
+
*/
|
|
76
|
+
export declare function getMaxRoofRectWidthFromAnchor(face: RoofSegmentWallFace, anchorU: number, growSign: number, vCenter: number, height: number): number;
|
|
77
|
+
/**
|
|
78
|
+
* Max height of a rect growing from an anchored horizontal edge
|
|
79
|
+
* (`anchorV`) in direction `growSign` (+1 = bottom anchored, grows up)
|
|
80
|
+
* while staying inside the face profile at the fixed horizontal center
|
|
81
|
+
* `uCenter`.
|
|
82
|
+
*/
|
|
83
|
+
export declare function getMaxRoofRectHeightFromAnchor(face: RoofSegmentWallFace, uCenter: number, width: number, anchorV: number, growSign: number): number;
|
|
84
|
+
/**
|
|
85
|
+
* Clamp a rect center so the rect fits inside the face profile.
|
|
86
|
+
*
|
|
87
|
+
* - `lockV: true` (doors): `v` is fixed; only `u` slides. Returns null
|
|
88
|
+
* when no `u` keeps the rect inside at that height.
|
|
89
|
+
* - otherwise (windows): the center is projected into the eroded convex
|
|
90
|
+
* region (cyclic projection — profiles are convex by construction).
|
|
91
|
+
*
|
|
92
|
+
* Returns null when the rect cannot fit anywhere on the face.
|
|
93
|
+
*/
|
|
94
|
+
export declare function clampRectToRoofWallFace(face: RoofSegmentWallFace, u: number, v: number, width: number, height: number, opts?: {
|
|
95
|
+
lockV?: boolean;
|
|
96
|
+
}): {
|
|
97
|
+
u: number;
|
|
98
|
+
v: number;
|
|
99
|
+
} | null;
|
|
100
|
+
export {};
|
|
101
|
+
//# sourceMappingURL=roof-segment-walls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roof-segment-walls.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/roof-segment-walls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAGrD;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;AAEhE,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,cAAc,CAAA;IAClB,6CAA6C;IAC7C,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAA;IACX,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAA;CAC5B,CAAA;AAED,KAAK,iBAAiB,GAAG,IAAI,CAC3B,eAAe,EACf,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,YAAY,GAAG,eAAe,GAAG,OAAO,CAC1E,GACC,OAAO,CACL,IAAI,CACF,eAAe,EACb,wBAAwB,GACxB,yBAAyB,GACzB,wBAAwB,GACxB,yBAAyB,GACzB,oBAAoB,GACpB,qBAAqB,GACrB,uBAAuB,CAC1B,CACF,CAAA;AAqJH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,iBAAiB,EACvB,EAAE,EAAE,cAAc,GACjB,mBAAmB,CAUrB;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,EAAE,CAStF;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,iBAAiB,EACvB,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAC9B;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAaxC;AA+CD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,iBAAiB,EACvB,EAAE,EAAE,cAAc,GACjB;IAAE,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAYnD;AAED,yEAAyE;AACzE,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,iBAAiB,EACvB,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAC9B,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAO1B;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,mBAAmB,EACzB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GACb,MAAM,CAWR;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,mBAAmB,EACzB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,MAAM,CASR;AAID;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,mBAAmB,EACzB,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACzB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAwCjC"}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { getDutchRoofMetrics, getSegmentSlopeFrame } from './roof-segment';
|
|
2
|
+
function getWallVolumeFrame(node) {
|
|
3
|
+
const { activeRh, tanTheta } = getSegmentSlopeFrame(node);
|
|
4
|
+
const wallThickness = node.wallThickness ?? 0.1;
|
|
5
|
+
const autoDrop = (wallThickness / 2) * tanTheta;
|
|
6
|
+
const wV = Math.max(0.01, node.width + wallThickness);
|
|
7
|
+
const dV = Math.max(0.01, node.depth + wallThickness);
|
|
8
|
+
const eaveY = Math.max(0.01, node.wallHeight - autoDrop);
|
|
9
|
+
let rh = activeRh;
|
|
10
|
+
if (activeRh > 0) {
|
|
11
|
+
rh = activeRh + autoDrop;
|
|
12
|
+
if (node.roofType === 'shed')
|
|
13
|
+
rh = activeRh + 2 * autoDrop;
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
wV,
|
|
17
|
+
dV,
|
|
18
|
+
eaveY,
|
|
19
|
+
peakY: eaveY + Math.max(0.001, rh),
|
|
20
|
+
tanTheta,
|
|
21
|
+
hasSlope: activeRh > 0,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const FACE_NORMALS = {
|
|
25
|
+
front: [0, 0, 1],
|
|
26
|
+
back: [0, 0, -1],
|
|
27
|
+
right: [1, 0, 0],
|
|
28
|
+
left: [-1, 0, 0],
|
|
29
|
+
};
|
|
30
|
+
const FACE_YAWS = {
|
|
31
|
+
front: 0,
|
|
32
|
+
back: Math.PI,
|
|
33
|
+
right: Math.PI / 2,
|
|
34
|
+
left: -Math.PI / 2,
|
|
35
|
+
};
|
|
36
|
+
function rectProfile(length, top) {
|
|
37
|
+
return [
|
|
38
|
+
[0, 0],
|
|
39
|
+
[length, 0],
|
|
40
|
+
[length, top],
|
|
41
|
+
[0, top],
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
function buildFaceProfile(node, frame, id) {
|
|
45
|
+
const { wV, dV, eaveY, peakY, tanTheta, hasSlope } = frame;
|
|
46
|
+
const isEnd = id === 'right' || id === 'left';
|
|
47
|
+
const length = isEnd ? dV : wV;
|
|
48
|
+
if (!hasSlope)
|
|
49
|
+
return rectProfile(length, eaveY);
|
|
50
|
+
switch (node.roofType) {
|
|
51
|
+
case 'gable': {
|
|
52
|
+
if (!isEnd)
|
|
53
|
+
return rectProfile(length, eaveY);
|
|
54
|
+
return [
|
|
55
|
+
[0, 0],
|
|
56
|
+
[length, 0],
|
|
57
|
+
[length, eaveY],
|
|
58
|
+
[length / 2, peakY],
|
|
59
|
+
[0, eaveY],
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
case 'gambrel': {
|
|
63
|
+
if (!isEnd)
|
|
64
|
+
return rectProfile(length, eaveY);
|
|
65
|
+
// Kink ring sits at z = ±mz on the nominal footprint (see
|
|
66
|
+
// getModuleFaces); both end faces are symmetric about u = length/2.
|
|
67
|
+
const ratio = node.gambrelLowerWidthRatio ?? 0.5;
|
|
68
|
+
const mz = Math.min((node.depth / 2) * ratio, length / 2);
|
|
69
|
+
const kinkY = eaveY + (length / 2 - mz) * tanTheta;
|
|
70
|
+
return [
|
|
71
|
+
[0, 0],
|
|
72
|
+
[length, 0],
|
|
73
|
+
[length, eaveY],
|
|
74
|
+
[length / 2 + mz, kinkY],
|
|
75
|
+
[length / 2, peakY],
|
|
76
|
+
[length / 2 - mz, kinkY],
|
|
77
|
+
[0, eaveY],
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
case 'shed': {
|
|
81
|
+
// Slope falls toward +Z: 'back' is the full-height wall, the end
|
|
82
|
+
// faces are right trapezoids rising toward the back edge.
|
|
83
|
+
if (id === 'front')
|
|
84
|
+
return rectProfile(length, eaveY);
|
|
85
|
+
if (id === 'back')
|
|
86
|
+
return rectProfile(length, peakY);
|
|
87
|
+
if (id === 'right') {
|
|
88
|
+
return [
|
|
89
|
+
[0, 0],
|
|
90
|
+
[length, 0],
|
|
91
|
+
[length, peakY],
|
|
92
|
+
[0, eaveY],
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
return [
|
|
96
|
+
[0, 0],
|
|
97
|
+
[length, 0],
|
|
98
|
+
[length, eaveY],
|
|
99
|
+
[0, peakY],
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
case 'dutch': {
|
|
103
|
+
const metrics = getDutchRoofMetrics(node);
|
|
104
|
+
const isDutchGableFace = (metrics.axis === 'x' && isEnd) ||
|
|
105
|
+
(metrics.axis === 'z' && (id === 'front' || id === 'back'));
|
|
106
|
+
if (!isDutchGableFace)
|
|
107
|
+
return rectProfile(length, eaveY);
|
|
108
|
+
const shoulderInset = metrics.axis === 'x' ? metrics.shoulderInsetAlongDepth : metrics.shoulderInsetAlongWidth;
|
|
109
|
+
if (!(shoulderInset > 0.001))
|
|
110
|
+
return rectProfile(length, eaveY);
|
|
111
|
+
const shoulderLo = Math.max(0, shoulderInset);
|
|
112
|
+
const shoulderHi = Math.min(length, length - shoulderInset);
|
|
113
|
+
if (!(shoulderHi - shoulderLo > 0.02))
|
|
114
|
+
return rectProfile(length, eaveY);
|
|
115
|
+
return [
|
|
116
|
+
[0, 0],
|
|
117
|
+
[length, 0],
|
|
118
|
+
[length, eaveY],
|
|
119
|
+
[shoulderHi, eaveY],
|
|
120
|
+
[length / 2, peakY],
|
|
121
|
+
[shoulderLo, eaveY],
|
|
122
|
+
[0, eaveY],
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
default:
|
|
126
|
+
return rectProfile(length, eaveY);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function getRoofSegmentWallFace(node, id) {
|
|
130
|
+
const frame = getWallVolumeFrame(node);
|
|
131
|
+
const isEnd = id === 'right' || id === 'left';
|
|
132
|
+
return {
|
|
133
|
+
id,
|
|
134
|
+
normal: FACE_NORMALS[id],
|
|
135
|
+
yaw: FACE_YAWS[id],
|
|
136
|
+
length: isEnd ? frame.dV : frame.wV,
|
|
137
|
+
profile: buildFaceProfile(node, frame, id),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function getRoofSegmentWallFaces(node) {
|
|
141
|
+
const frame = getWallVolumeFrame(node);
|
|
142
|
+
return ['front', 'back', 'right', 'left'].map((id) => ({
|
|
143
|
+
id,
|
|
144
|
+
normal: FACE_NORMALS[id],
|
|
145
|
+
yaw: FACE_YAWS[id],
|
|
146
|
+
length: id === 'right' || id === 'left' ? frame.dV : frame.wV,
|
|
147
|
+
profile: buildFaceProfile(node, frame, id),
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Segment-local point → face coords. `dist` is the signed offset off the
|
|
152
|
+
* outer wall plane along the face normal (0 = on the plane, positive =
|
|
153
|
+
* outside the volume).
|
|
154
|
+
*/
|
|
155
|
+
export function segmentPointToRoofWallFace(node, id, point) {
|
|
156
|
+
const { wV, dV } = getWallVolumeFrame(node);
|
|
157
|
+
const [x, y, z] = point;
|
|
158
|
+
switch (id) {
|
|
159
|
+
case 'front':
|
|
160
|
+
return { u: x + wV / 2, v: y, dist: z - dV / 2 };
|
|
161
|
+
case 'back':
|
|
162
|
+
return { u: wV / 2 - x, v: y, dist: -z - dV / 2 };
|
|
163
|
+
case 'right':
|
|
164
|
+
return { u: dV / 2 - z, v: y, dist: x - wV / 2 };
|
|
165
|
+
case 'left':
|
|
166
|
+
return { u: z + dV / 2, v: y, dist: -x - wV / 2 };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Inward half-plane constraints of the raw profile polygon (CCW →
|
|
171
|
+
* interior is to the left of each edge): a point p is inside when
|
|
172
|
+
* `nu·p.u + nv·p.v ≥ c` for every constraint.
|
|
173
|
+
*/
|
|
174
|
+
function getProfileConstraints(face) {
|
|
175
|
+
const constraints = [];
|
|
176
|
+
const pts = face.profile;
|
|
177
|
+
for (let i = 0; i < pts.length; i++) {
|
|
178
|
+
const a = pts[i];
|
|
179
|
+
const b = pts[(i + 1) % pts.length];
|
|
180
|
+
const du = b[0] - a[0];
|
|
181
|
+
const dv = b[1] - a[1];
|
|
182
|
+
const len = Math.hypot(du, dv);
|
|
183
|
+
if (len < 1e-9)
|
|
184
|
+
continue;
|
|
185
|
+
const nu = -dv / len;
|
|
186
|
+
const nv = du / len;
|
|
187
|
+
constraints.push({ nu, nv, c: nu * a[0] + nv * a[1] });
|
|
188
|
+
}
|
|
189
|
+
return constraints;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Half-plane constraints for the CENTER of a `width × height` rect that
|
|
193
|
+
* must fit inside the face profile — the raw constraints eroded by the
|
|
194
|
+
* rect's half-extents projected on each edge normal.
|
|
195
|
+
*/
|
|
196
|
+
function getRectCenterConstraints(face, width, height) {
|
|
197
|
+
return getProfileConstraints(face).map(({ nu, nv, c }) => ({
|
|
198
|
+
nu,
|
|
199
|
+
nv,
|
|
200
|
+
c: c + (Math.abs(nu) * width) / 2 + (Math.abs(nv) * height) / 2,
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The face's render frame in segment-local space: a group placed at
|
|
205
|
+
* `origin` and yawed by `yaw` maps face coords to segment space —
|
|
206
|
+
* frame X = U (along the face), frame Y = V (height), frame Z = the
|
|
207
|
+
* outward normal, with z = 0 on the WALL MID-PLANE. The mid-plane of
|
|
208
|
+
* the generated wall volume lands exactly on the nominal footprint
|
|
209
|
+
* (`±width/2` / `±depth/2`), so hosted children use the same position
|
|
210
|
+
* conventions as wall children (openings at z = 0, wall-side items
|
|
211
|
+
* pushed +thickness/2 at render time). Renderers derive this from the
|
|
212
|
+
* live-override-merged segment, which is what makes hosted children
|
|
213
|
+
* track segment edits live instead of jumping on commit.
|
|
214
|
+
*/
|
|
215
|
+
export function getRoofWallFaceFrame(node, id) {
|
|
216
|
+
const { wV, dV } = getWallVolumeFrame(node);
|
|
217
|
+
switch (id) {
|
|
218
|
+
case 'front':
|
|
219
|
+
return { origin: [-wV / 2, 0, node.depth / 2], yaw: FACE_YAWS.front };
|
|
220
|
+
case 'back':
|
|
221
|
+
return { origin: [wV / 2, 0, -node.depth / 2], yaw: FACE_YAWS.back };
|
|
222
|
+
case 'right':
|
|
223
|
+
return { origin: [node.width / 2, 0, dV / 2], yaw: FACE_YAWS.right };
|
|
224
|
+
case 'left':
|
|
225
|
+
return { origin: [-node.width / 2, 0, -dV / 2], yaw: FACE_YAWS.left };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** Face-frame point ([u, v, z-from-mid-plane]) → segment-local point. */
|
|
229
|
+
export function roofFacePointToSegment(node, id, point) {
|
|
230
|
+
const { origin, yaw } = getRoofWallFaceFrame(node, id);
|
|
231
|
+
const cos = Math.cos(yaw);
|
|
232
|
+
const sin = Math.sin(yaw);
|
|
233
|
+
const [u, v, z] = point;
|
|
234
|
+
// rotation-y: +x → (cos, 0, -sin), +z → (sin, 0, cos)
|
|
235
|
+
return [origin[0] + u * cos + z * sin, origin[1] + v, origin[2] - u * sin + z * cos];
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Max width of a rect growing from an anchored vertical edge (`anchorU`)
|
|
239
|
+
* in direction `growSign` (±1 along U) while staying inside the face
|
|
240
|
+
* profile at the fixed vertical center `vCenter`. Resize-handle limit:
|
|
241
|
+
* the anchored-edge model matches the handles' apply math (opposite
|
|
242
|
+
* edge stays put, center re-derives).
|
|
243
|
+
*/
|
|
244
|
+
export function getMaxRoofRectWidthFromAnchor(face, anchorU, growSign, vCenter, height) {
|
|
245
|
+
let max = Number.POSITIVE_INFINITY;
|
|
246
|
+
for (const { nu, nv, c } of getProfileConstraints(face)) {
|
|
247
|
+
// Center at anchorU + growSign·w/2, eroded by |nu|·w/2 + |nv|·h/2:
|
|
248
|
+
// base + k·w ≥ 0 with k ≤ 0 only when growth approaches the edge.
|
|
249
|
+
const k = (nu * growSign - Math.abs(nu)) / 2;
|
|
250
|
+
if (k >= -1e-9)
|
|
251
|
+
continue;
|
|
252
|
+
const base = nu * anchorU + nv * vCenter - c - (Math.abs(nv) * height) / 2;
|
|
253
|
+
max = Math.min(max, Math.max(0, base / -k));
|
|
254
|
+
}
|
|
255
|
+
return max;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Max height of a rect growing from an anchored horizontal edge
|
|
259
|
+
* (`anchorV`) in direction `growSign` (+1 = bottom anchored, grows up)
|
|
260
|
+
* while staying inside the face profile at the fixed horizontal center
|
|
261
|
+
* `uCenter`.
|
|
262
|
+
*/
|
|
263
|
+
export function getMaxRoofRectHeightFromAnchor(face, uCenter, width, anchorV, growSign) {
|
|
264
|
+
let max = Number.POSITIVE_INFINITY;
|
|
265
|
+
for (const { nu, nv, c } of getProfileConstraints(face)) {
|
|
266
|
+
const k = (nv * growSign - Math.abs(nv)) / 2;
|
|
267
|
+
if (k >= -1e-9)
|
|
268
|
+
continue;
|
|
269
|
+
const base = nu * uCenter + nv * anchorV - c - (Math.abs(nu) * width) / 2;
|
|
270
|
+
max = Math.min(max, Math.max(0, base / -k));
|
|
271
|
+
}
|
|
272
|
+
return max;
|
|
273
|
+
}
|
|
274
|
+
const CLAMP_EPSILON = 1e-4;
|
|
275
|
+
/**
|
|
276
|
+
* Clamp a rect center so the rect fits inside the face profile.
|
|
277
|
+
*
|
|
278
|
+
* - `lockV: true` (doors): `v` is fixed; only `u` slides. Returns null
|
|
279
|
+
* when no `u` keeps the rect inside at that height.
|
|
280
|
+
* - otherwise (windows): the center is projected into the eroded convex
|
|
281
|
+
* region (cyclic projection — profiles are convex by construction).
|
|
282
|
+
*
|
|
283
|
+
* Returns null when the rect cannot fit anywhere on the face.
|
|
284
|
+
*/
|
|
285
|
+
export function clampRectToRoofWallFace(face, u, v, width, height, opts) {
|
|
286
|
+
const constraints = getRectCenterConstraints(face, width, height);
|
|
287
|
+
if (constraints.length < 3)
|
|
288
|
+
return null;
|
|
289
|
+
if (opts?.lockV) {
|
|
290
|
+
let lo = Number.NEGATIVE_INFINITY;
|
|
291
|
+
let hi = Number.POSITIVE_INFINITY;
|
|
292
|
+
for (const { nu, nv, c } of constraints) {
|
|
293
|
+
const rhs = c - nv * v;
|
|
294
|
+
if (Math.abs(nu) < 1e-9) {
|
|
295
|
+
if (rhs > CLAMP_EPSILON)
|
|
296
|
+
return null;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (nu > 0)
|
|
300
|
+
lo = Math.max(lo, rhs / nu);
|
|
301
|
+
else
|
|
302
|
+
hi = Math.min(hi, rhs / nu);
|
|
303
|
+
}
|
|
304
|
+
if (lo > hi + CLAMP_EPSILON)
|
|
305
|
+
return null;
|
|
306
|
+
return { u: Math.min(Math.max(u, lo), hi), v };
|
|
307
|
+
}
|
|
308
|
+
let pu = u;
|
|
309
|
+
let pv = v;
|
|
310
|
+
for (let iter = 0; iter < 32; iter++) {
|
|
311
|
+
let worst = null;
|
|
312
|
+
let worstViolation = CLAMP_EPSILON;
|
|
313
|
+
for (const constraint of constraints) {
|
|
314
|
+
const violation = constraint.c - (constraint.nu * pu + constraint.nv * pv);
|
|
315
|
+
if (violation > worstViolation) {
|
|
316
|
+
worstViolation = violation;
|
|
317
|
+
worst = constraint;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (!worst)
|
|
321
|
+
return { u: pu, v: pv };
|
|
322
|
+
pu += worst.nu * worstViolation;
|
|
323
|
+
pv += worst.nv * worstViolation;
|
|
324
|
+
}
|
|
325
|
+
for (const { nu, nv, c } of constraints) {
|
|
326
|
+
if (nu * pu + nv * pv < c - 1e-3)
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
return { u: pu, v: pv };
|
|
330
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roof-segment-walls.test.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/roof-segment-walls.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { RoofSegmentNode } from './roof-segment';
|
|
3
|
+
import { getRoofSegmentWallFace, getRoofSegmentWallFaces, getRoofWallFaceFrame, roofFacePointToSegment, segmentPointToRoofWallFace, } from './roof-segment-walls';
|
|
4
|
+
function segment(overrides = {}) {
|
|
5
|
+
return RoofSegmentNode.parse({
|
|
6
|
+
id: 'rseg_test',
|
|
7
|
+
type: 'roof-segment',
|
|
8
|
+
roofType: 'gable',
|
|
9
|
+
width: 8,
|
|
10
|
+
depth: 6,
|
|
11
|
+
wallHeight: 2.6,
|
|
12
|
+
wallThickness: 0.1,
|
|
13
|
+
pitch: 40,
|
|
14
|
+
...overrides,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
describe('roof wall face frames', () => {
|
|
18
|
+
test('frame z = 0 lands on the nominal footprint (wall mid-plane)', () => {
|
|
19
|
+
const seg = segment();
|
|
20
|
+
// front face, u at the face middle, v = 1, mid-plane.
|
|
21
|
+
const point = roofFacePointToSegment(seg, 'front', [(8 + 0.1) / 2, 1, 0]);
|
|
22
|
+
expect(point[0]).toBeCloseTo(0);
|
|
23
|
+
expect(point[1]).toBeCloseTo(1);
|
|
24
|
+
expect(point[2]).toBeCloseTo(3); // depth / 2 — the footprint plane
|
|
25
|
+
});
|
|
26
|
+
test('frame +z is the outward normal on every face', () => {
|
|
27
|
+
const seg = segment();
|
|
28
|
+
for (const [faceId, axis, sign] of [
|
|
29
|
+
['front', 2, 1],
|
|
30
|
+
['back', 2, -1],
|
|
31
|
+
['right', 0, 1],
|
|
32
|
+
['left', 0, -1],
|
|
33
|
+
]) {
|
|
34
|
+
const onPlane = roofFacePointToSegment(seg, faceId, [1, 1, 0]);
|
|
35
|
+
const pushed = roofFacePointToSegment(seg, faceId, [1, 1, 0.5]);
|
|
36
|
+
expect(pushed[axis] - onPlane[axis]).toBeCloseTo(0.5 * sign);
|
|
37
|
+
// The other horizontal axis is unaffected by the push.
|
|
38
|
+
const other = axis === 2 ? 0 : 2;
|
|
39
|
+
expect(pushed[other] - onPlane[other]).toBeCloseTo(0);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
test('face frame agrees with the hit resolver coordinates', () => {
|
|
43
|
+
const seg = segment();
|
|
44
|
+
// A point on the outer surface (z = +thickness/2 off the mid-plane)
|
|
45
|
+
// must read back with the same u/v and dist ≈ 0 off the outer plane.
|
|
46
|
+
const segLocal = roofFacePointToSegment(seg, 'right', [2.5, 1.25, 0.05]);
|
|
47
|
+
const { u, v, dist } = segmentPointToRoofWallFace(seg, 'right', segLocal);
|
|
48
|
+
expect(u).toBeCloseTo(2.5);
|
|
49
|
+
expect(v).toBeCloseTo(1.25);
|
|
50
|
+
expect(dist).toBeCloseTo(0);
|
|
51
|
+
});
|
|
52
|
+
test('resizing the segment moves the frame, not the stored coords', () => {
|
|
53
|
+
// The core live-tracking property: the same face-local point maps to
|
|
54
|
+
// the new plane after a depth change — children follow by re-render.
|
|
55
|
+
const before = roofFacePointToSegment(segment(), 'front', [2, 1, 0]);
|
|
56
|
+
const after = roofFacePointToSegment(segment({ depth: 8 }), 'front', [2, 1, 0]);
|
|
57
|
+
expect(before[2]).toBeCloseTo(3);
|
|
58
|
+
expect(after[2]).toBeCloseTo(4);
|
|
59
|
+
expect(after[1]).toBeCloseTo(before[1]);
|
|
60
|
+
});
|
|
61
|
+
test('frame yaw matches the face descriptor yaw', () => {
|
|
62
|
+
const seg = segment();
|
|
63
|
+
for (const faceId of ['front', 'back', 'right', 'left']) {
|
|
64
|
+
expect(getRoofWallFaceFrame(seg, faceId).yaw).toBe(getRoofSegmentWallFace(seg, faceId).yaw);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
test('dutch width-axis roofs expose hostable gable-end wall profiles on the short ends', () => {
|
|
68
|
+
const faces = getRoofSegmentWallFaces(segment({
|
|
69
|
+
roofType: 'dutch',
|
|
70
|
+
width: 8,
|
|
71
|
+
depth: 6,
|
|
72
|
+
dutchHipWidthRatio: 0.25,
|
|
73
|
+
dutchWaistLengthRatio: 1,
|
|
74
|
+
}));
|
|
75
|
+
const left = faces.find((face) => face.id === 'left');
|
|
76
|
+
const right = faces.find((face) => face.id === 'right');
|
|
77
|
+
expect(left?.profile.length).toBeGreaterThan(5);
|
|
78
|
+
expect(right?.profile.length).toBeGreaterThan(5);
|
|
79
|
+
expect(left?.profile[3]?.[1]).toBeCloseTo(left?.profile[2]?.[1] ?? 0);
|
|
80
|
+
});
|
|
81
|
+
test('dutch long-side faces stay rectangular while only the gable ends rise above the eave', () => {
|
|
82
|
+
const faces = getRoofSegmentWallFaces(segment({
|
|
83
|
+
roofType: 'dutch',
|
|
84
|
+
width: 8,
|
|
85
|
+
depth: 6,
|
|
86
|
+
}));
|
|
87
|
+
const front = faces.find((face) => face.id === 'front');
|
|
88
|
+
const back = faces.find((face) => face.id === 'back');
|
|
89
|
+
expect(front?.profile).toHaveLength(4);
|
|
90
|
+
expect(back?.profile).toHaveLength(4);
|
|
91
|
+
});
|
|
92
|
+
});
|