@pascal-app/editor 0.9.1 → 0.9.2

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 (262) hide show
  1. package/package.json +12 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +63 -25
  5. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  6. package/src/components/editor/editor-layout-v2.tsx +18 -0
  7. package/src/components/editor/export-manager.tsx +71 -63
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
  10. package/src/components/editor/first-person/build-collider-world.ts +32 -5
  11. package/src/components/editor/first-person-controls.tsx +118 -7
  12. package/src/components/editor/floating-action-menu.tsx +410 -33
  13. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +1804 -849
  17. package/src/components/editor/grid.tsx +166 -39
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +102 -64
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +192 -1
  24. package/src/components/editor/group-transform-shared.ts +240 -7
  25. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  26. package/src/components/editor/handles/use-handle-drag.ts +37 -4
  27. package/src/components/editor/index.tsx +248 -27
  28. package/src/components/editor/measurement-pill.tsx +62 -22
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +366 -173
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +812 -466
  36. package/src/components/editor/site-edge-labels.tsx +4 -4
  37. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  38. package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +136 -43
  41. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  42. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  43. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  44. package/src/components/editor/wall-measurement-label.tsx +3 -13
  45. package/src/components/editor/wall-move-side-handles.tsx +36 -21
  46. package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
  47. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  48. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
  49. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  50. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  51. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  52. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  53. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  54. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
  55. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
  56. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  57. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  58. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  59. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
  60. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  61. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
  62. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  63. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
  64. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  65. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  66. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  67. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  68. package/src/components/systems/zone/zone-system.tsx +42 -13
  69. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  70. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  71. package/src/components/tools/fence/fence-drafting.ts +80 -8
  72. package/src/components/tools/item/move-tool.tsx +17 -13
  73. package/src/components/tools/item/placement-math.test.ts +13 -1
  74. package/src/components/tools/item/placement-math.ts +28 -2
  75. package/src/components/tools/item/placement-strategies.ts +246 -5
  76. package/src/components/tools/item/placement-types.ts +13 -1
  77. package/src/components/tools/item/use-draft-node.ts +31 -1
  78. package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
  79. package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
  80. package/src/components/tools/roof/roof-tool.tsx +320 -94
  81. package/src/components/tools/select/box-select-state.ts +18 -7
  82. package/src/components/tools/select/box-select-tool.tsx +176 -40
  83. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  84. package/src/components/tools/select/marquee-geometry.ts +155 -0
  85. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  86. package/src/components/tools/select/select-candidates.ts +1 -1
  87. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  88. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  89. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  90. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  91. package/src/components/tools/shared/placement-box.tsx +183 -1
  92. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  93. package/src/components/tools/site/site-boundary-editor.tsx +88 -36
  94. package/src/components/tools/stair/stair-defaults.ts +1 -0
  95. package/src/components/tools/stair/stair-tool.tsx +82 -11
  96. package/src/components/tools/tool-manager.tsx +86 -25
  97. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  98. package/src/components/tools/wall/wall-drafting.ts +180 -103
  99. package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
  100. package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
  101. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  102. package/src/components/tools/zone/zone-tool.tsx +82 -88
  103. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  104. package/src/components/ui/action-menu/control-modes.tsx +40 -93
  105. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  106. package/src/components/ui/action-menu/index.tsx +2 -4
  107. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  108. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  109. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  110. package/src/components/ui/command-palette/editor-commands.tsx +2 -2
  111. package/src/components/ui/command-palette/index.tsx +4 -3
  112. package/src/components/ui/controls/material-paint-panel.tsx +86 -17
  113. package/src/components/ui/controls/material-picker.tsx +83 -152
  114. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  115. package/src/components/ui/controls/metric-control.tsx +118 -44
  116. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  117. package/src/components/ui/controls/slider-control.tsx +88 -38
  118. package/src/components/ui/floating-level-selector.tsx +1 -1
  119. package/src/components/ui/helpers/building-helper.tsx +10 -23
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  121. package/src/components/ui/helpers/helper-manager.tsx +227 -14
  122. package/src/components/ui/helpers/item-helper.tsx +28 -32
  123. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  124. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  125. package/src/components/ui/icon-ref.tsx +47 -0
  126. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  127. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/panels/node-display.ts +17 -17
  130. package/src/components/ui/panels/panel-manager.tsx +22 -13
  131. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  132. package/src/components/ui/panels/parametric-inspector.tsx +15 -2
  133. package/src/components/ui/panels/reference-panel.tsx +54 -19
  134. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  135. package/src/components/ui/primitives/sidebar.tsx +1 -0
  136. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  137. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  138. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  139. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
  140. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
  141. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  142. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  144. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  145. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  146. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  147. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  148. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  149. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  150. package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
  151. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  152. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  153. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  154. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  155. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  156. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  157. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  158. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  159. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  160. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  161. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  162. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  163. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  164. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  165. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  166. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  167. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  168. package/src/components/ui/snap-target-badge.tsx +88 -0
  169. package/src/components/viewer-overlay.tsx +85 -10
  170. package/src/components/viewer-zone-system.tsx +6 -1
  171. package/src/hooks/use-auto-save.test.ts +14 -0
  172. package/src/hooks/use-auto-save.ts +54 -7
  173. package/src/hooks/use-ceiling-events.ts +3 -2
  174. package/src/hooks/use-drag-action.ts +4 -1
  175. package/src/hooks/use-keyboard.ts +385 -40
  176. package/src/index.tsx +219 -6
  177. package/src/lib/active-placement-surface.ts +35 -0
  178. package/src/lib/ceiling-plan-snap.ts +24 -0
  179. package/src/lib/contextual-help.test.ts +112 -0
  180. package/src/lib/contextual-help.ts +144 -0
  181. package/src/lib/continuation.ts +64 -0
  182. package/src/lib/direct-manipulation.test.ts +192 -0
  183. package/src/lib/direct-manipulation.ts +109 -0
  184. package/src/lib/editor-api.ts +23 -35
  185. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  186. package/src/lib/floorplan/apply-alignment.ts +18 -12
  187. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  188. package/src/lib/floorplan/geometry.ts +53 -0
  189. package/src/lib/floorplan/index.ts +3 -0
  190. package/src/lib/floorplan/items.ts +5 -2
  191. package/src/lib/floorplan/plan-coords.ts +21 -0
  192. package/src/lib/fresh-planar-placement.test.ts +240 -3
  193. package/src/lib/fresh-planar-placement.ts +68 -1
  194. package/src/lib/glb-export.test.ts +441 -0
  195. package/src/lib/glb-export.ts +874 -0
  196. package/src/lib/history.ts +9 -0
  197. package/src/lib/interaction/hot-set.test.ts +133 -0
  198. package/src/lib/interaction/hot-set.ts +67 -0
  199. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  200. package/src/lib/interaction/overlay-policy.ts +59 -0
  201. package/src/lib/interaction/scope.ts +211 -0
  202. package/src/lib/level-selection.ts +2 -2
  203. package/src/lib/material-paint.ts +36 -49
  204. package/src/lib/measurement-kind.test.ts +30 -0
  205. package/src/lib/measurement-kind.ts +19 -0
  206. package/src/lib/measurement-label.test.ts +47 -0
  207. package/src/lib/measurement-label.ts +68 -0
  208. package/src/lib/measurement-parser.ts +116 -0
  209. package/src/lib/measurements.test.ts +183 -0
  210. package/src/lib/measurements.ts +199 -0
  211. package/src/lib/paint-scope.test.ts +454 -0
  212. package/src/lib/paint-scope.ts +461 -0
  213. package/src/lib/placement-drag-release.ts +23 -0
  214. package/src/lib/planar-cursor-placement.test.ts +57 -0
  215. package/src/lib/plugin-panels.test.ts +19 -0
  216. package/src/lib/plugin-panels.ts +91 -0
  217. package/src/lib/quick-measurement.test.ts +77 -0
  218. package/src/lib/quick-measurement.ts +109 -0
  219. package/src/lib/roof-duplication.ts +6 -6
  220. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  221. package/src/lib/roof-wall-hit.ts +164 -0
  222. package/src/lib/scene-clipboard.test.ts +196 -0
  223. package/src/lib/scene-clipboard.ts +73 -8
  224. package/src/lib/scene.ts +28 -5
  225. package/src/lib/selection-routing.test.ts +298 -0
  226. package/src/lib/selection-routing.ts +175 -0
  227. package/src/lib/sfx/index.ts +1 -0
  228. package/src/lib/sfx/movement-tick.test.ts +65 -0
  229. package/src/lib/sfx/movement-tick.ts +18 -0
  230. package/src/lib/sfx-bus.ts +50 -13
  231. package/src/lib/sfx-player.test.ts +124 -0
  232. package/src/lib/sfx-player.ts +111 -66
  233. package/src/lib/slab-plan-snap.test.ts +93 -0
  234. package/src/lib/slab-plan-snap.ts +108 -0
  235. package/src/lib/snapping-mode.test.ts +138 -0
  236. package/src/lib/snapping-mode.ts +179 -0
  237. package/src/lib/stair-duplication.ts +4 -4
  238. package/src/lib/surface-plan-snap.test.ts +71 -0
  239. package/src/lib/surface-plan-snap.ts +256 -0
  240. package/src/lib/use-linear-display.ts +40 -0
  241. package/src/lib/world-grid-snap.ts +37 -5
  242. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  243. package/src/store/use-editor.tsx +484 -114
  244. package/src/store/use-facing-pose.ts +44 -0
  245. package/src/store/use-fence-curve-draft.ts +21 -0
  246. package/src/store/use-floorplan-draft-preview.ts +101 -0
  247. package/src/store/use-floorplan-marquee.ts +50 -0
  248. package/src/store/use-interaction-scope.test.ts +186 -0
  249. package/src/store/use-interaction-scope.ts +132 -0
  250. package/src/store/use-measurement-draft.test.ts +530 -0
  251. package/src/store/use-measurement-draft.ts +543 -0
  252. package/src/store/use-opening-guides.ts +41 -0
  253. package/src/store/use-placement-preview.ts +11 -3
  254. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  255. package/src/store/use-quick-measurement-hud.ts +77 -0
  256. package/src/store/use-segment-draft-chain.ts +28 -0
  257. package/src/store/use-stair-build-preview.ts +43 -0
  258. package/src/store/use-wall-snap-indicator.ts +2 -0
  259. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  260. package/src/components/editor/group-move-handle.tsx +0 -316
  261. package/src/components/ui/panels/paint-panel.tsx +0 -163
  262. package/src/lib/level-name.ts +0 -11
