@pascal-app/nodes 1.0.1 → 1.0.3

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 (319) hide show
  1. package/dist/block/definition.d.ts.map +1 -1
  2. package/dist/block/definition.js +4 -1
  3. package/dist/block/face-host.d.ts.map +1 -1
  4. package/dist/block/face-host.js +19 -2
  5. package/dist/block/hosted-edit.d.ts +13 -0
  6. package/dist/block/hosted-edit.d.ts.map +1 -0
  7. package/dist/block/hosted-edit.js +205 -0
  8. package/dist/block/last-operation.d.ts +1 -1
  9. package/dist/block/last-operation.d.ts.map +1 -1
  10. package/dist/block/last-operation.js +6 -4
  11. package/dist/block/modal-session.d.ts +2 -1
  12. package/dist/block/modal-session.d.ts.map +1 -1
  13. package/dist/block/modal-session.js +2 -2
  14. package/dist/block/selection.d.ts.map +1 -1
  15. package/dist/block/selection.js +82 -24
  16. package/dist/block/surface.d.ts +4 -0
  17. package/dist/block/surface.d.ts.map +1 -0
  18. package/dist/block/surface.js +54 -0
  19. package/dist/block/use-block-face-operation.d.ts +2 -2
  20. package/dist/block/use-block-face-operation.d.ts.map +1 -1
  21. package/dist/block/use-block-face-operation.js +25 -11
  22. package/dist/box-vent/definition.d.ts.map +1 -1
  23. package/dist/box-vent/definition.js +1 -0
  24. package/dist/building/renderer.d.ts.map +1 -1
  25. package/dist/building/renderer.js +1 -1
  26. package/dist/cabinet/definition.d.ts.map +1 -1
  27. package/dist/cabinet/definition.js +3 -1
  28. package/dist/cabinet/hosted-resize.d.ts +3 -6
  29. package/dist/cabinet/hosted-resize.d.ts.map +1 -1
  30. package/dist/cabinet/hosted-resize.js +9 -153
  31. package/dist/cabinet/placement-snap.d.ts.map +1 -1
  32. package/dist/cabinet/placement-snap.js +4 -2
  33. package/dist/cabinet/surfaces.d.ts +1 -0
  34. package/dist/cabinet/surfaces.d.ts.map +1 -1
  35. package/dist/cabinet/surfaces.js +13 -1
  36. package/dist/cabinet/tool.d.ts.map +1 -1
  37. package/dist/cabinet/tool.js +8 -4
  38. package/dist/ceiling/definition.d.ts.map +1 -1
  39. package/dist/ceiling/definition.js +5 -0
  40. package/dist/ceiling/renderer.d.ts.map +1 -1
  41. package/dist/ceiling/renderer.js +3 -2
  42. package/dist/chimney/definition.d.ts.map +1 -1
  43. package/dist/chimney/definition.js +1 -0
  44. package/dist/column/definition.d.ts.map +1 -1
  45. package/dist/column/definition.js +10 -3
  46. package/dist/column/floorplan-affordances.d.ts.map +1 -1
  47. package/dist/column/floorplan-affordances.js +6 -1
  48. package/dist/column/floorplan.d.ts.map +1 -1
  49. package/dist/column/floorplan.js +23 -11
  50. package/dist/column/hosted-resize.d.ts +116 -0
  51. package/dist/column/hosted-resize.d.ts.map +1 -0
  52. package/dist/column/hosted-resize.js +9 -0
  53. package/dist/column/panel.d.ts.map +1 -1
  54. package/dist/column/panel.js +6 -3
  55. package/dist/column/renderer.d.ts.map +1 -1
  56. package/dist/column/renderer.js +25 -103
  57. package/dist/column/shape.d.ts +22 -0
  58. package/dist/column/shape.d.ts.map +1 -0
  59. package/dist/column/shape.js +120 -0
  60. package/dist/column/surface.d.ts +4 -0
  61. package/dist/column/surface.d.ts.map +1 -0
  62. package/dist/column/surface.js +66 -0
  63. package/dist/cupola/definition.d.ts.map +1 -1
  64. package/dist/cupola/definition.js +1 -0
  65. package/dist/door/definition.d.ts.map +1 -1
  66. package/dist/door/definition.js +40 -1
  67. package/dist/door/door-math.d.ts +1 -1
  68. package/dist/door/door-math.d.ts.map +1 -1
  69. package/dist/door/door-math.js +7 -10
  70. package/dist/door/floorplan-affordances.d.ts.map +1 -1
  71. package/dist/door/floorplan-affordances.js +8 -12
  72. package/dist/door/move-tool.d.ts.map +1 -1
  73. package/dist/door/move-tool.js +3 -4
  74. package/dist/door/panel.d.ts.map +1 -1
  75. package/dist/door/panel.js +23 -52
  76. package/dist/door/tool.d.ts.map +1 -1
  77. package/dist/door/tool.js +20 -2
  78. package/dist/dormer/definition.d.ts.map +1 -1
  79. package/dist/dormer/definition.js +1 -0
  80. package/dist/downspout/definition.d.ts.map +1 -1
  81. package/dist/downspout/definition.js +1 -0
  82. package/dist/duct-segment/tool.d.ts.map +1 -1
  83. package/dist/duct-segment/tool.js +11 -0
  84. package/dist/duct-terminal/tool.d.ts.map +1 -1
  85. package/dist/duct-terminal/tool.js +77 -8
  86. package/dist/elevator/definition.d.ts.map +1 -1
  87. package/dist/elevator/definition.js +2 -0
  88. package/dist/elevator/parametrics.d.ts.map +1 -1
  89. package/dist/elevator/parametrics.js +98 -1
  90. package/dist/eyebrow-vent/definition.d.ts.map +1 -1
  91. package/dist/eyebrow-vent/definition.js +1 -0
  92. package/dist/fence/definition.d.ts.map +1 -1
  93. package/dist/fence/definition.js +1 -0
  94. package/dist/fence/tool.js +2 -2
  95. package/dist/guide/definition.d.ts.map +1 -1
  96. package/dist/guide/definition.js +1 -0
  97. package/dist/gutter/definition.d.ts.map +1 -1
  98. package/dist/gutter/definition.js +1 -0
  99. package/dist/hvac-equipment/tool.d.ts.map +1 -1
  100. package/dist/hvac-equipment/tool.js +8 -2
  101. package/dist/item/__fixtures__/main-floorplan.d.ts +26 -0
  102. package/dist/item/__fixtures__/main-floorplan.d.ts.map +1 -0
  103. package/dist/item/__fixtures__/main-floorplan.js +297 -0
  104. package/dist/item/definition.d.ts.map +1 -1
  105. package/dist/item/definition.js +3 -1
  106. package/dist/item/floorplan-move.d.ts.map +1 -1
  107. package/dist/item/floorplan-move.js +209 -130
  108. package/dist/item/floorplan.d.ts +22 -0
  109. package/dist/item/floorplan.d.ts.map +1 -1
  110. package/dist/item/floorplan.js +61 -2
  111. package/dist/item/move-tool.d.ts +1 -1
  112. package/dist/item/move-tool.d.ts.map +1 -1
  113. package/dist/item/move-tool.js +15 -7
  114. package/dist/item/renderer.d.ts.map +1 -1
  115. package/dist/item/renderer.js +8 -2
  116. package/dist/level/renderer.js +1 -1
  117. package/dist/lineset/tool.d.ts.map +1 -1
  118. package/dist/lineset/tool.js +1 -0
  119. package/dist/liquid-line/tool.d.ts.map +1 -1
  120. package/dist/liquid-line/tool.js +5 -1
  121. package/dist/measurement/definition.d.ts.map +1 -1
  122. package/dist/measurement/definition.js +1 -0
  123. package/dist/pipe-segment/tool.d.ts.map +1 -1
  124. package/dist/pipe-segment/tool.js +14 -3
  125. package/dist/pipe-trap/tool.d.ts.map +1 -1
  126. package/dist/pipe-trap/tool.js +7 -8
  127. package/dist/procedural-item/definition.d.ts.map +1 -1
  128. package/dist/procedural-item/definition.js +4 -1
  129. package/dist/ridge-vent/definition.d.ts.map +1 -1
  130. package/dist/ridge-vent/definition.js +1 -0
  131. package/dist/roof/definition.d.ts.map +1 -1
  132. package/dist/roof/definition.js +4 -1
  133. package/dist/roof/panel-model.d.ts +4 -0
  134. package/dist/roof/panel-model.d.ts.map +1 -0
  135. package/dist/roof/panel-model.js +151 -0
  136. package/dist/roof/panel.d.ts.map +1 -1
  137. package/dist/roof/panel.js +3 -4
  138. package/dist/roof/parametrics.d.ts.map +1 -1
  139. package/dist/roof/parametrics.js +16 -1
  140. package/dist/roof-segment/definition.d.ts.map +1 -1
  141. package/dist/roof-segment/definition.js +1 -0
  142. package/dist/roof-segment/parametrics.d.ts.map +1 -1
  143. package/dist/roof-segment/parametrics.js +176 -1
  144. package/dist/scan/definition.d.ts.map +1 -1
  145. package/dist/scan/definition.js +1 -0
  146. package/dist/shared/commit-opening-move.d.ts +4 -0
  147. package/dist/shared/commit-opening-move.d.ts.map +1 -0
  148. package/dist/shared/commit-opening-move.js +9 -0
  149. package/dist/shared/curtain-opening-limits.d.ts +14 -0
  150. package/dist/shared/curtain-opening-limits.d.ts.map +1 -0
  151. package/dist/shared/curtain-opening-limits.js +53 -0
  152. package/dist/shared/draft-axis-guides.d.ts +0 -6
  153. package/dist/shared/draft-axis-guides.d.ts.map +1 -1
  154. package/dist/shared/draft-axis-guides.js +1 -8
  155. package/dist/shared/hosted-resize.d.ts +12 -0
  156. package/dist/shared/hosted-resize.d.ts.map +1 -0
  157. package/dist/shared/hosted-resize.js +166 -0
  158. package/dist/shared/opening-grid-follow.d.ts +8 -0
  159. package/dist/shared/opening-grid-follow.d.ts.map +1 -0
  160. package/dist/shared/opening-grid-follow.js +12 -0
  161. package/dist/shared/opening-property-preview-host.d.ts +3 -0
  162. package/dist/shared/opening-property-preview-host.d.ts.map +1 -0
  163. package/dist/shared/opening-property-preview-host.js +21 -0
  164. package/dist/shared/opening-property-preview.d.ts +19 -0
  165. package/dist/shared/opening-property-preview.d.ts.map +1 -0
  166. package/dist/shared/opening-property-preview.js +99 -0
  167. package/dist/shared/resting-surface-plan.d.ts +2 -1
  168. package/dist/shared/resting-surface-plan.d.ts.map +1 -1
  169. package/dist/shared/resting-surface-plan.js +29 -4
  170. package/dist/shared/surface-counterparts.d.ts +12 -0
  171. package/dist/shared/surface-counterparts.d.ts.map +1 -0
  172. package/dist/shared/surface-counterparts.js +27 -0
  173. package/dist/shared/wall-attach-target.d.ts +1 -0
  174. package/dist/shared/wall-attach-target.d.ts.map +1 -1
  175. package/dist/shared/wall-attach-target.js +4 -1
  176. package/dist/shared/wall-event-from-grid.d.ts +5 -0
  177. package/dist/shared/wall-event-from-grid.d.ts.map +1 -0
  178. package/dist/shared/wall-event-from-grid.js +36 -0
  179. package/dist/shelf/definition.d.ts.map +1 -1
  180. package/dist/shelf/definition.js +3 -1
  181. package/dist/shelf/floorplan.d.ts.map +1 -1
  182. package/dist/shelf/floorplan.js +9 -2
  183. package/dist/site/renderer.d.ts.map +1 -1
  184. package/dist/site/renderer.js +9 -2
  185. package/dist/skylight/definition.d.ts.map +1 -1
  186. package/dist/skylight/definition.js +1 -0
  187. package/dist/slab/definition.d.ts.map +1 -1
  188. package/dist/slab/definition.js +4 -0
  189. package/dist/solar-panel/definition.d.ts.map +1 -1
  190. package/dist/solar-panel/definition.js +1 -0
  191. package/dist/spawn/definition.d.ts.map +1 -1
  192. package/dist/spawn/definition.js +1 -0
  193. package/dist/stair/definition.d.ts.map +1 -1
  194. package/dist/stair/definition.js +2 -0
  195. package/dist/stair/parametrics.d.ts.map +1 -1
  196. package/dist/stair/parametrics.js +164 -1
  197. package/dist/stair-segment/definition.d.ts.map +1 -1
  198. package/dist/stair-segment/definition.js +1 -0
  199. package/dist/stair-segment/parametrics.d.ts.map +1 -1
  200. package/dist/stair-segment/parametrics.js +81 -1
  201. package/dist/turbine-vent/definition.d.ts.map +1 -1
  202. package/dist/turbine-vent/definition.js +1 -0
  203. package/dist/wall/actions.d.ts +3 -0
  204. package/dist/wall/actions.d.ts.map +1 -0
  205. package/dist/wall/actions.js +56 -0
  206. package/dist/wall/curtain-opening-frame.d.ts +14 -0
  207. package/dist/wall/curtain-opening-frame.d.ts.map +1 -0
  208. package/dist/wall/curtain-opening-frame.js +111 -0
  209. package/dist/wall/curtain-panel-highlight.d.ts +3 -0
  210. package/dist/wall/curtain-panel-highlight.d.ts.map +1 -0
  211. package/dist/wall/curtain-panel-highlight.js +113 -0
  212. package/dist/wall/curtain-wall-adapter.d.ts +3 -0
  213. package/dist/wall/curtain-wall-adapter.d.ts.map +1 -0
  214. package/dist/wall/curtain-wall-adapter.js +55 -0
  215. package/dist/wall/curtain-wall-floorplan.d.ts +3 -0
  216. package/dist/wall/curtain-wall-floorplan.d.ts.map +1 -0
  217. package/dist/wall/curtain-wall-floorplan.js +91 -0
  218. package/dist/wall/curtain-wall-geometry.d.ts +4 -0
  219. package/dist/wall/curtain-wall-geometry.d.ts.map +1 -0
  220. package/dist/wall/curtain-wall-geometry.js +226 -0
  221. package/dist/wall/curtain-wall-layout.d.ts +16 -0
  222. package/dist/wall/curtain-wall-layout.d.ts.map +1 -0
  223. package/dist/wall/curtain-wall-layout.js +176 -0
  224. package/dist/wall/curtain-wall-materials.d.ts +5 -0
  225. package/dist/wall/curtain-wall-materials.d.ts.map +1 -0
  226. package/dist/wall/curtain-wall-materials.js +55 -0
  227. package/dist/wall/curtain-wall-panel.d.ts +14 -0
  228. package/dist/wall/curtain-wall-panel.d.ts.map +1 -0
  229. package/dist/wall/curtain-wall-panel.js +167 -0
  230. package/dist/wall/curtain-wall-piece-geometry.d.ts +5 -0
  231. package/dist/wall/curtain-wall-piece-geometry.d.ts.map +1 -0
  232. package/dist/wall/curtain-wall-piece-geometry.js +220 -0
  233. package/dist/wall/curtain-wall-shadow.d.ts +4 -0
  234. package/dist/wall/curtain-wall-shadow.d.ts.map +1 -0
  235. package/dist/wall/curtain-wall-shadow.js +15 -0
  236. package/dist/wall/definition.d.ts +0 -15
  237. package/dist/wall/definition.d.ts.map +1 -1
  238. package/dist/wall/definition.js +65 -4
  239. package/dist/wall/drawing-mode.d.ts +12 -0
  240. package/dist/wall/drawing-mode.d.ts.map +1 -0
  241. package/dist/wall/drawing-mode.js +38 -0
  242. package/dist/wall/floorplan-tool.d.ts +4 -0
  243. package/dist/wall/floorplan-tool.d.ts.map +1 -0
  244. package/dist/wall/floorplan-tool.js +107 -0
  245. package/dist/wall/floorplan.d.ts.map +1 -1
  246. package/dist/wall/floorplan.js +6 -2
  247. package/dist/wall/length-patch.d.ts +3 -0
  248. package/dist/wall/length-patch.d.ts.map +1 -0
  249. package/dist/wall/length-patch.js +14 -0
  250. package/dist/wall/move-tool.d.ts.map +1 -1
  251. package/dist/wall/move-tool.js +23 -1
  252. package/dist/wall/paint.d.ts.map +1 -1
  253. package/dist/wall/paint.js +39 -4
  254. package/dist/wall/panel-model.d.ts +4 -0
  255. package/dist/wall/panel-model.d.ts.map +1 -0
  256. package/dist/wall/panel-model.js +117 -0
  257. package/dist/wall/panel.d.ts.map +1 -1
  258. package/dist/wall/panel.js +20 -24
  259. package/dist/wall/property-preview.d.ts +7 -0
  260. package/dist/wall/property-preview.d.ts.map +1 -0
  261. package/dist/wall/property-preview.js +31 -0
  262. package/dist/wall/rectangle-command.d.ts +149 -0
  263. package/dist/wall/rectangle-command.d.ts.map +1 -0
  264. package/dist/wall/rectangle-command.js +22 -0
  265. package/dist/wall/rectangle-tool.d.ts +2 -0
  266. package/dist/wall/rectangle-tool.d.ts.map +1 -0
  267. package/dist/wall/rectangle-tool.js +147 -0
  268. package/dist/wall/renderer.d.ts.map +1 -1
  269. package/dist/wall/renderer.js +7 -4
  270. package/dist/wall/slots.d.ts +2 -2
  271. package/dist/wall/slots.d.ts.map +1 -1
  272. package/dist/wall/slots.js +10 -2
  273. package/dist/wall/split-floorplan-layer.d.ts +4 -0
  274. package/dist/wall/split-floorplan-layer.d.ts.map +1 -0
  275. package/dist/wall/split-floorplan-layer.js +71 -0
  276. package/dist/wall/split-pointer.d.ts +3 -0
  277. package/dist/wall/split-pointer.d.ts.map +1 -0
  278. package/dist/wall/split-pointer.js +95 -0
  279. package/dist/wall/split-preview.d.ts +55 -0
  280. package/dist/wall/split-preview.d.ts.map +1 -0
  281. package/dist/wall/split-preview.js +106 -0
  282. package/dist/wall/split-session.d.ts +16 -0
  283. package/dist/wall/split-session.d.ts.map +1 -0
  284. package/dist/wall/split-session.js +138 -0
  285. package/dist/wall/split-store.d.ts +16 -0
  286. package/dist/wall/split-store.d.ts.map +1 -0
  287. package/dist/wall/split-store.js +6 -0
  288. package/dist/wall/split-tool.d.ts +10 -0
  289. package/dist/wall/split-tool.d.ts.map +1 -0
  290. package/dist/wall/split-tool.js +95 -0
  291. package/dist/wall/system.d.ts.map +1 -1
  292. package/dist/wall/system.js +3 -1
  293. package/dist/wall/tool.d.ts.map +1 -1
  294. package/dist/wall/tool.js +14 -6
  295. package/dist/wall/wall-batch-system.d.ts.map +1 -1
  296. package/dist/wall/wall-batch-system.js +3 -0
  297. package/dist/window/definition.d.ts.map +1 -1
  298. package/dist/window/definition.js +48 -1
  299. package/dist/window/floorplan-affordances.d.ts.map +1 -1
  300. package/dist/window/floorplan-affordances.js +8 -10
  301. package/dist/window/move-tool.d.ts.map +1 -1
  302. package/dist/window/move-tool.js +4 -6
  303. package/dist/window/panel.d.ts.map +1 -1
  304. package/dist/window/panel.js +21 -59
  305. package/dist/window/tool.d.ts.map +1 -1
  306. package/dist/window/tool.js +34 -2
  307. package/dist/window/window-math.d.ts +1 -1
  308. package/dist/window/window-math.d.ts.map +1 -1
  309. package/dist/window/window-math.js +8 -11
  310. package/dist/zone/definition.d.ts.map +1 -1
  311. package/dist/zone/definition.js +1 -0
  312. package/dist/zone/quantities-panel.d.ts.map +1 -1
  313. package/dist/zone/quantities-panel.js +3 -19
  314. package/dist/zone/renderer.d.ts.map +1 -1
  315. package/dist/zone/renderer.js +1 -1
  316. package/package.json +8 -8
  317. package/dist/shared/opening-documentation-fields.d.ts +0 -16
  318. package/dist/shared/opening-documentation-fields.d.ts.map +0 -1
  319. package/dist/shared/opening-documentation-fields.js +0 -54
