@pascal-app/editor 1.0.1 → 1.0.4

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 (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,400 @@
1
+ /**
2
+ * VECTOR EDGES FROM THE VIEWER — the elevation's lines taken from the same
3
+ * meshes the picture is rendered from, with hidden-line removal.
4
+ *
5
+ * A drawing built from a second model of the house (a plugin's section solids)
6
+ * never quite matches the render: its roof sat on the plate plane while the
7
+ * 3D roof stacks a deck and shingles on it, its door was the opening while
8
+ * the render showed the leaf. This is how Revit and Chief Architect avoid
9
+ * that — ONE model, the view's lines derived from it:
10
+ *
11
+ * 1. every visible mesh's FEATURE EDGES (creases over `thresholdDeg`,
12
+ * boundaries — three's EdgesGeometry, cached per geometry), carried
13
+ * to world space, instance by instance for InstancedMeshes;
14
+ * 2. a DEPTH pass of the scene through the capture camera — view-space
15
+ * depth as a float colour, read back once;
16
+ * 3. each edge projected through that camera and SAMPLED along its length
17
+ * against the depth buffer: a point deeper than what the buffer saw is
18
+ * behind something. The visible runs come back as world segments.
19
+ *
20
+ * Orthographic only (an elevation, a section): depth is linear across a face
21
+ * so the interpolated edge depth is exact. Not a boolean operation — an
22
+ * edge test, cheap enough to run at capture time.
23
+ */
24
+ import * as THREE from 'three'
25
+ import { normalView, positionView, vec4 } from 'three/tsl'
26
+ import { MeshBasicNodeMaterial, RenderTarget } from 'three/webgpu'
27
+
28
+ export type VisibleEdges = {
29
+ /** World-space segments, six floats each: ax ay az bx by bz. */
30
+ segments: Float32Array
31
+ count: number
32
+ /** How many candidate edges were tested, and the depth pass' size. */
33
+ tested: number
34
+ width: number
35
+ height: number
36
+ ms: number
37
+ /** Whether the normal pass came back (without it every in-view edge is kept). */
38
+ normals: boolean
39
+ }
40
+
41
+ const edgeCache = new WeakMap<THREE.BufferGeometry, Map<number, Float32Array | null>>()
42
+
43
+ /** The feature edges of a geometry, local space, as pairs of points (cached per threshold; null when the geometry is too heavy). */
44
+ function featureEdgesOf(
45
+ geometry: THREE.BufferGeometry,
46
+ thresholdDeg: number,
47
+ maxTriangles: number,
48
+ ): Float32Array | null {
49
+ const position = geometry.getAttribute('position')
50
+ if (!position) return null
51
+ const triangles = geometry.index ? geometry.index.count / 3 : position.count / 3
52
+ if (triangles > maxTriangles) return null
53
+ let byThreshold = edgeCache.get(geometry)
54
+ if (!byThreshold) {
55
+ byThreshold = new Map()
56
+ edgeCache.set(geometry, byThreshold)
57
+ }
58
+ const cached = byThreshold.get(thresholdDeg)
59
+ if (cached !== undefined) return cached
60
+ let edges: Float32Array | null = null
61
+ try {
62
+ const eg = new THREE.EdgesGeometry(geometry, thresholdDeg)
63
+ const attr = eg.getAttribute('position')
64
+ edges = attr ? new Float32Array(attr.array as ArrayLike<number>) : null
65
+ eg.dispose()
66
+ } catch {
67
+ edges = null
68
+ }
69
+ byThreshold.set(thresholdDeg, edges)
70
+ return edges
71
+ }
72
+
73
+ function isShown(object: THREE.Object3D): boolean {
74
+ let o: THREE.Object3D | null = object
75
+ while (o) {
76
+ if (!o.visible) return false
77
+ o = o.parent
78
+ }
79
+ return true
80
+ }
81
+
82
+ /**
83
+ * Whether a material draws anything. `colorWrite: false` is how a pick-only
84
+ * collider hides on the GPU (a plugin's utility-pole proxy, 0.5 × 10.7 m, once
85
+ * drew a tall rectangle through the elevations, 2026-09-23 — the rule
86
+ * glb-export's `isRenderableMesh` already follows).
87
+ */
88
+ export function materialShows(material: THREE.Material | THREE.Material[]): boolean {
89
+ const list = Array.isArray(material) ? material : [material]
90
+ return list.some(
91
+ (m) =>
92
+ m && m.visible !== false && m.colorWrite !== false && !(m.transparent && m.opacity <= 0.02),
93
+ )
94
+ }
95
+
96
+ /**
97
+ * The shown meshes whose own materials draw nothing — pick proxies, hit
98
+ * boxes — hidden for the length of the buffer passes: under the passes'
99
+ * override material they would write depth and hide the real edges behind
100
+ * them. The returned function shows them again.
101
+ */
102
+ export function hideUndrawnMeshes(scene: THREE.Object3D): () => void {
103
+ const hidden: THREE.Object3D[] = []
104
+ scene.traverse((object) => {
105
+ const mesh = object as THREE.Mesh
106
+ if (!mesh.isMesh || !mesh.visible || materialShows(mesh.material)) return
107
+ mesh.visible = false
108
+ hidden.push(mesh)
109
+ })
110
+ return () => {
111
+ for (const mesh of hidden) mesh.visible = true
112
+ }
113
+ }
114
+
115
+ /**
116
+ * One pass of the scene through `material` into a float target, read back:
117
+ * the depth pass writes view-space depth (metres in front of the camera) in
118
+ * red; the normal pass the view-space normal in RGB.
119
+ */
120
+ async function bufferPass(
121
+ renderer: THREE.WebGLRenderer,
122
+ scene: THREE.Scene,
123
+ camera: THREE.Camera,
124
+ width: number,
125
+ height: number,
126
+ colorNode: unknown,
127
+ ): Promise<{ data: Float32Array; topDown: boolean } | null> {
128
+ const material = new MeshBasicNodeMaterial()
129
+ material.colorNode = colorNode as never
130
+ material.side = THREE.DoubleSide
131
+ material.transparent = false
132
+ material.depthTest = true
133
+ material.depthWrite = true
134
+ const target = new RenderTarget(width, height, {
135
+ type: THREE.FloatType,
136
+ format: THREE.RGBAFormat,
137
+ depthBuffer: true,
138
+ minFilter: THREE.NearestFilter,
139
+ magFilter: THREE.NearestFilter,
140
+ generateMipmaps: false,
141
+ })
142
+ const gl = renderer as unknown as {
143
+ getRenderTarget: () => unknown
144
+ setRenderTarget: (t: unknown) => void
145
+ getClearColor: (c: THREE.Color) => THREE.Color
146
+ getClearAlpha: () => number
147
+ setClearColor: (c: THREE.Color | number, a?: number) => void
148
+ renderAsync: (s: THREE.Scene, c: THREE.Camera) => Promise<void>
149
+ readRenderTargetPixelsAsync: (
150
+ t: unknown,
151
+ x: number,
152
+ y: number,
153
+ w: number,
154
+ h: number,
155
+ ) => Promise<ArrayLike<number>>
156
+ backend?: { device?: unknown }
157
+ }
158
+ const previousTarget = gl.getRenderTarget()
159
+ const previousOverride = scene.overrideMaterial
160
+ const previousClear = gl.getClearColor(new THREE.Color())
161
+ const previousAlpha = gl.getClearAlpha()
162
+ try {
163
+ scene.overrideMaterial = material
164
+ gl.setClearColor(new THREE.Color(0, 0, 0), 0)
165
+ gl.setRenderTarget(target)
166
+ await gl.renderAsync(scene, camera)
167
+ gl.setRenderTarget(null)
168
+ const raw = await gl.readRenderTargetPixelsAsync(target, 0, 0, width, height)
169
+ const data = raw instanceof Float32Array ? raw : new Float32Array(raw)
170
+ if (data.length < width * height * 4) return null
171
+ // WebGPU reads rows top-down; the WebGL fallback bottom-up
172
+ return { data, topDown: Boolean(gl.backend?.device) }
173
+ } catch (error) {
174
+ console.warn('[vector-edges] buffer pass failed', error)
175
+ return null
176
+ } finally {
177
+ scene.overrideMaterial = previousOverride
178
+ gl.setClearColor(previousClear, previousAlpha)
179
+ gl.setRenderTarget(previousTarget)
180
+ target.dispose()
181
+ material.dispose()
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Every drawn mesh's feature edges in world space, six floats per edge —
187
+ * instance by instance for an InstancedMesh. What draws nothing (a hidden
188
+ * mesh, a pick proxy, a layer the camera does not see) has no lines.
189
+ */
190
+ export function candidateEdges(
191
+ scene: THREE.Object3D,
192
+ camera: THREE.Camera,
193
+ thresholdDeg: number,
194
+ maxTriangles: number,
195
+ ): number[] {
196
+ const world: number[] = []
197
+ const a = new THREE.Vector3()
198
+ const b = new THREE.Vector3()
199
+ const instance = new THREE.Matrix4()
200
+ const composed = new THREE.Matrix4()
201
+ scene.traverse((object) => {
202
+ const mesh = object as THREE.Mesh
203
+ if (!mesh.isMesh || !mesh.geometry) return
204
+ if ((mesh as unknown as { isSkinnedMesh?: boolean }).isSkinnedMesh) return
205
+ if (!camera.layers.test(mesh.layers)) return
206
+ if (!isShown(mesh) || !materialShows(mesh.material)) return
207
+ const edges = featureEdgesOf(mesh.geometry, thresholdDeg, maxTriangles)
208
+ if (!edges || edges.length < 6) return
209
+ const instanced = mesh as unknown as THREE.InstancedMesh
210
+ const matrices: THREE.Matrix4[] = []
211
+ if (instanced.isInstancedMesh) {
212
+ for (let i = 0; i < instanced.count; i++) {
213
+ instanced.getMatrixAt(i, instance)
214
+ matrices.push(composed.copy(mesh.matrixWorld).multiply(instance).clone())
215
+ }
216
+ } else {
217
+ matrices.push(mesh.matrixWorld)
218
+ }
219
+ for (const m of matrices) {
220
+ for (let i = 0; i + 5 < edges.length; i += 6) {
221
+ a.set(edges[i] as number, edges[i + 1] as number, edges[i + 2] as number).applyMatrix4(m)
222
+ b.set(edges[i + 3] as number, edges[i + 4] as number, edges[i + 5] as number).applyMatrix4(
223
+ m,
224
+ )
225
+ world.push(a.x, a.y, a.z, b.x, b.y, b.z)
226
+ }
227
+ }
228
+ })
229
+ return world
230
+ }
231
+
232
+ /**
233
+ * The visible feature edges of `scene` through `camera` (orthographic), as
234
+ * world segments. `width` sets the depth pass (a multiple of 16 keeps the
235
+ * WebGPU readback rows unpadded); `height` follows the camera's aspect.
236
+ */
237
+ export async function extractVisibleEdges(
238
+ renderer: THREE.WebGLRenderer,
239
+ scene: THREE.Scene,
240
+ camera: THREE.OrthographicCamera,
241
+ options: { width?: number; thresholdDeg?: number; maxTriangles?: number; bias?: number } = {},
242
+ ): Promise<VisibleEdges | null> {
243
+ const started = performance.now()
244
+ const thresholdDeg = options.thresholdDeg ?? 20
245
+ const maxTriangles = options.maxTriangles ?? 400_000
246
+ const aspect = (camera.right - camera.left) / Math.max(1e-6, camera.top - camera.bottom)
247
+ const width = Math.max(16, Math.round((options.width ?? 2048) / 16) * 16)
248
+ const height = Math.max(16, Math.round(width / aspect / 16) * 16)
249
+ camera.updateMatrixWorld(true)
250
+ scene.updateMatrixWorld(true)
251
+
252
+ const showUndrawn = hideUndrawnMeshes(scene)
253
+ let pass: Awaited<ReturnType<typeof bufferPass>>
254
+ let normalPass: Awaited<ReturnType<typeof bufferPass>> = null
255
+ try {
256
+ pass = await bufferPass(
257
+ renderer,
258
+ scene,
259
+ camera,
260
+ width,
261
+ height,
262
+ vec4(positionView.z.negate(), 0, 0, 1),
263
+ )
264
+ if (pass)
265
+ normalPass = await bufferPass(renderer, scene, camera, width, height, vec4(normalView, 1))
266
+ } finally {
267
+ showUndrawn()
268
+ }
269
+ if (!pass) return null
270
+ const { data: depth, topDown } = pass
271
+ const normals = normalPass?.data ?? null
272
+
273
+ const world = candidateEdges(scene, camera, thresholdDeg, maxTriangles)
274
+
275
+ // ── the visibility test ──────────────────────────────────────────────
276
+ const view = camera.matrixWorldInverse
277
+ const proj = camera.projectionMatrix
278
+ const bias = options.bias ?? 0.008
279
+ const out: number[] = []
280
+ const va = new THREE.Vector3()
281
+ const vb = new THREE.Vector3()
282
+ const pa = new THREE.Vector3()
283
+ const pb = new THREE.Vector3()
284
+ const indexAt = (px: number, py: number): number => {
285
+ const x = Math.floor(px)
286
+ const y = Math.floor(py)
287
+ if (x < 0 || y < 0 || x >= width || y >= height) return -1
288
+ const row = topDown ? y : height - 1 - y
289
+ return (row * width + x) * 4
290
+ }
291
+ const sampleAt = (px: number, py: number): number => {
292
+ const k = indexAt(px, py)
293
+ return k < 0 ? Number.POSITIVE_INFINITY : (depth[k] as number)
294
+ }
295
+ // A line is only a line where the render CHANGES across it: a step in
296
+ // depth (a silhouette, a reveal, the far side of a box) or a crease in
297
+ // the normal (wall meets roof, the corner of a post). A flat face's own
298
+ // triangulation — the CSG walls come with T-junctions, so the edge
299
+ // finder cannot pair those triangles — shows neither and is dropped.
300
+ const CREASE = Math.cos((15 * Math.PI) / 180)
301
+ const ACROSS = 2
302
+ const featureAt = (px: number, py: number, nx: number, ny: number): boolean => {
303
+ const c = sampleAt(px, py)
304
+ const l = sampleAt(px + nx * ACROSS, py + ny * ACROSS)
305
+ const r = sampleAt(px - nx * ACROSS, py - ny * ACROSS)
306
+ // the sky on either side, or off the frame: a silhouette
307
+ if (c === 0 || l === 0 || r === 0) return true
308
+ if (!Number.isFinite(l) || !Number.isFinite(r) || !Number.isFinite(c)) return true
309
+ // a step, not a slope: the second difference is zero on any plane
310
+ if (Math.abs(l + r - 2 * c) > 0.012) return true
311
+ if (!normals) return true
312
+ const kl = indexAt(px + nx * ACROSS, py + ny * ACROSS)
313
+ const kr = indexAt(px - nx * ACROSS, py - ny * ACROSS)
314
+ if (kl < 0 || kr < 0) return true
315
+ // unsigned: a CSG face's triangles may wind either way, and a flipped
316
+ // normal on a flat wall is not a corner
317
+ const dot =
318
+ (normals[kl] as number) * (normals[kr] as number) +
319
+ (normals[kl + 1] as number) * (normals[kr + 1] as number) +
320
+ (normals[kl + 2] as number) * (normals[kr + 2] as number)
321
+ return Math.abs(dot) < CREASE
322
+ }
323
+ const tested = world.length / 6
324
+ for (let i = 0; i + 5 < world.length; i += 6) {
325
+ va.set(world[i] as number, world[i + 1] as number, world[i + 2] as number).applyMatrix4(view)
326
+ vb.set(world[i + 3] as number, world[i + 4] as number, world[i + 5] as number).applyMatrix4(
327
+ view,
328
+ )
329
+ const da = -va.z
330
+ const db = -vb.z
331
+ if (da <= camera.near && db <= camera.near) continue
332
+ pa.copy(va).applyMatrix4(proj)
333
+ pb.copy(vb).applyMatrix4(proj)
334
+ const ax = ((pa.x + 1) / 2) * width
335
+ const ay = ((1 - pa.y) / 2) * height
336
+ const bx = ((pb.x + 1) / 2) * width
337
+ const by = ((1 - pb.y) / 2) * height
338
+ // wholly off the frame: nothing to test
339
+ if (
340
+ (ax < 0 && bx < 0) ||
341
+ (ax >= width && bx >= width) ||
342
+ (ay < 0 && by < 0) ||
343
+ (ay >= height && by >= height)
344
+ ) {
345
+ continue
346
+ }
347
+ const lengthPx = Math.hypot(bx - ax, by - ay)
348
+ const n = Math.min(4096, Math.max(2, Math.ceil(lengthPx)))
349
+ const slope = Math.abs(db - da) / n
350
+ const tolerance = bias + slope * 1.5
351
+ // the screen-space perpendicular, to look across the edge
352
+ const nx = lengthPx > 1e-6 ? -(by - ay) / lengthPx : 1
353
+ const ny = lengthPx > 1e-6 ? (bx - ax) / lengthPx : 0
354
+ let runStart = -1
355
+ let gap = 0
356
+ const emit = (t0: number, t1: number) => {
357
+ if (t1 - t0 <= 0) return
358
+ out.push(
359
+ (world[i] as number) + ((world[i + 3] as number) - (world[i] as number)) * t0,
360
+ (world[i + 1] as number) + ((world[i + 4] as number) - (world[i + 1] as number)) * t0,
361
+ (world[i + 2] as number) + ((world[i + 5] as number) - (world[i + 2] as number)) * t0,
362
+ (world[i] as number) + ((world[i + 3] as number) - (world[i] as number)) * t1,
363
+ (world[i + 1] as number) + ((world[i + 4] as number) - (world[i + 1] as number)) * t1,
364
+ (world[i + 2] as number) + ((world[i + 5] as number) - (world[i + 2] as number)) * t1,
365
+ )
366
+ }
367
+ for (let s = 0; s <= n; s++) {
368
+ const t = s / n
369
+ const px = ax + (bx - ax) * t
370
+ const py = ay + (by - ay) * t
371
+ const d = da + (db - da) * t
372
+ const seen = sampleAt(px, py)
373
+ // the background (nothing drawn) reads 0; off the frame reads Infinity — both leave the edge in view
374
+ const inView = seen === 0 || seen === Number.POSITIVE_INFINITY || d <= seen + tolerance
375
+ const visible = inView && featureAt(px, py, nx, ny)
376
+ if (visible) {
377
+ if (runStart < 0) runStart = t
378
+ gap = 0
379
+ } else if (runStart >= 0) {
380
+ // a single hidden sample inside a run is rasterisation noise, not an occluder
381
+ gap++
382
+ if (gap > 1) {
383
+ emit(runStart, (s - gap) / n)
384
+ runStart = -1
385
+ gap = 0
386
+ }
387
+ }
388
+ }
389
+ if (runStart >= 0) emit(runStart, 1)
390
+ }
391
+ return {
392
+ segments: new Float32Array(out),
393
+ count: out.length / 6,
394
+ tested,
395
+ width,
396
+ height,
397
+ ms: Math.round(performance.now() - started),
398
+ normals: normals !== null,
399
+ }
400
+ }