@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
@@ -1,103 +1,19 @@
1
- import { collectAlignmentAnchors, createSceneApi, getBlockFaceFrame, getRoofWallFaceFrame, getScaledDimensions, movingFootprintAnchors, nodeRegistry, resolveSurfacePlacement, roofFacePointToSegment, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
2
- import { boundsOf, boxCorners, frame, transformPoint } from '@pascal-app/core/procedural-items';
3
- import { applyFloorplanAlignment, isGridSnapActive, isMagneticSnapActive, useEditor, } from '@pascal-app/editor';
1
+ import { cascadeDirty, clearFaceHostItemFields, collectAlignmentAnchors, collectDescendants, createSceneApi, getBlockFaceFrame, getScaledDimensions, getSurfaceProvider, movingFootprintAnchors, nodeRegistry, resolveSupportSlabPatch, resolveSurfacePlacement, surfaceRegionContainsPoint, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
2
+ import { boundsOf, boxCorners, composeFrames, frame, transformPoint, } from '@pascal-app/core/procedural-items';
3
+ import { applyFloorplanAlignment, isGridSnapActive, isMagneticSnapActive, surfaceAttachmentId, surfaceFramePose, updateSurfaceNode, useEditor, useInteractionScope, } from '@pascal-app/editor';
4
4
  import { createFloorplanCursorResolver } from '../shared/floorplan-cursor';
5
5
  import { restingNodePlanFrame } from '../shared/resting-surface-plan';
6
6
  import { findClosestWallInPlan, snapLocalXToNeighbors } from '../shared/wall-attach-target';
7
- function rotateVec(x, z, rotationY) {
8
- const c = Math.cos(rotationY);
9
- const s = Math.sin(rotationY);
10
- return [x * c + z * s, -x * s + z * c];
11
- }
12
- function resolveItemPlanTransform(item, nodes, cache = new Map()) {
13
- const cached = cache.get(item.id);
14
- if (cached)
15
- return cached;
16
- const localRotation = item.rotation[1] ?? 0;
17
- let result = {
18
- point: [item.position[0], item.position[2]],
19
- rotation: localRotation,
7
+ import { resolveItemTransform } from './floorplan';
8
+ function resolveItemPlanTransform(item, nodes) {
9
+ const pose = resolveItemTransform(item, { resolve: (id) => nodes[id] });
10
+ return {
11
+ point: pose ? [pose.x, pose.y] : [item.position[0], item.position[2]],
12
+ rotation: pose?.rotation ?? item.rotation[1],
20
13
  };
21
- const parent = item.parentId ? nodes[item.parentId] : null;
22
- if (parent?.type === 'wall') {
23
- const wallRotation = -Math.atan2(parent.end[1] - parent.start[1], parent.end[0] - parent.start[0]);
24
- const wallLocalZ = item.asset.attachTo === 'wall-side'
25
- ? ((parent.thickness ?? 0.1) / 2) * (item.side === 'front' ? 1 : -1)
26
- : item.position[2];
27
- const [offsetX, offsetZ] = rotateVec(item.position[0], wallLocalZ, wallRotation);
28
- result = {
29
- point: [parent.start[0] + offsetX, parent.start[1] + offsetZ],
30
- rotation: wallRotation + localRotation,
31
- };
32
- }
33
- else if (parent?.type === 'cabinet' ||
34
- parent?.type === 'cabinet-module' ||
35
- parent?.type === 'procedural-item') {
36
- const f = restingNodePlanFrame(item, (id) => nodes[id]);
37
- result = {
38
- point: [f.position[0], f.position[2]],
39
- rotation: Math.atan2(f.axes[2][0], f.axes[2][2]),
40
- };
41
- }
42
- else if (parent?.type === 'shelf') {
43
- const shelf = parent;
44
- const [offsetX, offsetZ] = rotateVec(item.position[0], item.position[2], shelf.rotation[1] ?? 0);
45
- result = {
46
- point: [shelf.position[0] + offsetX, shelf.position[2] + offsetZ],
47
- rotation: (shelf.rotation[1] ?? 0) + localRotation,
48
- };
49
- }
50
- else if (parent?.type === 'item') {
51
- const parentTransform = resolveItemPlanTransform(parent, nodes, cache);
52
- const [offsetX, offsetZ] = rotateVec(item.position[0], item.position[2], parentTransform.rotation);
53
- result = {
54
- point: [parentTransform.point[0] + offsetX, parentTransform.point[1] + offsetZ],
55
- rotation: parentTransform.rotation + localRotation,
56
- };
57
- }
58
- else if (parent?.type === 'roof-segment') {
59
- // Roof-hosted wall item: FACE-LOCAL position mapped through the face
60
- // frame, then composed through the segment's and roof's yaw +
61
- // position into level-local plan coords — without this the drag seed
62
- // jumps off the roof at move start.
63
- const segment = parent;
64
- const roof = segment.parentId
65
- ? nodes[segment.parentId]
66
- : undefined;
67
- if (roof?.type === 'roof' && item.roofFace) {
68
- const frame = getRoofWallFaceFrame(segment, item.roofFace);
69
- const segLocal = roofFacePointToSegment(segment, item.roofFace, item.position);
70
- const [sx, sz] = rotateVec(segLocal[0], segLocal[2], segment.rotation ?? 0);
71
- const [rx, rz] = rotateVec(sx + segment.position[0], sz + segment.position[2], roof.rotation ?? 0);
72
- result = {
73
- point: [rx + roof.position[0], rz + roof.position[2]],
74
- rotation: (roof.rotation ?? 0) + (segment.rotation ?? 0) + frame.yaw + localRotation,
75
- };
76
- }
77
- }
78
- else if (parent?.type === 'block' && item.blockFaceId) {
79
- const frame = getBlockFaceFrame(parent.topology, item.blockFaceId);
80
- if (frame) {
81
- const localX = frame.origin[0] +
82
- frame.xAxis[0] * item.position[0] +
83
- frame.yAxis[0] * item.position[1] +
84
- frame.normal[0] * item.position[2];
85
- const localZ = frame.origin[2] +
86
- frame.xAxis[2] * item.position[0] +
87
- frame.yAxis[2] * item.position[1] +
88
- frame.normal[2] * item.position[2];
89
- const [offsetX, offsetZ] = rotateVec(localX, localZ, parent.rotation ?? 0);
90
- result = {
91
- point: [parent.position[0] + offsetX, parent.position[2] + offsetZ],
92
- rotation: (parent.rotation ?? 0) - Math.atan2(frame.xAxis[2], frame.xAxis[0]) + localRotation,
93
- };
94
- }
95
- }
96
- cache.set(item.id, result);
97
- return result;
98
14
  }
99
- function resolveItemPlanPoint(item, nodes, cache = new Map()) {
100
- return resolveItemPlanTransform(item, nodes, cache).point;
15
+ function resolveItemPlanPoint(item, nodes) {
16
+ return resolveItemPlanTransform(item, nodes).point;
101
17
  }
102
18
  function createPlanarMovePointResolver(originalPlanPoint, node) {
103
19
  const resolveCursor = createFloorplanCursorResolver({
@@ -211,49 +127,185 @@ function buildWallItemSession(node, startLevelId) {
211
127
  * bookkeeping and the item drops out of the level→children DFS the
212
128
  * floor-plan layer walks → the polygon stops rendering mid-drag.
213
129
  *
214
- * For the 2D move we just translate `position` in level-local coords and
215
- * leave the parent as the level (matching the 3D `detachItemSurfaceToFloor`
216
- * in `use-placement-coordinator.tsx`). Snap to the 0.5m grid unless the
217
- * user holds Shift.
130
+ * A host exit converts the pose to the level frame, matching the 3D
131
+ * `detachItemSurfaceToFloor`; inherited support lift stays out of stored Y.
218
132
  */
219
133
  function buildFloorItemSession(node, startLevelId, nodes) {
220
- const rotationY = node.rotation[1] ?? 0;
221
- const resolvePlanPoint = createPlanarMovePointResolver(resolveItemPlanPoint(node, nodes), node);
134
+ const host = node.parentId ? nodes[node.parentId] : undefined;
135
+ const hosted = host && host.type !== 'level';
136
+ const surfaceId = surfaceAttachmentId(node);
137
+ const hostPose = surfaceFramePose(node.parentId, surfaceId, node, false);
138
+ const planTransform = resolveItemPlanTransform(node, nodes);
139
+ const levelFrame = hosted ? restingNodePlanFrame(node, (id) => nodes[id]) : null;
140
+ const rotationY = levelFrame
141
+ ? Math.atan2(levelFrame.axes[2][0], levelFrame.axes[2][2])
142
+ : planTransform.rotation;
143
+ const levelRotation = [...node.rotation];
144
+ if (hosted) {
145
+ if (node.type === 'procedural-item') {
146
+ // Generated footprints use the query frame; catalog footprints keep their legacy plan mapping.
147
+ planTransform.point = [levelFrame.position[0], levelFrame.position[2]];
148
+ const hostFrame = restingNodePlanFrame(host, (id) => nodes[id]);
149
+ const headingPose = surfaceFramePose(node.parentId, surfaceId, { position: node.position, rotation: [0, node.rotation[1], 0] }, false);
150
+ levelRotation[1] =
151
+ headingPose.rotation[1] + Math.atan2(hostFrame.axes[2][0], hostFrame.axes[2][2]);
152
+ }
153
+ else {
154
+ levelRotation.splice(0, 3, hostPose.rotation[0], rotationY, hostPose.rotation[2]);
155
+ }
156
+ }
157
+ if (host?.type === 'block' && node.blockFaceId)
158
+ levelRotation.splice(0, 3, 0, rotationY, 0);
159
+ const resolvePlanPoint = createPlanarMovePointResolver(planTransform.point, node);
222
160
  // Alignment candidates gathered once — scene is stable during the drag.
223
161
  const candidates = collectAlignmentAnchors(nodes, node.id);
162
+ const scene = createSceneApi(useScene);
163
+ const bounds = nodeRegistry.get(node.type)?.capabilities.dragBounds?.(node, nodes);
164
+ const dimensions = bounds?.size ?? getScaledDimensions(node);
165
+ const localBounds = bounds?.center
166
+ ? {
167
+ min: bounds.center.map((v, i) => v - dimensions[i] / 2),
168
+ max: bounds.center.map((v, i) => v + dimensions[i] / 2),
169
+ }
170
+ : {
171
+ min: [-dimensions[0] / 2, 0, -dimensions[2] / 2],
172
+ max: [dimensions[0] / 2, dimensions[1], dimensions[2] / 2],
173
+ };
174
+ const originalBounds = boundsOf(boxCorners(localBounds.min, localBounds.max).map((p) => transformPoint(frame(hostPose.position, hostPose.rotation), p)));
175
+ const originalPlacement = hosted
176
+ ? resolveSurfacePlacement({
177
+ host,
178
+ childKind: node.type,
179
+ childId: node.id,
180
+ childFootprint: {
181
+ size: dimensions,
182
+ rotationY: hostPose.rotation[1],
183
+ rotation: hostPose.rotation,
184
+ localBounds,
185
+ },
186
+ hit: {
187
+ point: [
188
+ (originalBounds.min[0] + originalBounds.max[0]) / 2,
189
+ originalBounds.min[1],
190
+ (originalBounds.min[2] + originalBounds.max[2]) / 2,
191
+ ],
192
+ normalWorldY: 1,
193
+ },
194
+ origin: hostPose.position,
195
+ scene,
196
+ })
197
+ : null;
198
+ const retainedSurfaceId = hosted && originalPlacement
199
+ ? getSurfaceProvider(host)
200
+ .surfaces?.(host, { scene })
201
+ .find((s) => s.id === (surfaceId ?? originalPlacement.surfaceId))?.id
202
+ : undefined;
203
+ const markMoved = () => {
204
+ for (const id of new Set([
205
+ ...cascadeDirty(node.id, { scene }),
206
+ ...collectDescendants(node.id, { scene }),
207
+ ]))
208
+ useScene.getState().markDirty(id);
209
+ };
224
210
  let lastPatch = null;
225
- return {
211
+ let lastInput = null;
212
+ let commitBlocked = false;
213
+ let lastHostedPatch = {
214
+ parentId: node.parentId,
215
+ position: [...node.position],
216
+ rotation: [...node.rotation],
217
+ supportSlabId: undefined,
218
+ };
219
+ const session = {
226
220
  affectedIds: [node.id],
227
- apply({ planPoint }) {
221
+ apply(input) {
222
+ lastInput = input;
223
+ commitBlocked = false;
224
+ const { planPoint } = input;
228
225
  const gridSnapped = resolvePlanPoint(planPoint);
229
226
  // Figma-style alignment layered on the grid snap, mode-driven (matching 3D):
230
227
  // guides are DISPLAYED in every snapping mode; the magnetic pull onto them
231
228
  // is applied only in "lines" mode (`applySnap`).
232
229
  const { point: snapped } = applyFloorplanAlignment(gridSnapped, movingFootprintAnchors(node, gridSnapped[0], gridSnapped[1], rotationY), candidates, { applySnap: isMagneticSnapActive() });
233
- const host = node.parentId ? nodes[node.parentId] : undefined;
234
- if (host?.type === 'cabinet' || host?.type === 'shelf') {
230
+ const liveHost = host && useScene.getState().nodes[host.id];
231
+ if (liveHost?.type === 'block' && node.blockFaceId) {
232
+ const face = getBlockFaceFrame(liveHost.topology, node.blockFaceId);
233
+ if (face && face.normal[1] > 0.99) {
234
+ const hostFrame = restingNodePlanFrame(liveHost, (id) => useScene.getState().nodes[id]);
235
+ const surface = composeFrames(hostFrame, {
236
+ position: face.origin,
237
+ axes: [face.xAxis, face.yAxis, face.normal],
238
+ });
239
+ const delta = [snapped[0] - surface.position[0], 0, snapped[1] - surface.position[2]];
240
+ const uv = surface.axes
241
+ .slice(0, 2)
242
+ .map((axis) => axis.reduce((sum, v, i) => sum + v * delta[i], 0));
243
+ const vertices = new Map(liveHost.topology.vertices.map((v) => [v.id, v.position]));
244
+ const polygon = liveHost.topology.faces
245
+ .find((f) => f.id === node.blockFaceId)
246
+ .vertexIds.map((id) => {
247
+ const v = vertices.get(id);
248
+ const delta = v.map((value, i) => value - face.origin[i]);
249
+ return [face.xAxis, face.yAxis].map((axis) => axis.reduce((sum, value, i) => sum + value * delta[i], 0));
250
+ });
251
+ if (surfaceRegionContainsPoint({ kind: 'polygon', points: polygon }, uv)) {
252
+ lastPatch = {
253
+ parentId: liveHost.id,
254
+ position: [uv[0], uv[1], node.position[2]],
255
+ rotation: [...node.rotation],
256
+ supportSlabId: undefined,
257
+ };
258
+ useLiveNodeOverrides.getState().set(node.id, lastPatch);
259
+ markMoved();
260
+ return;
261
+ }
262
+ }
263
+ }
264
+ if (hosted && liveHost && (liveHost.type !== 'block' || !node.blockFaceId)) {
265
+ const host = liveHost;
266
+ const retainedSurface = getSurfaceProvider(host)
267
+ .surfaces?.(host, { scene })
268
+ .find((s) => s.id === retainedSurfaceId);
235
269
  const hostFrame = restingNodePlanFrame(host, (id) => useScene.getState().nodes[id]);
236
- const dx = snapped[0] - hostFrame.position[0];
237
- const dz = snapped[1] - hostFrame.position[2];
238
- const local = [
239
- dx * hostFrame.axes[0][0] + dz * hostFrame.axes[0][2],
240
- node.position[1],
241
- dx * hostFrame.axes[2][0] + dz * hostFrame.axes[2][2],
242
- ];
243
- const bounds = nodeRegistry.get(node.type)?.capabilities.dragBounds?.(node, nodes);
244
- const dimensions = bounds?.size ?? getScaledDimensions(node);
245
- const center = bounds?.center;
246
- const localBounds = center
247
- ? {
248
- min: center.map((v, i) => v - dimensions[i] / 2),
249
- max: center.map((v, i) => v + dimensions[i] / 2),
270
+ if (host.type === 'item') {
271
+ const plan = resolveItemTransform(host, {
272
+ resolve: (id) => useScene.getState().nodes[id],
273
+ });
274
+ if (plan) {
275
+ hostFrame.position[0] = plan.x;
276
+ hostFrame.position[2] = plan.y;
250
277
  }
251
- : undefined;
252
- const projected = boundsOf(boxCorners(localBounds?.min ?? [-dimensions[0] / 2, 0, -dimensions[2] / 2], localBounds?.max ?? [dimensions[0] / 2, dimensions[1], dimensions[2] / 2]).map((p) => transformPoint(frame(local, node.rotation), p)));
278
+ }
279
+ const surfaceFrame = composeFrames(hostFrame, frame(retainedSurface ? [...retainedSurface.position] : [0, hostPose.position[1], 0], retainedSurface ? [...(retainedSurface.rotation ?? [0, 0, 0])] : [0, 0, 0]));
280
+ const normal = surfaceFrame.axes[1];
281
+ const localRotation = originalPlacement?.surfaceLocal?.rotation ?? hostPose.rotation;
282
+ const bottomOffset = retainedSurface
283
+ ? -Math.min(...boxCorners(localBounds.min, localBounds.max).map((p) => transformPoint(frame([0, 0, 0], [...localRotation]), p)[1]))
284
+ : 0;
285
+ const y = surfaceFrame.position[1] +
286
+ (bottomOffset -
287
+ normal[0] * (snapped[0] - surfaceFrame.position[0]) -
288
+ normal[2] * (snapped[1] - surfaceFrame.position[2])) /
289
+ normal[1];
290
+ const delta = [
291
+ snapped[0] - hostFrame.position[0],
292
+ y - hostFrame.position[1],
293
+ snapped[1] - hostFrame.position[2],
294
+ ];
295
+ const local = hostFrame.axes.map((axis) => axis.reduce((sum, v, i) => sum + v * delta[i], 0));
296
+ const projected = boundsOf(boxCorners(localBounds?.min ?? [-dimensions[0] / 2, 0, -dimensions[2] / 2], localBounds?.max ?? [dimensions[0] / 2, dimensions[1], dimensions[2] / 2]).map((p) => transformPoint(frame(local, hostPose.rotation), p)));
297
+ let occupied = false;
253
298
  const pose = resolveSurfacePlacement({
254
299
  host,
300
+ surface: retainedSurface,
255
301
  childKind: node.type,
256
- childFootprint: { size: dimensions, rotationY, rotation: node.rotation, localBounds },
302
+ childId: node.id,
303
+ childFootprint: {
304
+ size: dimensions,
305
+ rotationY: hostPose.rotation[1],
306
+ rotation: hostPose.rotation,
307
+ localBounds,
308
+ },
257
309
  hit: {
258
310
  point: [
259
311
  (projected.min[0] + projected.max[0]) / 2,
@@ -264,36 +316,63 @@ function buildFloorItemSession(node, startLevelId, nodes) {
264
316
  },
265
317
  origin: local,
266
318
  scene: createSceneApi(useScene),
319
+ onReject: (reason) => {
320
+ occupied = reason === 'surface-occupied';
321
+ },
267
322
  });
268
- if (pose && Math.abs(pose.position[1] - node.position[1]) < 1e-5) {
269
- lastPatch = { parentId: host.id, position: [...pose.position], supportSlabId: undefined };
323
+ if (occupied) {
324
+ commitBlocked = useInteractionScope.getState().ownedSubtree?.creation.rootId === node.id;
325
+ lastPatch = lastHostedPatch;
326
+ useLiveNodeOverrides.getState().set(node.id, lastPatch);
327
+ markMoved();
328
+ return;
329
+ }
330
+ if (pose &&
331
+ (pose.childFrame === 'surface-local' ? pose.surfaceId === surfaceId : surfaceId === null)) {
332
+ const stored = pose.childFrame === 'surface-local' ? pose.surfaceLocal : pose;
333
+ lastPatch = {
334
+ parentId: host.id,
335
+ position: [...stored.position],
336
+ rotation: [...node.rotation],
337
+ supportSlabId: undefined,
338
+ };
339
+ lastHostedPatch = lastPatch;
270
340
  useLiveNodeOverrides.getState().set(node.id, lastPatch);
271
- useScene.getState().markDirty(node.id);
341
+ markMoved();
272
342
  return;
273
343
  }
274
344
  }
275
- const sourceY = node.position[1];
276
- const nextPosition = [snapped[0], sourceY, snapped[1]];
345
+ const nextPosition = [
346
+ snapped[0],
347
+ hosted ? 0 : node.position[1],
348
+ snapped[1],
349
+ ];
277
350
  lastPatch = {
351
+ ...(node.blockFaceId ? clearFaceHostItemFields(host) : {}),
278
352
  position: nextPosition,
353
+ rotation: levelRotation,
279
354
  // Keep parent as the level we resolved at session-start. If
280
355
  // somehow it's null (e.g. orphaned item), fall back to the
281
356
  // existing parent so we don't write `null` and detach.
282
357
  parentId: startLevelId ?? node.parentId,
283
358
  };
359
+ Object.assign(lastPatch, resolveSupportSlabPatch({ ...node, ...lastPatch }, nodes));
284
360
  useLiveNodeOverrides.getState().set(node.id, lastPatch);
285
- useScene.getState().markDirty(node.id);
361
+ markMoved();
286
362
  },
287
363
  canCommit() {
288
- return lastPatch !== null;
364
+ return lastPatch !== null && !commitBlocked;
289
365
  },
290
366
  commit() {
367
+ if (lastInput)
368
+ session.apply(lastInput);
291
369
  if (!lastPatch)
292
370
  return;
371
+ updateSurfaceNode(node.id, lastPatch);
293
372
  useLiveNodeOverrides.getState().clear(node.id);
294
- useScene.getState().updateNodes([{ id: node.id, data: lastPatch }]);
295
373
  },
296
374
  };
375
+ return session;
297
376
  }
298
377
  /**
299
378
  * Ceiling items reparent to whichever ceiling polygon contains the
@@ -1,4 +1,26 @@
1
1
  import { type FloorplanGeometry, type GeometryContext, type ItemNode } from '@pascal-app/core';
2
+ /**
3
+ * Stage C floor-plan builder for item.
4
+ *
5
+ * Items can be parented to a wall, ceiling, slab, or another item.
6
+ * Position is in the parent's local frame, so we walk the parent chain
7
+ * via `ctx.resolve` to compute the world-space (level-local) transform.
8
+ *
9
+ * Mirrors `getItemFloorplanTransform` from editor/lib/floorplan/items.ts
10
+ * but uses the registry's resolve callback instead of a node map. Logic
11
+ * is identical so visual output matches the legacy.
12
+ *
13
+ * Returns a rotated rectangle of width × depth at the resolved position.
14
+ * Phase 5 follow-up may render `asset.floorPlanUrl` as a custom image
15
+ * overlay when present.
16
+ */
17
+ type Transform = {
18
+ x: number;
19
+ y: number;
20
+ rotation: number;
21
+ };
22
+ export declare function resolveItemTransform(item: ItemNode, ctx: GeometryContext, cache?: Map<`roof_${string}` | `rseg_${string}` | `site_${string}` | `building_${string}` | `level_${string}` | `elevator_${string}` | `unit_${string}` | `zone_${string}` | `block_${string}` | `ceiling_${string}` | `item_${string}` | `procedural-item_${string}` | `column_${string}` | `construction-dimension_${string}` | `wall_${string}` | `slab_${string}` | `fence_${string}` | `structural-grid_${string}` | `imesh_${string}` | `stair_${string}` | `scan_${string}` | `guide_${string}` | `measurement_${string}` | `spawn_${string}` | `shelf_${string}` | `duct-segment_${string}` | `duct-fitting_${string}` | `duct-terminal_${string}` | `hvac-equipment_${string}` | `lineset_${string}` | `liquid-line_${string}` | `pipe-segment_${string}` | `pipe-fitting_${string}` | `pipe-trap_${string}` | `leanto_${string}` | `door_${string}` | `window_${string}` | `cabinet_${string}` | `cabinet-module_${string}` | `sseg_${string}` | `bvent_${string}` | `rvent_${string}` | `tvent_${string}` | `cupola_${string}` | `eyebrow-vent_${string}` | `gutter_${string}` | `chimney_${string}` | `solarpanel_${string}` | `skylight_${string}` | `dormer_${string}` | `downspout_${string}`, Transform | null>): Transform | null;
2
23
  export declare function buildItemContextualDimensions(node: ItemNode, ctx: GeometryContext): FloorplanGeometry | null;
3
24
  export declare function buildItemFloorplan(node: ItemNode, ctx: GeometryContext): FloorplanGeometry | null;
25
+ export {};
4
26
  //# sourceMappingURL=floorplan.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"floorplan.d.ts","sourceRoot":"","sources":["../../src/item/floorplan.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,iBAAiB,EAEtB,KAAK,eAAe,EAIpB,KAAK,QAAQ,EAId,MAAM,kBAAkB,CAAA;AAkLzB,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,QAAQ,EACd,GAAG,EAAE,eAAe,GACnB,iBAAiB,GAAG,IAAI,CA+C1B;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,eAAe,GAAG,iBAAiB,GAAG,IAAI,CAmGjG"}
1
+ {"version":3,"file":"floorplan.d.ts","sourceRoot":"","sources":["../../src/item/floorplan.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,iBAAiB,EAEtB,KAAK,eAAe,EAKpB,KAAK,QAAQ,EAId,MAAM,kBAAkB,CAAA;AAIzB;;;;;;;;;;;;;;GAcG;AACH,KAAK,SAAS,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAA;AA2D3D,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,QAAQ,EACd,GAAG,EAAE,eAAe,EACpB,KAAK,0pCAAyC,GAC7C,SAAS,GAAG,IAAI,CAwKlB;AAED,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,QAAQ,EACd,GAAG,EAAE,eAAe,GACnB,iBAAiB,GAAG,IAAI,CA+C1B;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,eAAe,GAAG,iBAAiB,GAAG,IAAI,CAmGjG"}
@@ -1,4 +1,4 @@
1
- import { getBlockFaceFrame, getRoofWallFaceFrame, getScaledDimensions, roofFacePointToSegment, useLiveTransforms, } from '@pascal-app/core';
1
+ import { getBlockFaceFrame, getEffectiveNode, getRoofWallFaceFrame, getScaledDimensions, roofFacePointToSegment, useLiveTransforms, } from '@pascal-app/core';
2
2
  import { formatLinearMeasurement, readFloorplanMetricNotationOverride } from '@pascal-app/editor';
3
3
  import { restingNodePlanFrame } from '../shared/resting-surface-plan';
4
4
  // Plan-space rotation convention used by the legacy `rotatePlanVector`
@@ -13,10 +13,57 @@ function rotateVec(x, y, angle) {
13
13
  const s = Math.sin(angle);
14
14
  return [x * c + y * s, -x * s + y * c];
15
15
  }
16
- function resolveItemTransform(item, ctx, cache = new Map()) {
16
+ function needsFullAncestorFrame(item, ctx) {
17
+ let id = item.parentId;
18
+ const visited = new Set([item.id]);
19
+ while (id && !visited.has(id)) {
20
+ visited.add(id);
21
+ const parent = ctx.resolve(id);
22
+ if (!parent || parent.type === 'level')
23
+ break;
24
+ if (![
25
+ 'wall',
26
+ 'ceiling',
27
+ 'roof',
28
+ 'roof-segment',
29
+ 'item',
30
+ 'shelf',
31
+ 'cabinet',
32
+ 'cabinet-module',
33
+ 'procedural-item',
34
+ 'block',
35
+ 'slab',
36
+ ].includes(parent.type)) {
37
+ const effective = getEffectiveNode(parent);
38
+ const live = useLiveTransforms.getState().get(parent.id);
39
+ const position = live?.position ?? effective.position;
40
+ const rotation = effective.rotation;
41
+ // Main's level-local fallback already handles identity plugins directly on a level.
42
+ if (parent.type === 'column' ||
43
+ (parent.parentId && ctx.resolve(parent.parentId)?.type !== 'level') ||
44
+ position?.some((v) => v !== 0) ||
45
+ live?.rotation ||
46
+ (Array.isArray(rotation) ? rotation.some((v) => v !== 0) : rotation))
47
+ return true;
48
+ }
49
+ id = parent.parentId;
50
+ }
51
+ return false;
52
+ }
53
+ export function resolveItemTransform(item, ctx, cache = new Map()) {
17
54
  const cached = cache.get(item.id);
18
55
  if (cached !== undefined)
19
56
  return cached;
57
+ if (needsFullAncestorFrame(item, ctx)) {
58
+ const f = restingNodePlanFrame(item, ctx.resolve);
59
+ const result = {
60
+ x: f.position[0],
61
+ y: f.position[2],
62
+ rotation: Math.atan2(f.axes[2][0], f.axes[2][2]),
63
+ };
64
+ cache.set(item.id, result);
65
+ return result;
66
+ }
20
67
  const localRotation = item.rotation[1] ?? 0;
21
68
  let result = null;
22
69
  const parentNode = item.parentId
@@ -75,6 +122,18 @@ function resolveItemTransform(item, ctx, cache = new Map()) {
75
122
  // shelf's own entry follows via the layer's effectiveNode override.
76
123
  const shelf = parentNode;
77
124
  const live = useLiveTransforms.getState().get(shelf.id);
125
+ if (shelf.parentId &&
126
+ ['item', 'shelf', 'cabinet', 'cabinet-module', 'procedural-item'].includes(ctx.resolve(shelf.parentId)?.type ?? '')) {
127
+ const parentT = resolveItemTransform({
128
+ ...shelf,
129
+ position: live?.position ?? shelf.position,
130
+ rotation: [0, live?.rotation ?? shelf.rotation[1], 0],
131
+ }, ctx, cache);
132
+ if (!parentT)
133
+ return null;
134
+ const [x, y] = rotateVec(item.position[0], item.position[2], parentT.rotation);
135
+ return { x: parentT.x + x, y: parentT.y + y, rotation: parentT.rotation + localRotation };
136
+ }
78
137
  const shelfX = live?.position[0] ?? shelf.position[0];
79
138
  const shelfZ = live?.position[2] ?? shelf.position[2];
80
139
  const shelfRotationY = live?.rotation ?? shelf.rotation[1] ?? 0;
@@ -26,7 +26,7 @@ import { type PlacementState } from '@pascal-app/editor';
26
26
  * and dropped from editor.
27
27
  */
28
28
  export declare function getInitialState(node: ItemNode, parent?: AnyNode | undefined): PlacementState;
29
- export declare function MoveItemTool({ node }: {
29
+ export declare function MoveItemTool({ node: source }: {
30
30
  node: ItemNode;
31
31
  }): import("react").JSX.Element;
32
32
  export default MoveItemTool;
@@ -1 +1 @@
1
- {"version":3,"file":"move-tool.d.ts","sourceRoot":"","sources":["../../src/item/move-tool.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,OAAO,EAAkB,KAAK,QAAQ,EAAY,MAAM,kBAAkB,CAAA;AACxF,OAAO,EACL,KAAK,cAAc,EAKpB,MAAM,oBAAoB,CAAA;AAG3B;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,QAAQ,EACd,MAAM,GAAE,OAAO,GAAG,SAEL,GACZ,cAAc,CA4EhB;AAED,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,+BAsDxD;AAED,eAAe,YAAY,CAAA"}
1
+ {"version":3,"file":"move-tool.d.ts","sourceRoot":"","sources":["../../src/item/move-tool.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,OAAO,EAGZ,KAAK,QAAQ,EAEd,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,KAAK,cAAc,EAKpB,MAAM,oBAAoB,CAAA;AAI3B;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,QAAQ,EACd,MAAM,GAAE,OAAO,GAAG,SAEL,GACZ,cAAc,CAiFhB;AAED,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,+BAkEhE;AAED,eAAe,YAAY,CAAA"}
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
  import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
- import { useScene } from '@pascal-app/core';
3
+ import { canHostSurfaceChild, useScene, } from '@pascal-app/core';
4
4
  import { triggerSFX, useDraftNode, useEditor, usePlacementCoordinator, } from '@pascal-app/editor';
5
+ import { useMemo } from 'react';
5
6
  import { Vector3 } from 'three';
6
7
  /**
7
8
  * Phase 5 Stage D — item's registry-driven 3D move affordance.
@@ -78,7 +79,10 @@ export function getInitialState(node, parent = node.parentId
78
79
  // handler — which preserves the grab offset — instead of a fresh `enter()`
79
80
  // that snaps the item's origin under the cursor. Without this the item
80
81
  // teleports the instant it's grabbed.
81
- if (parent?.type === 'item' || parent?.type === 'cabinet' || parent?.type === 'procedural-item') {
82
+ if (parent?.type === 'item' ||
83
+ parent?.type === 'cabinet' ||
84
+ parent?.type === 'procedural-item' ||
85
+ (parent && parent.type !== 'shelf' && canHostSurfaceChild(parent, node.type, node.id))) {
82
86
  return {
83
87
  surface: 'item-surface',
84
88
  wallId: null,
@@ -107,12 +111,16 @@ export function getInitialState(node, parent = node.parentId
107
111
  shelfId: null,
108
112
  };
109
113
  }
110
- export function MoveItemTool({ node }) {
114
+ export function MoveItemTool({ node: source }) {
115
+ const node = useMemo(() => source.metadata?.isNew
116
+ ? (useScene.getState().nodes[source.id] ?? source)
117
+ : source, [source]);
111
118
  const draftNode = useDraftNode();
112
119
  const meta = typeof node.metadata === 'object' && node.metadata !== null
113
120
  ? node.metadata
114
121
  : {};
115
122
  const isNew = !!meta.isNew;
123
+ const isSceneDraft = useMemo(() => isNew && !!useScene.getState().nodes[node.id], [isNew, node.id]);
116
124
  const cursor = usePlacementCoordinator({
117
125
  asset: node.asset,
118
126
  draftNode,
@@ -120,7 +128,7 @@ export function MoveItemTool({ node }) {
120
128
  // items create their draft lazily inside the coordinator).
121
129
  slots: node.slots,
122
130
  // Duplicates start fresh in floor mode; wall/ceiling draft is created lazily by ensureDraft.
123
- initialState: isNew
131
+ initialState: isNew && !isSceneDraft
124
132
  ? {
125
133
  surface: 'floor',
126
134
  wallId: null,
@@ -132,9 +140,9 @@ export function MoveItemTool({ node }) {
132
140
  : getInitialState(node),
133
141
  // Preserve the original item's scale so Y-position calculations use the correct height.
134
142
  defaultScale: isNew ? node.scale : undefined,
135
- preserveDragOffset: true,
143
+ preserveDragOffset: !isSceneDraft,
136
144
  initDraft: (gridPosition) => {
137
- if (isNew) {
145
+ if (isNew && !isSceneDraft) {
138
146
  // Duplicate: floor items get a draft immediately; wall/ceiling
139
147
  // items are created lazily on surface entry.
140
148
  gridPosition.copy(new Vector3(...node.position));
@@ -144,7 +152,7 @@ export function MoveItemTool({ node }) {
144
152
  }
145
153
  else {
146
154
  draftNode.adopt(node);
147
- gridPosition.copy(new Vector3(...node.position));
155
+ gridPosition.copy(new Vector3(...draftNode.current.position));
148
156
  }
149
157
  },
150
158
  onCommitted: () => {
@@ -1 +1 @@
1
- {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/item/renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,QAAQ,EAYd,MAAM,kBAAkB,CAAA;AA6ZzB,eAAO,MAAM,YAAY,GAAI,qBAAqB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,gCA4DnE,CAAA;AA+CD,eAAO,MAAM,WAAW,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,gCAUvD,CAAA;AA4RD,eAAe,YAAY,CAAA"}
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/item/renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,QAAQ,EAYd,MAAM,kBAAkB,CAAA;AA6ZzB,eAAO,MAAM,YAAY,GAAI,qBAAqB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,gCAmEnE,CAAA;AA+CD,eAAO,MAAM,WAAW,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,gCAUvD,CAAA;AA4RD,eAAe,YAAY,CAAA"}
@@ -292,8 +292,14 @@ export const ItemRenderer = ({ node: storeNode }) => {
292
292
  setSettled(true);
293
293
  }, [roomClearPreview, setSettled]);
294
294
  const content = (_jsx("group", { position: node.position, ref: ref, rotation: node.rotation, visible: node.visible, children: roomClearPreview ? (_jsx(ClearPreviewModel, { node: node })) : (_jsxs(_Fragment, { children: [_jsx(ModelWithRetry, { node: node, setSettled: setSettled }, node.asset.src ?? 'no-src'), node.children?.map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId)))] })) }));
295
- if (node.blockFaceId && node.parentId) {
296
- return (_jsx(BlockFaceHostFrame, { blockId: node.parentId, faceId: node.blockFaceId, children: content }));
295
+ // A floor-exit override is still mounted beneath the stored block until commit.
296
+ const faceNode = liveOverrides?.parentId &&
297
+ useScene.getState().nodes[liveOverrides.parentId]?.type === 'level' &&
298
+ useScene.getState().nodes[storeNode.parentId]?.type === 'block'
299
+ ? storeNode
300
+ : node;
301
+ if (faceNode.blockFaceId && faceNode.parentId) {
302
+ return (_jsx(BlockFaceHostFrame, { blockId: faceNode.parentId, faceId: faceNode.blockFaceId, children: content }));
297
303
  }
298
304
  if (!node.roofSegmentId)
299
305
  return content;
@@ -7,6 +7,6 @@ export const LevelRenderer = ({ node }) => {
7
7
  const ref = useRef(null);
8
8
  useRegistry(node.id, node.type, ref);
9
9
  const handlers = useNodeEvents(node, 'level');
10
- return (_jsx("group", { ref: ref, ...handlers, children: node.children.map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId))) }));
10
+ return (_jsx("group", { ref: ref, visible: node.visible !== false, ...handlers, children: node.children.map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, childId))) }));
11
11
  };
12
12
  export default LevelRenderer;
@@ -1 +1 @@
1
- {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/lineset/tool.tsx"],"names":[],"mappings":"AAwFA,QAAA,MAAM,WAAW,0CA4QhB,CAAA;AAgCD,eAAe,WAAW,CAAA"}
1
+ {"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/lineset/tool.tsx"],"names":[],"mappings":"AAwFA,QAAA,MAAM,WAAW,0CA6QhB,CAAA;AAgCD,eAAe,WAAW,CAAA"}
@@ -96,6 +96,7 @@ const LinesetTool = () => {
96
96
  // mitered polyline run.
97
97
  const lineset = LinesetNode.parse({
98
98
  ...linesetDefinition.defaults(),
99
+ ...useEditor.getState().toolDefaults.lineset,
99
100
  name: 'Lineset',
100
101
  path: [start, end],
101
102
  });