@pascal-app/editor 1.1.0 → 1.1.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 +9 -15
  2. package/src/components/editor/action-menu-button.tsx +1 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
  4. package/src/components/editor/custom-camera-controls.tsx +11 -11
  5. package/src/components/editor/editor-layout-v2.tsx +1 -1
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +20 -1981
  8. package/src/components/editor/floating-action-menu.tsx +1 -1
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floor-region-controls.tsx +6 -2
  11. package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
  12. package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
  13. package/src/components/editor/floorplan-panel.tsx +26 -22
  14. package/src/components/editor/floorplan-wall-typing-keys.ts +1 -1
  15. package/src/components/editor/follow-pascal.test.tsx +1 -2
  16. package/src/components/editor/follow-pascal.tsx +1 -1
  17. package/src/components/editor/footprint-height-handle.tsx +1 -1
  18. package/src/components/editor/group-actions.ts +1 -2
  19. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  20. package/src/components/editor/group-move-3d.ts +1 -2
  21. package/src/components/editor/group-rotate-handle.tsx +1 -2
  22. package/src/components/editor/handles/use-handle-drag.ts +1 -2
  23. package/src/components/editor/index.tsx +24 -14
  24. package/src/components/editor/inspector-extension-actions.tsx +1 -1
  25. package/src/components/editor/node-arrow-handles.tsx +1 -1
  26. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  27. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  28. package/src/components/editor/room-controls.tsx +7 -4
  29. package/src/components/editor/room-handles-2d.tsx +2 -3
  30. package/src/components/editor/room-handles.tsx +1 -2
  31. package/src/components/editor/room-highlight.tsx +6 -105
  32. package/src/components/editor/room-labels.test.ts +8 -822
  33. package/src/components/editor/room-labels.tsx +40 -1671
  34. package/src/components/editor/room-webgpu-materials.test.ts +1 -1
  35. package/src/components/editor/selection-manager.tsx +5 -3
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
  38. package/src/components/editor/use-floorplan-background-placement.ts +2 -1
  39. package/src/components/editor/views/plugins-view.tsx +1 -1
  40. package/src/components/editor/views/view-bar.tsx +9 -9
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +1 -1
  43. package/src/components/editor/wall-pull-grip.tsx +2 -2
  44. package/src/components/editor-2d/floorplan-group-move.tsx +2 -3
  45. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1 -1
  46. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -1
  47. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +1 -2
  48. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
  49. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
  50. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
  51. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +1 -2
  52. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +8 -10
  53. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +25 -33
  54. package/src/components/systems/roof/roof-edit-system.tsx +4 -3
  55. package/src/components/tools/distribution/distribution-run-tool.tsx +1 -1
  56. package/src/components/tools/item/placement-history.test.tsx +1 -1
  57. package/src/components/tools/item/use-placement-coordinator.tsx +2 -3
  58. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +6 -2
  59. package/src/components/tools/registry/move-registry-node-tool.tsx +1 -2
  60. package/src/components/tools/shared/cursor-sphere.tsx +1 -1
  61. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  62. package/src/components/tools/shared/placement-box.tsx +1 -1
  63. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  64. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  65. package/src/components/ui/action-menu/camera-actions.tsx +2 -2
  66. package/src/components/ui/action-menu/control-modes.tsx +1 -1
  67. package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
  68. package/src/components/ui/action-menu/index.tsx +3 -3
  69. package/src/components/ui/action-menu/measurement-control.tsx +3 -3
  70. package/src/components/ui/action-menu/placement.ts +0 -18
  71. package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
  72. package/src/components/ui/action-menu/view-toggles.tsx +5 -5
  73. package/src/components/ui/controls/finish-swatch.tsx +1 -1
  74. package/src/components/ui/controls/material-paint-panel.tsx +2 -2
  75. package/src/components/ui/controls/scene-material-list.tsx +2 -2
  76. package/src/components/ui/controls/select-control.tsx +1 -1
  77. package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
  78. package/src/components/ui/floating-level-selector.tsx +3 -3
  79. package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
  80. package/src/components/ui/helpers/helper-manager.test.tsx +1 -1
  81. package/src/components/ui/helpers/helper-manager.tsx +1 -1
  82. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  83. package/src/components/ui/node-icon-image.test.tsx +46 -0
  84. package/src/components/ui/node-icon-image.tsx +33 -1
  85. package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
  86. package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
  87. package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
  88. package/src/components/ui/panels/node-display.ts +1 -1
  89. package/src/components/ui/panels/panel-manager.tsx +1 -1
  90. package/src/components/ui/panels/panel-wrapper.tsx +1 -1
  91. package/src/components/ui/panels/parametric-inspector.tsx +1 -1
  92. package/src/components/ui/panels/room-construction-rows.tsx +6 -6
  93. package/src/components/ui/panels/room-name-options.ts +1 -5
  94. package/src/components/ui/panels/room-panel.tsx +6 -8
  95. package/src/components/ui/primitives/color-dot.tsx +2 -2
  96. package/src/components/ui/primitives/opacity-control.tsx +2 -2
  97. package/src/components/ui/primitives/sidebar.tsx +3 -3
  98. package/src/components/ui/right-stack.tsx +1 -1
  99. package/src/components/ui/sidebar/icon-rail.tsx +1 -1
  100. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  101. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  102. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
  103. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
  104. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
  105. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
  106. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  107. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
  108. package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
  109. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
  110. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  111. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
  112. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
  113. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
  114. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
  115. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
  116. package/src/components/ui/sidebar/tab-bar.tsx +1 -1
  117. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
  118. package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
  119. package/src/components/viewer-overlay.tsx +1 -2
  120. package/src/hooks/use-auto-frame.ts +1 -2
  121. package/src/hooks/use-keyboard.ts +1 -2
  122. package/src/hooks/use-selected-room.ts +5 -7
  123. package/src/i18n/floor-footprint-name.ts +1 -1
  124. package/src/i18n/locales/en/messages.po +180 -182
  125. package/src/i18n/locales/es/messages.po +180 -182
  126. package/src/i18n/locales/fr/messages.po +180 -182
  127. package/src/i18n/locales/ja/messages.po +180 -182
  128. package/src/i18n/locales/zh-CN/messages.po +180 -182
  129. package/src/i18n/locales.test.ts +1 -1
  130. package/src/index.tsx +109 -131
  131. package/src/lib/below-grade.test.tsx +4 -4
  132. package/src/lib/camera-facing.test.ts +1 -1
  133. package/src/lib/camera-facing.ts +2 -3
  134. package/src/lib/editor-level-display.ts +1 -1
  135. package/src/lib/floor-footprints.test.ts +2 -2
  136. package/src/lib/floor-opening-draft.ts +1 -1
  137. package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
  138. package/src/lib/floorplan/annotation-visibility.ts +1 -1
  139. package/src/lib/floorplan/floorplan-export.test.ts +1 -1
  140. package/src/lib/floorplan/floorplan-export.tsx +8 -11
  141. package/src/lib/floorplan/floorplan-extension.ts +4 -157
  142. package/src/lib/floorplan/floorplan-mode.ts +2 -1
  143. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
  144. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
  145. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
  146. package/src/lib/floorplan/geometry.ts +2 -112
  147. package/src/lib/floorplan/index.ts +0 -9
  148. package/src/lib/floorplan/items.ts +2 -1
  149. package/src/lib/floorplan/schedules.ts +6 -3
  150. package/src/lib/floorplan/sheet-export.ts +7 -3
  151. package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
  152. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
  153. package/src/lib/follow-pascal-presentation.ts +1 -1
  154. package/src/lib/glb-export.ts +1 -1
  155. package/src/lib/material-paint.ts +1 -1
  156. package/src/lib/measurements.test.ts +2 -1
  157. package/src/lib/mezzanine-selection.test.ts +1 -1
  158. package/src/lib/paint-region-mode.ts +1 -1
  159. package/src/lib/point-ask/session.ts +1 -2
  160. package/src/lib/roof-duplication.test.ts +100 -0
  161. package/src/lib/roof-duplication.ts +30 -5
  162. package/src/lib/room-handle-drag.ts +2 -12
  163. package/src/lib/room-name-catalog.test.ts +2 -1
  164. package/src/lib/room-name-messages.test.ts +2 -1
  165. package/src/lib/room-name-messages.ts +1 -118
  166. package/src/lib/room-name-storage.test.ts +2 -1
  167. package/src/lib/room-selection-commands.ts +2 -2
  168. package/src/lib/room-selection.test.ts +6 -6
  169. package/src/lib/room-transform-session.ts +1 -2
  170. package/src/lib/room-zone-routing.ts +2 -2
  171. package/src/lib/selection-routing.test.ts +0 -28
  172. package/src/lib/selection-routing.ts +1 -40
  173. package/src/lib/sfx-bus.ts +8 -32
  174. package/src/lib/sfx-player.test.ts +72 -1
  175. package/src/lib/sfx-player.ts +49 -337
  176. package/src/lib/viewer-selection.test.ts +38 -10
  177. package/src/lib/wall-pull.test.ts +1 -1
  178. package/src/lib/wall-pull.ts +1 -27
  179. package/src/store/use-editor.tsx +17 -26
  180. package/src/store/use-floorplan-annotation-visibility.ts +5 -5
  181. package/src/store/use-wall-region-handles-selection.ts +1 -2
  182. package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
  183. package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
  184. package/src/components/editor/first-person/build-collider-world.ts +0 -549
  185. package/src/components/editor/first-person/ground-holes.test.ts +0 -128
  186. package/src/components/editor/first-person/ground-holes.ts +0 -130
  187. package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
  188. package/src/components/editor/first-person/mechanism-targets.ts +0 -88
  189. package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
  190. package/src/components/editor/first-person/terrain-collider.ts +0 -212
  191. package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
  192. package/src/components/editor/floorplan-camera-sync.ts +0 -213
  193. package/src/components/editor/localized-html.tsx +0 -22
  194. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
  195. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
  196. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
  197. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
  198. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
  199. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
  200. package/src/components/editor-2d/svg-paths.ts +0 -124
  201. package/src/components/ui/action-menu/action-button.tsx +0 -89
  202. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
  203. package/src/components/ui/primitives/button.tsx +0 -69
  204. package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
  205. package/src/components/ui/primitives/popover.tsx +0 -42
  206. package/src/components/ui/primitives/switch.tsx +0 -29
  207. package/src/components/ui/primitives/tooltip.tsx +0 -57
  208. package/src/components/ui/scene-loader.tsx +0 -73
  209. package/src/components/viewer/floorplan-compass-button.tsx +0 -67
  210. package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
  211. package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
  212. package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
  213. package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
  214. package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
  215. package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
  216. package/src/components/viewer/floorplan-preview.tsx +0 -1065
  217. package/src/components/viewer/render-settings-menu.tsx +0 -294
  218. package/src/components/viewer/sky-control.tsx +0 -710
  219. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
  220. package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
  221. package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
  222. package/src/components/viewer/viewer-controls-bar.tsx +0 -496
  223. package/src/components/viewer/viewer-hover-label.tsx +0 -42
  224. package/src/components/viewer/viewer-rooms.tsx +0 -248
  225. package/src/components/viewer/viewer-scene-header.tsx +0 -286
  226. package/src/components/viewer/viewer-selection-manager.tsx +0 -124
  227. package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
  228. package/src/components/viewer/viewer-stage-modes.ts +0 -49
  229. package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
  230. package/src/components/viewer/viewer-stage.test.tsx +0 -47
  231. package/src/components/viewer/viewer-stage.tsx +0 -221
  232. package/src/components/walkthrough-hud.tsx +0 -193
  233. package/src/hooks/use-mobile.ts +0 -19
  234. package/src/i18n/level-name.ts +0 -39
  235. package/src/i18n/scene-theme-name.ts +0 -16
  236. package/src/lib/below-grade.ts +0 -133
  237. package/src/lib/camera-pose.test.ts +0 -237
  238. package/src/lib/camera-pose.ts +0 -172
  239. package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
  240. package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
  241. package/src/lib/floorplan/floorplan-readonly.ts +0 -95
  242. package/src/lib/floorplan/marks.test.ts +0 -213
  243. package/src/lib/floorplan/marks.ts +0 -238
  244. package/src/lib/measurements.ts +0 -8
  245. package/src/lib/node-display-label.ts +0 -37
  246. package/src/lib/room-assembly-overlay.test.ts +0 -275
  247. package/src/lib/room-assembly-overlay.ts +0 -265
  248. package/src/lib/room-selection.ts +0 -417
  249. package/src/lib/scene-bounds.test.ts +0 -327
  250. package/src/lib/scene-bounds.ts +0 -233
  251. package/src/lib/typing-target.ts +0 -9
  252. package/src/lib/viewer-selection.ts +0 -595
  253. package/src/lib/zone-content.ts +0 -133
  254. package/src/store/camera-pose-store.test.ts +0 -29
  255. package/src/store/camera-pose-store.ts +0 -27
  256. package/src/store/navigation-sync-pose-store.test.ts +0 -34
  257. package/src/store/navigation-sync-pose-store.ts +0 -27
  258. package/src/store/use-audio.test.ts +0 -146
  259. package/src/store/use-audio.tsx +0 -169
  260. package/src/store/use-first-person-hud.ts +0 -40
  261. package/src/store/use-follow-pascal.test.ts +0 -62
  262. package/src/store/use-follow-pascal.ts +0 -53
