@pascal-app/editor 1.0.7 → 1.1.0

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 (136) hide show
  1. package/package.json +7 -7
  2. package/src/components/editor/custom-camera-controls.tsx +90 -34
  3. package/src/components/editor/duplicate-node.ts +2 -1
  4. package/src/components/editor/first-person-controls.tsx +5 -7
  5. package/src/components/editor/fit-scene-framing.test.ts +318 -0
  6. package/src/components/editor/fit-scene-framing.ts +142 -0
  7. package/src/components/editor/floating-action-menu.tsx +4 -2
  8. package/src/components/editor/floor-openings-overlay.tsx +1 -1
  9. package/src/components/editor/floor-region-controls.tsx +2 -2
  10. package/src/components/editor/floorplan-panel.tsx +173 -8
  11. package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
  12. package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
  13. package/src/components/editor/footprint-height-handle.tsx +1 -1
  14. package/src/components/editor/group-transform-shared.ts +3 -1
  15. package/src/components/editor/index.tsx +20 -0
  16. package/src/components/editor/measurement-pill.tsx +1 -1
  17. package/src/components/editor/node-arrow-handles.tsx +1 -1
  18. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -2
  19. package/src/components/editor/open-wall-ends-hint.tsx +1 -1
  20. package/src/components/editor/quick-measurement-card.tsx +1 -1
  21. package/src/components/editor/room-controls.tsx +3 -2
  22. package/src/components/editor/room-handles-2d.tsx +2 -1
  23. package/src/components/editor/room-handles.tsx +3 -3
  24. package/src/components/editor/room-labels.tsx +4 -2
  25. package/src/components/editor/site-edge-labels.tsx +6 -2
  26. package/src/components/editor/wall-measurement-label.tsx +30 -14
  27. package/src/components/editor/wall-move-side-handles.tsx +10 -6
  28. package/src/components/editor/wall-region-handles.tsx +2 -2
  29. package/src/components/editor/wall-snap-beacon-layer.tsx +53 -32
  30. package/src/components/editor-2d/floorplan-group-move.tsx +1 -1
  31. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +12 -12
  32. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -2
  33. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
  34. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +22 -4
  35. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
  36. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
  37. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
  38. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
  39. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +22 -0
  40. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +14 -2
  41. package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
  42. package/src/components/systems/roof/roof-edit-system.tsx +30 -19
  43. package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
  44. package/src/components/tools/distribution/distribution-run-tool.tsx +150 -32
  45. package/src/components/tools/item/move-tool.tsx +1 -6
  46. package/src/components/tools/item/use-placement-coordinator.tsx +1 -1
  47. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
  48. package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
  49. package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
  50. package/src/components/tools/shared/placement-box.tsx +1 -5
  51. package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
  52. package/src/components/tools/shared/segment-angle.ts +0 -4
  53. package/src/components/tools/tool-manager.tsx +1 -24
  54. package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
  55. package/src/components/tools/wall/wall-typed-entry.ts +70 -0
  56. package/src/components/ui/controls/slider-control.tsx +18 -6
  57. package/src/components/ui/controls/tool-options-panel.tsx +74 -44
  58. package/src/components/ui/helpers/helper-manager.test.tsx +53 -0
  59. package/src/components/ui/helpers/helper-manager.tsx +54 -0
  60. package/src/components/ui/panels/floor-foundation-panel.tsx +1 -1
  61. package/src/components/ui/panels/floor-opening-panel.tsx +1 -1
  62. package/src/components/ui/panels/mobile-selection-bar.tsx +28 -24
  63. package/src/components/ui/panels/multi-height-mode.tsx +3 -3
  64. package/src/components/ui/panels/multi-selection-panel.tsx +1 -1
  65. package/src/components/ui/panels/panel-manager.tsx +4 -2
  66. package/src/components/ui/panels/panel-wrapper.tsx +41 -18
  67. package/src/components/ui/panels/room-construction-rows.tsx +1 -1
  68. package/src/components/ui/panels/room-openings-field.tsx +1 -1
  69. package/src/components/ui/panels/room-panel.tsx +1 -1
  70. package/src/components/ui/right-stack.test.ts +13 -6
  71. package/src/components/ui/right-stack.tsx +21 -10
  72. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
  73. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +1 -1
  74. package/src/components/ui/sidebar/panels/site-panel/index.tsx +1 -8
  75. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +1 -1
  76. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +1 -1
  77. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
  78. package/src/components/viewer/viewer-rooms.tsx +1 -1
  79. package/src/i18n/locales/en/messages.po +856 -124
  80. package/src/i18n/locales/en/messages.ts +1 -1
  81. package/src/i18n/locales/en-XA/messages.ts +1 -1
  82. package/src/i18n/locales/es/messages.po +2087 -257
  83. package/src/i18n/locales/fr/messages.po +2087 -257
  84. package/src/i18n/locales/ja/messages.po +2087 -257
  85. package/src/i18n/locales/zh-CN/messages.po +2087 -257
  86. package/src/index.tsx +74 -47
  87. package/src/lib/contextual-help.ts +6 -0
  88. package/src/lib/elevation-guides.ts +21 -6
  89. package/src/lib/elevator-support.ts +1 -72
  90. package/src/lib/floor-footprints.test.ts +1 -1
  91. package/src/lib/floor-footprints.ts +1 -9
  92. package/src/lib/floor-foundation-support.test.ts +8 -3
  93. package/src/lib/floor-opening-draft.ts +1 -1
  94. package/src/lib/floor-region-session.ts +1 -1
  95. package/src/lib/floorplan/open-wall-ends.ts +2 -1
  96. package/src/lib/gesture-lifecycle.ts +3 -1
  97. package/src/lib/interaction/scope.ts +1 -0
  98. package/src/lib/layer-thickness.ts +1 -1
  99. package/src/lib/level-selection.test.ts +18 -2
  100. package/src/lib/level-selection.ts +31 -8
  101. package/src/lib/linear-display.ts +1 -1
  102. package/src/lib/live-gesture-round-trip.test.ts +1 -1
  103. package/src/lib/measurement-label.test.ts +1 -1
  104. package/src/lib/measurements.test.ts +8 -7
  105. package/src/lib/measurements.ts +1 -218
  106. package/src/lib/node-action-movement.test.tsx +115 -0
  107. package/src/lib/node-action-movement.ts +2 -0
  108. package/src/lib/placement-metadata.ts +4 -0
  109. package/src/lib/point-ask/describe-target.ts +6 -8
  110. package/src/lib/point-ask/region.ts +1 -1
  111. package/src/lib/print-shell-compiler.ts +12 -1
  112. package/src/lib/print-wall-solids.test.ts +11 -0
  113. package/src/lib/print-wall-solids.ts +9 -0
  114. package/src/lib/room-assembly-overlay.ts +1 -1
  115. package/src/lib/room-built-on.test.ts +2 -5
  116. package/src/lib/room-built-on.ts +1 -15
  117. package/src/lib/room-construction-commands.ts +3 -2
  118. package/src/lib/room-controls-phase6.test.ts +1 -1
  119. package/src/lib/room-handle-drag.ts +6 -25
  120. package/src/lib/room-push-dimensions.test.ts +1 -1
  121. package/src/lib/scene-bounds.test.ts +144 -0
  122. package/src/lib/scene-bounds.ts +79 -15
  123. package/src/lib/scene-clipboard.ts +2 -1
  124. package/src/lib/snapping-mode.test.ts +23 -1
  125. package/src/lib/viewer-selection.ts +1 -1
  126. package/src/lib/wall-region-list.ts +3 -1
  127. package/src/lib/window-interaction.test.ts +4 -2
  128. package/src/store/use-editor.tsx +5 -0
  129. package/src/store/use-placement-typing.test.ts +165 -0
  130. package/src/store/use-placement-typing.ts +186 -0
  131. package/src/components/tools/elevator/elevator-defaults.ts +0 -8
  132. package/src/components/tools/elevator/elevator-tool.tsx +0 -372
  133. package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
  134. package/src/lib/door-interaction.ts +0 -102
  135. package/src/lib/measurement-label.ts +0 -68
  136. package/src/lib/window-interaction.ts +0 -142
