@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
@@ -0,0 +1,147 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { emitter, useScene, wallRectangleCorners, } from '@pascal-app/core';
4
+ import { CursorSphere, clearPlacementSurface, DRAFT_LABEL_Y_OFFSET, DraftMeasurementLabel, EDITOR_LAYER, formatLinearMeasurement, isMagneticSnapActive, markToolCancelConsumed, NO_RAYCAST, publishHorizontalConstructionPlane, resolveEventConstructionPlane, snapWallDraftPointDetailed, triggerSFX, useEditor, useInteractionScope, useLinearDisplay, useRegistryToolContext, useWallSnapIndicator, } from '@pascal-app/editor';
5
+ import { getSceneTheme, useViewer } from '@pascal-app/viewer';
6
+ import { useThree } from '@react-three/fiber';
7
+ import { useEffect, useState } from 'react';
8
+ import { DoubleSide } from 'three';
9
+ import { createWallRectangle } from './rectangle-command';
10
+ export default function RectangleWallTool() {
11
+ const { activeLevelId, isCameraDragging } = useRegistryToolContext();
12
+ const canvas = useThree((s) => s.gl.domElement);
13
+ const { isImperial } = useLinearDisplay('m', 2);
14
+ const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark');
15
+ const measurementColor = isDark ? '#ffffff' : '#111111';
16
+ const measurementShadowColor = isDark ? '#111111' : '#ffffff';
17
+ const defaults = useEditor((s) => s.toolDefaults.wall);
18
+ const [draft, setDraft] = useState(null);
19
+ const [message, setMessage] = useState('');
20
+ const [cursor, setCursor] = useState(null);
21
+ const levelHeight = useScene((s) => activeLevelId && s.nodes[activeLevelId]?.type === 'level' ? s.nodes[activeLevelId].height : 3);
22
+ const height = typeof defaults?.height === 'number' ? defaults.height : (levelHeight ?? 3);
23
+ const thickness = typeof defaults?.thickness === 'number' ? defaults.thickness : 0.1;
24
+ useEffect(() => {
25
+ if (!activeLevelId)
26
+ return;
27
+ setDraft(null);
28
+ setMessage('');
29
+ let start = null;
30
+ let end = null;
31
+ let plane = null;
32
+ useInteractionScope.getState().begin({ kind: 'drafting', tool: 'wall' });
33
+ const pointFor = (e) => {
34
+ const result = snapWallDraftPointDetailed({
35
+ point: [e.localPosition[0], e.localPosition[2]],
36
+ walls: Object.values(useScene.getState().nodes).filter((n) => n.type === 'wall' && n.parentId === activeLevelId),
37
+ magnetic: isMagneticSnapActive(),
38
+ });
39
+ useWallSnapIndicator.getState().set(result.snap
40
+ ? {
41
+ x: result.point[0],
42
+ z: result.point[1],
43
+ kind: result.snap,
44
+ wallIds: result.targetWallIds,
45
+ }
46
+ : null);
47
+ return result.point;
48
+ };
49
+ const move = (e) => {
50
+ if (e.nativeEvent.target !== canvas || isCameraDragging())
51
+ return;
52
+ const point = pointFor(e);
53
+ const hoverPlane = plane ?? resolveEventConstructionPlane(e, null);
54
+ if (plane)
55
+ publishHorizontalConstructionPlane(e, plane);
56
+ setCursor([point[0], hoverPlane.localY, point[1]]);
57
+ setMessage('');
58
+ if (!start)
59
+ return;
60
+ // The line draft's tick: once per snapped corner position.
61
+ if (end && (end[0] !== point[0] || end[1] !== point[1]))
62
+ triggerSFX('sfx:grid-snap');
63
+ end = point;
64
+ setDraft({ start, end: point, y: hoverPlane.localY });
65
+ };
66
+ const leave = () => {
67
+ setCursor(null);
68
+ useWallSnapIndicator.getState().clear();
69
+ };
70
+ const cancel = () => {
71
+ if (start)
72
+ markToolCancelConsumed();
73
+ start = null;
74
+ end = null;
75
+ plane = null;
76
+ setDraft(null);
77
+ setMessage('');
78
+ leave();
79
+ clearPlacementSurface();
80
+ };
81
+ const click = (e) => {
82
+ if (e.nativeEvent.target !== canvas || e.nativeEvent.button !== 0 || isCameraDragging())
83
+ return;
84
+ const point = pointFor(e);
85
+ if (!start) {
86
+ start = point;
87
+ end = point;
88
+ plane = resolveEventConstructionPlane(e, null);
89
+ publishHorizontalConstructionPlane(e, plane);
90
+ setCursor([point[0], plane.localY, point[1]]);
91
+ setDraft({ start, end: point, y: plane.localY });
92
+ setMessage('');
93
+ triggerSFX('sfx:structure-build-start');
94
+ return;
95
+ }
96
+ try {
97
+ createWallRectangle(activeLevelId, start, point, useEditor.getState().toolDefaults.wall ?? {}, {
98
+ constructionElevation: plane?.elevation,
99
+ preferredSupportSlabId: plane?.supportSlabId,
100
+ constructionHeight: height,
101
+ });
102
+ cancel();
103
+ triggerSFX('sfx:structure-build');
104
+ }
105
+ catch (error) {
106
+ setMessage(error.message);
107
+ }
108
+ };
109
+ emitter.on('grid:move', move);
110
+ emitter.on('grid:click', click);
111
+ emitter.on('tool:cancel', cancel);
112
+ canvas.addEventListener('pointerleave', leave);
113
+ return () => {
114
+ emitter.off('grid:move', move);
115
+ emitter.off('grid:click', click);
116
+ emitter.off('tool:cancel', cancel);
117
+ canvas.removeEventListener('pointerleave', leave);
118
+ useWallSnapIndicator.getState().clear();
119
+ clearPlacementSurface();
120
+ useInteractionScope.getState().endIf((s) => s.kind === 'drafting' && s.tool === 'wall');
121
+ };
122
+ }, [activeLevelId, height, canvas, isCameraDragging]);
123
+ const corners = draft ? wallRectangleCorners(draft.start, draft.end) : [];
124
+ const unit = isImperial ? 'imperial' : 'metric';
125
+ // Label the two sides meeting at the cursor corner, as the floor plan does.
126
+ const cursorIndex = draft
127
+ ? corners.findIndex(([x, z]) => x === draft.end[0] && z === draft.end[1])
128
+ : -1;
129
+ const sideLabels = draft && cursorIndex >= 0
130
+ ? [cursorIndex + 3, cursorIndex].map((side) => {
131
+ const a = corners[side % 4];
132
+ const b = corners[(side + 1) % 4];
133
+ return {
134
+ label: formatLinearMeasurement(Math.hypot(b[0] - a[0], b[1] - a[1]), unit),
135
+ position: [
136
+ (a[0] + b[0]) / 2,
137
+ draft.y + height + DRAFT_LABEL_Y_OFFSET,
138
+ (a[1] + b[1]) / 2,
139
+ ],
140
+ };
141
+ })
142
+ : [];
143
+ return (_jsxs("group", { children: [_jsx(CursorSphere, { name: "rectangle-wall-cursor", height: height, position: cursor ?? [0, 0, 0], visible: !!cursor }), corners.map((a, i) => {
144
+ const b = corners[(i + 1) % 4];
145
+ return (_jsxs("mesh", { layers: EDITOR_LAYER, raycast: NO_RAYCAST, renderOrder: 1, position: [(a[0] + b[0]) / 2, draft.y + height / 2, (a[1] + b[1]) / 2], rotation: [0, -Math.atan2(b[1] - a[1], b[0] - a[0]), 0], children: [_jsx("boxGeometry", { args: [Math.hypot(b[0] - a[0], b[1] - a[1]), height, thickness] }), _jsx("meshBasicMaterial", { color: message ? '#ef4444' : '#818cf8', depthTest: false, depthWrite: false, opacity: 0.5, side: DoubleSide, transparent: true })] }, i));
146
+ }), draft && message ? (_jsx(DraftMeasurementLabel, { color: "#ef4444", label: message, position: [draft.end[0], draft.y + height + DRAFT_LABEL_Y_OFFSET, draft.end[1]], shadowColor: measurementShadowColor })) : (sideLabels.map((side, index) => (_jsx(DraftMeasurementLabel, { color: measurementColor, label: side.label, position: side.position, shadowColor: measurementShadowColor }, index))))] }));
147
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/wall/renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAOL,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AAsCzB;;;;;;;;;;;;;;;;;;GAkBG;AACH,QAAA,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,gCA4IjD,CAAA;AAED,eAAe,YAAY,CAAA"}
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/wall/renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAOL,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AAuCzB;;;;;;;;;;;;;;;;;;GAkBG;AACH,QAAA,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,gCAyJjD,CAAA;AAED,eAAe,YAAY,CAAA"}
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { hiddenWallPointerEventsHeld, useLiveNodeOverrides, useRegistry, useScene, } from '@pascal-app/core';
4
- import { getVisibleWallMaterials, NodeRenderer, useLibraryMaterialsVersion, useNodeEvents, useViewer, } from '@pascal-app/viewer';
4
+ import { NodeRenderer, SHADOW_ONLY_LAYER, useLibraryMaterialsVersion, useNodeEvents, useViewer, } from '@pascal-app/viewer';
5
5
  import { useEffect, useLayoutEffect, useMemo, useRef } from 'react';
6
6
  import { useShallow } from 'zustand/react/shallow';
7
7
  import { createPlaceholderGeometry } from '../shared/placeholder-geometry';
8
+ import { getCurtainAwareWallMaterials } from './curtain-wall-materials';
8
9
  import { extractWallSelectionRay, WALL_COLLISION_MESH_NAME, wallPointerEventsSuppressed, } from './pointer-transparency';
9
10
  import { createWallRayHitClassifier } from './selection-hit-owner';
10
11
  import { createWallTreatmentSelector, useWallTreatmentLevelData } from './treatment-level-data';
@@ -38,6 +39,7 @@ const WallRenderer = ({ node }) => {
38
39
  const ref = useRef(null);
39
40
  const placeholderGeometry = useMemo(() => createPlaceholderGeometry(3), []);
40
41
  const collisionPlaceholderGeometry = useMemo(() => createPlaceholderGeometry(), []);
42
+ const shadowPlaceholderGeometry = useMemo(() => createPlaceholderGeometry(), []);
41
43
  useRegistry(node.id, 'wall', ref);
42
44
  useLayoutEffect(() => {
43
45
  useScene.getState().markDirty(node.id);
@@ -46,8 +48,9 @@ const WallRenderer = ({ node }) => {
46
48
  return () => {
47
49
  placeholderGeometry.dispose();
48
50
  collisionPlaceholderGeometry.dispose();
51
+ shadowPlaceholderGeometry.dispose();
49
52
  };
50
- }, [collisionPlaceholderGeometry, placeholderGeometry]);
53
+ }, [collisionPlaceholderGeometry, placeholderGeometry, shadowPlaceholderGeometry]);
51
54
  const rawHandlers = useNodeEvents(node, 'wall');
52
55
  // Hidden walls participate in hover/selection NEAREST-FIRST: when the
53
56
  // wall-mode pass hides this wall (`WallCutout` stamps `userData.wallHidden`
@@ -110,7 +113,7 @@ const WallRenderer = ({ node }) => {
110
113
  // register after mount, and a dangling ref cached as the slot default must
111
114
  // re-resolve when they land.
112
115
  const libraryMaterialsVersion = useLibraryMaterialsVersion();
113
- const baseMaterials = getVisibleWallMaterials(node, shading, textures, colorPreset, sceneTheme, sceneMaterials);
116
+ const baseMaterials = getCurtainAwareWallMaterials(treatmentNode, shading, textures, colorPreset, sceneTheme, sceneMaterials).visible;
114
117
  // biome-ignore lint/correctness/useExhaustiveDependencies: libraryMaterialsVersion invalidates the ref resolution inside createWallExtraSlotMaterials
115
118
  const extraMaterials = useMemo(() => createWallExtraSlotMaterials(node, shading, sceneMaterials), [node, sceneMaterials, shading, libraryMaterialsVersion]);
116
119
  useEffect(() => () => {
@@ -119,6 +122,6 @@ const WallRenderer = ({ node }) => {
119
122
  for (const entry of owned)
120
123
  entry.dispose();
121
124
  }, [baseMaterials, extraMaterials]);
122
- return (_jsxs("mesh", { castShadow: true, geometry: placeholderGeometry, material: baseMaterials, receiveShadow: true, ref: ref, visible: node.visible, children: [_jsx("mesh", { geometry: collisionPlaceholderGeometry, name: WALL_COLLISION_MESH_NAME, visible: false, ...handlers }), hasWallTreatments(treatmentNode) && (_jsx(WallTreatmentSubscription, { childrenNodes: childNodes, materials: extraMaterials, node: treatmentNode })), (node.children ?? []).map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, `${node.id}:${childId}`)))] }));
125
+ return (_jsxs("mesh", { castShadow: node.wallType !== 'curtain', geometry: placeholderGeometry, material: baseMaterials, receiveShadow: true, ref: ref, visible: node.visible, children: [_jsx("mesh", { name: "curtain-wall-shadow", userData: { pascalExport: 'strip' }, geometry: shadowPlaceholderGeometry, layers: SHADOW_ONLY_LAYER, castShadow: true, visible: node.wallType === 'curtain', raycast: () => { }, children: _jsx("meshBasicMaterial", {}) }), _jsx("mesh", { geometry: collisionPlaceholderGeometry, name: WALL_COLLISION_MESH_NAME, visible: false, ...handlers }), node.wallType !== 'curtain' && hasWallTreatments(treatmentNode) && (_jsx(WallTreatmentSubscription, { childrenNodes: childNodes, materials: extraMaterials, node: treatmentNode })), (node.children ?? []).map((childId) => (_jsx(NodeRenderer, { nodeId: childId }, `${node.id}:${childId}`)))] }));
123
126
  };
