@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
@@ -4,8 +4,10 @@ import { useInteractive, useScene, WindowNode, } 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, 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
  function isSameWindowValue(current, next) {
10
12
  if (typeof current === 'number' && typeof next === 'number') {
11
13
  return Math.abs(current - next) < 1e-6;
@@ -26,9 +28,6 @@ function normalizeWindowCornerRadii(radii, width, height) {
26
28
  return next;
27
29
  return next.map((radius) => radius * scale);
28
30
  }
29
- function isSameRadiusTuple(current, next) {
30
- return current.every((value, index) => Math.abs(value - (next[index] ?? 0)) < 1e-6);
31
- }
32
31
  const windowTypeOptions = [
33
32
  { label: 'Fixed', value: 'fixed' },
34
33
  { label: 'Sliding', value: 'sliding' },
@@ -53,7 +52,10 @@ export default function WindowPanel() {
53
52
  const setSelection = useViewer((s) => s.setSelection);
54
53
  const deleteNode = useScene((s) => s.deleteNode);
55
54
  const setMovingNode = useEditor((s) => s.setMovingNode);
56
- const previewRef = useRef(null);
55
+ const preview = useMemo(() => selectedId
56
+ ? createOpeningPropertyPreview(selectedId, openingPropertyPreviewHost)
57
+ : null, [selectedId]);
58
+ useEffect(() => () => preview?.cancel(), [preview]);
57
59
  const node = useScene((s) => selectedId ? s.nodes[selectedId] : undefined);
58
60
  // Panel slider-drag fix recipe (plans/editor-node-registry.md). Without
59
61
  // it, the 15+ SliderControls in this panel would loop on drag.
@@ -63,6 +65,7 @@ export default function WindowPanel() {
63
65
  const liveNode = useScene.getState().nodes[selectedId];
64
66
  if (liveNode?.type !== 'window')
65
67
  return;
68
+ updates = constrainCurtainOpening(liveNode, updates, useScene.getState().nodes);
66
69
  const hasChange = Object.entries(updates).some(([key, value]) => {
67
70
  const currentValue = liveNode[key];
68
71
  return !isSameWindowValue(currentValue, value);
@@ -75,43 +78,8 @@ export default function WindowPanel() {
75
78
  if (liveNode.parentId)
76
79
  scene.dirtyNodes.add(liveNode.parentId);
77
80
  }, [selectedId]);
78
- const previewWindowUpdate = useCallback((key, value) => {
79
- if (!selectedId)
80
- return;
81
- const liveNode = useScene.getState().nodes[selectedId];
82
- if (liveNode?.type !== 'window')
83
- return;
84
- if (!(previewRef.current &&
85
- previewRef.current.id === selectedId &&
86
- previewRef.current.key === key)) {
87
- previewRef.current = {
88
- id: selectedId,
89
- key,
90
- value: liveNode[key],
91
- };
92
- }
93
- if (isSameWindowValue(liveNode[key], value))
94
- return;
95
- liveNode[key] = value;
96
- useScene.getState().dirtyNodes.add(selectedId);
97
- }, [selectedId]);
98
- const commitWindowPreview = useCallback((key, value) => {
99
- if (!selectedId)
100
- return;
101
- const scene = useScene.getState();
102
- const liveNode = scene.nodes[selectedId];
103
- const preview = previewRef.current;
104
- if (liveNode?.type === 'window' && preview?.id === selectedId && preview.key === key) {
105
- ;
106
- liveNode[key] = preview.value;
107
- scene.dirtyNodes.add(selectedId);
108
- }
109
- previewRef.current = null;
110
- useScene.getState().updateNode(selectedId, {
111
- [key]: value,
112
- });
113
- scene.dirtyNodes.add(selectedId);
114
- }, [selectedId]);
81
+ const previewWindowUpdate = (key, value) => preview?.preview({ [key]: value });
82
+ const commitWindowPreview = (key, value) => preview?.commit({ [key]: value });
115
83
  const handleClose = useCallback(() => {
116
84
  setSelection({ selectedIds: [] });
117
85
  }, [setSelection]);
@@ -187,6 +155,7 @@ export default function WindowPanel() {
187
155
  }, [node, setMovingNode, setSelection]);
188
156
  if (!(node && node.type === 'window' && selectedId))
189
157
  return null;
158
+ const limits = curtainOpeningLimits(node, useScene.getState().nodes);
190
159
  const numCols = node.columnRatios.length;
191
160
  const numRows = node.rowRatios.length;
192
161
  const colSum = node.columnRatios.reduce((a, b) => a + b, 0);
@@ -245,24 +214,17 @@ export default function WindowPanel() {
245
214
  const nextUpdates = { ...updates };
246
215
  if (openingShape === 'rounded') {
247
216
  if (openingRadiusMode === 'individual') {
248
- const currentRadii = openingCornerRadii;
249
217
  const nextRadii = normalizeWindowCornerRadii(openingCornerRadii, nextWidth, nextHeight);
250
- if (!isSameRadiusTuple(currentRadii, nextRadii)) {
251
- nextUpdates.openingCornerRadii = nextRadii;
252
- }
218
+ nextUpdates.openingCornerRadii = nextRadii;
253
219
  }
254
220
  else {
255
221
  const nextRadius = Math.min(Math.max(cornerRadius, 0), getMaxSharedWindowRadius(nextWidth, nextHeight));
256
- if (Math.abs(nextRadius - cornerRadius) > 1e-6) {
257
- nextUpdates.cornerRadius = nextRadius;
258
- }
222
+ nextUpdates.cornerRadius = nextRadius;
259
223
  }
260
224
  }
261
225
  if (openingShape === 'arch') {
262
226
  const nextArchHeight = Math.min(Math.max(archHeight, 0.05), Math.max(nextHeight, 0.05));
263
- if (Math.abs(nextArchHeight - archHeight) > 1e-6) {
264
- nextUpdates.archHeight = nextArchHeight;
265
- }
227
+ nextUpdates.archHeight = nextArchHeight;
266
228
  }
267
229
  return nextUpdates;
268
230
  };
@@ -319,7 +281,7 @@ export default function WindowPanel() {
319
281
  }), options: [
320
282
  { value: 'window', label: 'Window' },
321
283
  { value: 'opening', label: 'Opening' },
322
- ], value: node.openingKind ?? 'window' }) }), _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 }) }), showWindowTypeSection && (_jsxs(PanelSection, { title: "Window Type", children: [_jsx("div", { className: "grid grid-cols-2 gap-2 px-1 pt-1", children: windowTypeOptions.map((option) => {
284
+ ], value: node.openingKind ?? 'window' }) }), showWindowTypeSection && (_jsxs(PanelSection, { title: "Window Type", children: [_jsx("div", { className: "grid grid-cols-2 gap-2 px-1 pt-1", children: windowTypeOptions.map((option) => {
323
285
  const isSelected = displayedWindowType === option.value;
324
286
  return (_jsx("button", { className: cn('flex min-h-12 items-center rounded-lg border px-3 py-2.5 text-left text-xs transition-colors', isSelected
325
287
  ? 'border-orange-400/60 bg-orange-400/10 text-foreground'
@@ -343,7 +305,7 @@ export default function WindowPanel() {
343
305
  ], value: node.casementStyle ?? 'single' }), (node.casementStyle ?? 'single') === 'single' && (_jsx(SegmentedControl, { onChange: (value) => handleUpdate({ hingesSide: value }), options: [
344
306
  { value: 'left', label: 'Left' },
345
307
  { value: 'right', label: 'Right' },
346
- ], value: node.hingesSide ?? 'left' }))] })), showOperationSection && (_jsx("div", { className: "mt-2", children: _jsx(SliderControl, { label: operationLabel, max: 1, min: 0, onChange: setOperationState, precision: 2, restoreOnCommit: false, step: 0.05, value: Math.round((node.operationState ?? 0) * 100) / 100 }) }))] })), _jsxs(PanelSection, { title: "Position", children: [_jsx(SliderControl, { label: _jsxs(_Fragment, { children: ["X", _jsx("sub", { className: "ml-[1px] text-[11px] opacity-70", children: "pos" })] }), onChange: (v) => handleUpdate({ position: [v, node.position[1], node.position[2]] }), precision: 2, step: 0.1, unit: "m", value: node.position[0] }), _jsx(SliderControl, { label: _jsxs(_Fragment, { children: ["Y", _jsx("sub", { className: "ml-[1px] text-[11px] opacity-70", children: "pos" })] }), onChange: (v) => handleUpdate({ position: [node.position[0], v, node.position[2]] }), precision: 2, step: 0.1, unit: "m", value: node.position[1] }), 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 }) }))] }), _jsxs(PanelSection, { title: "Dimensions", children: [_jsx(SliderControl, { label: "Width", min: 0, onChange: (v) => handleUpdate(getDimensionUpdates({ width: v })), precision: 2, restoreOnCommit: false, step: 0.1, unit: "m", value: node.width }), _jsx(SliderControl, { label: "Height", min: 0, onChange: (v) => handleUpdate(getDimensionUpdates({ height: v })), precision: 2, restoreOnCommit: false, step: 0.1, unit: "m", value: node.height })] }), showWindowShapeSection && (_jsxs(PanelSection, { title: "Top Shape", children: [_jsx(SegmentedControl, { onChange: (value) => handleUpdate({
308
+ ], value: node.hingesSide ?? 'left' }))] })), showOperationSection && (_jsx("div", { className: "mt-2", children: _jsx(SliderControl, { label: operationLabel, max: 1, min: 0, onChange: setOperationState, precision: 2, restoreOnCommit: false, step: 0.05, value: Math.round((node.operationState ?? 0) * 100) / 100 }) }))] })), _jsxs(PanelSection, { title: "Position", children: [_jsx(SliderControl, { label: _jsxs(_Fragment, { children: ["X", _jsx("sub", { className: "ml-[1px] text-[11px] opacity-70", children: "pos" })] }), onChange: (v) => handleUpdate({ position: [v, node.position[1], node.position[2]] }), precision: 2, step: 0.1, unit: "m", value: node.position[0] }), _jsx(SliderControl, { label: _jsxs(_Fragment, { children: ["Y", _jsx("sub", { className: "ml-[1px] text-[11px] opacity-70", children: "pos" })] }), onChange: (v) => handleUpdate({ position: [node.position[0], v, node.position[2]] }), precision: 2, step: 0.1, unit: "m", value: node.position[1] }), 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 }) }))] }), _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: limits?.width, min: 0.01, onChange: (v) => preview?.preview(getDimensionUpdates({ width: v })), onCommit: (v) => preview?.commit(getDimensionUpdates({ 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, min: 0.01, onChange: (v) => preview?.preview(getDimensionUpdates({ height: v })), onCommit: (v) => preview?.commit(getDimensionUpdates({ height: v })), onCancel: () => preview?.cancel(), previewWhileTyping: true, precision: 2, restoreOnCommit: false, step: 0.01, unit: "m", value: node.height })] }), showWindowShapeSection && (_jsxs(PanelSection, { title: "Top Shape", children: [_jsx(SegmentedControl, { onChange: (value) => handleUpdate({
347
309
  openingShape: value,
348
310
  ...(value === 'rounded'
349
311
  ? {
@@ -362,24 +324,24 @@ export default function WindowPanel() {
362
324
  ], value: windowShape }), windowShape === 'rounded' && (_jsxs("div", { className: "mt-2 flex flex-col gap-1", children: [_jsx(SegmentedControl, { onChange: (value) => handleUpdate({ openingRadiusMode: value }), options: [
363
325
  { value: 'all', label: 'All' },
364
326
  { value: 'individual', label: 'Individual' },
365
- ], value: openingRadiusMode }), openingRadiusMode === 'all' ? (_jsx(SliderControl, { label: "Corner Radius", max: maxRoundedRadius, min: 0, onChange: (value) => previewWindowUpdate('cornerRadius', value), onCommit: (value) => commitWindowPreview('cornerRadius', value), precision: 2, step: 0.05, unit: "m", value: cornerRadius })) : (_jsx(_Fragment, { children: [
327
+ ], value: openingRadiusMode }), openingRadiusMode === 'all' ? (_jsx(SliderControl, { label: "Corner Radius", max: maxRoundedRadius, min: 0, onChange: (value) => previewWindowUpdate('cornerRadius', value), onCommit: (value) => commitWindowPreview('cornerRadius', value), onCancel: () => preview?.cancel(), previewWhileTyping: true, restoreOnCommit: false, precision: 2, step: 0.05, unit: "m", value: cornerRadius })) : (_jsx(_Fragment, { children: [
366
328
  ['Top Left', 0],
367
329
  ['Top Right', 1],
368
330
  ['Bottom Right', 2],
369
331
  ['Bottom Left', 3],
370
- ].map(([label, index]) => (_jsx(SliderControl, { label: label, max: maxRoundedRadius, min: 0, onChange: (value) => setOpeningCornerRadius(index, value), onCommit: (value) => setOpeningCornerRadius(index, value, true), precision: 2, step: 0.05, unit: "m", value: openingCornerRadii[index] ?? 0 }, label))) })), _jsx(SliderControl, { label: "Reveal Radius", max: 0.08, min: 0, onChange: (value) => previewWindowUpdate('openingRevealRadius', value), onCommit: (value) => commitWindowPreview('openingRevealRadius', value), precision: 3, step: 0.005, unit: "m", value: openingRevealRadius })] })), windowShape === 'arch' && (_jsx("div", { className: "mt-2 flex flex-col gap-1", children: _jsx(SliderControl, { label: "Arch Height", max: Math.max(0.05, node.height), min: 0.05, onChange: (value) => handleUpdate({ archHeight: value }), precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: archHeight }) }))] })), showOpeningShapeSection && (_jsxs(PanelSection, { title: "Opening Shape", children: [_jsx(SegmentedControl, { onChange: (value) => handleUpdate({ openingShape: value }), options: [
332
+ ].map(([label, index]) => (_jsx(SliderControl, { label: label, max: maxRoundedRadius, min: 0, onChange: (value) => setOpeningCornerRadius(index, value), onCommit: (value) => setOpeningCornerRadius(index, value, true), onCancel: () => preview?.cancel(), previewWhileTyping: true, restoreOnCommit: false, precision: 2, step: 0.05, unit: "m", value: openingCornerRadii[index] ?? 0 }, label))) })), _jsx(SliderControl, { label: "Reveal Radius", max: 0.08, min: 0, onChange: (value) => previewWindowUpdate('openingRevealRadius', value), onCommit: (value) => commitWindowPreview('openingRevealRadius', value), onCancel: () => preview?.cancel(), previewWhileTyping: true, restoreOnCommit: false, precision: 3, step: 0.005, unit: "m", value: openingRevealRadius })] })), windowShape === 'arch' && (_jsx("div", { className: "mt-2 flex flex-col gap-1", children: _jsx(SliderControl, { label: "Arch Height", max: Math.max(0.05, node.height), min: 0.05, onChange: (value) => previewWindowUpdate('archHeight', value), onCommit: (value) => commitWindowPreview('archHeight', value), onCancel: () => preview?.cancel(), previewWhileTyping: true, precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: archHeight }) }))] })), showOpeningShapeSection && (_jsxs(PanelSection, { title: "Opening Shape", children: [_jsx(SegmentedControl, { onChange: (value) => handleUpdate({ openingShape: value }), options: [
371
333
  { value: 'rectangle', label: 'Rect' },
372
334
  { value: 'rounded', label: 'Rounded' },
373
335
  { value: 'arch', label: 'Arch' },
374
336
  ], value: openingShape }), openingShape === 'rounded' && (_jsxs("div", { className: "mt-2 flex flex-col gap-1", children: [_jsx(SegmentedControl, { onChange: (value) => handleUpdate({ openingRadiusMode: value }), options: [
375
337
  { value: 'all', label: 'All' },
376
338
  { value: 'individual', label: 'Individual' },
377
- ], value: openingRadiusMode }), openingRadiusMode === 'all' ? (_jsx(SliderControl, { label: "Corner Radius", max: maxRoundedRadius, min: 0, onChange: (value) => previewWindowUpdate('cornerRadius', value), onCommit: (value) => commitWindowPreview('cornerRadius', value), precision: 2, step: 0.05, unit: "m", value: cornerRadius })) : (_jsx(_Fragment, { children: [
339
+ ], value: openingRadiusMode }), openingRadiusMode === 'all' ? (_jsx(SliderControl, { label: "Corner Radius", max: maxRoundedRadius, min: 0, onChange: (value) => previewWindowUpdate('cornerRadius', value), onCommit: (value) => commitWindowPreview('cornerRadius', value), onCancel: () => preview?.cancel(), previewWhileTyping: true, restoreOnCommit: false, precision: 2, step: 0.05, unit: "m", value: cornerRadius })) : (_jsx(_Fragment, { children: [
378
340
  ['Top Left', 0],
379
341
  ['Top Right', 1],
380
342
  ['Bottom Right', 2],
381
343
  ['Bottom Left', 3],
382
- ].map(([label, index]) => (_jsx(SliderControl, { label: label, max: maxRoundedRadius, min: 0, onChange: (value) => setOpeningCornerRadius(index, value), onCommit: (value) => setOpeningCornerRadius(index, value, true), precision: 2, step: 0.05, unit: "m", value: openingCornerRadii[index] ?? 0 }, label))) })), _jsx(SliderControl, { label: "Reveal Radius", max: 0.08, min: 0, onChange: (value) => previewWindowUpdate('openingRevealRadius', value), onCommit: (value) => commitWindowPreview('openingRevealRadius', value), precision: 3, step: 0.005, unit: "m", value: openingRevealRadius })] })), openingShape === 'arch' && (_jsx("div", { className: "mt-2 flex flex-col gap-1", children: _jsx(SliderControl, { label: "Arch Height", max: Math.max(0.05, node.height), min: 0.05, onChange: (value) => handleUpdate({ archHeight: value }), precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: archHeight }) }))] })), !isOpening && (_jsxs(_Fragment, { children: [showFrameSection && (_jsxs(PanelSection, { title: "Frame", children: [_jsx(SliderControl, { label: "Thickness", min: 0, onChange: (v) => handleUpdate({ frameThickness: v }), precision: 3, step: 0.01, unit: "m", value: node.frameThickness }), _jsx(SliderControl, { label: "Depth", min: 0, onChange: (v) => handleUpdate({ frameDepth: v }), precision: 3, step: 0.01, unit: "m", value: node.frameDepth })] })), showGridSection && (_jsxs(PanelSection, { title: "Grid", children: [_jsx(SliderControl, { label: "Columns", max: 8, min: 1, onChange: (v) => {
344
+ ].map(([label, index]) => (_jsx(SliderControl, { label: label, max: maxRoundedRadius, min: 0, onChange: (value) => setOpeningCornerRadius(index, value), onCommit: (value) => setOpeningCornerRadius(index, value, true), onCancel: () => preview?.cancel(), previewWhileTyping: true, restoreOnCommit: false, precision: 2, step: 0.05, unit: "m", value: openingCornerRadii[index] ?? 0 }, label))) })), _jsx(SliderControl, { label: "Reveal Radius", max: 0.08, min: 0, onChange: (value) => previewWindowUpdate('openingRevealRadius', value), onCommit: (value) => commitWindowPreview('openingRevealRadius', value), onCancel: () => preview?.cancel(), previewWhileTyping: true, restoreOnCommit: false, precision: 3, step: 0.005, unit: "m", value: openingRevealRadius })] })), openingShape === 'arch' && (_jsx("div", { className: "mt-2 flex flex-col gap-1", children: _jsx(SliderControl, { label: "Arch Height", max: Math.max(0.05, node.height), min: 0.05, onChange: (value) => previewWindowUpdate('archHeight', value), onCommit: (value) => commitWindowPreview('archHeight', value), onCancel: () => preview?.cancel(), previewWhileTyping: true, precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: archHeight }) }))] })), !isOpening && (_jsxs(_Fragment, { children: [showFrameSection && (_jsxs(PanelSection, { title: "Frame", children: [_jsx(SliderControl, { label: "Thickness", min: 0, onChange: (v) => previewWindowUpdate('frameThickness', v), onCommit: (v) => commitWindowPreview('frameThickness', v), onCancel: () => preview?.cancel(), previewWhileTyping: true, restoreOnCommit: false, precision: 3, step: 0.01, unit: "m", value: node.frameThickness }), _jsx(SliderControl, { label: "Depth", min: 0, onChange: (v) => previewWindowUpdate('frameDepth', v), onCommit: (v) => commitWindowPreview('frameDepth', v), onCancel: () => preview?.cancel(), previewWhileTyping: true, restoreOnCommit: false, precision: 3, step: 0.01, unit: "m", value: node.frameDepth })] })), showGridSection && (_jsxs(PanelSection, { title: "Grid", children: [_jsx(SliderControl, { label: "Columns", max: 8, min: 1, onChange: (v) => {
383
345
  const n = Math.max(1, Math.min(8, Math.round(v)));
384
346
  handleUpdate({ columnRatios: Array(n).fill(1 / n) });
385
347
  }, precision: 0, step: 1, value: numCols }), _jsx(SliderControl, { label: "Rows", max: 8, min: 1, onChange: (v) => {
@@ -1 +1 @@
1
- {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/window/tool.tsx"],"names":[],"mappings":"AA+FA;;;;;;;;;GASG;AACH,QAAA,MAAM,UAAU,EAAE,KAAK,CAAC,EA09BvB,CAAA;AAED,eAAe,UAAU,CAAA"}
1
+ {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/window/tool.tsx"],"names":[],"mappings":"AAoGA;;;;;;;;;GASG;AACH,QAAA,MAAM,UAAU,EAAE,KAAK,CAAC,EAmgCvB,CAAA;AAED,eAAe,UAAU,CAAA"}
@@ -5,8 +5,10 @@ import { useEffect, useMemo, useRef, useState } from 'react';
5
5
  import { BoxGeometry, EdgesGeometry, Vector3 } from 'three';
6
6
  import { LineBasicNodeMaterial } from 'three/webgpu';
7
7
  import { dormerEventFromHostedWindow, getDormerWindowWorldNormal, getDormerWindowWorldYaw, resolveDormerWindowTarget, } from '../shared/dormer-wall-opening-placement';
8
+ import { shouldFollowOpeningGrid, shouldHandleOpeningHostLeave, } from '../shared/opening-grid-follow';
8
9
  import { clearOpeningGuides3D, publishOpeningGuidesForWallEvent, resolveSillSnap, } from '../shared/opening-guides-runtime';
9
10
  import { getRoofWallOpeningCursorPose, resolveRoofWallOpeningTarget, worldToSelectedBuildingLocal, } from '../shared/roof-wall-opening-placement';
11
+ import { wallEventFromGrid } from '../shared/wall-event-from-grid';
10
12
  import { collectWallOpeningAlignmentCandidates, resolveWallSlideAlignment, } from '../shared/wall-opening-alignment';
11
13
  import { WindowFloorProjection } from './floor-projection';
12
14
  import WindowPreview from './preview';
@@ -519,7 +521,16 @@ const WindowTool = () => {
519
521
  commitWindowAtWall(event.node, clampedX, clampedY, side, itemRotation);
520
522
  event.stopPropagation();
521
523
  };
522
- const onWallLeave = () => {
524
+ // XR selectend produces a grid click without replaying the wall mesh's
525
+ // R3F pointer-up event. Use the latest valid wall hover for that release.
526
+ const onXRGridClick = (event) => {
527
+ if (event.nativeEvent?.pointerType !== 'xr' || !lastWallEvent)
528
+ return;
529
+ onWallClick(lastWallEvent);
530
+ };
531
+ const onWallLeave = (event) => {
532
+ if (!shouldHandleOpeningHostLeave(event.nativeEvent))
533
+ return;
523
534
  if (hostKind !== 'wall')
524
535
  return;
525
536
  lastWallEvent = null;
@@ -538,8 +549,18 @@ const WindowTool = () => {
538
549
  // timeStamp) — it owns the frame and has snapped the draft, so skip the
539
550
  // floor follow this tick.
540
551
  const ts = event.nativeEvent?.timeStamp ?? -1;
541
- if (ts === lastMeshEventTime)
552
+ if (!shouldFollowOpeningGrid({
553
+ eventTime: ts,
554
+ hasActiveHost: hostKind !== null,
555
+ lastHostEventTime: lastMeshEventTime,
556
+ pointerType: event.nativeEvent?.pointerType,
557
+ }))
558
+ return;
559
+ const wallEvent = wallEventFromGrid(event, activeLevelId, useScene.getState().nodes, sceneRegistry.nodes);
560
+ if (wallEvent) {
561
+ onWallHover(wallEvent);
542
562
  return;
563
+ }
543
564
  // Fresh floor-only frame: the cursor is off any wall/roof. Drop any draft
544
565
  // and free-follow the cursor with the invalid (unplaceable) ghost.
545
566
  hostKind = null;
@@ -548,6 +569,13 @@ const WindowTool = () => {
548
569
  destroyDraft();
549
570
  showGhostAt([x, y + FALLBACK_HEIGHT / 2 + FALLBACK_SILL_LIFT, z], y);
550
571
  };
572
+ const onGridPointerUp = (event) => {
573
+ if (isCameraDragging() || !draftRef.current)
574
+ return;
575
+ const wallEvent = wallEventFromGrid(event, activeLevelId, useScene.getState().nodes, sceneRegistry.nodes);
576
+ if (wallEvent)
577
+ onWallClick(wallEvent);
578
+ };
551
579
  // ── Dormer wall faces ──────────────────────────────────────────
552
580
  // Dormer windows use the same WindowNode mesh and inspector as regular
553
581
  // windows, but their host frame is supplied by DormerRenderer.
@@ -786,6 +814,7 @@ const WindowTool = () => {
786
814
  emitter.on('wall:enter', onWallHover);
787
815
  emitter.on('wall:move', onWallHover);
788
816
  emitter.on('wall:click', onWallClick);
817
+ emitter.on('grid:click', onXRGridClick);
789
818
  emitter.on('wall:leave', onWallLeave);
790
819
  emitter.on('roof:enter', onRoofHover);
791
820
  emitter.on('roof:move', onRoofHover);
@@ -800,6 +829,7 @@ const WindowTool = () => {
800
829
  emitter.on('window:click', onDormerWindowClick);
801
830
  emitter.on('window:leave', onDormerWindowLeave);
802
831
  emitter.on('grid:move', onGridFreeFollow);
832
+ emitter.on('grid:pointerup', onGridPointerUp);
803
833
  emitter.on('tool:cancel', onCancel);
804
834
  window.addEventListener('keydown', onKeyDown);
805
835
  // Placement tracks the cursor through wall events; keep walls hidden by
@@ -818,6 +848,7 @@ const WindowTool = () => {
818
848
  emitter.off('wall:enter', onWallHover);
819
849
  emitter.off('wall:move', onWallHover);
820
850
  emitter.off('wall:click', onWallClick);
851
+ emitter.off('grid:click', onXRGridClick);
821
852
  emitter.off('wall:leave', onWallLeave);
822
853
  emitter.off('roof:enter', onRoofHover);
823
854
  emitter.off('roof:move', onRoofHover);
@@ -832,6 +863,7 @@ const WindowTool = () => {
832
863
  emitter.off('window:click', onDormerWindowClick);
833
864
  emitter.off('window:leave', onDormerWindowLeave);
834
865
  emitter.off('grid:move', onGridFreeFollow);
866
+ emitter.off('grid:pointerup', onGridPointerUp);
835
867
  emitter.off('tool:cancel', onCancel);
836
868
  window.removeEventListener('keydown', onKeyDown);
837
869
  };
@@ -1,4 +1,4 @@
1
- import type { AnyNode, AnyNodeId, WallNode } from '@pascal-app/core';
1
+ import { type AnyNode, type AnyNodeId, type WallNode } from '@pascal-app/core';
2
2
  /**
3
3
  * Default sill height (metres from the floor to the BOTTOM of a window) for a
4
4
  * fresh window that has no wall-face height yet — the off-wall ghost and the
@@ -1 +1 @@
1
- {"version":3,"file":"window-math.d.ts","sourceRoot":"","sources":["../../src/window/window-math.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAGpE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,MAAM,CAAA;AAExC;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,YAAY,SAAI,EAChB,aAAa,SAAI,GAChB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAU1B;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,GAC1C;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CASxC;AAED;;;;GAIG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAA"}
1
+ {"version":3,"file":"window-math.d.ts","sourceRoot":"","sources":["../../src/window/window-math.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EAId,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AAGzB;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,MAAM,CAAA;AAExC;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,YAAY,SAAI,EAChB,aAAa,SAAI,GAChB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAI1B;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,GAC1C;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAQxC;AAED;;;;GAIG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { getCurtainWallConfig, getWallCurveFrameAt, getWallCurveLength, } from '@pascal-app/core';
1
2
  import { resolveWallOpeningCeiling } from '../shared/wall-opening-ceiling';
2
3
  /**
3
4
  * Default sill height (metres from the floor to the BOTTOM of a window) for a
@@ -15,12 +16,9 @@ export const DEFAULT_WINDOW_SILL_M = 0.5;
15
16
  * height — matching how WallSystem positions the wall mesh at slabElevation.
16
17
  */
17
18
  export function wallLocalToWorld(wallNode, localX, localY, levelYOffset = 0, slabElevation = 0) {
18
- const wallAngle = Math.atan2(wallNode.end[1] - wallNode.start[1], wallNode.end[0] - wallNode.start[0]);
19
- return [
20
- wallNode.start[0] + localX * Math.cos(wallAngle),
21
- slabElevation + localY + levelYOffset,
22
- wallNode.start[1] + localX * Math.sin(wallAngle),
23
- ];
19
+ const wallLength = getWallCurveLength(wallNode);
20
+ const frame = getWallCurveFrameAt(wallNode, wallLength > 1e-6 ? localX / wallLength : 0);
21
+ return [frame.point.x, slabElevation + localY + levelYOffset, frame.point.y];
24
22
  }
25
23
  /**
26
24
  * Clamps window center position so it stays fully within wall bounds. The Y
@@ -30,12 +28,11 @@ export function wallLocalToWorld(wallNode, localX, localY, levelYOffset = 0, sla
30
28
  * wall record.
31
29
  */
32
30
  export function clampToWall(wallNode, localX, localY, width, height, nodes) {
33
- const dx = wallNode.end[0] - wallNode.start[0];
34
- const dz = wallNode.end[1] - wallNode.start[1];
35
- const wallLength = Math.sqrt(dx * dx + dz * dz);
31
+ const wallLength = getWallCurveLength(wallNode);
36
32
  const wallHeight = resolveWallOpeningCeiling(wallNode, nodes);
37
- const clampedX = Math.max(width / 2, Math.min(wallLength - width / 2, localX));
38
- const clampedY = Math.max(height / 2, Math.min(wallHeight - height / 2, localY));
33
+ const margin = wallNode.wallType === 'curtain' ? getCurtainWallConfig(wallNode).perimeterWidth : 0;
34
+ const clampedX = Math.max(margin + width / 2, Math.min(wallLength - margin - width / 2, localX));
35
+ const clampedY = Math.max(margin + height / 2, Math.min(wallHeight - margin - height / 2, localY));
39
36
  return { clampedX, clampedY };
40
37
  }
41
38
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"definition.d.ts","sourceRoot":"","sources":["../../src/zone/definition.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,cAAc,EAGpB,MAAM,kBAAkB,CAAA;AAezB,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAGnC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,cAAc,CAAC,OAAO,QAAQ,CAmF1D,CAAA"}
1
+ {"version":3,"file":"definition.d.ts","sourceRoot":"","sources":["../../src/zone/definition.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,cAAc,EAGpB,MAAM,kBAAkB,CAAA;AAezB,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAGnC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,cAAc,CAAC,OAAO,QAAQ,CAoF1D,CAAA"}
@@ -52,6 +52,7 @@ export const zoneDefinition = {
52
52
  },
53
53
  // No dirty consumer rebuilds this kind — see NodeDefinition.dirtyTracking.
54
54
  dirtyTracking: false,
55
+ rendersChildren: false,
55
56
  renderer: {
56
57
  kind: 'parametric',
57
58
  module: () => import('./renderer'),
@@ -1 +1 @@
1
- {"version":3,"file":"quantities-panel.d.ts","sourceRoot":"","sources":["../../src/zone/quantities-panel.tsx"],"names":[],"mappings":"AA8RA,MAAM,CAAC,OAAO,UAAU,mBAAmB,uCA8F1C"}
1
+ {"version":3,"file":"quantities-panel.d.ts","sourceRoot":"","sources":["../../src/zone/quantities-panel.tsx"],"names":[],"mappings":"AAsMA,MAAM,CAAC,OAAO,UAAU,mBAAmB,uCA6F1C"}
@@ -1,11 +1,11 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { deriveZoneQuantityReport, resolveAutoZonePolygon, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
4
- import { formatAreaLabel, formatLinearMeasurement, formatVolumeLabel, MetricControl, PanelSection, ToggleControl, } from '@pascal-app/editor';
4
+ import { formatAreaLabel, formatLinearMeasurement, formatVolumeLabel, PanelSection, } from '@pascal-app/editor';
5
5
  import { useViewer } from '@pascal-app/viewer';
6
6
  import { useMemo } from 'react';
7
7
  import { useShallow } from 'zustand/react/shallow';
8
- import { PanelSelect, PanelTextField } from '../shared/panel-fields';
8
+ import { PanelSelect } from '../shared/panel-fields';
9
9
  import { buildingUnitsForZone } from './unit-membership';
10
10
  function ZonePlanSketch({ edgeLengths, metricNotation, polygon, unit, }) {
11
11
  if (polygon.length < 3) {
@@ -76,22 +76,6 @@ function ZoneUnitPanel({ zone }) {
76
76
  ...units.map((unit) => ({ label: unit.name || 'Unit', value: unit.id })),
77
77
  ], value: current?.id ?? '' }) }));
78
78
  }
79
- function RoomDocumentationPanel({ zone }) {
80
- const updateNode = useScene((state) => state.updateNode);
81
- const update = (patch) => updateNode(zone.id, patch);
82
- const isRoom = zone.spaceRole === 'room';
83
- return (_jsxs(PanelSection, { title: "Room documentation", children: [_jsx(ToggleControl, { checked: isRoom, label: "Architectural room", onChange: (checked) => update({ spaceRole: checked ? 'room' : 'generic' }) }), isRoom ? (_jsxs(_Fragment, { children: [_jsx(PanelTextField, { label: "Room name", onCommit: (name) => update({ name }), value: zone.name }), _jsx(PanelTextField, { label: "Room number", onCommit: (roomNumber) => update({ roomNumber }), value: zone.roomNumber }), _jsx(PanelSelect, { label: "Enclosure", onChange: (enclosureStatus) => update({ enclosureStatus: enclosureStatus }), options: [
84
- { label: 'Auto-detect', value: 'auto' },
85
- { label: 'Enclosed', value: 'enclosed' },
86
- { label: 'Open', value: 'open' },
87
- ], value: zone.enclosureStatus }), _jsx(PanelTextField, { label: "Occupancy / use", onCommit: (occupancy) => update({ occupancy }), value: zone.occupancy }), _jsx(PanelTextField, { label: "Floor finish", onCommit: (floorFinish) => update({ floorFinish }), value: zone.floorFinish }), _jsx(PanelTextField, { label: "Wall finish", onCommit: (wallFinish) => update({ wallFinish }), value: zone.wallFinish }), _jsx(PanelTextField, { label: "Ceiling finish", onCommit: (ceilingFinish) => update({ ceilingFinish }), value: zone.ceilingFinish }), _jsx(MetricControl, { label: "Ceiling height", max: 20, min: 0.1, onChange: (ceilingHeight) => update({ ceilingHeight }), precision: 2, step: 0.05, unit: "m", value: zone.ceilingHeight }), _jsx(PanelSelect, { label: "Clear dimensions", onChange: (clearDimensionPolicy) => update({
88
- clearDimensionPolicy: clearDimensionPolicy,
89
- }), options: [
90
- { label: 'None', value: 'none' },
91
- { label: 'Inside faces', value: 'inside-faces' },
92
- { label: 'Finish faces', value: 'finish-faces' },
93
- ], value: zone.clearDimensionPolicy })] })) : null] }));
94
- }
95
79
  export default function ZoneQuantitiesPanel() {
96
80
  const selectedZoneId = useViewer((state) => state.selection.zoneId);
97
81
  const unit = useViewer((state) => state.unit);
@@ -128,5 +112,5 @@ export default function ZoneQuantitiesPanel() {
128
112
  const report = useMemo(() => (effectiveZone ? deriveZoneQuantityReport(effectiveZone, effectiveNodes) : null), [effectiveNodes, effectiveZone]);
129
113
  if (!effectiveZone || !report)
130
114
  return null;
131
- return (_jsxs(_Fragment, { children: [_jsx(ZoneUnitPanel, { zone: effectiveZone }), _jsx(RoomDocumentationPanel, { zone: effectiveZone }), _jsxs(PanelSection, { title: effectiveZone.spaceRole === 'room' ? 'Room quantities' : 'Zone quantities', children: [_jsxs("div", { className: "overflow-hidden rounded-md border border-cyan-950/20 bg-[#f8faf7] text-slate-950", children: [_jsxs("div", { className: "flex items-center border-cyan-950/15 border-b px-2.5 py-2", children: [_jsx("span", { className: "font-semibold text-[11px]", children: effectiveZone.name }), _jsx("span", { className: "ml-auto rounded-full border border-cyan-800/25 bg-cyan-50 px-2 py-0.5 text-cyan-900 text-[9px]", children: report.classification === 'enclosed-room' ? 'Enclosed room' : 'Footprint only' })] }), _jsxs("div", { className: "flex items-baseline gap-2 px-2.5 py-2 font-mono text-[10px]", children: [_jsx("span", { className: "text-cyan-800", children: "A" }), _jsx("span", { children: formatAreaLabel(report.footprintArea, unit, 2) }), _jsx("span", { className: "ml-auto text-slate-600", children: "P" }), _jsx("span", { children: formatLinearMeasurement(report.perimeter, unit, metricNotation) })] })] }), _jsx(ZonePlanSketch, { edgeLengths: report.edgeLengths, metricNotation: metricNotation, polygon: effectiveZone.polygon, unit: unit }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(QuantityRow, { abbreviation: "Aw", format: (value) => formatAreaLabel(value, unit, 2), label: "Wall surface", quantity: report.wallSurface }), _jsx(QuantityRow, { abbreviation: "Af", format: (value) => formatAreaLabel(value, unit, 2), label: "Floor surface", quantity: report.floorSurface }), _jsx(QuantityRow, { abbreviation: "V", format: (value) => formatVolumeLabel(value, unit, 2), label: "Volume", quantity: report.volume })] })] })] }));
115
+ return (_jsxs(_Fragment, { children: [_jsx(ZoneUnitPanel, { zone: effectiveZone }), _jsxs(PanelSection, { title: effectiveZone.spaceRole === 'room' ? 'Room quantities' : 'Zone quantities', children: [_jsxs("div", { className: "overflow-hidden rounded-md border border-cyan-950/20 bg-[#f8faf7] text-slate-950", children: [_jsxs("div", { className: "flex items-center border-cyan-950/15 border-b px-2.5 py-2", children: [_jsx("span", { className: "font-semibold text-[11px]", children: effectiveZone.name }), _jsx("span", { className: "ml-auto rounded-full border border-cyan-800/25 bg-cyan-50 px-2 py-0.5 text-cyan-900 text-[9px]", children: report.classification === 'enclosed-room' ? 'Enclosed room' : 'Footprint only' })] }), _jsxs("div", { className: "flex items-baseline gap-2 px-2.5 py-2 font-mono text-[10px]", children: [_jsx("span", { className: "text-cyan-800", children: "A" }), _jsx("span", { children: formatAreaLabel(report.footprintArea, unit, 2) }), _jsx("span", { className: "ml-auto text-slate-600", children: "P" }), _jsx("span", { children: formatLinearMeasurement(report.perimeter, unit, metricNotation) })] })] }), _jsx(ZonePlanSketch, { edgeLengths: report.edgeLengths, metricNotation: metricNotation, polygon: effectiveZone.polygon, unit: unit }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(QuantityRow, { abbreviation: "Aw", format: (value) => formatAreaLabel(value, unit, 2), label: "Wall surface", quantity: report.wallSurface }), _jsx(QuantityRow, { abbreviation: "Af", format: (value) => formatAreaLabel(value, unit, 2), label: "Floor surface", quantity: report.floorSurface }), _jsx(QuantityRow, { abbreviation: "V", format: (value) => formatVolumeLabel(value, unit, 2), label: "Volume", quantity: report.volume })] })] })] }));
132
116
  }
@@ -1 +1 @@
1
- {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/zone/renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AA0GzB,eAAO,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,uCA+MxD,CAAA;AAED,eAAe,YAAY,CAAA"}
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/zone/renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AA0GzB,eAAO,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,uCAoNxD,CAAA;AAED,eAAe,YAAY,CAAA"}
@@ -176,7 +176,7 @@ export const ZoneRenderer = ({ node }) => {
176
176
  if (!(node && floorShape && wallGeometry && floorMaterial && wallMaterial)) {
177
177
  return null;
178
178
  }
179
- return (_jsx("group", { ref: ref, ...handlers, userData: { labelPosition: [centroid[0], 1, centroid[1]] }, children: showZones && (_jsxs(_Fragment, { children: [_jsx(Html, { name: "label", position: [centroid[0], 1, centroid[1]], style: { pointerEvents: 'none' }, zIndexRange: [10, 0], children: _jsxs("div", { id: `${node.id}-label`, style: {
179
+ return (_jsx("group", { ref: ref, visible: node.visible !== false, ...handlers, userData: { labelPosition: [centroid[0], 1, centroid[1]] }, children: showZones && (_jsxs(_Fragment, { children: [_jsx(Html, { name: "label", position: [centroid[0], 1, centroid[1]], style: { pointerEvents: 'none' }, zIndexRange: [10, 0], children: _jsxs("div", { id: `${node.id}-label`, style: {
180
180
  display: 'flex',
181
181
  flexDirection: 'column',
182
182
  alignItems: 'center',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/nodes",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Built-in node bundles for the Pascal 3D editor — one folder per kind, exported as `builtinPlugin`",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,9 +28,9 @@
28
28
  "prepublishOnly": "bun run build && bun test"
29
29
  },
30
30
  "peerDependencies": {
31
- "@pascal-app/core": "^1.0.1",
32
- "@pascal-app/editor": "^1.0.1",
33
- "@pascal-app/viewer": "^1.0.1",
31
+ "@pascal-app/core": "^1.0.3",
32
+ "@pascal-app/editor": "^1.0.3",
33
+ "@pascal-app/viewer": "^1.0.3",
34
34
  "@react-three/drei": "^10",
35
35
  "@react-three/fiber": "^9",
36
36
  "lucide-react": "^1",
@@ -40,9 +40,9 @@
40
40
  "zustand": "^5"
41
41
  },
42
42
  "devDependencies": {
43
- "@pascal-app/core": "^1.0.1",
44
- "@pascal-app/editor": "^1.0.1",
45
- "@pascal-app/viewer": "^1.0.1",
43
+ "@pascal-app/core": "^1.0.3",
44
+ "@pascal-app/editor": "^1.0.3",
45
+ "@pascal-app/viewer": "^1.0.3",
46
46
  "@pascal/typescript-config": "*",
47
47
  "@react-three/test-renderer": "^9.1.0",
48
48
  "@types/bun": "^1.3.0",
@@ -60,7 +60,7 @@
60
60
  ],
61
61
  "repository": {
62
62
  "type": "git",
63
- "url": "https://github.com/pascalorg/editor.git",
63
+ "url": "git+https://github.com/pascalorg/editor.git",
64
64
  "directory": "packages/nodes"
65
65
  },
66
66
  "license": "MIT",
@@ -1,16 +0,0 @@
1
- type OpeningDocumentationPatch = {
2
- mark?: string;
3
- constructionType?: 'framed' | 'masonry';
4
- dimensionReference?: 'nominal' | 'rough-opening' | 'masonry-opening' | 'finish-opening';
5
- roughOpeningWidth?: number;
6
- roughOpeningHeight?: number;
7
- masonryOpeningWidth?: number;
8
- masonryOpeningHeight?: number;
9
- finishOpeningWidth?: number;
10
- finishOpeningHeight?: number;
11
- };
12
- export declare function OpeningDocumentationFields({ mark, constructionType, dimensionReference, roughOpeningWidth, roughOpeningHeight, masonryOpeningWidth, masonryOpeningHeight, finishOpeningWidth, finishOpeningHeight, onChange, }: OpeningDocumentationPatch & {
13
- onChange: (patch: OpeningDocumentationPatch) => void;
14
- }): import("react").JSX.Element;
15
- export {};
16
- //# sourceMappingURL=opening-documentation-fields.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"opening-documentation-fields.d.ts","sourceRoot":"","sources":["../../src/shared/opening-documentation-fields.tsx"],"names":[],"mappings":"AAKA,KAAK,yBAAyB,GAAG;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,gBAAgB,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;IACvC,kBAAkB,CAAC,EAAE,SAAS,GAAG,eAAe,GAAG,iBAAiB,GAAG,gBAAgB,CAAA;IACvF,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAED,wBAAgB,0BAA0B,CAAC,EACzC,IAAI,EACJ,gBAA2B,EAC3B,kBAA8B,EAC9B,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,QAAQ,GACT,EAAE,yBAAyB,GAAG;IAC7B,QAAQ,EAAE,CAAC,KAAK,EAAE,yBAAyB,KAAK,IAAI,CAAA;CACrD,+BA+GA"}
@@ -1,54 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { getLinearUnitLabel, linearUnitToMeters, metersToLinearUnit } from '@pascal-app/editor';
4
- import { useViewer } from '@pascal-app/viewer';
5
- export function OpeningDocumentationFields({ mark, constructionType = 'framed', dimensionReference = 'nominal', roughOpeningWidth, roughOpeningHeight, masonryOpeningWidth, masonryOpeningHeight, finishOpeningWidth, finishOpeningHeight, onChange, }) {
6
- return (_jsxs("div", { className: "flex flex-col gap-2 px-1 pb-1", children: [_jsxs("label", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider", children: "Mark" }), _jsx("input", { className: "h-8 rounded-lg border border-border/50 bg-[#2C2C2E] px-2.5 font-mono text-foreground text-xs outline-none transition-colors placeholder:text-muted-foreground/50 focus:border-orange-400/60", defaultValue: mark ?? '', maxLength: 16, onBlur: (event) => {
7
- const next = event.currentTarget.value.trim().toLocaleUpperCase();
8
- if (next !== (mark ?? ''))
9
- onChange({ mark: next || undefined });
10
- }, onKeyDown: (event) => {
11
- if (event.key === 'Enter')
12
- event.currentTarget.blur();
13
- if (event.key === 'Escape') {
14
- event.currentTarget.value = mark ?? '';
15
- event.currentTarget.blur();
16
- }
17
- }, placeholder: "Auto-assigned" }, `mark:${mark ?? ''}`)] }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("label", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider", children: "Construction" }), _jsxs("select", { className: "h-8 rounded-lg border border-border/50 bg-[#2C2C2E] px-2 text-foreground text-xs outline-none focus:border-orange-400/60", onChange: (event) => {
18
- const next = event.currentTarget.value;
19
- onChange({
20
- constructionType: next,
21
- dimensionReference: next === 'masonry' && dimensionReference === 'nominal'
22
- ? 'masonry-opening'
23
- : dimensionReference,
24
- });
25
- }, value: constructionType, children: [_jsx("option", { value: "framed", children: "Framed" }), _jsx("option", { value: "masonry", children: "Masonry" })] })] }), _jsxs("label", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider", children: "Dimension to" }), _jsxs("select", { className: "h-8 rounded-lg border border-border/50 bg-[#2C2C2E] px-2 text-foreground text-xs outline-none focus:border-orange-400/60", onChange: (event) => onChange({
26
- dimensionReference: event.currentTarget
27
- .value,
28
- }), value: dimensionReference, children: [_jsx("option", { value: "nominal", children: "Nominal" }), _jsx("option", { value: "rough-opening", children: "Rough opening" }), _jsx("option", { value: "masonry-opening", children: "Masonry opening" }), _jsx("option", { value: "finish-opening", children: "Finish opening" })] })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(OptionalMeterInput, { label: "RO Width", onChange: (value) => onChange({ roughOpeningWidth: value }), value: roughOpeningWidth }), _jsx(OptionalMeterInput, { label: "RO Height", onChange: (value) => onChange({ roughOpeningHeight: value }), value: roughOpeningHeight })] }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(OptionalMeterInput, { label: "MO Width", onChange: (value) => onChange({ masonryOpeningWidth: value }), value: masonryOpeningWidth }), _jsx(OptionalMeterInput, { label: "MO Height", onChange: (value) => onChange({ masonryOpeningHeight: value }), value: masonryOpeningHeight })] }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(OptionalMeterInput, { label: "FO Width", onChange: (value) => onChange({ finishOpeningWidth: value }), value: finishOpeningWidth }), _jsx(OptionalMeterInput, { label: "FO Height", onChange: (value) => onChange({ finishOpeningHeight: value }), value: finishOpeningHeight })] }), _jsx("p", { className: "px-0.5 text-[10px] text-muted-foreground/65 leading-4", children: "Leave RO, MO, and FO values blank until verified by the applicable manufacturer or trade." })] }));
29
- }
30
- function OptionalMeterInput({ label, value, onChange, }) {
31
- const unit = useViewer((state) => state.unit);
32
- const displayValue = value === undefined ? '' : roundForInput(metersToLinearUnit(value, unit));
33
- return (_jsxs("label", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider", children: label }), _jsxs("div", { className: "flex h-8 items-center rounded-lg border border-border/50 bg-[#2C2C2E] focus-within:border-orange-400/60", children: [_jsx("input", { className: "min-w-0 flex-1 bg-transparent px-2 font-mono text-foreground text-xs outline-none placeholder:text-muted-foreground/50", defaultValue: displayValue, min: 0.01, onBlur: (event) => {
34
- const raw = event.currentTarget.value;
35
- if (raw === String(displayValue))
36
- return;
37
- const parsed = Number.parseFloat(raw);
38
- const next = raw === '' || !Number.isFinite(parsed) || parsed <= 0
39
- ? undefined
40
- : linearUnitToMeters(parsed, unit);
41
- if (next !== value)
42
- onChange(next);
43
- }, onKeyDown: (event) => {
44
- if (event.key === 'Enter')
45
- event.currentTarget.blur();
46
- if (event.key === 'Escape') {
47
- event.currentTarget.value = String(displayValue);
48
- event.currentTarget.blur();
49
- }
50
- }, onWheel: (event) => event.currentTarget.blur(), placeholder: "Verify", step: unit === 'imperial' ? 0.01 : 0.001, type: "number" }, `${label}:${displayValue}`), _jsx("span", { className: "pr-2 font-mono text-[10px] text-muted-foreground", children: getLinearUnitLabel(unit) })] })] }));
51
- }
52
- function roundForInput(value) {
53
- return Math.round(value * 1000) / 1000;
54
- }