package/src/index.tsx CHANGED
@@ -15,8 +15,12 @@ export {
15
15
  acquireSceneReadOnlyLease,
16
16
  applyScenePatch,
17
17
  applySceneSnapshot,
18
+ buildMeasurementAngleArcPoints,
18
19
  type CompiledTranslationMessages,
20
+ closeDoorOpenState,
21
+ closeWindowOpenState,
19
22
  createI18n,
23
+ cubicMetersToVolumeUnit,
20
24
  DEFAULT_CURVED_STAIR_INNER_RADIUS,
21
25
  DEFAULT_CURVED_STAIR_SWEEP_ANGLE,
22
26
  DEFAULT_SPIRAL_SHOW_CENTER_COLUMN,
@@ -33,15 +37,36 @@ export {
33
37
  DEFAULT_STAIR_TYPE,
34
38
  DEFAULT_STAIR_WIDTH,
35
39
  defaultI18n,
40
+ formatAngleRadians,
41
+ formatAreaLabel,
36
42
  formatLength,
43
+ formatLinearMeasurement,
37
44
  formatNumber,
45
+ formatVolumeLabel,
46
+ getAreaUnitLabel,
47
+ getDisplayedDoorValue,
48
+ getDisplayedWindowShutterValue,
49
+ getDisplayedWindowValue,
50
+ getLinearUnitLabel,
38
51
  getLocale,
52
+ getVolumeUnitLabel,
53
+ isOperableWindowType,
54
+ type LinearUnit,
55
+ linearControlValueToMeters,
56
+ linearUnitToMeters,
57
+ metersToLinearUnit,
39
58
  normalizeDecimalInput,
59
+ openDoorOpenState,
60
+ openWindowOpenState,
40
61
  type PascalI18n,
41
62
  PascalI18nProvider,
63
+ parseLinearMeasurementInput,
42
64
  parseLocalizedNumber,
43
65
  registerCompiledMessages,
44
66
  registerMessages,
67
+ resolveElevatorNodeSupportY,
68
+ resolveElevatorSupportLevelId,
69
+ resolveElevatorSupportY,
45
70
  type SceneCommit,
46
71
  type SceneCommitListener,
47
72
  type SceneCommitOrigin,
@@ -49,7 +74,10 @@ export {
49
74
  type SceneNodePatch,
50
75
  type ScenePatch,
51
76
  type SceneSnapshot,
77
+ separatorFloorElevation,
52
78
  setLocale,
79
+ setWindowShuttersOpenState,
80
+ squareMetersToAreaUnit,
53
81
  subscribe,
54
82
  subscribeSceneCommits,
55
83
  type TranslationMessage,
@@ -59,8 +87,18 @@ export {
59
87
  useLocale,
60
88
  usePascalI18n,
61
89
  useScene,
90
+ type WindowOpenAnimationOptions,
91
+ type WindowShutterField,
62
92
  } from '@pascal-app/core'
63
- export { useViewer } from '@pascal-app/viewer'
93
+ export {
94
+ MEASUREMENT_ACTIVE_COLOR,
95
+ MEASUREMENT_DANGLING_COLOR,
96
+ MEASUREMENT_PERSISTENT_COLOR,
97
+ measurementPolygonLabelAnchor,
98
+ measurementPresentationColor,
99
+ triangulateMeasurementPolygon,
100
+ useViewer,
101
+ } from '@pascal-app/viewer'
64
102
  export type { EditorProps } from './components/editor'
65
103
  export { default as Editor } from './components/editor'
66
104
  export { ActionMenuButton } from './components/editor/action-menu-button'
@@ -188,6 +226,7 @@ export {
188
226
  } from './components/tools/item/use-placement-coordinator'
189
227
  export { MoveRegistryNodeTool } from './components/tools/registry/move-registry-node-tool'
190
228
  export {
229
+ type RegistryToolContextValue,
191
230
  RegistryToolProvider,
192
231
  useRegistryToolContext,
193
232
  } from './components/tools/registry-tool-context'
@@ -207,6 +246,7 @@ export {
207
246
  resolveLevelConstructionPlane,
208
247
  } from './components/tools/shared/horizontal-construction-plane'
209
248
  export { PlacementBox } from './components/tools/shared/placement-box'
249
+ export { PlacementCoordinateInput } from './components/tools/shared/placement-coordinate-input'
210
250
  export { PlacementDimensionGuides } from './components/tools/shared/placement-dimension-guides'
211
251
  // Pointer-decided support surface (deck top vs floor underneath) — the
212
252
  // draw tools (wall / fence) ride their grid plane and commit cap on it.
@@ -222,7 +262,6 @@ export {
222
262
  type PolygonEditorProps,
223
263
  } from './components/tools/shared/polygon-editor'
224
264
  export {
225
- formatAngleRadians,
226
265
  getAngleArcToSegmentReference,
227
266
  getAngleToSegmentReference,
228
267
  getSegmentAngleReferenceAtPoint,
@@ -264,6 +303,11 @@ export {
264
303
  type WallPullSnapRadii,
265
304
  type WallPullSnapResult,
266
305
  } from './components/tools/wall/wall-pull-snap'
306
+ export {
307
+ formatWallDraftHeading,
308
+ resolveTypedWallEnd,
309
+ wallDraftHeadingDegrees,
310
+ } from './components/tools/wall/wall-typed-entry'
267
311
  // `ToolbarLeft` / `ToolbarRight` alias `ViewerToolbarLeft` / `ViewerToolbarRight`
268
312
  // (same components) under the names headless hosts compose their toolbars with.
269
313
  export {
@@ -476,7 +520,12 @@ export {
476
520
  resolveCeilingPlanPointSnap,
477
521
  } from './lib/ceiling-plan-snap'
478
522
  export { EDITOR_LAYER } from './lib/constants'
479
- export type { ContextualShortcutHint } from './lib/contextual-help'
523
+ export {
524
+ AXIS_MOVE_DRAG_LABEL,
525
+ type ContextualShortcutHint,
526
+ PATH_POINT_DRAG_LABEL,
527
+ ROTATE_HANDLE_DRAG_LABEL,
528
+ } from './lib/contextual-help'
480
529
  export {
481
530
  CONTEXTUAL_HELP_NODE_EXTENSION_KEY,
482
531
  type ContextualHelpNodeExtension,
@@ -491,11 +540,6 @@ export {
491
540
  nextContinuation,
492
541
  } from './lib/continuation'
493
542
  export { canDirectMoveNode } from './lib/direct-manipulation'
494
- export {
495
- closeDoorOpenState,
496
- getDisplayedDoorValue,
497
- openDoorOpenState,
498
- } from './lib/door-interaction'
499
543
  export { downloadProjectFile } from './lib/download-project-file'
500
544
  export { createEditorApi } from './lib/editor-api'
501
545
  export {
@@ -517,12 +561,7 @@ export {
517
561
  resolveElevationSnapMatch,
518
562
  resolveStructuralElevationSnap,
519
563
  } from './lib/elevation-guides'
520
- export {
521
- resolveCurrentBuildingId,
522
- resolveElevatorNodeSupportY,
523
- resolveElevatorSupportLevelId,
524
- resolveElevatorSupportY,
525
- } from './lib/elevator-support'
564
+ export { resolveCurrentBuildingId } from './lib/elevator-support'
526
565
  export type { ExportTextureUtils } from './lib/export-texture-utils'
527
566
  export { getFloatingMenuScale } from './lib/floating-menu-scale'
528
567
  export { startOpeningDraft } from './lib/floor-opening-draft'
@@ -665,6 +704,11 @@ export {
665
704
  editorFinishScriptHost,
666
705
  finishScriptCode,
667
706
  } from './lib/geometry-script/finish-scripts'
707
+ export {
708
+ beginGesture,
709
+ type GestureCancelReason,
710
+ type GestureHandle,
711
+ } from './lib/gesture-lifecycle'
668
712
  export { exportSceneToGlb, type GlbExportOptions } from './lib/glb-export'
669
713
  export {
670
714
  type EditorGridEvent,
@@ -750,10 +794,6 @@ export {
750
794
  isCreatableMeasurementKind,
751
795
  normalizeCreatableMeasurementKind,
752
796
  } from './lib/measurement-kind'
753
- export {
754
- measurementPolygonLabelAnchor,
755
- triangulateMeasurementPolygon,
756
- } from './lib/measurement-label'
757
797
  export {
758
798
  type LingoUnitSpec,
759
799
  lingoUnitSpec,
@@ -763,26 +803,8 @@ export {
763
803
  parseMeasurement,
764
804
  } from './lib/measurement-parser'
765
805
  export {
766
- buildMeasurementAngleArcPoints,
767
- cubicMetersToVolumeUnit,
768
- formatAreaLabel,
769
- formatLinearMeasurement,
770
- formatVolumeLabel,
771
- getAreaUnitLabel,
772
- getLinearUnitLabel,
773
- getVolumeUnitLabel,
774
- type LinearUnit,
775
- linearControlValueToMeters,
776
- linearUnitToMeters,
777
- MEASUREMENT_ACTIVE_COLOR,
778
- MEASUREMENT_DANGLING_COLOR,
779
806
  MEASUREMENT_FLOORPLAN_COLOR,
780
- MEASUREMENT_PERSISTENT_COLOR,
781
807
  measurementFloorplanPresentationColor,
782
- measurementPresentationColor,
783
- metersToLinearUnit,
784
- parseLinearMeasurementInput,
785
- squareMetersToAreaUnit,
786
808
  } from './lib/measurements'
787
809
  export type {
788
810
  ModelExport,
@@ -863,7 +885,6 @@ export { clearRoofDuplicateMetadata, duplicateRoofSubtree } from './lib/roof-dup
863
885
  export { hasRoofFaceChildOverlap, type RoofWallHit, resolveRoofWallHit } from './lib/roof-wall-hit'
864
886
  export {
865
887
  selectedWallDimensions,
866
- separatorFloorElevation,
867
888
  separatorPushHandles,
868
889
  WALL_PUSH_AFFORDANCE,
869
890
  type WallPushArrowPayload,
@@ -1029,16 +1050,6 @@ export {
1029
1050
  wallTypePlacementOf,
1030
1051
  } from './lib/wall-type'
1031
1052
  export { writeWindowChanges } from './lib/window-changes'
1032
- export {
1033
- closeWindowOpenState,
1034
- getDisplayedWindowShutterValue,
1035
- getDisplayedWindowValue,
1036
- isOperableWindowType,
1037
- openWindowOpenState,
1038
- setWindowShuttersOpenState,
1039
- type WindowOpenAnimationOptions,
1040
- type WindowShutterField,
1041
- } from './lib/window-interaction'
1042
1053
  export {
1043
1054
  getActiveBuildingPose,
1044
1055
  projectAlignmentGuidesWorldToActiveBuildingLocal,
@@ -1161,6 +1172,22 @@ export {
1161
1172
  default as usePlacementPreview,
1162
1173
  type PlacementPreviewDimension,
1163
1174
  } from './store/use-placement-preview'
1175
+ export {
1176
+ isPlacementTypingKey,
1177
+ isPlacementTypingStartKey,
1178
+ PLACEMENT_TYPING_DISTANCE_OFFSET,
1179
+ PLACEMENT_TYPING_LENGTH_ANGLE,
1180
+ type PlacementTypingField,
1181
+ type PlacementTypingKeyOptions,
1182
+ type PlacementTypingKeyOutcome,
1183
+ type PlacementTypingProjection,
1184
+ type PlacementTypingSession,
1185
+ type PlacementTypingState,
1186
+ type PlacementTypingValues,
1187
+ placementTypingLengthOptions,
1188
+ routePlacementTypingKey,
1189
+ usePlacementTyping,
1190
+ } from './store/use-placement-typing'
1164
1191
  export { usePointAsk } from './store/use-point-ask'
1165
1192
  export {
1166
1193
  activateQuickMeasurementHudSource,
@@ -31,6 +31,12 @@ export const RESIZE_HANDLE_DRAG_LABEL = 'resize-handle'
31
31
  export const GROUP_MOVE_DRAG_LABEL = 'group-move-handle'
32
32
  export const GROUP_ROTATE_DRAG_LABEL = 'group-rotate-handle'
33
33
 
34
+ // `activeHandleDrag.label`s for the MEP handles, in 3D and on the plan: one
35
+ // point of a run, where Alt detaches the joint, and a fitting, a run or one
36
+ // run segment slid along an axis. Their snap context is the kind's own.
37
+ export const PATH_POINT_DRAG_LABEL = 'path-point'
38
+ export const AXIS_MOVE_DRAG_LABEL = 'axis-move'
39
+
34
40
  export function resolveRotateHandleHelpHints(altPressed: boolean): ContextualShortcutHint[] {
35
41
  return [
36
42
  {
@@ -154,12 +154,27 @@ export function collectElevationSnapTargets(
154
154
  anchor: center,
155
155
  label: 'Wall base',
156
156
  })
157
- targets.push({
158
- id: `${node.id}:top`,
159
- elevation: base + getWallEffectiveHeightForNodes(node, nodes),
160
- anchor: center,
161
- label: 'Wall top',
162
- })
157
+ if (node.endHeightOffset) {
158
+ targets.push({
159
+ id: `${node.id}:top-start`,
160
+ elevation: base + getWallEffectiveHeightForNodes(node, nodes, 0),
161
+ anchor: node.start,
162
+ label: 'Wall top start',
163
+ })
164
+ targets.push({
165
+ id: `${node.id}:top-end`,
166
+ elevation: base + getWallEffectiveHeightForNodes(node, nodes, 1),
167
+ anchor: node.end,
168
+ label: 'Wall top end',
169
+ })
170
+ } else {
171
+ targets.push({
172
+ id: `${node.id}:top`,
173
+ elevation: base + getWallEffectiveHeightForNodes(node, nodes, 0.5),
174
+ anchor: center,
175
+ label: 'Wall top',
176
+ })
177
+ }
163
178
  continue
164
179
  }
165
180
 
@@ -1,26 +1,4 @@
1
- import {
2
- type AnyNode,
3
- type AnyNodeId,
4
- type BuildingNode,
5
- type ElevatorNode,
6
- type LevelNode,
7
- spatialGridManager,
8
- useScene,
9
- } from '@pascal-app/core'
10
-
11
- function getBuildingLevels(
12
- buildingId: string | null | undefined,
13
- nodes: Record<string, AnyNode>,
14
- ): LevelNode[] {
15
- if (!buildingId) return []
16
- const building = nodes[buildingId as AnyNodeId]
17
- if (building?.type !== 'building') return []
18
-
19
- return building.children
20
- .map((childId) => nodes[childId as AnyNodeId])
21
- .filter((entry): entry is LevelNode => entry?.type === 'level')
22
- .sort((left, right) => left.level - right.level)
23
- }
1
+ import type { AnyNode, AnyNodeId, BuildingNode, LevelNode } from '@pascal-app/core'
24
2
 
25
3
  export function resolveCurrentBuildingId({
26
4
  buildingId,
@@ -45,52 +23,3 @@ export function resolveCurrentBuildingId({
45
23
 
46
24
  return null
47
25
  }
48
-
49
- export function resolveElevatorSupportLevelId({
50
- buildingId,
51
- preferredLevelId,
52
- }: {
53
- buildingId: string | null | undefined
54
- preferredLevelId?: string | null
55
- }): LevelNode['id'] | null {
56
- const nodes = useScene.getState().nodes
57
- const preferred = preferredLevelId ? nodes[preferredLevelId as AnyNodeId] : undefined
58
- const levels = getBuildingLevels(buildingId, nodes)
59
- const preferredInBuilding = preferredLevelId
60
- ? levels.find((level) => level.id === preferredLevelId)
61
- : undefined
62
-
63
- if (preferredInBuilding) return preferredInBuilding.id
64
- if (levels.length === 0) return preferred?.type === 'level' ? preferred.id : null
65
-
66
- return levels[0]?.id ?? null
67
- }
68
-
69
- export function resolveElevatorSupportY({
70
- buildingId,
71
- preferredLevelId,
72
- x,
73
- z,
74
- }: {
75
- buildingId: string | null | undefined
76
- preferredLevelId?: string | null
77
- x: number
78
- z: number
79
- }): number {
80
- const levelId = resolveElevatorSupportLevelId({ buildingId, preferredLevelId })
81
- if (!levelId) return 0
82
-
83
- return Math.max(0, spatialGridManager.getSlabElevationAt(levelId, x, z))
84
- }
85
-
86
- export function resolveElevatorNodeSupportY(
87
- node: ElevatorNode,
88
- position: [number, number, number] = node.position,
89
- ): number {
90
- return resolveElevatorSupportY({
91
- buildingId: node.parentId,
92
- preferredLevelId: node.fromLevelId ?? node.defaultLevelId,
93
- x: position[0],
94
- z: position[2],
95
- })
96
- }
@@ -11,6 +11,7 @@ import {
11
11
  ItemNode,
12
12
  initSpaceDetectionSync,
13
13
  LevelNode,
14
+ levelFootprints,
14
15
  type SceneCommit,
15
16
  type SlabNode,
16
17
  SlabNode as SlabSchema,
@@ -29,7 +30,6 @@ import {
29
30
  footprintsToMoveTogether,
30
31
  foundationHeight,
31
32
  isFootprintConstructionHit,
32
- levelFootprints,
33
33
  presetPatch,
34
34
  roomFloorOwner,
35
35
  roomFootprint,
@@ -10,6 +10,7 @@ import {
10
10
  getRoomBaseElevation,
11
11
  groundFloorConstruction,
12
12
  intersection,
13
+ levelFootprints,
13
14
  roomDrawnFloor,
14
15
  type SlabNode,
15
16
  setFloorFoundation,
@@ -39,15 +40,6 @@ const ON_GROUND = 0.0005
39
40
  const isBase = (node: AnyNode | undefined): node is SlabNode =>
40
41
  node?.type === 'slab' && node.plateRole === 'base'
41
42
 
42
- const footprintArea = (plate: SlabNode) => area([{ outer: plate.polygon, holes: plate.holes }])
43
-
44
- /** The level's footprints, largest first (ties by id, so the order is stable). */
45
- export function levelFootprints(nodes: Record<string, AnyNode>, levelId: string): SlabNode[] {
46
- return Object.values(nodes)
47
- .filter((node): node is SlabNode => isBase(node) && node.parentId === levelId)
48
- .sort((a, b) => footprintArea(b) - footprintArea(a) || a.id.localeCompare(b.id))
49
- }
50
-
51
43
  /**
52
44
  * The drawn slab a room's floor is: the one it visibly stands on (a legacy
53
45
  * floor can show a hand-drawn slab over the one it was taken from), else the
@@ -11,6 +11,7 @@ import {
11
11
  getLevelElevations,
12
12
  initSpaceDetectionSync,
13
13
  LevelNode,
14
+ levelFootprints,
14
15
  type SlabNode,
15
16
  upperFloorHeightControl,
16
17
  useScene,
@@ -30,7 +31,6 @@ import {
30
31
  footprintHeightMinimum,
31
32
  footprintHeightPatch,
32
33
  footprintHeightValue,
33
- levelFootprints,
34
34
  presetPatch,
35
35
  setFootprintHeight,
36
36
  thickFloorAdvice,
@@ -169,7 +169,6 @@ describe('Upper-floor height drag release (round-final item 6)', () => {
169
169
  test(`a drag of ${delta} m lands on release`, () => {
170
170
  const bedroom = plateOf(UPPER, 'Bedroom')
171
171
  const start = footprintHeightValue(nodes(), bedroom)
172
- const past = useScene.temporal.getState().pastStates.length
173
172
  const preview = beginFootprintHeightPreview(bedroom.id)
174
173
  expect(preview.preview(start + delta / 2)).toBeNull()
175
174
  expect(preview.preview(start + delta)).toBeNull()
@@ -177,7 +176,13 @@ describe('Upper-floor height drag release (round-final item 6)', () => {
177
176
  const after = nodes()[bedroom.id] as SlabNode
178
177
  expect(after.thickness).toBeCloseTo(bedroom.thickness + delta, 6)
179
178
  expect(footprintHeightValue(nodes(), after)).toBeCloseTo(start + delta, 6)
180
- expect(useScene.temporal.getState().pastStates.length).toBe(past + 1)
179
+ useScene.temporal.getState().undo()
180
+ expect(footprintHeightValue(nodes(), nodes()[bedroom.id] as SlabNode)).toBeCloseTo(start, 6)
181
+ useScene.temporal.getState().redo()
182
+ expect(footprintHeightValue(nodes(), nodes()[bedroom.id] as SlabNode)).toBeCloseTo(
183
+ start + delta,
184
+ 6,
185
+ )
181
186
  })
182
187
  }
183
188
  })
@@ -7,6 +7,7 @@ import {
7
7
  generateId,
8
8
  type LevelNode,
9
9
  type MultiPolygon,
10
+ roomFloorElevation,
10
11
  type StructurePlan,
11
12
  structureChangeBatch,
12
13
  useScene,
@@ -31,7 +32,6 @@ import {
31
32
  import { type FloorRegionPoint, floorRegionSnapTargets } from './floor-region-snap'
32
33
  import { beginGesture, type GestureHandle } from './gesture-lifecycle'
33
34
  import { resolveRoomAssemblyHeights } from './room-assembly-overlay'
34
- import { roomFloorElevation } from './room-handle-drag'
35
35
  import { sfxEmitter } from './sfx-bus'
36
36
 
37
37
  // "Cut opening": draws an opening on a room's floor (or its ceiling) with the
@@ -7,6 +7,7 @@ import {
7
7
  ceilingRegionsOwner,
8
8
  type MultiPolygon,
9
9
  resolveCeilingHeight,
10
+ roomFloorElevation,
10
11
  union,
11
12
  useScene,
12
13
  } from '@pascal-app/core'
@@ -32,7 +33,6 @@ import { beginGesture, type GestureHandle } from './gesture-lifecycle'
32
33
  import { hasActivePaintMaterial } from './material-paint'
33
34
  import { isPaintErasing, usePaintRegionMode } from './paint-region-mode'
34
35
  import { addCeilingRegion, addFloorRegion } from './paint-regions'
35
- import { roomFloorElevation } from './room-handle-drag'
36
36
 
37
37
  // An outline drawn on one room's floor: a rectangle (press, drag, release; a
38
38
  // persistent tool also takes two clicks) or a polygon (click points, close on
@@ -1,11 +1,12 @@
1
1
  import {
2
+ formatLinearMeasurement,
3
+ type LinearUnit,
2
4
  type OpenWallEnd,
3
5
  planJoinOpenWallEnd,
4
6
  runAsSingleSceneHistoryStep,
5
7
  useScene,
6
8
  type WallJoinResult,
7
9
  } from '@pascal-app/core'
8
- import { formatLinearMeasurement, type LinearUnit } from '../measurements'
9
10
 
10
11
  const JOIN_REFUSALS: Record<Extract<WallJoinResult, { ok: false }>['reason'], string> = {
11
12
  'covered-existing-wall': 'An existing wall already covers this span',
@@ -33,7 +33,9 @@ export type GestureSpec = {
33
33
  kind: string
34
34
  /**
35
35
  * Drops the gesture's own in-flight state (draft, preview, listeners,
36
- * pending frames). Never writes the scene. Runs once, on cancel only.
36
+ * pending frames). Runs once, on cancel only. Undo and redo cancel before
37
+ * they jump, so this must record no history: a gesture that previews
38
+ * through scene writes under paused history reverts them here, then resumes.
37
39
  */
38
40
  onCancel: (reason: GestureCancelReason) => void
39
41
  /** A scope the owner begins for the gesture and releases only while it is still this one. */
@@ -28,6 +28,7 @@ export type ReshapeKind =
28
28
  | 'tangent'
29
29
  // A kind's own reshape (wall 'split'): mounted from `def.affordanceTools[reshape]`.
30
30
  | 'split'
31
+ | 'trim'
31
32
 
32
33
  export type InteractionScope =
33
34
  | { kind: 'idle' }
@@ -1,4 +1,4 @@
1
- import type { LinearUnit } from './measurements'
1
+ import type { LinearUnit } from '@pascal-app/core'
2
2
 
3
3
  const METRES_PER_INCH = 0.0254
4
4
 
@@ -3,6 +3,7 @@ import {
3
3
  type AnyNode,
4
4
  type AnyNodeId,
5
5
  BuildingNode,
6
+ ElevatorNode,
6
7
  LevelNode,
7
8
  RoofNode,
8
9
  useScene,
@@ -27,10 +28,14 @@ beforeEach(() => {
27
28
  metadata: { role: 'roof' },
28
29
  })
29
30
  const roof = RoofNode.parse({ id: 'roof_main', parentId: roofLevel.id })
30
- const building = BuildingNode.parse({ id: BUILDING, children: [GROUND, roofLevel.id] })
31
+ const elevator = ElevatorNode.parse({ id: 'elevator_lift', parentId: BUILDING })
32
+ const building = BuildingNode.parse({
33
+ id: BUILDING,
34
+ children: [GROUND, roofLevel.id, elevator.id],
35
+ })
31
36
  useScene.setState({
32
37
  nodes: Object.fromEntries(
33
- [building, ground, wall, roofLevel, roof].map((node) => [node.id, node as AnyNode]),
38
+ [building, ground, wall, roofLevel, roof, elevator].map((node) => [node.id, node as AnyNode]),
34
39
  ),
35
40
  rootNodeIds: [BUILDING],
36
41
  } as never)
@@ -54,4 +59,15 @@ describe('selectPlacedNode', () => {
54
59
  selectedIds: ['wall_a'],
55
60
  })
56
61
  })
62
+
63
+ test('a building-parented node keeps the floor in view and adopts its building', () => {
64
+ useViewer.getState().resetSelection()
65
+ useViewer.getState().setSelection({ levelId: GROUND })
66
+ selectPlacedNode('elevator_lift' as AnyNodeId)
67
+ expect(useViewer.getState().selection).toMatchObject({
68
+ buildingId: BUILDING,
69
+ levelId: GROUND,
70
+ selectedIds: ['elevator_lift'],
71
+ })
72
+ })
57
73
  })
@@ -1,4 +1,4 @@
1
- import type { AnyNodeId, BuildingNode, LevelNode } from '@pascal-app/core'
1
+ import type { AnyNode, AnyNodeId, BuildingNode, LevelNode } from '@pascal-app/core'
2
2
  import { findLevelAncestorId, useScene } from '@pascal-app/core'
3
3
  import { useViewer } from '@pascal-app/viewer'
4
4
 
@@ -30,17 +30,40 @@ export function deleteLevelWithFallbackSelection(levelId: AnyNodeId) {
30
30
  }
31
31
  }
32
32
 
33
+ function findBuildingAncestorId(
34
+ nodeId: AnyNodeId,
35
+ nodes: Record<string, AnyNode>,
36
+ ): BuildingNode['id'] | null {
37
+ let current: AnyNode | undefined = nodes[nodeId]
38
+ for (let guard = 0; current && guard < 16; guard++) {
39
+ if (current.type === 'building') return current.id
40
+ current = current.parentId ? nodes[current.parentId as AnyNodeId] : undefined
41
+ }
42
+ return null
43
+ }
44
+
33
45
  /**
34
46
  * Selects a node a tool just made, on its own level when that is not the one in view: a roof drawn
35
47
  * over a storey's walls lands on the level above, which the editor hides while the storey is
36
- * selected, so selecting it there alone would leave it out of sight.
48
+ * selected, so selecting it there alone would leave it out of sight. A node parented to the
49
+ * building itself (an elevator) has no level, so the floor in view stays active when it belongs
50
+ * to that building.
37
51
  */
38
52
  export function selectPlacedNode(nodeId: AnyNodeId) {
39
- const levelId = findLevelAncestorId(nodeId, useScene.getState().nodes)
53
+ const { nodes } = useScene.getState()
54
+ const levelId = findLevelAncestorId(nodeId, nodes) as LevelNode['id'] | null
55
+ const buildingId = findBuildingAncestorId(nodeId, nodes)
40
56
  const { selection, setSelection } = useViewer.getState()
41
- setSelection(
42
- levelId && levelId !== selection.levelId
43
- ? { levelId: levelId as LevelNode['id'], selectedIds: [nodeId] }
44
- : { selectedIds: [nodeId] },
45
- )
57
+ const updates: Partial<typeof selection> = { selectedIds: [nodeId] }
58
+ if (buildingId && buildingId !== selection.buildingId) {
59
+ updates.buildingId = buildingId
60
+ updates.levelId =
61
+ levelId ??
62
+ (selection.levelId && findBuildingAncestorId(selection.levelId, nodes) === buildingId
63
+ ? selection.levelId
64
+ : null)
65
+ } else if (levelId && levelId !== selection.levelId) {
66
+ updates.levelId = levelId
67
+ }
68
+ setSelection(updates)
46
69
  }
@@ -4,7 +4,7 @@ import {
4
4
  linearUnitToMeters,
5
5
  type MetricNotation,
6
6
  metersToLinearUnit,
7
- } from './measurements'
7
+ } from '@pascal-app/core'
8
8
 
9
9
  export function getLinearDisplay(
10
10
  unit: string,
@@ -6,6 +6,7 @@ import {
6
6
  createZoneDivisionContext,
7
7
  generateId,
8
8
  nodeRegistry,
9
+ roomFloorElevation,
9
10
  type SlabNode,
10
11
  useLiveNodeOverrides,
11
12
  useScene,
@@ -29,7 +30,6 @@ import { roomRelativeFloorHeight, stepRoomFloorElevation } from './room-construc
29
30
  import {
30
31
  commitRoomElevation,
31
32
  mezzanineEdgeHandles,
32
- roomFloorElevation,
33
33
  roomPushHandles,
34
34
  runFloorplanMezzanineEdge,
35
35
  runFloorplanRoomPush,
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import { measurementCentroid, pointInPolygon2D } from '@pascal-app/core'
3
- import { measurementPolygonLabelAnchor, triangulateMeasurementPolygon } from './measurement-label'
3
+ import { measurementPolygonLabelAnchor, triangulateMeasurementPolygon } from '@pascal-app/viewer'
4
4
 
5
5
  describe('measurementPolygonLabelAnchor', () => {
6
6
  test('keeps a concave polygon label inside its visible fill', () => {