124
127
  export default WallRenderer;
@@ -1,4 +1,4 @@
1
- import { type SlotDeclaration } from '@pascal-app/core';
1
+ import { type SlotDeclaration, type WallNode } from '@pascal-app/core';
2
2
  /**
3
3
  * A wall exposes two paintable faces — interior + exterior. Painting writes
4
4
  * `node.slots[interior|exterior]` via `wallPaint` like every other kind; this
@@ -6,5 +6,5 @@ import { type SlotDeclaration } from '@pascal-app/core';
6
6
  * keeps walls on the same `{ slotId, label, default }` contract. The defaults
7
7
  * come from core so the viewer's material resolver renders the identical value.
8
8
  */
9
- export declare function wallSlots(): SlotDeclaration[];
9
+ export declare function wallSlots(node?: WallNode): SlotDeclaration[];
10
10
  //# sourceMappingURL=slots.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../src/wall/slots.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAA8B,MAAM,kBAAkB,CAAA;AAEnF;;;;;;GAMG;AACH,wBAAgB,SAAS,IAAI,eAAe,EAAE,CA2E7C"}
1
+ {"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../src/wall/slots.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,eAAe,EAEpB,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AAEzB;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,eAAe,EAAE,CAmF5D"}
@@ -1,4 +1,4 @@
1
- import { WALL_SURFACE_SLOT_DEFAULTS } from '@pascal-app/core';
1
+ import { getCurtainWallConfig, WALL_SURFACE_SLOT_DEFAULTS, } from '@pascal-app/core';
2
2
  /**
3
3
  * A wall exposes two paintable faces — interior + exterior. Painting writes
4
4
  * `node.slots[interior|exterior]` via `wallPaint` like every other kind; this
@@ -6,7 +6,15 @@ import { WALL_SURFACE_SLOT_DEFAULTS } from '@pascal-app/core';
6
6
  * keeps walls on the same `{ slotId, label, default }` contract. The defaults
7
7
  * come from core so the viewer's material resolver renders the identical value.
8
8
  */