@@ -1,95 +0,0 @@
1
- import type { AnyNode, AnyNodeId, FloorplanPalette, GeometryContext } from '@pascal-app/core'
2
- import {
3
- createFloorplanContextExtensions,
4
- type FloorplanWallDimensionFaces,
5
- } from './floorplan-extension'
6
-
7
- export type FloorplanViewState = {
8
- automaticDimensions?: boolean
9
- selected: boolean
10
- unit: 'metric' | 'imperial'
11
- metricNotation?: 'meters' | 'millimeters'
12
- purpose?: 'edit' | 'document'
13
- /** Sheet drafting conventions (see `FloorplanContextExtension.drafting`). */
14
- drafting?: boolean
15
- /** Room labels drawn as screen pills (see `FloorplanContextExtension.roomLabelOverlay`). */
16
- roomLabelOverlay?: boolean
17
- wallDimensionFaces?: FloorplanWallDimensionFaces
18
- highlighted: boolean
19
- hovered: boolean
20
- moving: boolean
21
- focusedUnitId?: string
22
- focusedUnitMemberIds?: readonly string[]
23
- palette: FloorplanPalette | undefined
24
- }
25
-
26
- export function buildFloorplanContext(
27
- node: AnyNode,
28
- nodes: Record<string, AnyNode>,
29
- viewState: FloorplanViewState,
30
- levelData?: unknown,
31
- ): GeometryContext {
32
- const resolve = <N = AnyNode>(id: AnyNodeId): N | undefined => nodes[id] as N | undefined
33
- const childIds = (node as { children?: AnyNodeId[] }).children
34
- const children = Array.isArray(childIds)
35
- ? childIds.map((id) => nodes[id]).filter((child): child is AnyNode => child !== undefined)
36
- : []
37
- const parentId = node.parentId as AnyNodeId | null
38
- const parent = parentId ? (nodes[parentId] ?? null) : null
39
- let siblings: AnyNode[] = []
40
- if (parent) {
41
- const parentChildIds = (parent as { children?: AnyNodeId[] }).children
42
- siblings = Array.isArray(parentChildIds)
43
- ? parentChildIds
44
- .filter((id) => id !== node.id)
45
- .map((id) => nodes[id])
46
- .filter((sibling): sibling is AnyNode => sibling?.type === node.type)
47
- : Object.values(nodes).filter(
48
- (candidate) =>
49
- candidate.id !== node.id &&
50
- candidate.type === node.type &&
51
- candidate.parentId === node.parentId,
52
- )
53
- }
54
-
55
- return {
56
- resolve,
57
- children,
58
- siblings,
59
- parent,
60
- levelData,
61
- sceneNodes: nodes,
62
- extensions: createFloorplanContextExtensions({
63
- automaticDimensions: viewState.automaticDimensions,
64
- metricNotation: viewState.metricNotation ?? 'meters',
65
- purpose: viewState.purpose ?? 'edit',
66
- drafting: viewState.drafting === true,
67
- roomLabelOverlay: viewState.roomLabelOverlay === true,
68
- wallDimensionFaces: viewState.wallDimensionFaces,
69
- }),
70
- viewState: viewState.palette
71
- ? {
72
- selected: viewState.selected,
73
- unit: viewState.unit,
74
- highlighted: viewState.highlighted,
75
- hovered: viewState.hovered,
76
- moving: viewState.moving,
77
- focusedUnitId: viewState.focusedUnitId,
78
- focusedUnitMemberIds: viewState.focusedUnitMemberIds,
79
- palette: viewState.palette,
80
- }
81
- : undefined,
82
- }
83
- }
84
-
85
- export function floorplanLayerRank(type: string): number {
86
- switch (type) {
87
- case 'zone':
88
- return 0
89
- case 'slab':
90
- case 'ceiling':
91
- return 1
92
- default:
93
- return 2
94
- }
95
- }
@@ -1,213 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import type { AnyNode, AnyNodeId } from '@pascal-app/core'
3
- import { persistResolvedMarks, resolveMarkDetail, resolveMarks } from './marks'
4
-
5
- /**
6
- * Fixture: a rectangular level, walls running clockwise on screen from the
7
- * north-west corner.
8
- *
9
- * north [0,0] → [6,0] doors d_n1 @1, d_n2 @4
10
- * east [6,0] → [6,4] window w_e1 @2
11
- * south [6,4] → [0,4] door d_s1 @2
12
- * west [0,4] → [0,0] (interior partition, no openings)
13
- */
14
- function fixture(levelOrdinal = 0): Record<string, AnyNode> {
15
- const nodes: Record<string, AnyNode> = {}
16
- const node = (value: Record<string, unknown>) => {
17
- nodes[value.id as string] = {
18
- object: 'node',
19
- parentId: null,
20
- visible: true,
21
- metadata: {},
22
- children: [],
23
- ...value,
24
- } as unknown as AnyNode
25
- }
26
-
27
- node({
28
- id: 'level_1',
29
- type: 'level',
30
- level: levelOrdinal,
31
- children: ['w_n', 'w_e', 'w_s', 'w_w'],
32
- })
33
- node({
34
- id: 'w_n',
35
- type: 'wall',
36
- parentId: 'level_1',
37
- start: [0, 0],
38
- end: [6, 0],
39
- thickness: 0.15,
40
- frontSide: 'exterior',
41
- backSide: 'interior',
42
- children: ['d_n1', 'd_n2'],
43
- })
44
- node({
45
- id: 'w_e',
46
- type: 'wall',
47
- parentId: 'level_1',
48
- start: [6, 0],
49
- end: [6, 4],
50
- thickness: 0.15,
51
- frontSide: 'exterior',
52
- backSide: 'interior',
53
- children: ['win_e1'],
54
- })
55
- node({
56
- id: 'w_s',
57
- type: 'wall',
58
- parentId: 'level_1',
59
- start: [6, 4],
60
- end: [0, 4],
61
- thickness: 0.15,
62
- frontSide: 'exterior',
63
- backSide: 'interior',
64
- children: ['d_s1'],
65
- })
66
- node({
67
- id: 'w_w',
68
- type: 'wall',
69
- parentId: 'level_1',
70
- start: [0, 4],
71
- end: [0, 0],
72
- thickness: 0.1,
73
- frontSide: 'interior',
74
- backSide: 'interior',
75
- children: [],
76
- })
77
-
78
- const door = (id: string, wallId: string, along: number) =>
79
- node({
80
- id,
81
- type: 'door',
82
- parentId: wallId,
83
- wallId,
84
- width: 0.9,
85
- height: 2.1,
86
- position: [along, 1.05, 0],
87
- })
88
- door('d_n1', 'w_n', 1)
89
- door('d_n2', 'w_n', 4)
90
- door('d_s1', 'w_s', 2)
91
- node({
92
- id: 'win_e1',
93
- type: 'window',
94
- parentId: 'w_e',
95
- wallId: 'w_e',
96
- width: 1.2,
97
- height: 1.2,
98
- position: [2, 1.1, 0],
99
- })
100
- return nodes
101
- }
102
-
103
- describe('resolveMarks', () => {
104
- test('doors are D101…, windows W101… on level 0', () => {
105
- const marks = resolveMarks(fixture(), 'level_1' as AnyNodeId)
106
- expect(marks.get('d_n1')).toBe('D101')
107
- expect(marks.get('d_n2')).toBe('D102')
108
- expect(marks.get('d_s1')).toBe('D103')
109
- expect(marks.get('win_e1')).toBe('W101')
110
- })
111
-
112
- test('level ordinal shifts the base by 100', () => {
113
- const marks = resolveMarks(fixture(1), 'level_1' as AnyNodeId)
114
- expect(marks.get('d_n1')).toBe('D201')
115
- expect(marks.get('win_e1')).toBe('W201')
116
- })
117
-
118
- test('openings are ordered clockwise from the north-west, exterior walls first', () => {
119
- const nodes = fixture()
120
- // Move the west partition's opening onto it to prove interior comes last.
121
- ;(nodes.w_w as unknown as { children: string[] }).children = ['d_w1']
122
- nodes.d_w1 = {
123
- object: 'node',
124
- id: 'd_w1',
125
- type: 'door',
126
- parentId: 'w_w',
127
- wallId: 'w_w',
128
- visible: true,
129
- metadata: {},
130
- width: 0.8,
131
- height: 2.1,
132
- position: [2, 1.05, 0],
133
- children: [],
134
- } as unknown as AnyNode
135
- const marks = resolveMarks(nodes, 'level_1' as AnyNodeId)
136
- // North (exterior) first, then the other exterior walls, interior last.
137
- expect(marks.get('d_w1')).toBe('D104')
138
- })
139
-
140
- test('an explicit mark wins and is never reassigned', () => {
141
- const nodes = fixture()
142
- ;(nodes.d_n2 as unknown as { mark: string }).mark = 'D-GARAGE'
143
- const marks = resolveMarks(nodes, 'level_1' as AnyNodeId)
144
- expect(marks.get('d_n2')).toBe('D-GARAGE')
145
- expect(marks.get('d_n1')).toBe('D101')
146
- expect(marks.get('d_s1')).toBe('D102')
147
- })
148
-
149
- test('duplicate explicit marks are reported, not silently renumbered', () => {
150
- const nodes = fixture()
151
- ;(nodes.d_n1 as unknown as { mark: string }).mark = 'D101'
152
- ;(nodes.d_n2 as unknown as { mark: string }).mark = 'D101'
153
- const resolution = resolveMarkDetail(nodes, 'level_1' as AnyNodeId)
154
- expect(resolution.issues).toEqual(['Duplicate door mark D101 (2 instances)'])
155
- })
156
-
157
- test('numbering is stable under insertion once persisted', () => {
158
- const nodes = fixture()
159
- const first = resolveMarkDetail(nodes, 'level_1' as AnyNodeId)
160
- expect([...first.assignments.values()]).toEqual(['D101', 'D102', 'D103', 'W101'])
161
-
162
- // Persist, the way an edit session does the first time it resolves.
163
- const written = persistResolvedMarks(first, (id, data) => {
164
- ;(nodes[id] as unknown as Record<string, unknown>).mark = data.mark
165
- })
166
- expect(written).toBe(4)
167
-
168
- // Insert a door BETWEEN d_n1 and d_n2 on the same wall.
169
- ;(nodes.w_n as unknown as { children: string[] }).children = ['d_n1', 'd_new', 'd_n2']
170
- nodes.d_new = {
171
- object: 'node',
172
- id: 'd_new',
173
- type: 'door',
174
- parentId: 'w_n',
175
- wallId: 'w_n',
176
- visible: true,
177
- metadata: {},
178
- width: 0.8,
179
- height: 2.1,
180
- position: [2.5, 1.05, 0],
181
- children: [],
182
- } as unknown as AnyNode
183
-
184
- const second = resolveMarkDetail(nodes, 'level_1' as AnyNodeId)
185
- // Existing doors keep their marks; the new one takes the next free number.
186
- expect(second.marks.get('d_n1')).toBe('D101')
187
- expect(second.marks.get('d_n2')).toBe('D102')
188
- expect(second.marks.get('d_s1')).toBe('D103')
189
- expect(second.marks.get('d_new')).toBe('D104')
190
- expect([...second.assignments.keys()]).toEqual(['d_new'])
191
- })
192
-
193
- test('WITHOUT persisting, inserting mid-wall renumbers — which is why we persist', () => {
194
- const nodes = fixture()
195
- ;(nodes.w_n as unknown as { children: string[] }).children = ['d_n1', 'd_new', 'd_n2']
196
- nodes.d_new = {
197
- object: 'node',
198
- id: 'd_new',
199
- type: 'door',
200
- parentId: 'w_n',
201
- wallId: 'w_n',
202
- visible: true,
203
- metadata: {},
204
- width: 0.8,
205
- height: 2.1,
206
- position: [2.5, 1.05, 0],
207
- children: [],
208
- } as unknown as AnyNode
209
- const marks = resolveMarks(nodes, 'level_1' as AnyNodeId)
210
- expect(marks.get('d_new')).toBe('D102')
211
- expect(marks.get('d_n2')).toBe('D103')
212
- })
213
- })
@@ -1,238 +0,0 @@
1
- import type {
2
- AnyNode,
3
- AnyNodeId,
4
- DoorNode,
5
- LevelNode,
6
- WallNode,
7
- WindowNode,
8
- } from '@pascal-app/core'
9
-
10
- /**
11
- * Deterministic door / window marks per level — WS3.
12
- *
13
- * `packages/core/src/schema/nodes/door.ts:75-79` promises "the deterministic
14
- * level fallback (101, 102, ...)"; this is the implementation.
15
- *
16
- * Numbering
17
- * ---------
18
- * base = (level.level + 1) * 100 — level 0 → 100, level 1 → 200
19
- * doors = `D${base + n}` → D101, D102 …
20
- * windows= `W${base + n}` → W101, W102 …
21
- *
22
- * Order
23
- * -----
24
- * Openings are visited **clockwise** around the level, starting from the
25
- * north-west-most exterior wall, exterior walls first and interior walls
26
- * after. Clockwise is the ascending-angle sweep of the wall midpoint about
27
- * the level centroid, anchored on the north-west diagonal (plan axes are
28
- * x east, z south — z grows down the screen — so a screen-clockwise sweep
29
- * is increasing atan2(z, x)). Openings on the same wall are ordered along
30
- * the wall direction.
31
- *
32
- * Stability
33
- * ---------
34
- * An explicit `mark` on the node always wins and is never reassigned.
35
- * `resolveMarks` also reports, in `assignments`, the marks it invented for
36
- * nodes that had none — the caller is expected to PERSIST those back onto
37
- * `node.mark` the first time they are resolved in an edit session
38
- * (`persistResolvedMarks`). That is what makes numbering stable under
39
- * insertion: once written, an existing opening keeps its number forever and
40
- * a newly inserted opening only ever takes the next free number. Without
41
- * the write-back, inserting a door in the middle of a wall would silently
42
- * renumber every door after it.
43
- */
44
-
45
- export type OpeningMarkKind = 'door' | 'window'
46
-
47
- export type MarkResolution = {
48
- /** Every door and window on the level → its mark. */
49
- marks: ReadonlyMap<string, string>
50
- /** Only the marks that were invented (node.mark was empty). */
51
- assignments: ReadonlyMap<string, string>
52
- /** Duplicate explicit marks and other reportable defects. */
53
- issues: readonly string[]
54
- }
55
-
56
- export type MarkSceneInput = {
57
- nodes: Readonly<Record<string, AnyNode>>
58
- }
59
-
60
- /**
61
- * Resolve marks for every door and window under `levelId`.
62
- *
63
- * `scene` may be a `{ nodes }` snapshot or the raw node map.
64
- */
65
- export function resolveMarks(
66
- scene: MarkSceneInput | Readonly<Record<string, AnyNode>>,
67
- levelId: AnyNodeId,
68
- ): Map<string, string> {
69
- return new Map(resolveMarkDetail(scene, levelId).marks)
70
- }
71
-
72
- export function resolveMarkDetail(
73
- scene: MarkSceneInput | Readonly<Record<string, AnyNode>>,
74
- levelId: AnyNodeId,
75
- ): MarkResolution {
76
- const nodes = normalizeNodes(scene)
77
- const level = nodes[levelId]
78
- const ordinal = level && level.type === 'level' ? ((level as LevelNode).level ?? 0) : 0
79
- const base = (Math.max(0, ordinal) + 1) * 100
80
-
81
- const openings = orderedOpenings(nodes, levelId)
82
- const marks = new Map<string, string>()
83
- const assignments = new Map<string, string>()
84
- const issues: string[] = []
85
-
86
- for (const kind of ['door', 'window'] as const) {
87
- const prefix = kind === 'door' ? 'D' : 'W'
88
- const list = openings.filter((opening) => opening.type === kind)
89
- const used = new Set<string>()
90
- const explicitOwners = new Map<string, string[]>()
91
-
92
- for (const opening of list) {
93
- const explicit = opening.mark?.trim()
94
- if (!explicit) continue
95
- marks.set(opening.id, explicit)
96
- const normalized = explicit.toLocaleUpperCase()
97
- used.add(normalized)
98
- const owners = explicitOwners.get(normalized)
99
- if (owners) owners.push(opening.id)
100
- else explicitOwners.set(normalized, [opening.id])
101
- }
102
-
103
- let sequence = 1
104
- for (const opening of list) {
105
- if (marks.has(opening.id)) continue
106
- let candidate = `${prefix}${base + sequence}`
107
- while (used.has(candidate.toLocaleUpperCase())) {
108
- sequence++
109
- candidate = `${prefix}${base + sequence}`
110
- }
111
- marks.set(opening.id, candidate)
112
- assignments.set(opening.id, candidate)
113
- used.add(candidate.toLocaleUpperCase())
114
- sequence++
115
- }
116
-
117
- for (const [mark, owners] of explicitOwners) {
118
- if (owners.length > 1) {
119
- issues.push(`Duplicate ${kind} mark ${mark} (${owners.length} instances)`)
120
- }
121
- }
122
- }
123
-
124
- return { marks, assignments, issues }
125
- }
126
-
127
- /**
128
- * Write invented marks back onto the nodes so numbering never drifts.
129
- *
130
- * Call once per level per edit session (the caller decides when — this
131
- * module stays pure). `update` is typically `sceneApi.update`.
132
- */
133
- export function persistResolvedMarks(
134
- resolution: MarkResolution,
135
- update: (id: AnyNodeId, data: Record<string, unknown>) => void,
136
- ): number {
137
- let written = 0
138
- for (const [id, mark] of resolution.assignments) {
139
- update(id as AnyNodeId, { mark })
140
- written++
141
- }
142
- return written
143
- }
144
-
145
- // ── Ordering ─────────────────────────────────────────────────────────
146
-
147
- type Opening = (DoorNode | WindowNode) & { type: 'door' | 'window' }
148
-
149
- /**
150
- * Every door and window under the level, clockwise from the NW-most
151
- * exterior wall, exterior walls first.
152
- */
153
- export function orderedOpenings(
154
- nodes: Readonly<Record<string, AnyNode>>,
155
- levelId: AnyNodeId,
156
- ): Opening[] {
157
- const walls: WallNode[] = []
158
- const orphanOpenings: Opening[] = []
159
-
160
- const visit = (id: string) => {
161
- const node = nodes[id]
162
- if (!node) return
163
- if (node.visible === false) return
164
- if (node.type === 'wall') walls.push(node as WallNode)
165
- else if (node.type === 'door' || node.type === 'window') {
166
- const opening = node as Opening
167
- if (!opening.wallId || !nodes[opening.wallId]) orphanOpenings.push(opening)
168
- }
169
- for (const childId of (node as { children?: string[] }).children ?? []) visit(childId)
170
- }
171
- visit(levelId)
172
-
173
- const centroid = wallCentroid(walls)
174
- const ordered = [...walls].sort((left, right) => {
175
- const leftExterior = isExteriorWall(left) ? 0 : 1
176
- const rightExterior = isExteriorWall(right) ? 0 : 1
177
- if (leftExterior !== rightExterior) return leftExterior - rightExterior
178
- return clockwiseKey(left, centroid) - clockwiseKey(right, centroid)
179
- })
180
-
181
- const result: Opening[] = []
182
- for (const wall of ordered) {
183
- const wallOpenings: Opening[] = []
184
- for (const childId of wall.children ?? []) {
185
- const child = nodes[childId]
186
- if (child && child.visible !== false && (child.type === 'door' || child.type === 'window')) {
187
- wallOpenings.push(child as Opening)
188
- }
189
- }
190
- wallOpenings.sort((left, right) => left.position[0] - right.position[0])
191
- result.push(...wallOpenings)
192
- }
193
- result.push(...orphanOpenings)
194
- return result
195
- }
196
-
197
- function wallCentroid(walls: readonly WallNode[]): [number, number] {
198
- if (walls.length === 0) return [0, 0]
199
- let x = 0
200
- let z = 0
201
- for (const wall of walls) {
202
- x += (wall.start[0] + wall.end[0]) / 2
203
- z += (wall.start[1] + wall.end[1]) / 2
204
- }
205
- return [x / walls.length, z / walls.length]
206
- }
207
-
208
- /**
209
- * Sweep key: 0 at the north-west direction, increasing clockwise on screen.
210
- *
211
- * Plan axes are x east, z south — z grows DOWN the screen — so a clockwise
212
- * screen sweep (up → right → down → left) is the INCREASING atan2(z, x)
213
- * direction. The north-west diagonal sits at atan2(-1, -1) = -3π/4, so
214
- * anchoring there and wrapping into [0, 2π) yields north, east, south, west.
215
- */
216
- const NORTH_WEST_ANGLE = (-3 * Math.PI) / 4
217
-
218
- function clockwiseKey(wall: WallNode, centroid: readonly [number, number]): number {
219
- const midX = (wall.start[0] + wall.end[0]) / 2 - centroid[0]
220
- const midZ = (wall.start[1] + wall.end[1]) / 2 - centroid[1]
221
- let sweep = Math.atan2(midZ, midX) - NORTH_WEST_ANGLE
222
- while (sweep < 0) sweep += Math.PI * 2
223
- while (sweep >= Math.PI * 2) sweep -= Math.PI * 2
224
- return sweep
225
- }
226
-
227
- function isExteriorWall(wall: WallNode): boolean {
228
- return wall.frontSide === 'exterior' || wall.backSide === 'exterior'
229
- }
230
-
231
- function normalizeNodes(
232
- scene: MarkSceneInput | Readonly<Record<string, AnyNode>>,
233
- ): Readonly<Record<string, AnyNode>> {
234
- const candidate = scene as MarkSceneInput
235
- return candidate && typeof candidate === 'object' && 'nodes' in candidate
236
- ? candidate.nodes
237
- : (scene as Readonly<Record<string, AnyNode>>)
238
- }
@@ -1,8 +0,0 @@
1
- import { MEASUREMENT_ACTIVE_COLOR, MEASUREMENT_DANGLING_COLOR } from '@pascal-app/viewer'
2
-
3
- export const MEASUREMENT_FLOORPLAN_COLOR = '#4f46e5'
4
-
5
- export function measurementFloorplanPresentationColor(dangling: boolean, active: boolean): string {
6
- if (dangling) return MEASUREMENT_DANGLING_COLOR
7
- return active ? MEASUREMENT_ACTIVE_COLOR : MEASUREMENT_FLOORPLAN_COLOR
8
- }
@@ -1,37 +0,0 @@
1
- import type { AnyNode } from '@pascal-app/core'
2
-
3
- /**
4
- * A node's human label, the same in the baked artifact (`extras.label`) and the
5
- * viewer's breadcrumb: an explicit name wins, items fall back to their catalog
6
- * asset name, other kinds to a type word.
7
- */
8
- export function nodeDisplayLabel(node: AnyNode): string {
9
- if (node.name) return node.name
10
- switch (node.type) {
11
- case 'item':
12
- return (node as { asset?: { name?: string } }).asset?.name || 'Item'
13
- case 'wall':
14
- return 'Wall'
15
- case 'door':
16
- return 'Door'
17
- case 'window':
18
- return 'Window'
19
- case 'cabinet':
20
- case 'cabinet-module':
21
- return 'Cabinet'
22
- case 'slab':
23
- return 'Slab'
24
- case 'ceiling':
25
- return 'Ceiling'
26
- case 'roof':
27
- return 'Roof'
28
- case 'fence':
29
- return 'Fence'
30
- case 'column':
31
- return 'Column'
32
- case 'stair':
33
- return 'Stairs'
34
- default:
35
- return node.type
36
- }
37
- }