@@ -5,6 +5,7 @@ import { useViewer } from '@pascal-app/viewer';
5
5
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
6
  import { BoxGeometry, EdgesGeometry, Vector3 } from 'three';
7
7
  import { LineBasicNodeMaterial } from 'three/webgpu';
8
+ import { commitOpeningMove } from '../shared/commit-opening-move';
8
9
  import { clearOpeningGuides3D, publishOpeningGuidesForWallEvent, } from '../shared/opening-guides-runtime';
9
10
  import { beginOpeningMoveHistorySession } from '../shared/opening-move-history';
10
11
  import { isWallMeshHidden, shouldIgnoreWallEventForOpeningMove, } from '../shared/opening-move-wall-gate';
@@ -451,14 +452,13 @@ const MoveDoorTool = ({ node: movingDoorNode }) => {
451
452
  visible: original.visible,
452
453
  });
453
454
  history.commitStep(() => {
454
- useScene.getState().updateNode(movingDoorNode.id, {
455
+ commitOpeningMove(movingDoorNode.id, {
455
456
  position: [target.clampedX, target.clampedY, 0],
456
457
  rotation: [0, target.itemRotation, 0],
457
458
  side: target.side,
458
459
  parentId: target.wallId,
459
460
  wallId: target.wallId,
460
461
  roofSegmentId: undefined,
461
- metadata: {},
462
462
  visible: true,
463
463
  });
464
464
  });
@@ -712,7 +712,7 @@ const MoveDoorTool = ({ node: movingDoorNode }) => {
712
712
  visible: original.visible,
713
713
  });
714
714
  history.commitStep(() => {
715
- useScene.getState().updateNode(movingDoorNode.id, {
715
+ commitOpeningMove(movingDoorNode.id, {
716
716
  position: target.position,
717
717
  rotation: [0, 0, 0],
718
718
  side: 'front',
@@ -720,7 +720,6 @@ const MoveDoorTool = ({ node: movingDoorNode }) => {
720
720
  wallId: undefined,
721
721
  roofSegmentId: segmentId,
722
722
  roofFace: target.face.id,
723
- metadata: {},
724
723
  visible: true,
725
724
  });
726
725
  });
@@ -1 +1 @@
1
- {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/door/panel.tsx"],"names":[],"mappings":"AAuIA,MAAM,CAAC,OAAO,UAAU,SAAS,uCAkpChC"}
1
+ {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/door/panel.tsx"],"names":[],"mappings":"AAyIA,MAAM,CAAC,OAAO,UAAU,SAAS,uCA+nChC"}
@@ -4,8 +4,10 @@ import { DoorNode, useInteractive, useScene } from '@pascal-app/core';
4
4
  import { ActionButton, ActionGroup, cn, PanelSection, PanelWrapper, SegmentedControl, SliderControl, ToggleControl, triggerSFX, useEditor, } from '@pascal-app/editor';
5
5
  import { useViewer } from '@pascal-app/viewer';
6
6
  import { Copy, DoorOpen, FlipHorizontal2, Move, Trash2 } from 'lucide-react';
7
- import { useCallback, useRef } from 'react';
8
- import { OpeningDocumentationFields } from '../shared/opening-documentation-fields';
7
+ import { useCallback, useEffect, useMemo } from 'react';
8
+ import { constrainCurtainOpening, curtainOpeningLimits } from '../shared/curtain-opening-limits';
9
+ import { createOpeningPropertyPreview } from '../shared/opening-property-preview';
10
+ import { openingPropertyPreviewHost } from '../shared/opening-property-preview-host';
9
11
  import { scaleHandleHeight } from './door-math';
10
12
  const doorTypeOptions = [
11
13
  { label: 'Hinged', value: 'hinged', available: true },
@@ -106,7 +108,10 @@ export default function DoorPanel() {
106
108
  const setSelection = useViewer((s) => s.setSelection);
107
109
  const deleteNode = useScene((s) => s.deleteNode);
108
110
  const setMovingNode = useEditor((s) => s.setMovingNode);
109
- const previewRef = useRef(null);
111
+ const preview = useMemo(() => selectedId
112
+ ? createOpeningPropertyPreview(selectedId, openingPropertyPreviewHost)
113
+ : null, [selectedId]);
114
+ useEffect(() => () => preview?.cancel(), [preview]);
110
115
  const node = useScene((s) => selectedId ? s.nodes[selectedId] : undefined);
111
116
  // Panel slider-drag fix recipe (plans/editor-node-registry.md). Without
112
117
  // it, the 29+ SliderControls in this panel would loop on drag.
@@ -116,6 +121,7 @@ export default function DoorPanel() {
116
121
  const liveNode = useScene.getState().nodes[selectedId];
117
122
  if (liveNode?.type !== 'door')
118
123
  return;
124
+ updates = constrainCurtainOpening(liveNode, updates, useScene.getState().nodes);
119
125
  const hasChange = Object.entries(updates).some(([key, value]) => {
120
126
  const currentValue = liveNode[key];
121
127
  return !isSameDoorValue(currentValue, value);
@@ -131,43 +137,8 @@ export default function DoorPanel() {
131
137
  if (liveNode.parentId)
132
138
  scene.dirtyNodes.add(liveNode.parentId);
133
139
  }, [selectedId]);
134
- const previewDoorUpdate = useCallback((key, value) => {
135
- if (!selectedId)
136
- return;
137
- const liveNode = useScene.getState().nodes[selectedId];
138
- if (liveNode?.type !== 'door')
139
- return;
140
- if (!(previewRef.current &&
141
- previewRef.current.id === selectedId &&
142
- previewRef.current.key === key)) {
143
- previewRef.current = {
144
- id: selectedId,
145
- key,
146
- value: liveNode[key],
147
- };
148
- }
149
- if (isSameDoorValue(liveNode[key], value))
150
- return;
151
- liveNode[key] = value;
152
- useScene.getState().dirtyNodes.add(selectedId);
153
- }, [selectedId]);
154
- const commitDoorPreview = useCallback((key, value) => {
155
- if (!selectedId)
156
- return;
157
- const scene = useScene.getState();
158
- const liveNode = scene.nodes[selectedId];
159
- const preview = previewRef.current;
160
- if (liveNode?.type === 'door' && preview?.id === selectedId && preview.key === key) {
161
- ;
162
- liveNode[key] = preview.value;
163
- scene.dirtyNodes.add(selectedId);
164
- }
165
- previewRef.current = null;
166
- useScene
167
- .getState()
168
- .updateNode(selectedId, { [key]: value });
169
- scene.dirtyNodes.add(selectedId);
170
- }, [selectedId]);
140
+ const previewDoorUpdate = (key, value) => preview?.preview({ [key]: value });
141
+ const commitDoorPreview = (key, value) => preview?.commit({ [key]: value });
171
142
  const handleClose = useCallback(() => {
172
143
  setSelection({ selectedIds: [] });
173
144
  }, [setSelection]);
@@ -254,6 +225,12 @@ export default function DoorPanel() {
254
225
  };
255
226
  if (!(node && node.type === 'door' && selectedId))
256
227
  return null;
228
+ const limits = curtainOpeningLimits(node, useScene.getState().nodes);
229
+ const heightUpdates = (height) => ({
230
+ height,
231
+ position: [node.position[0], height / 2, node.position[2]],
232
+ handleHeight: scaleHandleHeight(node.handleHeight, node.height, height),
233
+ });
257
234
  const hSum = node.segments.reduce((s, seg) => s + seg.heightRatio, 0);
258
235
  const normHeights = node.segments.map((seg) => seg.heightRatio / hSum);
259
236
  const isOpening = node.openingKind === 'opening';
@@ -478,19 +455,13 @@ export default function DoorPanel() {
478
455
  ? 'border-orange-400/60 bg-orange-400/10 text-foreground'
479
456
  : 'border-border/50 bg-[#2C2C2E] text-muted-foreground hover:bg-[#3e3e3e] hover:text-foreground', !option.available &&
480
457
  'cursor-not-allowed opacity-45 hover:bg-[#2C2C2E] hover:text-muted-foreground'), disabled: !option.available, onClick: () => handleUpdate(getDoorTypeUpdates(option.value)), type: "button", children: [_jsx(DoorOpen, { className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "truncate font-medium", children: option.label })] }, option.value));
481
- }) }))] }), _jsx(PanelSection, { title: "Documentation", children: _jsx(OpeningDocumentationFields, { constructionType: node.constructionType, dimensionReference: node.dimensionReference, finishOpeningHeight: node.finishOpeningHeight, finishOpeningWidth: node.finishOpeningWidth, mark: node.mark, masonryOpeningHeight: node.masonryOpeningHeight, masonryOpeningWidth: node.masonryOpeningWidth, onChange: handleUpdate, roughOpeningHeight: node.roughOpeningHeight, roughOpeningWidth: node.roughOpeningWidth }) }), _jsxs(PanelSection, { title: "Position", children: [_jsx(SliderControl, { label: _jsxs(_Fragment, { children: ["X", _jsx("sub", { className: "ml-[1px] text-[11px] opacity-70", children: "wall" })] }), max: 10, min: -10, onChange: (v) => handleUpdate({ position: [v, node.position[1], node.position[2]] }), precision: 2, step: 0.1, unit: "m", value: node.position[0] }), showFlipSide && (_jsx("div", { className: "px-1 pt-2 pb-1", children: _jsx(ActionButton, { className: "w-full", icon: _jsx(FlipHorizontal2, { className: "h-4 w-4" }), label: "Flip Side", onClick: handleFlip }) }))] }), showFoldSection && (_jsxs(PanelSection, { title: "Fold", children: [_jsx("div", { className: "flex flex-col gap-2 px-1 pb-1", children: _jsxs("div", { className: "space-y-1", children: [_jsx("span", { className: "font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider", children: "Panels" }), _jsx(SegmentedControl, { onChange: (v) => handleUpdate({ leafCount: v === '2' ? 2 : 4 }), options: [
458
+ }) }))] }), _jsxs(PanelSection, { title: "Position", children: [_jsx(SliderControl, { label: _jsxs(_Fragment, { children: ["X", _jsx("sub", { className: "ml-[1px] text-[11px] opacity-70", children: "wall" })] }), max: 10, min: -10, onChange: (v) => handleUpdate({ position: [v, node.position[1], node.position[2]] }), precision: 2, step: 0.1, unit: "m", value: node.position[0] }), showFlipSide && (_jsx("div", { className: "px-1 pt-2 pb-1", children: _jsx(ActionButton, { className: "w-full", icon: _jsx(FlipHorizontal2, { className: "h-4 w-4" }), label: "Flip Side", onClick: handleFlip }) }))] }), showFoldSection && (_jsxs(PanelSection, { title: "Fold", children: [_jsx("div", { className: "flex flex-col gap-2 px-1 pb-1", children: _jsxs("div", { className: "space-y-1", children: [_jsx("span", { className: "font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider", children: "Panels" }), _jsx(SegmentedControl, { onChange: (v) => handleUpdate({ leafCount: v === '2' ? 2 : 4 }), options: [
482
459
  { label: '2', value: '2' },
483
460
  { label: '4', value: '4' },
484
461
  ], value: node.leafCount === 2 ? '2' : '4' })] }) }), _jsx(SliderControl, { label: "Open", max: 100, min: 0, onChange: (v) => handleUpdate({ operationState: v / 100 }), precision: 0, restoreOnCommit: false, step: 5, unit: "%", value: Math.round((node.operationState ?? 0) * 100) })] })), showSlideSection && (_jsxs(PanelSection, { title: "Slide", children: [_jsx("div", { className: "flex flex-col gap-2 px-1 pb-1", children: _jsxs("div", { className: "space-y-1", children: [_jsx("span", { className: "font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider", children: doorType === 'pocket' ? 'Pocket' : doorType === 'barn' ? 'Rail' : 'Panel' }), _jsx(SegmentedControl, { onChange: (v) => handleUpdate({ slideDirection: v }), options: [
485
462
  { label: 'Left', value: 'left' },
486
463
  { label: 'Right', value: 'right' },
487
- ], value: node.slideDirection ?? 'left' })] }) }), _jsx(SliderControl, { label: "Open", max: 100, min: 0, onChange: (v) => handleUpdate({ operationState: v / 100 }), precision: 0, restoreOnCommit: false, step: 5, unit: "%", value: Math.round((node.operationState ?? 0) * 100) })] })), showGarageSection && (_jsxs(PanelSection, { title: "Garage", children: [_jsx(SliderControl, { label: "Open", max: 100, min: 0, onChange: (v) => handleUpdate({ operationState: v / 100 }), precision: 0, restoreOnCommit: false, step: 5, unit: "%", value: Math.round((node.operationState ?? 0) * 100) }), isSectionalGarageDoor && (_jsx(SliderControl, { label: "Panels", max: 8, min: 3, onChange: (v) => handleUpdate({ garagePanelCount: Math.round(v) }), precision: 0, restoreOnCommit: false, step: 1, value: node.garagePanelCount ?? 4 }))] })), _jsxs(PanelSection, { title: "Dimensions", children: [_jsx(SliderControl, { label: "Width", max: maxDoorWidth, min: 0.5, onChange: (v) => handleUpdate({ width: v }), precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: node.width }), _jsx(SliderControl, { label: "Height", max: 1000, min: 1.0, onChange: (v) => handleUpdate({
488
- height: v,
489
- position: [node.position[0], v / 2, node.position[2]],
490
- // Keep the handle at the same relative height as the door resizes,
491
- // matching the height-resize arrow.
492
- handleHeight: scaleHandleHeight(node.handleHeight, node.height, v),
493
- }), precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: node.height })] }), showDoorShapeSection && (_jsxs(PanelSection, { title: "Top Shape", children: [_jsx("div", { className: "flex flex-col gap-2 px-1 pb-1", children: _jsx(SegmentedControl, { onChange: (v) => handleUpdate({
464
+ ], value: node.slideDirection ?? 'left' })] }) }), _jsx(SliderControl, { label: "Open", max: 100, min: 0, onChange: (v) => handleUpdate({ operationState: v / 100 }), precision: 0, restoreOnCommit: false, step: 5, unit: "%", value: Math.round((node.operationState ?? 0) * 100) })] })), showGarageSection && (_jsxs(PanelSection, { title: "Garage", children: [_jsx(SliderControl, { label: "Open", max: 100, min: 0, onChange: (v) => handleUpdate({ operationState: v / 100 }), precision: 0, restoreOnCommit: false, step: 5, unit: "%", value: Math.round((node.operationState ?? 0) * 100) }), isSectionalGarageDoor && (_jsx(SliderControl, { label: "Panels", max: 8, min: 3, onChange: (v) => handleUpdate({ garagePanelCount: Math.round(v) }), precision: 0, restoreOnCommit: false, step: 1, value: node.garagePanelCount ?? 4 }))] })), _jsxs(PanelSection, { title: "Dimensions", children: [limits && (_jsx("p", { className: "text-[11px] text-muted-foreground", children: "Size is limited to the wall, including clearance for the opening frame." })), _jsx(SliderControl, { label: "Width", max: Math.min(maxDoorWidth, limits?.width ?? Infinity), min: Math.min(0.5, limits?.width ?? 0.5), onChange: (v) => previewDoorUpdate('width', v), onCommit: (v) => commitDoorPreview('width', v), onCancel: () => preview?.cancel(), previewWhileTyping: true, precision: 2, restoreOnCommit: false, step: 0.01, unit: "m", value: node.width }), _jsx(SliderControl, { label: "Height", max: limits?.height ?? 1000, min: Math.min(1, limits?.height ?? 1), onChange: (v) => preview?.preview(heightUpdates(v)), onCommit: (v) => preview?.commit(heightUpdates(v)), onCancel: () => preview?.cancel(), previewWhileTyping: true, precision: 2, restoreOnCommit: false, step: 0.01, unit: "m", value: node.height })] }), showDoorShapeSection && (_jsxs(PanelSection, { title: "Top Shape", children: [_jsx("div", { className: "flex flex-col gap-2 px-1 pb-1", children: _jsx(SegmentedControl, { onChange: (v) => handleUpdate({
494
465
  openingShape: v,
495
466
  ...(v === 'rounded'
496
467
  ? {
@@ -508,10 +479,10 @@ export default function DoorPanel() {
508
479
  ], value: doorShape }) }), doorShape === 'rounded' && (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex flex-col gap-2 px-1 pb-1", children: _jsx(SegmentedControl, { onChange: (v) => handleUpdate({ openingRadiusMode: v }), options: [
509
480
  { label: 'All', value: 'all' },
510
481
  { label: 'Individual', value: 'individual' },
511
- ], value: openingRadiusMode }) }), openingRadiusMode === 'all' ? (_jsx(SliderControl, { label: "Corner Radius", max: maxRoundedRadius, min: 0, onChange: (v) => previewDoorUpdate('cornerRadius', v), onCommit: (v) => commitDoorPreview('cornerRadius', v), precision: 2, step: 0.05, unit: "m", value: cornerRadius })) : (_jsx(_Fragment, { children: [
482
+ ], value: openingRadiusMode }) }), openingRadiusMode === 'all' ? (_jsx(SliderControl, { label: "Corner Radius", max: maxRoundedRadius, min: 0, onChange: (v) => previewDoorUpdate('cornerRadius', v), onCommit: (v) => commitDoorPreview('cornerRadius', v), onCancel: () => preview?.cancel(), previewWhileTyping: true, restoreOnCommit: false, precision: 2, step: 0.05, unit: "m", value: cornerRadius })) : (_jsx(_Fragment, { children: [
512
483
  ['Top Left', 0],
513
484
  ['Top Right', 1],
514
- ].map(([label, index]) => (_jsx(SliderControl, { label: label, max: maxRoundedRadius, min: 0, onChange: (v) => setOpeningTopRadius(index, v), onCommit: (v) => setOpeningTopRadius(index, v, true), precision: 2, step: 0.05, unit: "m", value: openingTopRadii[index] ?? 0 }, label))) })), _jsx(SliderControl, { label: "Reveal Radius", max: 0.08, min: 0, onChange: (v) => previewDoorUpdate('openingRevealRadius', v), onCommit: (v) => commitDoorPreview('openingRevealRadius', v), precision: 3, step: 0.005, unit: "m", value: openingRevealRadius })] })), doorShape === 'arch' && (_jsx(SliderControl, { label: "Arch Height", max: node.height, min: 0.05, onChange: (v) => handleUpdate({ archHeight: v }), precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: archHeight }))] })), showOpeningShapeSection && (_jsxs(PanelSection, { title: "Opening Shape", children: [_jsx("div", { className: "flex flex-col gap-2 px-1 pb-1", children: _jsx(SegmentedControl, { onChange: (v) => handleUpdate({
485
+ ].map(([label, index]) => (_jsx(SliderControl, { label: label, max: maxRoundedRadius, min: 0, onChange: (v) => setOpeningTopRadius(index, v), onCommit: (v) => setOpeningTopRadius(index, v, true), onCancel: () => preview?.cancel(), previewWhileTyping: true, restoreOnCommit: false, precision: 2, step: 0.05, unit: "m", value: openingTopRadii[index] ?? 0 }, label))) })), _jsx(SliderControl, { label: "Reveal Radius", max: 0.08, min: 0, onChange: (v) => previewDoorUpdate('openingRevealRadius', v), onCommit: (v) => commitDoorPreview('openingRevealRadius', v), onCancel: () => preview?.cancel(), previewWhileTyping: true, restoreOnCommit: false, precision: 3, step: 0.005, unit: "m", value: openingRevealRadius })] })), doorShape === 'arch' && (_jsx(SliderControl, { label: "Arch Height", max: node.height, min: 0.05, onChange: (v) => previewDoorUpdate('archHeight', v), onCommit: (v) => commitDoorPreview('archHeight', v), onCancel: () => preview?.cancel(), previewWhileTyping: true, precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: archHeight }))] })), showOpeningShapeSection && (_jsxs(PanelSection, { title: "Opening Shape", children: [_jsx("div", { className: "flex flex-col gap-2 px-1 pb-1", children: _jsx(SegmentedControl, { onChange: (v) => handleUpdate({
515
486
  openingShape: v,
516
487
  ...(v === 'rounded'
517
488
  ? { openingRadiusMode, openingTopRadii, cornerRadius, openingRevealRadius }
@@ -524,10 +495,10 @@ export default function DoorPanel() {
524
495
  ], value: openingShape }) }), openingShape === 'rounded' && (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex flex-col gap-2 px-1 pb-1", children: _jsx(SegmentedControl, { onChange: (v) => handleUpdate({ openingRadiusMode: v }), options: [
525
496
  { label: 'All', value: 'all' },
526
497
  { label: 'Individual', value: 'individual' },
527
- ], value: openingRadiusMode }) }), openingRadiusMode === 'all' ? (_jsx(SliderControl, { label: "Corner Radius", max: maxRoundedRadius, min: 0, onChange: (v) => previewDoorUpdate('cornerRadius', v), onCommit: (v) => commitDoorPreview('cornerRadius', v), precision: 2, step: 0.05, unit: "m", value: cornerRadius })) : (_jsx(_Fragment, { children: [
498
+ ], value: openingRadiusMode }) }), openingRadiusMode === 'all' ? (_jsx(SliderControl, { label: "Corner Radius", max: maxRoundedRadius, min: 0, onChange: (v) => previewDoorUpdate('cornerRadius', v), onCommit: (v) => commitDoorPreview('cornerRadius', v), onCancel: () => preview?.cancel(), previewWhileTyping: true, restoreOnCommit: false, precision: 2, step: 0.05, unit: "m", value: cornerRadius })) : (_jsx(_Fragment, { children: [
528
499
  ['Top Left', 0],
529
500
  ['Top Right', 1],
530
- ].map(([label, index]) => (_jsx(SliderControl, { label: label, max: maxRoundedRadius, min: 0, onChange: (v) => setOpeningTopRadius(index, v), onCommit: (v) => setOpeningTopRadius(index, v, true), precision: 2, step: 0.05, unit: "m", value: openingTopRadii[index] ?? 0 }, label))) })), _jsx(SliderControl, { label: "Reveal Radius", max: 0.08, min: 0, onChange: (v) => previewDoorUpdate('openingRevealRadius', v), onCommit: (v) => commitDoorPreview('openingRevealRadius', v), precision: 3, step: 0.005, unit: "m", value: openingRevealRadius })] })), openingShape === 'arch' && (_jsx(SliderControl, { label: "Arch Height", max: node.height, min: 0.05, onChange: (v) => handleUpdate({ archHeight: v }), precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: archHeight }))] })), !isCutoutOnly && (_jsxs(_Fragment, { children: [showFrameSection && (_jsxs(PanelSection, { title: "Frame", children: [_jsx(SliderControl, { label: "Thickness", max: 0.2, min: 0.01, onChange: (v) => handleUpdate({ frameThickness: v }), precision: 3, step: 0.01, unit: "m", value: node.frameThickness }), _jsx(SliderControl, { label: "Depth", max: 0.3, min: 0.01, onChange: (v) => handleUpdate({ frameDepth: v }), precision: 3, step: 0.01, unit: "m", value: node.frameDepth })] })), showContentPaddingSection && (_jsxs(PanelSection, { title: "Content Padding", children: [_jsx(SliderControl, { label: "Horizontal", max: 0.2, min: 0, onChange: (v) => handleUpdate({ contentPadding: [v, node.contentPadding[1]] }), precision: 3, step: 0.005, unit: "m", value: node.contentPadding[0] }), _jsx(SliderControl, { label: "Vertical", max: 0.2, min: 0, onChange: (v) => handleUpdate({ contentPadding: [node.contentPadding[0], v] }), precision: 3, step: 0.005, unit: "m", value: node.contentPadding[1] })] })), showSwingSection && (_jsx(PanelSection, { title: "Swing", children: _jsxs("div", { className: "flex flex-col gap-2 px-1 pb-1", children: [supportsHingeSide && (_jsxs("div", { className: "space-y-1", children: [_jsx("span", { className: "font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider", children: "Hinges Side" }), _jsx(SegmentedControl, { onChange: (v) => handleUpdate({ hingesSide: v }), options: [
501
+ ].map(([label, index]) => (_jsx(SliderControl, { label: label, max: maxRoundedRadius, min: 0, onChange: (v) => setOpeningTopRadius(index, v), onCommit: (v) => setOpeningTopRadius(index, v, true), onCancel: () => preview?.cancel(), previewWhileTyping: true, restoreOnCommit: false, precision: 2, step: 0.05, unit: "m", value: openingTopRadii[index] ?? 0 }, label))) })), _jsx(SliderControl, { label: "Reveal Radius", max: 0.08, min: 0, onChange: (v) => previewDoorUpdate('openingRevealRadius', v), onCommit: (v) => commitDoorPreview('openingRevealRadius', v), onCancel: () => preview?.cancel(), previewWhileTyping: true, restoreOnCommit: false, precision: 3, step: 0.005, unit: "m", value: openingRevealRadius })] })), openingShape === 'arch' && (_jsx(SliderControl, { label: "Arch Height", max: node.height, min: 0.05, onChange: (v) => previewDoorUpdate('archHeight', v), onCommit: (v) => commitDoorPreview('archHeight', v), onCancel: () => preview?.cancel(), previewWhileTyping: true, precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: archHeight }))] })), !isCutoutOnly && (_jsxs(_Fragment, { children: [showFrameSection && (_jsxs(PanelSection, { title: "Frame", children: [_jsx(SliderControl, { label: "Thickness", max: 0.2, min: 0.01, onChange: (v) => handleUpdate({ frameThickness: v }), precision: 3, step: 0.01, unit: "m", value: node.frameThickness }), _jsx(SliderControl, { label: "Depth", max: 0.3, min: 0.01, onChange: (v) => handleUpdate({ frameDepth: v }), precision: 3, step: 0.01, unit: "m", value: node.frameDepth })] })), showContentPaddingSection && (_jsxs(PanelSection, { title: "Content Padding", children: [_jsx(SliderControl, { label: "Horizontal", max: 0.2, min: 0, onChange: (v) => handleUpdate({ contentPadding: [v, node.contentPadding[1]] }), precision: 3, step: 0.005, unit: "m", value: node.contentPadding[0] }), _jsx(SliderControl, { label: "Vertical", max: 0.2, min: 0, onChange: (v) => handleUpdate({ contentPadding: [node.contentPadding[0], v] }), precision: 3, step: 0.005, unit: "m", value: node.contentPadding[1] })] })), showSwingSection && (_jsx(PanelSection, { title: "Swing", children: _jsxs("div", { className: "flex flex-col gap-2 px-1 pb-1", children: [supportsHingeSide && (_jsxs("div", { className: "space-y-1", children: [_jsx("span", { className: "font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider", children: "Hinges Side" }), _jsx(SegmentedControl, { onChange: (v) => handleUpdate({ hingesSide: v }), options: [
531
502
  { label: 'Left', value: 'left' },
532
503
  { label: 'Right', value: 'right' },
533
504
  ], value: node.hingesSide })] })), _jsxs("div", { className: "space-y-1", children: [_jsx("span", { className: "font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider", children: "Direction" }), _jsx(SegmentedControl, { onChange: (v) => handleUpdate({ swingDirection: v }), options: [
@@ -1 +1 @@
1
- {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/door/tool.tsx"],"names":[],"mappings":"AAiEA;;;;;;;;;;;GAWG;AACH,QAAA,MAAM,QAAQ,EAAE,KAAK,CAAC,EAqsBrB,CAAA;AAED,eAAe,QAAQ,CAAA"}
1
+ {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/door/tool.tsx"],"names":[],"mappings":"AAqEA;;;;;;;;;;;GAWG;AACH,QAAA,MAAM,QAAQ,EAAE,KAAK,CAAC,EAwtBrB,CAAA;AAED,eAAe,QAAQ,CAAA"}
package/dist/door/tool.js CHANGED
@@ -5,6 +5,7 @@ import { useViewer } from '@pascal-app/viewer';
5
5
  import { useEffect, useMemo, useRef, useState } from 'react';
6
6
  import { BoxGeometry, EdgesGeometry, Vector3 } from 'three';
7
7
  import { LineBasicNodeMaterial } from 'three/webgpu';
8
+ import { shouldFollowOpeningGrid, shouldHandleOpeningHostLeave, } from '../shared/opening-grid-follow';
8
9
  import { clearOpeningGuides3D, publishOpeningGuidesForWallEvent, } from '../shared/opening-guides-runtime';
9
10
  import { getRoofWallOpeningCursorPose, resolveRoofWallOpeningTarget, worldToSelectedBuildingLocal, } from '../shared/roof-wall-opening-placement';
10
11
  import { collectWallOpeningAlignmentCandidates, resolveWallSlideAlignment, } from '../shared/wall-opening-alignment';
@@ -385,7 +386,17 @@ const DoorTool = () => {
385
386
  commitDoorAtWall(event.node, clampedX, clampedY, side, itemRotation);
386
387
  event.stopPropagation();
387
388
  };
388
- const onWallLeave = () => {
389
+ // XR runtimes deliver selectend as a grid click, but do not replay the
390
+ // R3F wall pointer-up event. Commit the latest valid wall hover so a
391
+ // controller/hand release places the opening on the wall it visibly hit.
392
+ const onXRGridClick = (event) => {
393
+ if (event.nativeEvent?.pointerType !== 'xr' || !lastWallEvent)
394
+ return;
395
+ onWallClick(lastWallEvent);
396
+ };
397
+ const onWallLeave = (event) => {
398
+ if (!shouldHandleOpeningHostLeave(event.nativeEvent))
399
+ return;
389
400
  if (hostKind !== 'wall')
390
401
  return;
391
402
  lastWallEvent = null;
@@ -404,7 +415,12 @@ const DoorTool = () => {
404
415
  // timeStamp) — it owns the frame and has snapped the draft, so skip the
405
416
  // floor follow this tick.
406
417
  const ts = event.nativeEvent?.timeStamp ?? -1;
407
- if (ts === lastMeshEventTime)
418
+ if (!shouldFollowOpeningGrid({
419
+ eventTime: ts,
420
+ hasActiveHost: hostKind !== null,
421
+ lastHostEventTime: lastMeshEventTime,
422
+ pointerType: event.nativeEvent?.pointerType,
423
+ }))
408
424
  return;
409
425
  // Fresh floor-only frame: the cursor is off any wall/roof. Drop any draft
410
426
  // and free-follow the cursor with the invalid (unplaceable) ghost.
@@ -575,6 +591,7 @@ const DoorTool = () => {
575
591
  emitter.on('wall:enter', onWallHover);
576
592
  emitter.on('wall:move', onWallHover);
577
593
  emitter.on('wall:click', onWallClick);
594
+ emitter.on('grid:click', onXRGridClick);
578
595
  emitter.on('wall:leave', onWallLeave);
579
596
  emitter.on('roof:enter', onRoofHover);
580
597
  emitter.on('roof:move', onRoofHover);
@@ -599,6 +616,7 @@ const DoorTool = () => {
599
616
  emitter.off('wall:enter', onWallHover);
600
617
  emitter.off('wall:move', onWallHover);
601
618
  emitter.off('wall:click', onWallClick);
619
+ emitter.off('grid:click', onXRGridClick);
602
620
  emitter.off('wall:leave', onWallLeave);
603
621
  emitter.off('roof:enter', onRoofHover);
604
622
  emitter.off('roof:move', onRoofHover);
@@ -1 +1 @@
1
- {"version":3,"file":"definition.d.ts","sourceRoot":"","sources":["../../src/dormer/definition.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAA;AAKzB,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAqPrC;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,EAAE,cAAc,CAAC,OAAO,UAAU,CA8E9D,CAAA"}
1
+ {"version":3,"file":"definition.d.ts","sourceRoot":"","sources":["../../src/dormer/definition.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAA;AAKzB,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAqPrC;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,EAAE,cAAc,CAAC,OAAO,UAAU,CA+E9D,CAAA"}
@@ -293,6 +293,7 @@ export const dormerDefinition = {
293
293
  parametrics: dormerParametrics,
294
294
  handles: dormerHandles,
295
295
  floorplan: buildDormerFloorplan,
296
+ rendersChildren: false,
296
297
  renderer: {
297
298
  kind: 'parametric',
298
299
  module: () => import('./renderer'),
@@ -1 +1 @@
1
- {"version":3,"file":"definition.d.ts","sourceRoot":"","sources":["../../src/downspout/definition.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,cAAc,EAGpB,MAAM,kBAAkB,CAAA;AASzB,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAiIxC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,EAAE,cAAc,CAAC,OAAO,aAAa,CA4DpE,CAAA"}
1
+ {"version":3,"file":"definition.d.ts","sourceRoot":"","sources":["../../src/downspout/definition.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,cAAc,EAGpB,MAAM,kBAAkB,CAAA;AASzB,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAiIxC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,EAAE,cAAc,CAAC,OAAO,aAAa,CA6DpE,CAAA"}
@@ -158,6 +158,7 @@ export const downspoutDefinition = {
158
158
  },
159
159
  parametrics: downspoutParametrics,
160
160
  handles: downspoutHandles,
161
+ rendersChildren: false,
161
162
  renderer: {
162
163
  kind: 'parametric',
163
164
  module: () => import('./renderer'),
@@ -1 +1 @@
1
- {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/duct-segment/tool.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,eAAe,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAuBtF,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,qCAAqC,CAAA;AAYpG,OAAO,EAKL,KAAK,UAAU,EACf,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAA;AA2IxB,qEAAqE;AACrE,KAAK,YAAY,GAAG;IAClB,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAA;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,UAAQ,GAAG,MAAM,CAUjF;AAgDD;;;;;6BAK6B;AAC7B,KAAK,YAAY,GAAG;IAClB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,KAAK,EAAE,eAAe,EAAE,CAAA;IACxB,KAAK,EAAE,eAAe,EAAE,CAAA;IACxB,OAAO,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,EAAE,CAAA;IACxD,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAA;CACxB,CAAA;AA4CD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAC/B,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAC7B,SAAS,EAAE,SAAS,GAAG,IAAI,EAC3B,SAAS,EAAE,UAAU,GAAG,IAAI,EAC5B,OAAO,EAAE,SAAS,GAAG,IAAI,EACzB,OAAO,EAAE,UAAU,GAAG,IAAI,EAC1B,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI,EACjC,WAAW,UAAQ,EACnB,YAAY,GAAE,OAAO,CAAC,eAAe,CAAM,EAC3C,WAAW,GAAE,QAAQ,GAAG,QAAmB,GAC1C,YAAY,GAAG,IAAI,CAsLrB;AAED,QAAA,MAAM,eAAe,0CAuQpB,CAAA;AAED,eAAe,eAAe,CAAA"}
1
+ {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/duct-segment/tool.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,eAAe,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAuBtF,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,qCAAqC,CAAA;AAYpG,OAAO,EAKL,KAAK,UAAU,EACf,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAA;AA2IxB,qEAAqE;AACrE,KAAK,YAAY,GAAG;IAClB,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAA;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,UAAQ,GAAG,MAAM,CAUjF;AAgDD;;;;;6BAK6B;AAC7B,KAAK,YAAY,GAAG;IAClB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,KAAK,EAAE,eAAe,EAAE,CAAA;IACxB,KAAK,EAAE,eAAe,EAAE,CAAA;IACxB,OAAO,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,EAAE,CAAA;IACxD,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAA;CACxB,CAAA;AA4CD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAC/B,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAC7B,SAAS,EAAE,SAAS,GAAG,IAAI,EAC3B,SAAS,EAAE,UAAU,GAAG,IAAI,EAC5B,OAAO,EAAE,SAAS,GAAG,IAAI,EACzB,OAAO,EAAE,UAAU,GAAG,IAAI,EAC1B,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI,EACjC,WAAW,UAAQ,EACnB,YAAY,GAAE,OAAO,CAAC,eAAe,CAAM,EAC3C,WAAW,GAAE,QAAQ,GAAG,QAAmB,GAC1C,YAAY,GAAG,IAAI,CAsLrB;AAED,QAAA,MAAM,eAAe,0CAoRpB,CAAA;AAED,eAAe,eAAe,CAAA"}
@@ -390,6 +390,7 @@ export function planDuctDraw(start, end, startPort, startBody, endPort, endBody,
390
390
  }
391
391
  const DuctSegmentTool = () => {
392
392
  const { activeLevelId, sceneApi, unit } = useRegistryToolContext();
393
+ const toolDefaults = useEditor((s) => s.toolDefaults['duct-segment']);
393
394
  const cursorRef = useRef(null);
394
395
  const continuationSeedRef = useRef(currentDuctContinuationSeed());
395
396
  const continuationSeed = continuationSeedRef.current;
@@ -523,6 +524,16 @@ const DuctSegmentTool = () => {
523
524
  }
524
525
  },
525
526
  });
527
+ useEffect(() => {
528
+ if (!toolDefaults)
529
+ return;
530
+ setProfile((current) => ({
531
+ shape: toolDefaults.shape ?? current.shape,
532
+ diameter: toolDefaults.diameter ?? current.diameter,
533
+ width: toolDefaults.width ?? current.width,
534
+ height: toolDefaults.height ?? current.height,
535
+ }));
536
+ }, [toolDefaults]);
526
537
  const previewPlan = useMemo(() => {
527
538
  if (!(activeLevelId && run.start && run.cursor))
528
539
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/duct-terminal/tool.tsx"],"names":[],"mappings":"AAgJA;;;;;;;;;GASG;AACH,QAAA,MAAM,gBAAgB,0CAgUrB,CAAA;AAED,eAAe,gBAAgB,CAAA"}
1
+ {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/duct-terminal/tool.tsx"],"names":[],"mappings":"AAmJA;;;;;;;;;GASG;AACH,QAAA,MAAM,gBAAgB,0CAyYrB,CAAA;AAED,eAAe,gBAAgB,CAAA"}
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { DuctTerminalNode, emitter, pointInPolygon, resolveCeilingHeight, resolveLevelId, resolveSupportSlabPatch, sceneRegistry, useScene, } from '@pascal-app/core';
4
- import { CursorSphere, getFloorStackPreviewPosition, isGridSnapActive, isMagneticSnapActive, triggerSFX, useEditor, } from '@pascal-app/editor';
4
+ import { CursorSphere, getFloorStackPreviewPosition, getSpatialPointerId, isGridSnapActive, isMagneticSnapActive, triggerSFX, useEditor, } from '@pascal-app/editor';
5
5
  import { useViewer } from '@pascal-app/viewer';
6
6
  import { Html } from '@react-three/drei';
7
7
  import { useThree } from '@react-three/fiber';
@@ -111,6 +111,7 @@ function resolvePortSnap(position, fallbackYaw) {
111
111
  const DuctTerminalTool = () => {
112
112
  const { camera, gl } = useThree();
113
113
  const activeLevelId = useViewer((s) => s.selection.levelId);
114
+ const toolDefaults = useEditor((s) => s.toolDefaults['duct-terminal']);
114
115
  const [mount, setMount] = useState('floor');
115
116
  const [placement, setPlacement] = useState(null);
116
117
  const mountRef = useRef('floor');
@@ -121,11 +122,12 @@ const DuctTerminalTool = () => {
121
122
  // override the manual M selection (port direction picks floor / ceiling /
122
123
  // wall), so the preview must show the inferred mount, not the toolbar one.
123
124
  const effectiveMount = placement?.mount ?? mount;
124
- const previewNode = useMemo(() => DuctTerminalNode.parse({
125
+ const configuredNode = useMemo(() => DuctTerminalNode.parse({
125
126
  ...ductTerminalDefinition.defaults(),
127
+ ...toolDefaults,
126
128
  name: 'Register',
127
- mount: effectiveMount,
128
- }), [effectiveMount]);
129
+ }), [toolDefaults]);
130
+ const previewNode = useMemo(() => DuctTerminalNode.parse({ ...configuredNode, mount: effectiveMount }), [configuredNode, effectiveMount]);
129
131
  const ghost = useMemo(() => {
130
132
  const group = buildDuctTerminalGeometry(previewNode);
131
133
  group.traverse((child) => {
@@ -137,6 +139,11 @@ const DuctTerminalTool = () => {
137
139
  });
138
140
  return group;
139
141
  }, [previewNode]);
142
+ useEffect(() => {
143
+ mountRef.current = configuredNode.mount;
144
+ setMount(configuredNode.mount);
145
+ setPlacement(null);
146
+ }, [configuredNode.mount]);
140
147
  useEffect(() => {
141
148
  if (!activeLevelId)
142
149
  return;
@@ -237,6 +244,7 @@ const DuctTerminalTool = () => {
237
244
  const commit = (p) => {
238
245
  const terminal = DuctTerminalNode.parse({
239
246
  ...ductTerminalDefinition.defaults(),
247
+ ...toolDefaults,
240
248
  name: 'Register',
241
249
  mount: p.mount,
242
250
  position: p.position,
@@ -251,17 +259,63 @@ const DuctTerminalTool = () => {
251
259
  useViewer.getState().setSelection({ selectedIds: [committedTerminal.id] });
252
260
  triggerSFX('sfx:item-place');
253
261
  };
254
- // ---- Floor / ceiling: own raycast against a horizontal plane ----
255
262
  let lastPointer = null;
256
263
  let lastWall = null;
264
+ let lastGrid = null;
265
+ let lastXRNode = null;
266
+ const resolveGrid = (event) => {
267
+ const step = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0;
268
+ const position = alignDrawPoint([snap(event.localPosition[0], step), 0, snap(event.localPosition[2], step)], { applySnap: isMagneticSnapActive(), bypass: !isMagneticSnapActive() });
269
+ const snapEnabled = isGridSnapActive() || isMagneticSnapActive();
270
+ const mated = snapEnabled ? resolvePortSnap(position, yawRef.current) : null;
271
+ return mated
272
+ ? { position: mated.position, yaw: mated.yaw, mount: mated.mount, snapped: true }
273
+ : { position, yaw: yawRef.current, mount: 'floor' };
274
+ };
275
+ const onGridMove = (event) => {
276
+ lastGrid = event;
277
+ if (mountRef.current === 'floor')
278
+ setPlacement(resolveGrid(event));
279
+ };
280
+ const onGridClick = (event) => {
281
+ if (mountRef.current === 'floor')
282
+ commit(resolveGrid(event));
283
+ };
284
+ const resolveXRCeiling = (event) => {
285
+ if (mountRef.current !== 'ceiling' || event.node.type !== 'ceiling')
286
+ return null;
287
+ if (getSpatialPointerId(event.nativeEvent) == null)
288
+ return null;
289
+ const world = new Vector3(...event.position);
290
+ const level = activeLevelMesh();
291
+ const local = level ? level.worldToLocal(world) : world;
292
+ return {
293
+ position: [local.x, resolveCeilingHeight(event.node, useScene.getState().nodes), local.z],
294
+ yaw: yawRef.current,
295
+ mount: 'ceiling',
296
+ };
297
+ };
298
+ const onXRNodeMove = (event) => {
299
+ lastXRNode = event;
300
+ const p = resolveXRCeiling(event);
301
+ if (p)
302
+ setPlacement(p);
303
+ };
304
+ const onXRNodeClick = (event) => {
305
+ const p = resolveXRCeiling(event);
306
+ if (p)
307
+ commit(p);
308
+ };
309
+ // ---- Ceiling: desktop owns a canvas ray; XR uses the spatial node ray. ----
257
310
  const onPointerMove = (e) => {
258
311
  lastPointer = e;
259
- if (mountRef.current === 'wall')
312
+ lastXRNode = null;
313
+ if (mountRef.current !== 'ceiling')
260
314
  return;
261
315
  setPlacement(resolvePlanar(e));
262
316
  };
263
317
  const onCanvasClick = (e) => {
264
- if (mountRef.current === 'wall')
318
+ if (mountRef.current !== 'ceiling')
265
319
  return;
266
320
  if (useViewer.getState().cameraDragging)
267
321
  return;
@@ -350,11 +404,22 @@ const DuctTerminalTool = () => {
350
404
  if (lastWall)
351
405
  onWallMove(lastWall);
352
406
  }
407
+ else if (mountRef.current === 'floor') {
408
+ if (lastGrid)
409
+ onGridMove(lastGrid);
410
+ }
411
+ else if (lastXRNode && getSpatialPointerId(lastXRNode.nativeEvent) != null) {
412
+ onXRNodeMove(lastXRNode);
413
+ }
353
414
  else if (lastPointer)
354
415
  onPointerMove(lastPointer);
355
416
  });
356
417
  canvas.addEventListener('pointermove', onPointerMove);
357
418
  canvas.addEventListener('click', onCanvasClick);
419
+ emitter.on('grid:move', onGridMove);
420
+ emitter.on('grid:click', onGridClick);
421
+ emitter.on('node:move', onXRNodeMove);
422
+ emitter.on('node:click', onXRNodeClick);
358
423
  emitter.on('wall:move', onWallMove);
359
424
  emitter.on('wall:click', onWallClick);
360
425
  window.addEventListener('keydown', onKeyDown, true);
@@ -362,12 +427,16 @@ const DuctTerminalTool = () => {
362
427
  unsubscribeSnapping();
363
428
  canvas.removeEventListener('pointermove', onPointerMove);
364
429
  canvas.removeEventListener('click', onCanvasClick);
430
+ emitter.off('grid:move', onGridMove);
431
+ emitter.off('grid:click', onGridClick);
432
+ emitter.off('node:move', onXRNodeMove);
433
+ emitter.off('node:click', onXRNodeClick);
365
434
  emitter.off('wall:move', onWallMove);
366
435
  emitter.off('wall:click', onWallClick);
367
436
  window.removeEventListener('keydown', onKeyDown, true);
368
437
  clearDrawAlignment();
369
438
  };
370
- }, [activeLevelId, camera, gl]);
439
+ }, [activeLevelId, camera, gl, toolDefaults]);
371
440
  if (!activeLevelId || !placement)
372
441
  return null;
373
442
  const mountLabel = effectiveMount.charAt(0).toUpperCase() + effectiveMount.slice(1);
@@ -1 +1 @@
1
- {"version":3,"file":"definition.d.ts","sourceRoot":"","sources":["../../src/elevator/definition.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,cAAc,EAEpB,MAAM,kBAAkB,CAAA;AAKzB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAsJvC;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,OAAO,YAAY,CA8GlE,CAAA"}
1
+ {"version":3,"file":"definition.d.ts","sourceRoot":"","sources":["../../src/elevator/definition.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,cAAc,EAEpB,MAAM,kBAAkB,CAAA;AAKzB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAsJvC;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,OAAO,YAAY,CAgHlE,CAAA"}
@@ -173,6 +173,7 @@ export const elevatorDefinition = {
173
173
  return rest;
174
174
  },
175
175
  capabilities: {
176
+ surfacePlacement: 'floor-only',
176
177
  selectable: { hitVolume: 'bbox' },
177
178
  // Generic XZ translate so the floating action menu's Move button
178
179
  // (and the side move-arrows emitted from `def.floorplan`) drive the
@@ -220,6 +221,7 @@ export const elevatorDefinition = {
220
221
  handles: elevatorHandles,
221
222
  // No dirty consumer rebuilds this kind — see NodeDefinition.dirtyTracking.
222
223
  dirtyTracking: false,
224
+ rendersChildren: false,
223
225
  renderer: {
224
226
  kind: 'parametric',
225
227
  module: () => import('./renderer'),
@@ -1 +1 @@
1
- {"version":3,"file":"parametrics.d.ts","sourceRoot":"","sources":["../../src/elevator/parametrics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAE1E,eAAO,MAAM,mBAAmB,EAAE,oBAAoB,CAAC,YAAY,CAGlE,CAAA"}
1
+ {"version":3,"file":"parametrics.d.ts","sourceRoot":"","sources":["../../src/elevator/parametrics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAE1E,eAAO,MAAM,mBAAmB,EAAE,oBAAoB,CAAC,YAAY,CAoGlE,CAAA"}
@@ -1,4 +1,101 @@
1
1
  export const elevatorParametrics = {
2
- groups: [],
2
+ groups: [
3
+ {
4
+ label: 'Position',
5
+ fields: [
6
+ { key: 'position', kind: 'vec3' },
7
+ {
8
+ key: 'rotation',
9
+ kind: 'number',
10
+ label: 'Rotation (rad)',
11
+ min: -Math.PI,
12
+ max: Math.PI,
13
+ step: Math.PI / 36,
14
+ },
15
+ ],
16
+ },
17
+ {
18
+ label: 'Cab',
19
+ fields: [
20
+ { key: 'width', kind: 'number', unit: 'm', min: 0.8, max: 1000, step: 0.05 },
21
+ { key: 'depth', kind: 'number', unit: 'm', min: 0.8, max: 1000, step: 0.05 },
22
+ { key: 'cabHeight', kind: 'number', unit: 'm', min: 1.8, max: 1000, step: 0.05 },
23
+ ],
24
+ },
25
+ {
26
+ label: 'Shaft',
27
+ fields: [
28
+ {
29
+ key: 'shaftWallThickness',
30
+ kind: 'number',
31
+ label: 'Wall thickness',
32
+ unit: 'm',
33
+ min: 0.04,
34
+ max: 0.4,
35
+ step: 0.01,
36
+ },
37
+ {
38
+ key: 'shaftStyle',
39
+ kind: 'enum',
40
+ options: ['solid', 'glass'],
41
+ display: 'segmented',
42
+ },
43
+ ],
44
+ },
45
+ {
46
+ label: 'Doors',
47
+ fields: [
48
+ { key: 'doorWidth', kind: 'number', unit: 'm', min: 0.45, max: 1000, step: 0.05 },
49
+ { key: 'doorHeight', kind: 'number', unit: 'm', min: 1.2, max: 1000, step: 0.05 },
50
+ {
51
+ key: 'doorStyle',
52
+ kind: 'enum',
53
+ options: ['center-opening', 'single-left', 'single-right'],
54
+ },
55
+ {
56
+ key: 'doorPanelStyle',
57
+ kind: 'enum',
58
+ label: 'Panel style',
59
+ options: ['glass-frame', 'solid-panel', 'segmented-panel'],
60
+ },
61
+ ],
62
+ },
63
+ {
64
+ label: 'Levels',
65
+ fields: [
66
+ { key: 'fromLevelId', kind: 'ref', label: 'From level', refKind: 'level' },
67
+ { key: 'toLevelId', kind: 'ref', label: 'To level', refKind: 'level' },
68
+ { key: 'defaultLevelId', kind: 'ref', label: 'Default level', refKind: 'level' },
69
+ ],
70
+ },
71
+ {
72
+ label: 'Motion',
73
+ fields: [
74
+ { key: 'speed', kind: 'number', unit: 'm/s', min: 0.5, max: 8, step: 0.1 },
75
+ {
76
+ key: 'doorDurationMs',
77
+ kind: 'number',
78
+ label: 'Door time',
79
+ unit: 'ms',
80
+ min: 300,
81
+ max: 2200,
82
+ step: 50,
83
+ },
84
+ {
85
+ key: 'dwellMs',
86
+ kind: 'number',
87
+ label: 'Dwell',
88
+ unit: 'ms',
89
+ min: 300,
90
+ max: 5000,
91
+ step: 100,
92
+ },
93
+ ],
94
+ },
95
+ ],
96
+ derive: (next) => ({
97
+ doorHeight: Math.min(next.doorHeight, Math.max(1.2, next.cabHeight - 0.1)),
98
+ doorWidth: Math.min(next.doorWidth, Math.max(0.45, next.width - 0.1)),
99
+ }),
3
100
  customPanel: () => import('./panel'),
4
101
  };