9
- export function wallSlots() {
9
+ export function wallSlots(node) {
10
+ if (node?.wallType === 'curtain') {
11
+ const config = getCurtainWallConfig(node);
12
+ return [
13
+ { slotId: 'curtain-frame', label: 'Frame', default: config.frameColor },
14
+ { slotId: 'curtain-glass', label: 'Glass', default: config.glassColor },
15
+ { slotId: 'curtain-solid', label: 'Solid panels', default: config.solidColor },
16
+ ];
17
+ }
10
18
  return [
11
19
  { slotId: 'interior', label: 'Interior', default: WALL_SURFACE_SLOT_DEFAULTS.interior },
12
20
  { slotId: 'exterior', label: 'Exterior', default: WALL_SURFACE_SLOT_DEFAULTS.exterior },
@@ -0,0 +1,4 @@
1
+ import { type FloorplanToolContext } from '@pascal-app/editor';
2
+ /** The plan side of the wall's `split` reshape (`reshapeLayers.split`): the same draft as 3D. */
3
+ export default function WallSplitFloorplanLayer(_props: FloorplanToolContext): import("react").JSX.Element | null;
4
+ //# sourceMappingURL=split-floorplan-layer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"split-floorplan-layer.d.ts","sourceRoot":"","sources":["../../src/wall/split-floorplan-layer.tsx"],"names":[],"mappings":"AAQA,OAAO,EAGL,KAAK,oBAAoB,EAG1B,MAAM,oBAAoB,CAAA;AAO3B,iGAAiG;AACjG,MAAM,CAAC,OAAO,UAAU,uBAAuB,CAAC,MAAM,EAAE,oBAAoB,sCAgG3E"}
@@ -0,0 +1,71 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { getWallCurveFrameAt, getWallCurveLength, getWallThickness, useScene, } from '@pascal-app/core';
4
+ import { clientToPlan, FloorplanDraftWallMeasurement, formatLinearMeasurement, useFloorplanRender, } from '@pascal-app/editor';
5
+ import { getSceneTheme, useViewer } from '@pascal-app/viewer';
6
+ import { useEffect } from 'react';
7
+ import { bindWallSplitPointer } from './split-pointer';
8
+ import { wallSplitDistance, wallSplitMarkerColor, wallSplitSegmentLabels } from './split-preview';
9
+ import { useWallSplit } from './split-store';
10
+ /** The plan side of the wall's `split` reshape (`reshapeLayers.split`): the same draft as 3D. */
11
+ export default function WallSplitFloorplanLayer(_props) {
12
+ const draft = useWallSplit((s) => s.draft);
13
+ const wallId = draft?.wallId;
14
+ const wall = useScene((s) => (draft ? s.nodes[draft.wallId] : undefined));
15
+ const levelId = useViewer((s) => s.selection.levelId);
16
+ const unit = useViewer((s) => s.unit);
17
+ const metricNotation = useViewer((s) => s.metricNotation);
18
+ const isDark = useViewer((s) => getSceneTheme(s.sceneTheme).appearance === 'dark');
19
+ const context = useFloorplanRender();
20
+ const upp = context?.unitsPerPixel ?? 0.01;
21
+ useEffect(() => {
22
+ if (!wallId || useScene.getState().nodes[wallId]?.parentId !== levelId)
23
+ return;
24
+ const scene = document.querySelector('g[data-floorplan-scene]');
25
+ const surface = scene?.ownerSVGElement;
26
+ if (!surface)
27
+ return;
28
+ return bindWallSplitPointer(surface, (event) => {
29
+ if (!(event.target instanceof Node) || !surface.contains(event.target))
30
+ return null;
31
+ const point = clientToPlan(event.clientX, event.clientY);
32
+ const current = useScene.getState().nodes[wallId];
33
+ if (!point || current?.type !== 'wall')
34
+ return null;
35
+ const distance = wallSplitDistance(current, point);
36
+ // Only the wall's own stroke is a target, including its endpoint exclusion zones.
37
+ const frame = frameAt(current, distance);
38
+ if (Math.hypot(frame.point.x - point[0], frame.point.y - point[1]) > hitHalfWidth(current, upp))
39
+ return null;
40
+ return distance;
41
+ });
42
+ }, [wallId, levelId, upp]);
43
+ if (!draft || wall?.type !== 'wall' || wall.parentId !== levelId)
44
+ return null;
45
+ const { preview, snap } = draft;
46
+ const half = hitHalfWidth(wall, upp);
47
+ const color = wallSplitMarkerColor(preview.valid);
48
+ const cut = preview.frames[0]?.point;
49
+ return (_jsxs("g", { pointerEvents: "none", "data-testid": "pascal-split-marker-2d", children: [snap?.kind === 'alignment' && cut && (_jsx("line", { stroke: "#818cf8", strokeDasharray: `${4 * upp} ${3 * upp}`, strokeOpacity: 0.8, strokeWidth: upp, x1: snap.anchor[0], x2: cut.x, y1: snap.anchor[1], y2: cut.y })), preview.frames.map(({ point, normal }, index) => {
50
+ const line = {
51
+ x1: point.x - normal.x * half,
52
+ y1: point.y - normal.y * half,
53
+ x2: point.x + normal.x * half,
54
+ y2: point.y + normal.y * half,
55
+ };
56
+ return (_jsxs("g", { children: [_jsx("line", { ...line, stroke: "#f7f3ed", strokeWidth: 7 * upp, strokeLinecap: "round" }), _jsx("line", { ...line, stroke: color, strokeWidth: 3 * upp, strokeLinecap: "round" })] }, index));
57
+ }), !preview.valid && cut ? (_jsx("text", { fill: color, fontSize: 12 * upp, paintOrder: "stroke", stroke: isDark ? '#0f172a' : '#ffffff', strokeWidth: 3 * upp, textAnchor: "middle", x: cut.x, y: cut.y - half - 6 * upp, children: preview.message })) : (wallSplitSegmentLabels(wall, preview).map((segment, index) => (_jsx(FloorplanDraftWallMeasurement, { labelBackground: isDark ? '#0f172a' : '#ffffff', labelText: isDark ? '#e2e8f0' : '#171717', measurement: {
58
+ lengthLabel: `${formatLinearMeasurement(segment.length, unit, metricNotation)}${segment.count > 1 ? ` × ${segment.count}` : ''}`,
59
+ midpoint: segment.midpoint,
60
+ direction: segment.direction,
61
+ angleLabels: [],
62
+ }, measurementStroke: context?.palette.measurementStroke ?? color, sceneRotationDeg: context?.sceneRotationDeg ?? 0, unitsPerPixel: upp }, index))))] }));
63
+ }
64
+ /** The wall's stroke plus a finger's width, so the cut can be grabbed at any zoom. */
65
+ function hitHalfWidth(wall, upp) {
66
+ return getWallThickness(wall) / 2 + 8 * upp;
67
+ }
68
+ function frameAt(wall, distance) {
69
+ const length = getWallCurveLength(wall);
70
+ return getWallCurveFrameAt(wall, length > 0 ? distance / length : 0);
71
+ }
@@ -0,0 +1,3 @@
1
+ /** Both viewports feed a wall distance; neither writes scene nodes while hovering. */
2
+ export declare function bindWallSplitPointer(surface: Element, distanceAt: (event: PointerEvent) => number | null): () => void;
3
+ //# sourceMappingURL=split-pointer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"split-pointer.d.ts","sourceRoot":"","sources":["../../src/wall/split-pointer.ts"],"names":[],"mappings":"AAUA,sFAAsF;AACtF,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,MAAM,GAAG,IAAI,cA2EnD"}
@@ -0,0 +1,95 @@
1
+ import { commitWallSplit, hoverWallSplit, setWallSplitCuts } from './split-session';
2
+ import { useWallSplit } from './split-store';
3
+ // Pixels of continuous wheel travel per cut: a short trackpad swipe.
4
+ const WHEEL_STEP_PX = 60;
5
+ // A wheel event after this pause is a new notch or gesture and steps at once,
6
+ // so each notch of a notched wheel is one cut whatever pixels the OS reports
7
+ // for it; a continuous stream (trackpad, fast spin) steps by travel.
8
+ const WHEEL_GESTURE_GAP_MS = 80;
9
+ /** Both viewports feed a wall distance; neither writes scene nodes while hovering. */
10
+ export function bindWallSplitPointer(surface, distanceAt) {
11
+ let pressed = null;
12
+ let wheelTravel = 0;
13
+ let lastWheelAt = Number.NEGATIVE_INFINITY;
14
+ const move = (event) => {
15
+ if (event.buttons && pressed !== event.pointerId)
16
+ return;
17
+ const distance = distanceAt(event);
18
+ if (distance !== null)
19
+ hoverWallSplit(distance, event.altKey);
20
+ };
21
+ const down = (event) => {
22
+ if (event.button !== 0)
23
+ return;
24
+ const distance = distanceAt(event);
25
+ if (distance === null)
26
+ return;
27
+ pressed = event.pointerId;
28
+ event.preventDefault();
29
+ event.stopImmediatePropagation();
30
+ hoverWallSplit(distance, event.altKey);
31
+ };
32
+ const up = (event) => {
33
+ if (pressed !== event.pointerId)
34
+ return;
35
+ pressed = null;
36
+ event.preventDefault();
37
+ event.stopImmediatePropagation();
38
+ const distance = distanceAt(event);
39
+ if (distance === null)
40
+ return;
41
+ hoverWallSplit(distance, event.altKey);
42
+ // Retain ownership through the click dispatched after pointerup, so normal
43
+ // selection cannot consume that same click after the cut closes its session.
44
+ const swallow = (click) => {
45
+ click.preventDefault();
46
+ click.stopImmediatePropagation();
47
+ };
48
+ surface.addEventListener('click', swallow, { capture: true, once: true });
49
+ setTimeout(() => surface.removeEventListener('click', swallow, true), 0);
50
+ commitWallSplit();
51
+ };
52
+ const cancel = () => {
53
+ pressed = null;
54
+ };
55
+ // Scrolling over the viewport changes the cut count, as in a loop cut;
56
+ // pinch (Ctrl + wheel) still zooms.
57
+ const wheel = (event) => {
58
+ if (event.ctrlKey || !(event.target instanceof Node) || !surface.contains(event.target))
59
+ return;
60
+ event.preventDefault();
61
+ event.stopImmediatePropagation();
62
+ if (!event.deltaY)
63
+ return;
64
+ const fresh = event.timeStamp - lastWheelAt > WHEEL_GESTURE_GAP_MS;
65
+ lastWheelAt = event.timeStamp;
66
+ let steps;
67
+ // Line-mode deltas only come from a notched wheel: one line event, one notch.
68
+ if (fresh || event.deltaMode !== 0) {
69
+ wheelTravel = 0;
70
+ steps = Math.sign(event.deltaY);
71
+ }
72
+ else {
73
+ wheelTravel += event.deltaY;
74
+ steps = Math.trunc(wheelTravel / WHEEL_STEP_PX);
75
+ wheelTravel -= steps * WHEEL_STEP_PX;
76
+ }
77
+ if (!steps)
78
+ return;
79
+ const draft = useWallSplit.getState().draft;
80
+ if (draft)
81
+ setWallSplitCuts(draft.cuts - steps);
82
+ };
83
+ window.addEventListener('pointermove', move, true);
84
+ window.addEventListener('pointerdown', down, true);
85
+ window.addEventListener('pointerup', up, true);
86
+ window.addEventListener('pointercancel', cancel, true);
87
+ window.addEventListener('wheel', wheel, { capture: true, passive: false });
88
+ return () => {
89
+ window.removeEventListener('pointermove', move, true);
90
+ window.removeEventListener('pointerdown', down, true);
91
+ window.removeEventListener('pointerup', up, true);
92
+ window.removeEventListener('pointercancel', cancel, true);
93
+ window.removeEventListener('wheel', wheel, true);
94
+ };
95
+ }
@@ -0,0 +1,55 @@
1
+ import { type AnyNode, type AnyNodeId, type WallNode, type WallPlanPoint } from '@pascal-app/core';
2
+ export declare const WALL_SPLIT_MAX_CUTS = 32;
3
+ /** Cut markers in both views: warm when the cuts can commit, red when they can't. */
4
+ export declare function wallSplitMarkerColor(valid: boolean): "#c86f45" | "#a63d2e";
5
+ /** The pointer projected onto the wall, as a distance from its start; curved walls included. */
6
+ export declare function wallSplitDistance(wall: WallNode, point: readonly [number, number]): number;
7
+ /** Like a loop cut: one cut follows the pointer, more cuts divide the wall evenly. */
8
+ export declare function wallSplitDistances(length: number, cuts: number, distance: number): number[];
9
+ export type WallSplitSnap = {
10
+ kind: 'grid';
11
+ } | {
12
+ kind: 'midpoint';
13
+ } | {
14
+ kind: 'alignment';
15
+ anchor: WallPlanPoint;
16
+ } | null;
17
+ export type WallSplitSnapOptions = {
18
+ /** Grid step while the 'grid' snapping mode is active. */
19
+ gridStep: number | null;
20
+ /** Other wall ends to align with while the 'lines' mode is active. */
21
+ anchors: readonly WallPlanPoint[] | null;
22
+ /** How far (m) the pointer may be from an alignment target to catch it. */
23
+ tolerance: number;
24
+ };
25
+ /**
26
+ * Snaps a single cut along its wall with the active snapping mode: grid steps
27
+ * counted from the wall start, or — in 'lines' — the midpoint and where the
28
+ * level's other wall ends project onto the wall.
29
+ */
30
+ export declare function snapWallSplitDistance(wall: WallNode, raw: number, options: WallSplitSnapOptions): {
31
+ distance: number;
32
+ snap: WallSplitSnap;
33
+ };
34
+ /** Ends of the level's other walls, for 'lines' alignment. */
35
+ export declare function wallSplitAnchors(nodes: Record<AnyNodeId, AnyNode>, wall: WallNode): WallPlanPoint[];
36
+ export declare function wallSplitPreview(nodes: Record<AnyNodeId, AnyNode>, wall: WallNode, distances: readonly number[]): {
37
+ distances: number[];
38
+ length: number;
39
+ frames: {
40
+ point: import("@pascal-app/core").Point2D;
41
+ tangent: import("@pascal-app/core").Point2D;
42
+ normal: import("@pascal-app/core").Point2D;
43
+ }[];
44
+ valid: boolean;
45
+ message: string;
46
+ };
47
+ export type WallSplitPreview = ReturnType<typeof wallSplitPreview>;
48
+ /** What to label: both sides of a single cut, or one length repeated across even cuts. */
49
+ export declare function wallSplitSegmentLabels(wall: WallNode, preview: WallSplitPreview): {
50
+ length: number;
51
+ count: number;
52
+ midpoint: WallPlanPoint;
53
+ direction: WallPlanPoint;
54
+ }[];
55
+ //# sourceMappingURL=split-preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"split-preview.d.ts","sourceRoot":"","sources":["../../src/wall/split-preview.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EAKd,KAAK,QAAQ,EACb,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAA;AAEzB,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC,qFAAqF;AACrF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,yBAElD;AAID,gGAAgG;AAChG,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAuB1F;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAG3F;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GAC5C,IAAI,CAAA;AAER,MAAM,MAAM,oBAAoB,GAAG;IACjC,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,sEAAsE;IACtE,OAAO,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAAA;IACxC,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,QAAQ,EACd,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,oBAAoB,GAC5B;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,aAAa,CAAA;CAAE,CAyB3C;AAED,8DAA8D;AAC9D,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,IAAI,EAAE,QAAQ,GACb,aAAa,EAAE,CAQjB;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,IAAI,EAAE,QAAQ,EACd,SAAS,EAAE,SAAS,MAAM,EAAE;;;;;;;;;;EAqB7B;AAED,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAA;AAElE,0FAA0F;AAC1F,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,gBAAgB;;;cAY1C,aAAa;eACR,aAAa;IAGvD"}
@@ -0,0 +1,106 @@
1
+ import { getWallArcData, getWallCurveFrameAt, getWallCurveLength, planWallDivisions, } from '@pascal-app/core';
2
+ export const WALL_SPLIT_MAX_CUTS = 32;
3
+ /** Cut markers in both views: warm when the cuts can commit, red when they can't. */
4
+ export function wallSplitMarkerColor(valid) {
5
+ return valid ? '#c86f45' : '#a63d2e';
6
+ }
7
+ // The planner refuses cuts closer than this to either end.
8
+ const END_CLEARANCE = 0.05;
9
+ /** The pointer projected onto the wall, as a distance from its start; curved walls included. */
10
+ export function wallSplitDistance(wall, point) {
11
+ const length = getWallCurveLength(wall);
12
+ const arc = getWallArcData(wall);
13
+ if (arc) {
14
+ const angle = Math.atan2(point[1] - arc.center.y, point[0] - arc.center.x);
15
+ const turn = Math.PI * 2;
16
+ const delta = (((arc.direction * (angle - arc.startAngle)) % turn) + turn) % turn;
17
+ if (delta <= Math.abs(arc.delta))
18
+ return (delta / Math.abs(arc.delta)) * length;
19
+ return Math.hypot(point[0] - wall.start[0], point[1] - wall.start[1]) <=
20
+ Math.hypot(point[0] - wall.end[0], point[1] - wall.end[1])
21
+ ? 0
22
+ : length;
23
+ }
24
+ if (length === 0)
25
+ return 0;
26
+ return Math.max(0, Math.min(length, ((point[0] - wall.start[0]) * (wall.end[0] - wall.start[0]) +
27
+ (point[1] - wall.start[1]) * (wall.end[1] - wall.start[1])) /
28
+ length));
29
+ }
30
+ /** Like a loop cut: one cut follows the pointer, more cuts divide the wall evenly. */
31
+ export function wallSplitDistances(length, cuts, distance) {
32
+ if (cuts <= 1)
33
+ return [distance];
34
+ return Array.from({ length: cuts }, (_, index) => (length * (index + 1)) / (cuts + 1));
35
+ }
36
+ /**
37
+ * Snaps a single cut along its wall with the active snapping mode: grid steps
38
+ * counted from the wall start, or — in 'lines' — the midpoint and where the
39
+ * level's other wall ends project onto the wall.
40
+ */
41
+ export function snapWallSplitDistance(wall, raw, options) {
42
+ const length = getWallCurveLength(wall);
43
+ const inside = (distance) => distance >= END_CLEARANCE && distance <= length - END_CLEARANCE;
44
+ if (options.gridStep) {
45
+ const snapped = Math.round(raw / options.gridStep) * options.gridStep;
46
+ return inside(snapped)
47
+ ? { distance: snapped, snap: { kind: 'grid' } }
48
+ : { distance: raw, snap: null };
49
+ }
50
+ if (!options.anchors)
51
+ return { distance: raw, snap: null };
52
+ let best = {
53
+ distance: raw,
54
+ snap: null,
55
+ gap: options.tolerance,
56
+ };
57
+ const consider = (distance, snap) => {
58
+ const gap = Math.abs(distance - raw);
59
+ if (gap < best.gap && inside(distance))
60
+ best = { distance, snap, gap };
61
+ };
62
+ consider(length / 2, { kind: 'midpoint' });
63
+ for (const anchor of options.anchors) {
64
+ consider(wallSplitDistance(wall, anchor), { kind: 'alignment', anchor });
65
+ }
66
+ return { distance: best.distance, snap: best.snap };
67
+ }
68
+ /** Ends of the level's other walls, for 'lines' alignment. */
69
+ export function wallSplitAnchors(nodes, wall) {
70
+ const own = (point) => [wall.start, wall.end].some((end) => end[0] === point[0] && end[1] === point[1]);
71
+ return Object.values(nodes).flatMap((node) => node.type === 'wall' && node.id !== wall.id && node.parentId === wall.parentId
72
+ ? [node.start, node.end].filter((point) => !own(point))
73
+ : []);
74
+ }
75
+ export function wallSplitPreview(nodes, wall, distances) {
76
+ const length = getWallCurveLength(wall);
77
+ const frames = distances.map((distance) => getWallCurveFrameAt(wall, length > 0 ? distance / length : 0));
78
+ let message = '';
79
+ if ([wall, ...Object.values(nodes).filter((n) => n.parentId === wall.id)].some((n) => n.metadata.arrayModifier || n.metadata.linkedArray)) {
80
+ message = 'Make the linked array real before splitting this wall.';
81
+ }
82
+ else {
83
+ try {
84
+ planWallDivisions(nodes, wall.id, distances);
85
+ }
86
+ catch (error) {
87
+ message = error instanceof Error ? error.message : 'This wall cannot be split here.';
88
+ }
89
+ }
90
+ return { distances: [...distances], length, frames, valid: !message, message };
91
+ }
92
+ /** What to label: both sides of a single cut, or one length repeated across even cuts. */
93
+ export function wallSplitSegmentLabels(wall, preview) {
94
+ const bounds = [0, ...preview.distances, preview.length];
95
+ const segments = bounds.slice(1).map((end, index) => [bounds[index], end]);
96
+ const count = preview.distances.length === 1 ? 1 : segments.length;
97
+ return (count === 1 ? segments : segments.slice(0, 1)).map(([from, to]) => {
98
+ const { point, tangent } = getWallCurveFrameAt(wall, preview.length > 0 ? (from + to) / 2 / preview.length : 0);
99
+ return {
100
+ length: to - from,
101
+ count,
102
+ midpoint: [point.x, point.y],
103
+ direction: [tangent.x, tangent.y],
104
+ };
105
+ });
106
+ }
@@ -0,0 +1,16 @@
1
+ import { type WallNode } from '@pascal-app/core';
2
+ /**
3
+ * A split session: the wall's `reshaping` scope (so the snapping chip and the
4
+ * kind's HUD hints resolve), one centred cut, and the watchers that end it —
5
+ * Esc, the wall or its edit rights going away, another interaction, leaving
6
+ * select mode. Both views render the same draft; either can commit.
7
+ */
8
+ export declare function openWallSplit(wall: WallNode): void;
9
+ export declare function closeWallSplit(): void;
10
+ /** Pointer projected onto the wall; Alt (`free`) skips snapping. */
11
+ export declare function hoverWallSplit(raw: number, free?: boolean): void;
12
+ export declare function setWallSplitCuts(cuts: number): void;
13
+ /** Re-plan against the current scene (after an external edit). */
14
+ export declare function refreshWallSplit(): void;
15
+ export declare function commitWallSplit(): void;
16
+ //# sourceMappingURL=split-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"split-session.d.ts","sourceRoot":"","sources":["../../src/wall/split-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AAuCzB;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,QA6C3C;AAED,wBAAgB,cAAc,SAQ7B;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,UAAQ,QAevD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,QAQ5C;AAED,kEAAkE;AAClE,wBAAgB,gBAAgB,SAM/B;AAED,wBAAgB,eAAe,SAY9B"}