@pascal-app/core 1.0.0-beta.5 → 1.0.0

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.
Files changed (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -0
  3. package/dist/capture/index.d.ts +3 -0
  4. package/dist/capture/index.d.ts.map +1 -0
  5. package/dist/capture/index.js +2 -0
  6. package/dist/capture/schema.d.ts +417 -0
  7. package/dist/capture/schema.d.ts.map +1 -0
  8. package/dist/capture/schema.js +348 -0
  9. package/dist/capture/source.d.ts +60 -0
  10. package/dist/capture/source.d.ts.map +1 -0
  11. package/dist/capture/source.js +254 -0
  12. package/dist/events/bus.d.ts +55 -7
  13. package/dist/events/bus.d.ts.map +1 -1
  14. package/dist/events/hidden-wall-pointer-hold.d.ts +28 -0
  15. package/dist/events/hidden-wall-pointer-hold.d.ts.map +1 -0
  16. package/dist/events/hidden-wall-pointer-hold.js +37 -0
  17. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +2785 -1
  18. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  19. package/dist/hooks/spatial-grid/spatial-grid-sync.js +132 -19
  20. package/dist/hooks/spatial-grid/support-host-patch.d.ts +45 -1
  21. package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -1
  22. package/dist/hooks/spatial-grid/support-host-patch.js +171 -6
  23. package/dist/index.d.ts +14 -6
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +16 -5
  26. package/dist/lib/asset-storage.d.ts.map +1 -1
  27. package/dist/lib/asset-storage.js +4 -1
  28. package/dist/lib/polygon-union.d.ts +4 -0
  29. package/dist/lib/polygon-union.d.ts.map +1 -0
  30. package/dist/lib/polygon-union.js +306 -0
  31. package/dist/lib/roof-overlap.d.ts +32 -0
  32. package/dist/lib/roof-overlap.d.ts.map +1 -0
  33. package/dist/lib/roof-overlap.js +72 -0
  34. package/dist/lib/slab-polygon.d.ts +35 -2
  35. package/dist/lib/slab-polygon.d.ts.map +1 -1
  36. package/dist/lib/slab-polygon.js +133 -27
  37. package/dist/lib/space-detection.d.ts +11 -0
  38. package/dist/lib/space-detection.d.ts.map +1 -1
  39. package/dist/lib/space-detection.js +42 -14
  40. package/dist/registry/handles.d.ts +24 -3
  41. package/dist/registry/handles.d.ts.map +1 -1
  42. package/dist/registry/index.d.ts +3 -4
  43. package/dist/registry/index.d.ts.map +1 -1
  44. package/dist/registry/index.js +1 -2
  45. package/dist/registry/registry.d.ts +13 -6
  46. package/dist/registry/registry.d.ts.map +1 -1
  47. package/dist/registry/registry.js +37 -4
  48. package/dist/registry/scene-api.d.ts +16 -0
  49. package/dist/registry/scene-api.d.ts.map +1 -1
  50. package/dist/registry/scene-api.js +49 -1
  51. package/dist/registry/types.d.ts +226 -13
  52. package/dist/registry/types.d.ts.map +1 -1
  53. package/dist/schema/__bench__/node-parsers.bench.d.ts +24 -0
  54. package/dist/schema/__bench__/node-parsers.bench.d.ts.map +1 -0
  55. package/dist/schema/__bench__/node-parsers.bench.js +314 -0
  56. package/dist/schema/__fixtures__/node-fixtures.d.ts +28 -0
  57. package/dist/schema/__fixtures__/node-fixtures.d.ts.map +1 -0
  58. package/dist/schema/__fixtures__/node-fixtures.js +125 -0
  59. package/dist/schema/base.d.ts +1 -1
  60. package/dist/schema/base.d.ts.map +1 -1
  61. package/dist/schema/base.js +8 -1
  62. package/dist/schema/compiled-node-parsers.d.ts +40 -0
  63. package/dist/schema/compiled-node-parsers.d.ts.map +1 -0
  64. package/dist/schema/compiled-node-parsers.js +95 -0
  65. package/dist/schema/index.d.ts +19 -15
  66. package/dist/schema/index.d.ts.map +1 -1
  67. package/dist/schema/index.js +17 -12
  68. package/dist/schema/material.d.ts +2 -1
  69. package/dist/schema/material.d.ts.map +1 -1
  70. package/dist/schema/material.js +1 -0
  71. package/dist/schema/nodes/automatic-downspout.d.ts +17 -0
  72. package/dist/schema/nodes/automatic-downspout.d.ts.map +1 -0
  73. package/dist/schema/nodes/automatic-downspout.js +237 -0
  74. package/dist/schema/nodes/block.d.ts +107 -0
  75. package/dist/schema/nodes/block.d.ts.map +1 -0
  76. package/dist/schema/nodes/block.js +207 -0
  77. package/dist/schema/nodes/box-vent.d.ts +8 -2
  78. package/dist/schema/nodes/box-vent.d.ts.map +1 -1
  79. package/dist/schema/nodes/box-vent.js +2 -0
  80. package/dist/schema/nodes/building.d.ts +1 -1
  81. package/dist/schema/nodes/cabinet.d.ts +25 -3
  82. package/dist/schema/nodes/cabinet.d.ts.map +1 -1
  83. package/dist/schema/nodes/cabinet.js +26 -5
  84. package/dist/schema/nodes/ceiling.d.ts +1 -1
  85. package/dist/schema/nodes/chimney.d.ts +2 -2
  86. package/dist/schema/nodes/column.d.ts +6 -6
  87. package/dist/schema/nodes/construction-dimension.d.ts +4 -4
  88. package/dist/schema/nodes/cupola.d.ts +9 -1
  89. package/dist/schema/nodes/cupola.d.ts.map +1 -1
  90. package/dist/schema/nodes/cupola.js +2 -0
  91. package/dist/schema/nodes/door.d.ts +1 -1
  92. package/dist/schema/nodes/dormer.d.ts +44 -1
  93. package/dist/schema/nodes/dormer.d.ts.map +1 -1
  94. package/dist/schema/nodes/dormer.js +131 -7
  95. package/dist/schema/nodes/downspout.d.ts +11 -1
  96. package/dist/schema/nodes/downspout.d.ts.map +1 -1
  97. package/dist/schema/nodes/downspout.js +24 -0
  98. package/dist/schema/nodes/duct-fitting.d.ts +18 -1
  99. package/dist/schema/nodes/duct-fitting.d.ts.map +1 -1
  100. package/dist/schema/nodes/duct-fitting.js +25 -4
  101. package/dist/schema/nodes/duct-segment.d.ts +23 -1
  102. package/dist/schema/nodes/duct-segment.d.ts.map +1 -1
  103. package/dist/schema/nodes/duct-segment.js +20 -0
  104. package/dist/schema/nodes/duct-terminal.d.ts +1 -1
  105. package/dist/schema/nodes/elevator.d.ts +1 -1
  106. package/dist/schema/nodes/eyebrow-vent.d.ts +8 -2
  107. package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -1
  108. package/dist/schema/nodes/eyebrow-vent.js +2 -0
  109. package/dist/schema/nodes/fence.d.ts +1 -1
  110. package/dist/schema/nodes/guide.d.ts +1 -1
  111. package/dist/schema/nodes/gutter.d.ts +36 -2
  112. package/dist/schema/nodes/gutter.d.ts.map +1 -1
  113. package/dist/schema/nodes/gutter.js +308 -1
  114. package/dist/schema/nodes/hanger-overrides.d.ts +7 -0
  115. package/dist/schema/nodes/hanger-overrides.d.ts.map +1 -0
  116. package/dist/schema/nodes/hanger-overrides.js +6 -0
  117. package/dist/schema/nodes/hvac-equipment.d.ts +1 -1
  118. package/dist/schema/nodes/item.d.ts +4 -3
  119. package/dist/schema/nodes/item.d.ts.map +1 -1
  120. package/dist/schema/nodes/item.js +5 -0
  121. package/dist/schema/nodes/lean-to-extension.d.ts +212 -0
  122. package/dist/schema/nodes/lean-to-extension.d.ts.map +1 -0
  123. package/dist/schema/nodes/lean-to-extension.js +114 -0
  124. package/dist/schema/nodes/level.d.ts +2 -2
  125. package/dist/schema/nodes/level.d.ts.map +1 -1
  126. package/dist/schema/nodes/lineset.d.ts +1 -1
  127. package/dist/schema/nodes/liquid-line.d.ts +1 -1
  128. package/dist/schema/nodes/measurement.d.ts +1 -1
  129. package/dist/schema/nodes/pipe-fitting.d.ts +9 -1
  130. package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -1
  131. package/dist/schema/nodes/pipe-fitting.js +6 -2
  132. package/dist/schema/nodes/pipe-segment.d.ts +23 -1
  133. package/dist/schema/nodes/pipe-segment.d.ts.map +1 -1
  134. package/dist/schema/nodes/pipe-segment.js +20 -0
  135. package/dist/schema/nodes/pipe-trap.d.ts +1 -1
  136. package/dist/schema/nodes/ridge-vent.d.ts +1 -1
  137. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
  138. package/dist/schema/nodes/ridge-vent.js +4 -1
  139. package/dist/schema/nodes/roof-segment-shape.d.ts +4 -0
  140. package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -1
  141. package/dist/schema/nodes/roof-segment-shape.js +64 -1
  142. package/dist/schema/nodes/roof-segment-walls.d.ts +3 -1
  143. package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -1
  144. package/dist/schema/nodes/roof-segment-walls.js +1 -1
  145. package/dist/schema/nodes/roof-segment.d.ts +41 -1
  146. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  147. package/dist/schema/nodes/roof-segment.js +79 -2
  148. package/dist/schema/nodes/roof.d.ts +22 -1
  149. package/dist/schema/nodes/roof.d.ts.map +1 -1
  150. package/dist/schema/nodes/roof.js +14 -0
  151. package/dist/schema/nodes/scan.d.ts +19 -2
  152. package/dist/schema/nodes/scan.d.ts.map +1 -1
  153. package/dist/schema/nodes/scan.js +11 -1
  154. package/dist/schema/nodes/shelf.d.ts +2 -2
  155. package/dist/schema/nodes/site.d.ts +1 -1
  156. package/dist/schema/nodes/skylight.d.ts +1 -1
  157. package/dist/schema/nodes/slab.d.ts +1 -1
  158. package/dist/schema/nodes/solar-panel.d.ts +1 -1
  159. package/dist/schema/nodes/spawn.d.ts +1 -1
  160. package/dist/schema/nodes/stair-segment.d.ts +1 -1
  161. package/dist/schema/nodes/stair.d.ts +1 -1
  162. package/dist/schema/nodes/structural-grid.d.ts +1 -1
  163. package/dist/schema/nodes/turbine-vent.d.ts +7 -1
  164. package/dist/schema/nodes/turbine-vent.d.ts.map +1 -1
  165. package/dist/schema/nodes/turbine-vent.js +2 -0
  166. package/dist/schema/nodes/wall.d.ts +4 -4
  167. package/dist/schema/nodes/wall.d.ts.map +1 -1
  168. package/dist/schema/nodes/wall.js +7 -1
  169. package/dist/schema/nodes/window.d.ts +10 -3
  170. package/dist/schema/nodes/window.d.ts.map +1 -1
  171. package/dist/schema/nodes/window.js +4 -0
  172. package/dist/schema/nodes/zone.d.ts +2 -2
  173. package/dist/schema/nodes/zone.js +1 -1
  174. package/dist/schema/types.d.ts +484 -109
  175. package/dist/schema/types.d.ts.map +1 -1
  176. package/dist/schema/types.js +27 -1
  177. package/dist/services/hosting.d.ts +2 -2
  178. package/dist/services/hosting.d.ts.map +1 -1
  179. package/dist/services/hosting.js +4 -4
  180. package/dist/services/index.d.ts +1 -1
  181. package/dist/services/index.d.ts.map +1 -1
  182. package/dist/services/index.js +1 -1
  183. package/dist/services/system-graph.d.ts +11 -0
  184. package/dist/services/system-graph.d.ts.map +1 -1
  185. package/dist/services/system-graph.js +35 -5
  186. package/dist/store/actions/node-actions.d.ts +7 -0
  187. package/dist/store/actions/node-actions.d.ts.map +1 -1
  188. package/dist/store/actions/node-actions.js +429 -17
  189. package/dist/store/history-invalidation.d.ts +3 -0
  190. package/dist/store/history-invalidation.d.ts.map +1 -0
  191. package/dist/store/history-invalidation.js +72 -0
  192. package/dist/store/scene-hydration.d.ts +5 -0
  193. package/dist/store/scene-hydration.d.ts.map +1 -0
  194. package/dist/store/scene-hydration.js +57 -0
  195. package/dist/store/use-scene.d.ts +3 -0
  196. package/dist/store/use-scene.d.ts.map +1 -1
  197. package/dist/store/use-scene.js +332 -64
  198. package/dist/systems/roof/roof-elevation-system.d.ts +3 -0
  199. package/dist/systems/roof/roof-elevation-system.d.ts.map +1 -0
  200. package/dist/systems/roof/roof-elevation-system.js +94 -0
  201. package/dist/systems/roof/roof-elevation.d.ts +4 -0
  202. package/dist/systems/roof/roof-elevation.d.ts.map +1 -0
  203. package/dist/systems/roof/roof-elevation.js +95 -0
  204. package/dist/systems/roof/roof-footprint.d.ts +17 -0
  205. package/dist/systems/roof/roof-footprint.d.ts.map +1 -0
  206. package/dist/systems/roof/roof-footprint.js +83 -0
  207. package/dist/systems/stair/stair-flight.d.ts +17 -0
  208. package/dist/systems/stair/stair-flight.d.ts.map +1 -0
  209. package/dist/systems/stair/stair-flight.js +27 -0
  210. package/dist/systems/stair/stair-opening-preview.d.ts +1555 -1328
  211. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  212. package/dist/systems/stair/stair-opening-system.d.ts +1 -0
  213. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
  214. package/dist/systems/stair/stair-opening-system.js +93 -96
  215. package/dist/systems/stair/stair-rise.d.ts.map +1 -1
  216. package/dist/systems/stair/stair-rise.js +20 -21
  217. package/dist/systems/wall/wall-curve.d.ts +1 -0
  218. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  219. package/dist/systems/wall/wall-curve.js +73 -0
  220. package/dist/systems/wall/wall-footprint.d.ts +1 -0
  221. package/dist/systems/wall/wall-footprint.d.ts.map +1 -1
  222. package/dist/systems/wall/wall-footprint.js +1 -0
  223. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  224. package/dist/utils/clone-scene-graph.js +26 -0
  225. package/dist/validation/validate-build-json.d.ts +3 -0
  226. package/dist/validation/validate-build-json.d.ts.map +1 -1
  227. package/dist/validation/validate-build-json.js +49 -2
  228. package/package.json +10 -3
  229. package/dist/systems/elevator/elevator-runtime-system.d.ts +0 -2
  230. package/dist/systems/elevator/elevator-runtime-system.d.ts.map +0 -1
  231. package/dist/systems/elevator/elevator-runtime-system.js +0 -8
@@ -0,0 +1,94 @@
1
+ 'use client';
2
+ import { useEffect } from 'react';
3
+ import { getSceneHistoryPauseDepth, notifySceneCommit, pauseSceneHistory, resumeSceneHistory, } from '../../store/history-control';
4
+ import useScene from '../../store/use-scene';
5
+ import { resolveRoofElevation } from './roof-elevation';
6
+ const ROOF_ELEVATION_EPSILON = 1e-4;
7
+ function sceneSnapshot() {
8
+ const { nodes, rootNodeIds, collections, materials, installedPlugins } = useScene.getState();
9
+ return { nodes, rootNodeIds, collections, materials, installedPlugins };
10
+ }
11
+ function isElevationRelevant(node) {
12
+ return (node?.type === 'roof' ||
13
+ node?.type === 'wall' ||
14
+ node?.type === 'slab' ||
15
+ node?.type === 'level' ||
16
+ node?.type === 'building' ||
17
+ node?.type === 'site');
18
+ }
19
+ export function initializeRoofElevationSync() {
20
+ let disposed = false;
21
+ let queued = false;
22
+ let syncing = false;
23
+ const schedule = () => {
24
+ if (queued)
25
+ return;
26
+ queued = true;
27
+ // Wall bases use the spatial grid, whose listener must finish before this pass.
28
+ queueMicrotask(() => {
29
+ queued = false;
30
+ if (disposed)
31
+ return;
32
+ const { nodes, updateNodes, readOnly } = useScene.getState();
33
+ if (readOnly)
34
+ return;
35
+ const updates = [];
36
+ for (const node of Object.values(nodes)) {
37
+ if (node.type !== 'roof' || node.support?.kind !== 'walls')
38
+ continue;
39
+ const elevation = resolveRoofElevation(node, nodes);
40
+ if (Math.abs(node.position[1] - elevation) <= ROOF_ELEVATION_EPSILON)
41
+ continue;
42
+ updates.push({
43
+ id: node.id,
44
+ data: { position: [node.position[0], elevation, node.position[2]] },
45
+ });
46
+ }
47
+ if (updates.length === 0)
48
+ return;
49
+ const before = sceneSnapshot();
50
+ const publishCommit = useScene.temporal.getState().isTracking && getSceneHistoryPauseDepth() === 0;
51
+ syncing = true;
52
+ pauseSceneHistory(useScene);
53
+ try {
54
+ updateNodes(updates);
55
+ }
56
+ finally {
57
+ resumeSceneHistory(useScene);
58
+ syncing = false;
59
+ }
60
+ // Deferred writes miss the originating commit; publish the settled roof without
61
+ // another undo step. An outer pause belongs to a gesture that owns its commit.
62
+ if (publishCommit) {
63
+ notifySceneCommit({
64
+ origin: 'local',
65
+ before,
66
+ current: sceneSnapshot(),
67
+ changedNodeIds: new Set(updates.map(({ id }) => id)),
68
+ });
69
+ }
70
+ });
71
+ };
72
+ const unsubscribe = useScene.subscribe((state, previous) => {
73
+ if (syncing || state.nodes === previous.nodes)
74
+ return;
75
+ const ids = new Set([...Object.keys(state.nodes), ...Object.keys(previous.nodes)]);
76
+ for (const id of ids) {
77
+ const next = state.nodes[id];
78
+ const prev = previous.nodes[id];
79
+ if (next !== prev && (isElevationRelevant(next) || isElevationRelevant(prev))) {
80
+ schedule();
81
+ return;
82
+ }
83
+ }
84
+ });
85
+ schedule();
86
+ return () => {
87
+ disposed = true;
88
+ unsubscribe();
89
+ };
90
+ }
91
+ export function RoofElevationSystem() {
92
+ useEffect(initializeRoofElevationSync, []);
93
+ return null;
94
+ }
@@ -0,0 +1,4 @@
1
+ import type { AnyNode, LevelNode, RoofNode, WallNode } from '../../schema';
2
+ export declare function resolveRoofWallTopElevation(targetLevelId: LevelNode['id'], wall: WallNode, nodes: Readonly<Record<string, AnyNode>>, elevations?: Map<string, import("../..").LevelElevation>): number;
3
+ export declare function resolveRoofElevation(roof: RoofNode, nodes: Readonly<Record<string, AnyNode>>): number;
4
+ //# sourceMappingURL=roof-elevation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roof-elevation.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-elevation.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAmB,QAAQ,EAAE,MAAM,cAAc,CAAA;AAM3F,wBAAgB,2BAA2B,CACzC,aAAa,EAAE,SAAS,CAAC,IAAI,CAAC,EAC9B,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,UAAU,8CAA4B,GACrC,MAAM,CASR;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,MAAM,CAwFR"}
@@ -0,0 +1,95 @@
1
+ import { getWallBaseElevationForNodes, getWallEffectiveHeightForNodes, } from '../../hooks/spatial-grid/spatial-grid-manager';
2
+ import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync';
3
+ import { findLevelBelowId, getLevelElevations } from '../../services/storey';
4
+ import { wallOverlapsSlabFootprint } from '../slab/slab-support';
5
+ import { getWallArcData } from '../wall/wall-curve';
6
+ import { resolveRoomRoofFootprintOnLevel } from './roof-footprint';
7
+ export function resolveRoofWallTopElevation(targetLevelId, wall, nodes, elevations = getLevelElevations(nodes)) {
8
+ const sourceLevelY = elevations.get(resolveLevelId(wall, nodes))?.baseY ?? 0;
9
+ const targetLevelY = elevations.get(targetLevelId)?.baseY ?? 0;
10
+ return (sourceLevelY +
11
+ getWallBaseElevationForNodes(wall, nodes) +
12
+ getWallEffectiveHeightForNodes(wall, nodes) -
13
+ targetLevelY);
14
+ }
15
+ export function resolveRoofElevation(roof, nodes) {
16
+ if (roof.support?.kind !== 'walls')
17
+ return roof.position[1];
18
+ const levelId = resolveLevelId(roof, nodes);
19
+ if (nodes[levelId]?.type !== 'level')
20
+ return roof.position[1];
21
+ const elevations = getLevelElevations(nodes);
22
+ // A roof usually sits on the storey above its walls, but the top floor (or a
23
+ // roof armed from the walls' own level) keeps roof and walls on one level.
24
+ const belowId = findLevelBelowId(levelId, elevations);
25
+ const candidateWallIds = [levelId, belowId]
26
+ .map((id) => (id ? nodes[id] : undefined))
27
+ .filter((node) => node?.type === 'level')
28
+ .flatMap((level) => level.children);
29
+ const conicalSegments = roof.children
30
+ .map((id) => nodes[id])
31
+ .filter((node) => node?.type === 'roof-segment' && node.roofType === 'conical');
32
+ const cos = Math.cos(roof.rotation);
33
+ const sin = Math.sin(roof.rotation);
34
+ const toLevel = (x, z) => [
35
+ roof.position[0] + x * cos + z * sin,
36
+ roof.position[2] - x * sin + z * cos,
37
+ ];
38
+ // Walls under the footprint, closed room or not: a room missing a wall, an
39
+ // L-shaped room whose centre falls outside, or a redrawn enclosure all still
40
+ // hold the roof up. The band test is curve- and thickness-aware and
41
+ // boundary-inclusive, so perimeter walls on the footprint edge count.
42
+ const footprints = roof.children
43
+ .map((id) => nodes[id])
44
+ .filter((node) => node?.type === 'roof-segment')
45
+ .map((segment) => {
46
+ const c = Math.cos(segment.rotation);
47
+ const s = Math.sin(segment.rotation);
48
+ const halfW = segment.width / 2;
49
+ const halfD = segment.depth / 2;
50
+ const corners = [
51
+ [-halfW, -halfD],
52
+ [halfW, -halfD],
53
+ [halfW, halfD],
54
+ [-halfW, halfD],
55
+ ];
56
+ return corners.map(([x, z]) => toLevel(segment.position[0] + x * c + z * s, segment.position[2] - x * s + z * c));
57
+ });
58
+ const wallIds = conicalSegments.length
59
+ ? candidateWallIds.filter((id) => {
60
+ const wall = nodes[id];
61
+ if (wall?.type !== 'wall')
62
+ return false;
63
+ const arc = getWallArcData(wall);
64
+ if (!arc)
65
+ return false;
66
+ return conicalSegments.some((segment) => {
67
+ const [centerX, centerZ] = toLevel(segment.position[0], segment.position[2]);
68
+ return (Math.hypot(arc.center.x - centerX, arc.center.y - centerZ) <= 1e-4 &&
69
+ Math.abs(arc.radius - segment.width / 2) <= 1e-4);
70
+ });
71
+ })
72
+ : footprints.length
73
+ ? candidateWallIds.filter((id) => {
74
+ const wall = nodes[id];
75
+ return (wall?.type === 'wall' &&
76
+ footprints.some((polygon) => wallOverlapsSlabFootprint(wall, polygon)));
77
+ })
78
+ : ([levelId, belowId]
79
+ .map((id) => id
80
+ ? resolveRoomRoofFootprintOnLevel(id, nodes, [
81
+ roof.position[0],
82
+ roof.position[2],
83
+ ])
84
+ : null)
85
+ .find((target) => target !== null)?.wallIds ?? []);
86
+ let highest;
87
+ for (const id of wallIds) {
88
+ const wall = nodes[id];
89
+ if (wall?.type !== 'wall')
90
+ continue;
91
+ const top = resolveRoofWallTopElevation(levelId, wall, nodes, elevations);
92
+ highest = highest === undefined ? top : Math.max(highest, top);
93
+ }
94
+ return highest ?? roof.position[1];
95
+ }
@@ -0,0 +1,17 @@
1
+ import type { AnyNode, LevelNode, WallNode } from '../../schema';
2
+ export type RoofFootprintTarget = {
3
+ id: string;
4
+ polygon: Array<[number, number]>;
5
+ wallIds: WallNode['id'][];
6
+ center: [number, number];
7
+ width: number;
8
+ depth: number;
9
+ rotation: number;
10
+ rectangular: boolean;
11
+ };
12
+ export declare function fitRoofFootprint(id: string, polygon: Array<[number, number]>, wallIds: WallNode['id'][]): RoofFootprintTarget | null;
13
+ export declare function resolveRoomRoofFootprint(levelId: LevelNode['id'], nodes: Readonly<Record<string, AnyNode>>, point: [number, number], options?: {
14
+ rectangularOnly?: boolean;
15
+ }): RoofFootprintTarget | null;
16
+ export declare function resolveRoomRoofFootprintOnLevel(levelId: LevelNode['id'], nodes: Readonly<Record<string, AnyNode>>, point: [number, number]): RoofFootprintTarget | null;
17
+ //# sourceMappingURL=roof-footprint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roof-footprint.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-footprint.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAGhE,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAChC,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAA;IACzB,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,OAAO,CAAA;CACrB,CAAA;AAWD,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAChC,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,GACxB,mBAAmB,GAAG,IAAI,CAqD5B;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,EACxB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EACvB,OAAO,GAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAO,GAC1C,mBAAmB,GAAG,IAAI,CAW5B;AAED,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,EACxB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GACtB,mBAAmB,GAAG,IAAI,CAW5B"}
@@ -0,0 +1,83 @@
1
+ import { pointInPolygon as pointInPolygon2D } from '../../lib/polygon-relations';
2
+ import { detectSpacesForLevel } from '../../lib/space-detection';
3
+ import { getLevelBelow } from '../../services/storey';
4
+ function polygonArea(polygon) {
5
+ return Math.abs(polygon.reduce((area, point, index) => {
6
+ const next = polygon[(index + 1) % polygon.length];
7
+ return next ? area + point[0] * next[1] - next[0] * point[1] : area;
8
+ }, 0) / 2);
9
+ }
10
+ export function fitRoofFootprint(id, polygon, wallIds) {
11
+ if (polygon.length < 3)
12
+ return null;
13
+ let best;
14
+ for (let index = 0; index < polygon.length; index++) {
15
+ const point = polygon[index];
16
+ const next = polygon[(index + 1) % polygon.length];
17
+ if (!(point && next))
18
+ continue;
19
+ const rotation = Math.atan2(next[1] - point[1], next[0] - point[0]);
20
+ const cos = Math.cos(rotation);
21
+ const sin = Math.sin(rotation);
22
+ const rotated = polygon.map(([x, z]) => [x * cos + z * sin, -x * sin + z * cos]);
23
+ const xs = rotated.map(([x]) => x);
24
+ const zs = rotated.map(([, z]) => z);
25
+ const minX = Math.min(...xs);
26
+ const maxX = Math.max(...xs);
27
+ const minZ = Math.min(...zs);
28
+ const maxZ = Math.max(...zs);
29
+ const width = maxX - minX;
30
+ const depth = maxZ - minZ;
31
+ const area = width * depth;
32
+ if (area <= 0 || (best && best.area <= area))
33
+ continue;
34
+ const localCenterX = (minX + maxX) / 2;
35
+ const localCenterZ = (minZ + maxZ) / 2;
36
+ best = {
37
+ center: [localCenterX * cos - localCenterZ * sin, localCenterX * sin + localCenterZ * cos],
38
+ width,
39
+ depth,
40
+ rotation: -rotation,
41
+ area,
42
+ };
43
+ }
44
+ if (!best)
45
+ return null;
46
+ return {
47
+ id,
48
+ polygon,
49
+ wallIds,
50
+ center: best.center,
51
+ width: best.width,
52
+ depth: best.depth,
53
+ rotation: best.rotation,
54
+ rectangular: polygonArea(polygon) / best.area >= 0.96,
55
+ };
56
+ }
57
+ export function resolveRoomRoofFootprint(levelId, nodes, point, options = {}) {
58
+ const activeTarget = resolveRoomRoofFootprintOnLevel(levelId, nodes, point);
59
+ if (activeTarget && (!options.rectangularOnly || activeTarget.rectangular))
60
+ return activeTarget;
61
+ if (activeTarget)
62
+ return null;
63
+ const levelBelow = getLevelBelow(levelId, nodes);
64
+ const levelBelowTarget = levelBelow
65
+ ? resolveRoomRoofFootprintOnLevel(levelBelow.id, nodes, point)
66
+ : null;
67
+ return levelBelowTarget && (!options.rectangularOnly || levelBelowTarget.rectangular)
68
+ ? levelBelowTarget
69
+ : null;
70
+ }
71
+ export function resolveRoomRoofFootprintOnLevel(levelId, nodes, point) {
72
+ const level = nodes[levelId];
73
+ if (level?.type !== 'level')
74
+ return null;
75
+ const walls = level.children
76
+ .map((id) => nodes[id])
77
+ .filter((node) => node?.type === 'wall');
78
+ const spaces = detectSpacesForLevel(levelId, walls)
79
+ .spaces.filter((space) => !space.isExterior && pointInPolygon2D(point, space.polygon))
80
+ .sort((left, right) => polygonArea(left.polygon) - polygonArea(right.polygon));
81
+ const space = spaces[0];
82
+ return space ? fitRoofFootprint(space.id, space.polygon, space.wallIds) : null;
83
+ }
@@ -0,0 +1,17 @@
1
+ import type { AnyNode, StairNode } from '../../schema';
2
+ import { StairSegmentNode } from '../../schema';
3
+ export type StairFlightOverrides = Partial<Pick<StairSegmentNode, 'width' | 'length' | 'height' | 'stepCount' | 'attachmentSide' | 'fillToFloor' | 'thickness'>>;
4
+ /**
5
+ * The single definition of a default straight flight. Anything left out falls
6
+ * through to the `StairSegmentNode` schema defaults (length 3 m, 10 steps,
7
+ * filled to floor) rather than being spelled again per call site, so the stair
8
+ * tool's seed segment, the flight the panel materializes when a curved stair
9
+ * becomes straight, and the viewer's fallback body all describe one stair.
10
+ */
11
+ export declare function createDefaultStairSegment(overrides?: StairFlightOverrides): StairSegmentNode;
12
+ /**
13
+ * The flight a straight stair implies from its own fields — used wherever a
14
+ * straight stair has to stand in for missing `stair-segment` children.
15
+ */
16
+ export declare function createStairFlightFromStair(stair: StairNode, nodes: Record<string, AnyNode>): StairSegmentNode;
17
+ //# sourceMappingURL=stair-flight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stair-flight.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-flight.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAM/C,MAAM,MAAM,oBAAoB,GAAG,OAAO,CACxC,IAAI,CACF,gBAAgB,EAChB,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,gBAAgB,GAAG,aAAa,GAAG,WAAW,CAC7F,CACF,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,GAAE,oBAAyB,GAAG,gBAAgB,CAEhG;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,SAAS,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,gBAAgB,CAQlB"}
@@ -0,0 +1,27 @@
1
+ import { StairSegmentNode } from '../../schema';
2
+ import { resolveStairTotalRise } from './stair-rise';
3
+ const MIN_STAIR_FLIGHT_RISE = 0.1;
4
+ const MIN_STAIR_FLIGHT_STEP_COUNT = 2;
5
+ /**
6
+ * The single definition of a default straight flight. Anything left out falls
7
+ * through to the `StairSegmentNode` schema defaults (length 3 m, 10 steps,
8
+ * filled to floor) rather than being spelled again per call site, so the stair
9
+ * tool's seed segment, the flight the panel materializes when a curved stair
10
+ * becomes straight, and the viewer's fallback body all describe one stair.
11
+ */
12
+ export function createDefaultStairSegment(overrides = {}) {
13
+ return StairSegmentNode.parse({ segmentType: 'stair', position: [0, 0, 0], ...overrides });
14
+ }
15
+ /**
16
+ * The flight a straight stair implies from its own fields — used wherever a
17
+ * straight stair has to stand in for missing `stair-segment` children.
18
+ */
19
+ export function createStairFlightFromStair(stair, nodes) {
20
+ return createDefaultStairSegment({
21
+ width: stair.width,
22
+ height: Math.max(resolveStairTotalRise(stair, nodes), MIN_STAIR_FLIGHT_RISE),
23
+ stepCount: Math.max(MIN_STAIR_FLIGHT_STEP_COUNT, Math.round(stair.stepCount ?? 10)),
24
+ thickness: stair.thickness,
25
+ fillToFloor: stair.fillToFloor,
26
+ });
27
+ }