@@ -0,0 +1,144 @@
1
+ 'use client'
2
+
3
+ import { useViewer } from '@pascal-app/viewer'
4
+ import { Html } from '@react-three/drei'
5
+ import { memo, useEffect, useLayoutEffect, useMemo } from 'react'
6
+ import { BufferGeometry, Float32BufferAttribute, Line as ThreeLine } from 'three'
7
+ import { LineBasicNodeMaterial } from 'three/webgpu'
8
+ import { EDITOR_LAYER } from '../../lib/constants'
9
+ import useOpeningGuides, {
10
+ type OpeningGuide3D,
11
+ type OpeningGuideVec3,
12
+ } from '../../store/use-opening-guides'
13
+ import { formatMeasurement } from './measurement-pill'
14
+
15
+ const DIMENSION_COLOR = 0x81_8c_f8 // indigo — a neutral measurement
16
+ const ALIGN_COLOR = 0xef_44_44 // red — a snapped alignment (matches the 2D guide accent)
17
+ const DIMENSION_PILL = '#6366f1'
18
+ const BADGE_PILL = '#ec4899' // pink — matches the 2D equal-spacing badge
19
+
20
+ // Shared depth-test-off materials so the guides read on top of the wall and
21
+ // don't rebuild GPU buffers as guides churn during a drag.
22
+ const dimensionMaterial = new LineBasicNodeMaterial({
23
+ color: DIMENSION_COLOR,
24
+ depthTest: false,
25
+ depthWrite: false,
26
+ toneMapped: false,
27
+ transparent: true,
28
+ })
29
+ const alignMaterial = new LineBasicNodeMaterial({
30
+ color: ALIGN_COLOR,
31
+ depthTest: false,
32
+ depthWrite: false,
33
+ toneMapped: false,
34
+ transparent: true,
35
+ })
36
+
37
+ const mid = (a: OpeningGuideVec3, b: OpeningGuideVec3): OpeningGuideVec3 => [
38
+ (a[0] + b[0]) / 2,
39
+ (a[1] + b[1]) / 2,
40
+ (a[2] + b[2]) / 2,
41
+ ]
42
+
43
+ /**
44
+ * Wall-plane proximity / alignment guides for the 3D editor — the spatial twin
45
+ * of the floor-plan placement dimensions + equal-spacing badges. Subscribes to
46
+ * `useOpeningGuides` (published by the door/window move, placement, and resize
47
+ * interactions each drag tick) and draws sill/head + edge-proximity dimensions, a sill-alignment line, and
48
+ * equal-spacing badges. Coordinates are already in the move tool's render frame
49
+ * (the producer reuses the cursor's `wallLocalToWorld`, so they share the cursor's
50
+ * building-local frame), so this layer mounts beside `Alignment3DGuideLayer` and
51
+ * renders them as-is.
52
+ */
53
+ export const OpeningGuides3DLayer = memo(function OpeningGuides3DLayer() {
54
+ const guides = useOpeningGuides((s) => s.guides)
55
+ const unit = useViewer((s) => s.unit)
56
+ if (guides.length === 0) return null
57
+ return (
58
+ <>
59
+ {guides.map((guide) => (
60
+ <OpeningGuide guide={guide} key={guide.id} unit={unit} />
61
+ ))}
62
+ </>
63
+ )
64
+ })
65
+
66
+ function OpeningGuide({ guide, unit }: { guide: OpeningGuide3D; unit: 'metric' | 'imperial' }) {
67
+ if (guide.kind === 'badge') {
68
+ return (
69
+ <Html
70
+ center
71
+ position={guide.at}
72
+ style={{ pointerEvents: 'none', userSelect: 'none' }}
73
+ zIndexRange={[20, 0]}
74
+ >
75
+ <div
76
+ className="whitespace-nowrap rounded-[3px] px-[5px] py-[2px] font-sans font-semibold text-[11px] text-white"
77
+ style={{ backgroundColor: BADGE_PILL }}
78
+ >
79
+ {`= ${formatMeasurement(guide.value, unit)}`}
80
+ </div>
81
+ </Html>
82
+ )
83
+ }
84
+
85
+ const material = guide.kind === 'align-line' ? alignMaterial : dimensionMaterial
86
+ return (
87
+ <>
88
+ <GuideSegment from={guide.from} material={material} to={guide.to} />
89
+ {guide.kind === 'dimension' ? (
90
+ <Html
91
+ center
92
+ position={mid(guide.from, guide.to)}
93
+ style={{ pointerEvents: 'none', userSelect: 'none' }}
94
+ zIndexRange={[20, 0]}
95
+ >
96
+ <div
97
+ className="whitespace-nowrap rounded-[3px] px-[5px] py-[2px] font-medium font-sans text-[11px] text-white"
98
+ style={{ backgroundColor: DIMENSION_PILL }}
99
+ >
100
+ {formatMeasurement(guide.value, unit)}
101
+ </div>
102
+ </Html>
103
+ ) : null}
104
+ </>
105
+ )
106
+ }
107
+
108
+ function GuideSegment({
109
+ from,
110
+ to,
111
+ material,
112
+ }: {
113
+ from: OpeningGuideVec3
114
+ to: OpeningGuideVec3
115
+ material: LineBasicNodeMaterial
116
+ }) {
117
+ // Build the THREE.Line once with a preallocated 2-point position buffer and
118
+ // mount it via <primitive> (the intrinsic <line> JSX element collides with
119
+ // React's SVG <line>). `material` is a module-level constant, so this memo
120
+ // runs exactly once per mounted slot; subsequent drag ticks mutate the
121
+ // existing buffer in place via the layout effect below rather than rebuilding
122
+ // the geometry, line, and GPU buffer every frame.
123
+ const { line, position } = useMemo(() => {
124
+ const position = new Float32BufferAttribute(new Float32Array(6), 3)
125
+ const geometry = new BufferGeometry()
126
+ geometry.setAttribute('position', position)
127
+ const line = new ThreeLine(geometry, material)
128
+ line.frustumCulled = false
129
+ line.layers.set(EDITOR_LAYER)
130
+ line.renderOrder = 1000
131
+ return { line, position }
132
+ }, [material])
133
+
134
+ const [fx, fy, fz] = from
135
+ const [tx, ty, tz] = to
136
+ useLayoutEffect(() => {
137
+ position.setXYZ(0, fx, fy, fz)
138
+ position.setXYZ(1, tx, ty, tz)
139
+ position.needsUpdate = true
140
+ }, [position, fx, fy, fz, tx, ty, tz])
141
+
142
+ useEffect(() => () => line.geometry.dispose(), [line])
143
+ return <primitive object={line} />
144
+ }
@@ -0,0 +1,70 @@
1
+ 'use client'
2
+
3
+ import type { QuickMeasurementMetric, QuickMeasurementReport } from '@pascal-app/core'
4
+ import { Crosshair, MapPin } from 'lucide-react'
5
+ import { memo } from 'react'
6
+ import { formatAreaLabel, formatLinearMeasurement, formatVolumeLabel } from '../../lib/measurements'
7
+
8
+ function formatMetric(metric: QuickMeasurementMetric, unit: 'metric' | 'imperial'): string {
9
+ if (metric.quantity === 'area') return formatAreaLabel(metric.value, unit, 2)
10
+ if (metric.quantity === 'volume') return formatVolumeLabel(metric.value, unit, 2)
11
+ return formatLinearMeasurement(metric.value, unit)
12
+ }
13
+
14
+ export const QuickMeasurementCard = memo(function QuickMeasurementCard({
15
+ report,
16
+ unit,
17
+ lensState,
18
+ }: {
19
+ report: QuickMeasurementReport
20
+ unit: 'metric' | 'imperial'
21
+ lensState: 'live' | 'pinned'
22
+ }) {
23
+ const pinned = lensState === 'pinned'
24
+
25
+ return (
26
+ <div
27
+ aria-label={`${pinned ? 'Pinned' : 'Live'} ${report.kindLabel.toLowerCase()} measurements`}
28
+ className="w-full min-w-0 overflow-hidden rounded-lg border border-border/45 bg-background/96 text-foreground shadow-elevation-3 backdrop-blur-xl"
29
+ data-quick-measure-card
30
+ data-quick-measure-state={lensState}
31
+ role="status"
32
+ >
33
+ <div className="flex items-center gap-2 border-border/60 border-b px-3 py-2">
34
+ {pinned ? (
35
+ <MapPin aria-hidden="true" className="h-3.5 w-3.5 shrink-0 text-foreground" />
36
+ ) : (
37
+ <Crosshair aria-hidden="true" className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
38
+ )}
39
+ <div className="min-w-0">
40
+ <div className="truncate font-medium text-xs leading-tight">{report.title}</div>
41
+ <div className="text-[10px] text-muted-foreground leading-tight">{report.kindLabel}</div>
42
+ </div>
43
+ <span className="ml-auto shrink-0 rounded-full bg-muted px-2 py-0.5 font-medium text-[10px] text-muted-foreground">
44
+ {pinned ? 'Pinned' : 'Live lens'}
45
+ </span>
46
+ </div>
47
+
48
+ <div className="grid grid-cols-[repeat(auto-fit,minmax(6.5rem,1fr))] gap-1.5 p-2">
49
+ {report.metrics.map((metric) => (
50
+ <div className="min-w-0 rounded-md bg-muted/60 px-2.5 py-1.5" key={metric.key}>
51
+ <div className="flex items-baseline gap-1 text-[10px] text-muted-foreground">
52
+ <span className="font-medium text-foreground/80">{metric.abbreviation}</span>
53
+ <span className="truncate">{metric.label}</span>
54
+ </div>
55
+ <div className="mt-0.5 truncate font-mono font-medium text-xs tabular-nums">
56
+ {formatMetric(metric, unit)}
57
+ </div>
58
+ </div>
59
+ ))}
60
+ </div>
61
+
62
+ <div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 border-border/60 border-t px-3 py-1.5 text-[10px] text-muted-foreground leading-tight">
63
+ {report.note ? <span className="min-w-48 flex-1">{report.note}</span> : <span />}
64
+ <span className="ml-auto shrink-0 text-foreground/70">
65
+ {pinned ? 'Click another surface to replace' : 'Click surface to pin'}
66
+ </span>
67
+ </div>
68
+ </div>
69
+ )
70
+ })
@@ -0,0 +1,28 @@
1
+ 'use client'
2
+
3
+ import { useViewer } from '@pascal-app/viewer'
4
+ import useEditor from '../../store/use-editor'
5
+ import {
6
+ selectQuickMeasurementHudEntry,
7
+ useQuickMeasurementHud,
8
+ } from '../../store/use-quick-measurement-hud'
9
+ import { QuickMeasurementCard } from './quick-measurement-card'
10
+
11
+ export function QuickMeasurementHud() {
12
+ const viewMode = useEditor((state) => state.viewMode)
13
+ const entry = useQuickMeasurementHud((state) => selectQuickMeasurementHudEntry(state, viewMode))
14
+ const unit = useViewer((state) => state.unit)
15
+
16
+ if (!entry) return null
17
+
18
+ return (
19
+ <div
20
+ className="pointer-events-none absolute inset-x-3 top-3 z-40 flex justify-center"
21
+ data-quick-measure-hud
22
+ >
23
+ <div className="w-full max-w-[34rem]">
24
+ <QuickMeasurementCard lensState={entry.lensState} report={entry.report} unit={unit} />
25
+ </div>
26
+ </div>
27
+ )
28
+ }
@@ -0,0 +1,137 @@
1
+ 'use client'
2
+
3
+ import { type AnyNodeId, buildRiserDiagram, useScene } from '@pascal-app/core'
4
+ import { useViewer } from '@pascal-app/viewer'
5
+ import { X } from 'lucide-react'
6
+ import { useMemo } from 'react'
7
+ import useEditor from '../../store/use-editor'
8
+
9
+ const WASTE_COLOR = '#0ea5e9'
10
+ const VENT_COLOR = '#a855f7'
11
+ const MARKER_COLOR = '#1e293b'
12
+ const PADDING = 32
13
+ /** Meters → SVG units. The iso projection is in meters; scale up so a
14
+ * typical house drain (a few meters) fills the panel. */
15
+ const SCALE = 90
16
+
17
+ /**
18
+ * DWV riser diagram — the plumbing isometric drawn from the scene's
19
+ * drain/waste/vent nodes. Read-only; toggled from the view controls.
20
+ * Vertical stacks read vertical, sloped drains lean at 30°, with size +
21
+ * vent-termination annotations, matching the permit-drawing convention.
22
+ * Clicking a line/marker selects its node in 3D.
23
+ */
24
+ export function RiserDiagramPanel() {
25
+ const isOpen = useEditor((s) => s.isRiserOpen)
26
+ // Only the open flag lives here. The whole-scene subscription that drives
27
+ // the diagram lives in the child, mounted only while the panel is open —
28
+ // so a closed panel doesn't re-render on every scene mutation.
29
+ if (!isOpen) return null
30
+ return <RiserDiagramContent />
31
+ }
32
+
33
+ function RiserDiagramContent() {
34
+ const setRiserOpen = useEditor((s) => s.setRiserOpen)
35
+ const nodes = useScene((s) => s.nodes)
36
+ const selectedIds = useViewer((s) => s.selection.selectedIds)
37
+
38
+ const diagram = useMemo(() => buildRiserDiagram(nodes), [nodes])
39
+
40
+ const select = (nodeId: AnyNodeId) => useViewer.getState().setSelection({ selectedIds: [nodeId] })
41
+
42
+ const width = diagram ? (diagram.bounds.maxX - diagram.bounds.minX) * SCALE + PADDING * 2 : 320
43
+ const height = diagram ? (diagram.bounds.maxY - diagram.bounds.minY) * SCALE + PADDING * 2 : 200
44
+ const tx = diagram ? -diagram.bounds.minX * SCALE + PADDING : 0
45
+ const ty = diagram ? -diagram.bounds.minY * SCALE + PADDING : 0
46
+
47
+ return (
48
+ <div className="dark pointer-events-auto absolute top-4 right-4 z-30 flex max-h-[80vh] w-[26rem] flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 text-foreground shadow-lg backdrop-blur-xl">
49
+ <div className="flex items-center justify-between border-border/40 border-b px-4 py-2.5">
50
+ <div className="flex flex-col">
51
+ <span className="font-medium text-sm">Riser Diagram</span>
52
+ <span className="text-muted-foreground text-xs">DWV plumbing isometric</span>
53
+ </div>
54
+ <button
55
+ className="flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-white/10"
56
+ onClick={() => setRiserOpen(false)}
57
+ >
58
+ <X className="h-4 w-4 text-muted-foreground" />
59
+ </button>
60
+ </div>
61
+
62
+ <div className="flex items-center gap-3 border-border/40 border-b px-4 py-2 text-xs">
63
+ <span className="flex items-center gap-1.5">
64
+ <span className="h-0.5 w-4" style={{ background: WASTE_COLOR }} /> Waste
65
+ </span>
66
+ <span className="flex items-center gap-1.5">
67
+ <span className="h-0 w-4 border-t-2 border-dashed" style={{ borderColor: VENT_COLOR }} />{' '}
68
+ Vent
69
+ </span>
70
+ </div>
71
+
72
+ <div className="overflow-auto p-2">
73
+ {diagram ? (
74
+ <svg
75
+ height={Math.max(height, 120)}
76
+ role="img"
77
+ aria-label="DWV riser diagram"
78
+ viewBox={`0 0 ${Math.max(width, 200)} ${Math.max(height, 120)}`}
79
+ width="100%"
80
+ >
81
+ <g transform={`translate(${tx}, ${ty})`}>
82
+ {diagram.lines.map((line, i) => {
83
+ const isSel = selectedIds.includes(line.nodeId)
84
+ const color = line.system === 'waste' ? WASTE_COLOR : VENT_COLOR
85
+ return (
86
+ <g key={`${line.nodeId}-${i}`}>
87
+ <line
88
+ className="cursor-pointer"
89
+ onClick={() => select(line.nodeId)}
90
+ stroke={color}
91
+ strokeDasharray={line.system === 'vent' ? '5 4' : undefined}
92
+ strokeLinecap="round"
93
+ strokeWidth={(line.vertical ? 3.5 : 2.5) + (isSel ? 2 : 0)}
94
+ x1={line.from[0] * SCALE}
95
+ x2={line.to[0] * SCALE}
96
+ y1={line.from[1] * SCALE}
97
+ y2={line.to[1] * SCALE}
98
+ />
99
+ <text
100
+ fill={color}
101
+ fontSize={9}
102
+ x={((line.from[0] + line.to[0]) / 2) * SCALE + 4}
103
+ y={((line.from[1] + line.to[1]) / 2) * SCALE - 3}
104
+ >
105
+ {line.diameter}"
106
+ </text>
107
+ </g>
108
+ )
109
+ })}
110
+ {diagram.markers.map((marker, i) => (
111
+ <g
112
+ className="cursor-pointer"
113
+ key={`${marker.nodeId}-${i}`}
114
+ onClick={() => select(marker.nodeId)}
115
+ transform={`translate(${marker.point[0] * SCALE}, ${marker.point[1] * SCALE})`}
116
+ >
117
+ {marker.kind === 'vent-termination' ? (
118
+ <path d="M -5 0 L 0 -7 L 5 0" fill="none" stroke={VENT_COLOR} strokeWidth={2} />
119
+ ) : (
120
+ <circle fill={MARKER_COLOR} r={3} stroke={MARKER_COLOR} strokeWidth={1.5} />
121
+ )}
122
+ <text fill={MARKER_COLOR} fontSize={9} x={8} y={3}>
123
+ {marker.label}
124
+ </text>
125
+ </g>
126
+ ))}
127
+ </g>
128
+ </svg>
129
+ ) : (
130
+ <div className="flex h-32 items-center justify-center px-6 text-center text-muted-foreground text-sm">
131
+ No drain, waste, or vent pipes yet. Draw plumbing to see the riser diagram.
132
+ </div>
133
+ )}
134
+ </div>
135
+ </div>
136
+ )
137
+ }