@pascal-app/editor 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/package.json +12 -6
  2. package/src/components/editor/bake-exporter.tsx +4 -0
  3. package/src/components/editor/bake-thumbnail.tsx +4 -1
  4. package/src/components/editor/custom-camera-controls.tsx +22 -74
  5. package/src/components/editor/editor-layout-mobile.tsx +1 -0
  6. package/src/components/editor/floating-action-menu.tsx +14 -1
  7. package/src/components/editor/floorplan-panel.tsx +233 -166
  8. package/src/components/editor/grid.tsx +31 -7
  9. package/src/components/editor/group-actions.ts +5 -9
  10. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  11. package/src/components/editor/group-move-3d.ts +4 -6
  12. package/src/components/editor/group-rotate-handle.tsx +81 -35
  13. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  14. package/src/components/editor/group-transform-shared.test.ts +126 -1
  15. package/src/components/editor/group-transform-shared.ts +79 -73
  16. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  17. package/src/components/editor/handles/handle-placement.test.ts +111 -0
  18. package/src/components/editor/handles/handle-placement.ts +69 -0
  19. package/src/components/editor/handles/linear-resize-drag.ts +13 -2
  20. package/src/components/editor/handles/use-handle-drag.ts +80 -8
  21. package/src/components/editor/index.tsx +36 -13
  22. package/src/components/editor/node-action-menu.tsx +2 -0
  23. package/src/components/editor/node-arrow-handles.tsx +162 -69
  24. package/src/components/editor/registry-action-contributions.tsx +53 -0
  25. package/src/components/editor/selection-manager.tsx +49 -0
  26. package/src/components/editor/thumbnail-generator.tsx +11 -2
  27. package/src/components/editor/wall-move-side-handles.tsx +71 -21
  28. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  29. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  30. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  31. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
  32. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +154 -49
  33. package/src/components/editor-2d/renderers/floorplan-draft-wall-measurement.tsx +166 -0
  34. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +49 -0
  35. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +113 -24
  36. package/src/components/procedural-items/inspector.tsx +223 -0
  37. package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
  38. package/src/components/systems/zone/zone-system.tsx +8 -4
  39. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  40. package/src/components/tools/item/item-surface-placement.test.ts +169 -0
  41. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +204 -0
  42. package/src/components/tools/item/placement-strategies.ts +212 -200
  43. package/src/components/tools/item/placement-types.ts +4 -0
  44. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
  45. package/src/components/tools/item/use-draft-node.ts +136 -47
  46. package/src/components/tools/item/use-placement-coordinator.tsx +391 -157
  47. package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
  48. package/src/components/tools/registry/item-surface-move.test.ts +509 -0
  49. package/src/components/tools/registry/item-surface-move.ts +463 -0
  50. package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
  51. package/src/components/tools/registry/move-registry-node-tool.tsx +237 -78
  52. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  53. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  54. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  55. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  56. package/src/components/tools/shared/pointer-support-cap.ts +27 -13
  57. package/src/components/tools/shared/shelf-stickiness.ts +54 -0
  58. package/src/components/tools/shared/surface-hit.test.ts +56 -0
  59. package/src/components/tools/shared/surface-hit.ts +23 -0
  60. package/src/components/tools/shared/surface-rejection.test.tsx +107 -0
  61. package/src/components/tools/shared/surface-rejection.tsx +43 -0
  62. package/src/components/tools/tool-manager.tsx +36 -3
  63. package/src/components/tools/zone/zone-tool.tsx +13 -4
  64. package/src/components/ui/command-palette/editor-commands.tsx +54 -1
  65. package/src/components/ui/controls/material-paint-panel.tsx +6 -67
  66. package/src/components/ui/controls/material-picker.tsx +5 -67
  67. package/src/components/ui/controls/metric-control.tsx +29 -13
  68. package/src/components/ui/controls/slider-control.tsx +87 -35
  69. package/src/components/ui/floating-level-selector.tsx +81 -5
  70. package/src/components/ui/helpers/helper-manager.tsx +24 -6
  71. package/src/components/ui/helpers/registered-tool-helper.tsx +2 -32
  72. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  73. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  74. package/src/components/ui/panels/multi-parametric-inspector.tsx +6 -3
  75. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  76. package/src/components/ui/panels/parametric-inspector.tsx +9 -17
  77. package/src/components/ui/panels/reference-panel.tsx +3 -3
  78. package/src/components/ui/primitives/checkbox.tsx +34 -0
  79. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  80. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  81. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +669 -336
  82. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
  83. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +31 -4
  84. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +30 -11
  85. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
  86. package/src/components/ui/sidebar/panels/site-panel/index.tsx +106 -4
  87. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
  88. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
  89. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +6 -1
  90. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
  91. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
  92. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
  93. package/src/components/ui/snap-target-badge.tsx +2 -0
  94. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  95. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  96. package/src/components/viewer/floorplan-preview.tsx +1 -12
  97. package/src/components/viewer/viewer-scene-header.tsx +3 -0
  98. package/src/components/viewer/viewer-units-panel.tsx +116 -0
  99. package/src/components/viewer-zone-system.tsx +7 -2
  100. package/src/hooks/use-auto-save.integration.test.tsx +197 -0
  101. package/src/hooks/use-auto-save.ts +21 -6
  102. package/src/hooks/use-ceiling-events.test.ts +25 -2
  103. package/src/hooks/use-ceiling-events.ts +7 -1
  104. package/src/hooks/use-grid-events.test.ts +2 -0
  105. package/src/hooks/use-grid-events.ts +16 -11
  106. package/src/hooks/use-keyboard.test.ts +7 -0
  107. package/src/hooks/use-keyboard.ts +43 -28
  108. package/src/index.tsx +68 -3
  109. package/src/lib/catalog-panel-model.ts +39 -0
  110. package/src/lib/catalog-selection.test.ts +27 -0
  111. package/src/lib/catalog-selection.ts +8 -0
  112. package/src/lib/contextual-help.test.ts +27 -1
  113. package/src/lib/contextual-help.ts +16 -9
  114. package/src/lib/export-texture-utils.test.ts +167 -0
  115. package/src/lib/export-texture-utils.ts +121 -0
  116. package/src/lib/export-user-data.test.ts +48 -0
  117. package/src/lib/export-user-data.ts +69 -0
  118. package/src/lib/floorplan/floorplan-export.test.ts +142 -2
  119. package/src/lib/floorplan/floorplan-export.tsx +9 -3
  120. package/src/lib/floorplan/floorplan-extension.ts +16 -0
  121. package/src/lib/floorplan/floorplan-readonly.ts +4 -0
  122. package/src/lib/fresh-planar-placement.ts +173 -33
  123. package/src/lib/glb-export-serialize.test.ts +280 -0
  124. package/src/lib/glb-export.test.ts +265 -16
  125. package/src/lib/glb-export.ts +197 -37
  126. package/src/lib/grid-frame.test.ts +19 -0
  127. package/src/lib/grid-frame.ts +18 -0
  128. package/src/lib/interaction/scope.ts +9 -1
  129. package/src/lib/keyboard-pan.test.ts +62 -0
  130. package/src/lib/keyboard-pan.ts +89 -0
  131. package/src/lib/level-duplication.test.ts +73 -0
  132. package/src/lib/level-duplication.ts +32 -2
  133. package/src/lib/linear-display.test.ts +59 -0
  134. package/src/lib/linear-display.ts +39 -0
  135. package/src/lib/local-guide-image.test.ts +24 -0
  136. package/src/lib/local-guide-image.ts +34 -1
  137. package/src/lib/material-catalog-model.ts +72 -0
  138. package/src/lib/material-paint-panel-model.ts +76 -0
  139. package/src/lib/node-action-movement.ts +21 -0
  140. package/src/lib/overlay-transform.test.ts +28 -0
  141. package/src/lib/overlay-transform.ts +12 -0
  142. package/src/lib/panel-rows.ts +75 -0
  143. package/src/lib/panel-tool-options.test.ts +34 -0
  144. package/src/lib/panel-tool-options.ts +46 -0
  145. package/src/lib/parametric-node-update.ts +37 -0
  146. package/src/lib/portable-export.test.ts +89 -0
  147. package/src/lib/portable-export.ts +84 -11
  148. package/src/lib/print-shell-compiler-manifold-core.ts +59 -10
  149. package/src/lib/print-shell-compiler-manifold-worker.ts +14 -1
  150. package/src/lib/print-shell-compiler-manifold.worker.ts +1 -1
  151. package/src/lib/print-shell-compiler-protocol.ts +8 -0
  152. package/src/lib/scene-clipboard.ts +1 -1
  153. package/src/lib/scene.test.ts +99 -0
  154. package/src/lib/scene.ts +21 -3
  155. package/src/lib/send-to-app.test.ts +108 -0
  156. package/src/lib/send-to-app.ts +121 -0
  157. package/src/lib/sfx/movement-tick.ts +37 -0
  158. package/src/lib/snapping-mode.test.ts +13 -2
  159. package/src/lib/snapping-mode.ts +11 -6
  160. package/src/lib/spatial-drag-plane.test.ts +53 -0
  161. package/src/lib/spatial-drag-plane.ts +19 -0
  162. package/src/lib/spatial-pointer-input.test.ts +72 -0
  163. package/src/lib/spatial-pointer-input.ts +62 -0
  164. package/src/lib/spatial-snap.test.ts +47 -0
  165. package/src/lib/surface-attachment.ts +77 -0
  166. package/src/lib/terrain-panel-model.ts +117 -0
  167. package/src/lib/units.ts +254 -0
  168. package/src/lib/usdz-export.ts +43 -16
  169. package/src/lib/use-linear-display.ts +8 -32
  170. package/src/lib/world-grid-snap.test.ts +48 -0
  171. package/src/lib/world-grid-snap.ts +9 -5
  172. package/src/store/rotation-snapping.test.ts +53 -0
  173. package/src/store/use-editor.tsx +10 -2
  174. package/src/store/use-floorplan-draft-preview.ts +10 -1
  175. package/src/store/use-handle-group.ts +4 -0
  176. package/src/store/use-interaction-scope.ts +155 -6
  177. package/src/store/use-send-to-app.ts +21 -0
package/src/index.tsx CHANGED
@@ -49,7 +49,7 @@ export { FloatingActionMenu as FloatingMenu } from './components/editor/floating
49
49
  // camera controls via the `useViewer.inputDragging` / `useEditor.movingNode`
50
50
  // flags. Tools place onto `useViewer.selection.levelId`, so the host must set a
51
51
  // building + level selection first.
52
- export { Grid } from './components/editor/grid'
52
+ export { EDITOR_GRID_INPUT_NAME, Grid } from './components/editor/grid'
53
53
  export {
54
54
  DimensionPill,
55
55
  type DimensionPillPart,
@@ -79,12 +79,18 @@ export {
79
79
  useInvisibleHitAreaMaterial,
80
80
  } from './components/editor/node-arrow-handles'
81
81
  export { QuickMeasurementCard } from './components/editor/quick-measurement-card'
82
+ export { SelectionManager } from './components/editor/selection-manager'
82
83
  export {
83
84
  type SnapshotCameraData,
84
85
  ThumbnailGenerator,
85
86
  } from './components/editor/thumbnail-generator'
87
+ export { WallMoveSideHandles } from './components/editor/wall-move-side-handles'
86
88
  export { useFloorplanRender } from './components/editor-2d/floorplan-render-context'
87
89
  export { FloorplanDimensionRenderer } from './components/editor-2d/renderers/floorplan-dimension-renderer'
90
+ export {
91
+ type DraftWallMeasurement,
92
+ FloorplanDraftWallMeasurement,
93
+ } from './components/editor-2d/renderers/floorplan-draft-wall-measurement'
88
94
  export { FloorplanGeometryRenderer } from './components/editor-2d/renderers/floorplan-geometry-renderer'
89
95
  export {
90
96
  FloorplanNodePreview,
@@ -137,8 +143,13 @@ export {
137
143
  type PlacementCoordinatorConfig,
138
144
  usePlacementCoordinator,
139
145
  } from './components/tools/item/use-placement-coordinator'
146
+ export { MoveRegistryNodeTool } from './components/tools/registry/move-registry-node-tool'
140
147
  export { useRegistryToolContext } from './components/tools/registry-tool-context'
141
148
  export { CursorSphere } from './components/tools/shared/cursor-sphere'
149
+ export {
150
+ DRAFT_LABEL_Y_OFFSET,
151
+ DraftMeasurementLabel,
152
+ } from './components/tools/shared/draft-measurement-label'
142
153
  export { DragBoundingBox } from './components/tools/shared/drag-bounding-box'
143
154
  export { getFloorStackPreviewPosition } from './components/tools/shared/floor-stack-preview'
144
155
  export { useFreshPlacementVisibility } from './components/tools/shared/fresh-placement-visibility'
@@ -220,6 +231,7 @@ export {
220
231
  CameraActions as ToolbarRight,
221
232
  CameraActions as ViewerToolbarRight,
222
233
  } from './components/ui/action-menu/camera-actions'
234
+ export { furnishTools } from './components/ui/action-menu/furnish-tools'
223
235
  export {
224
236
  ViewToggles as ToolbarLeft,
225
237
  ViewToggles as ViewerToolbarLeft,
@@ -246,6 +258,19 @@ export { FloatingLevelSelector } from './components/ui/floating-level-selector'
246
258
  export { CATALOG_ITEMS } from './components/ui/item-catalog/catalog-items'
247
259
  // Item collections UI — used by the kind-owned ItemPanel in nodes/.
248
260
  export { CollectionsPopover } from './components/ui/panels/collections/collections-popover'
261
+ export {
262
+ resolveHomogeneousSelection,
263
+ resolveUniqueSelectionIds,
264
+ } from './components/ui/panels/homogeneous-selection'
265
+ export {
266
+ commitMultiNodeFields,
267
+ fieldVisibleForAll,
268
+ firstNumericFieldValue,
269
+ firstVec3FieldValue,
270
+ reduceFieldValue,
271
+ reduceHeightBoundMode,
272
+ } from './components/ui/panels/multi-field-value'
273
+ export { applyMultiHeightMode } from './components/ui/panels/multi-height-mode'
249
274
  // Phase 5 Stage E — kinds with bespoke editors (slab holes list,
250
275
  // ceiling height presets, etc.) use `parametrics.customPanel` to mount
251
276
  // a kind-owned panel and need PanelWrapper for the chrome.
@@ -268,6 +293,7 @@ export {
268
293
  } from './components/ui/primitives/shortcut-token'
269
294
  export { useSidebarStore } from './components/ui/primitives/sidebar'
270
295
  export { Slider } from './components/ui/primitives/slider'
296
+ export { Tooltip, TooltipContent, TooltipTrigger } from './components/ui/primitives/tooltip'
271
297
  export { SceneLoader } from './components/ui/scene-loader'
272
298
  export type { ExtraPanel } from './components/ui/sidebar/icon-rail'
273
299
  export { ItemsPanel } from './components/ui/sidebar/panels/items-panel'
@@ -317,6 +343,7 @@ export {
317
343
  ViewerStageSwitcher,
318
344
  type ViewerStageSwitcherProps,
319
345
  } from './components/viewer/viewer-stage-switcher'
346
+ export { ViewerUnitsPanel } from './components/viewer/viewer-units-panel'
320
347
  export {
321
348
  WalkthroughHud,
322
349
  type WalkthroughHudProps,
@@ -328,7 +355,7 @@ export type { SaveStatus } from './hooks/use-auto-save'
328
355
  // can express their affordances declaratively in their own folder.
329
356
  export { type UseDragActionArgs, useDragAction } from './hooks/use-drag-action'
330
357
  // Phase 5 Stage D — extras for kind-owned placement tools (FenceTool etc.).
331
- export { markToolCancelConsumed } from './hooks/use-keyboard'
358
+ export { cancelActiveTool, markToolCancelConsumed } from './hooks/use-keyboard'
332
359
  export { useReducedMotion } from './hooks/use-reduced-motion'
333
360
  export { type Selection, useSelection } from './hooks/use-selection'
334
361
  export {
@@ -337,6 +364,11 @@ export {
337
364
  type PlacementSurface,
338
365
  publishPlacementSurface,
339
366
  } from './lib/active-placement-surface'
367
+ export {
368
+ activateCatalogItem,
369
+ filterCatalogItems,
370
+ isCatalogItemSelected,
371
+ } from './lib/catalog-panel-model'
340
372
  export {
341
373
  CEILING_ALIGNMENT_THRESHOLD_M,
342
374
  type CeilingPlanSnapInput,
@@ -359,6 +391,7 @@ export {
359
391
  continuationContextOf,
360
392
  nextContinuation,
361
393
  } from './lib/continuation'
394
+ export { canDirectMoveNode } from './lib/direct-manipulation'
362
395
  export { createEditorApi } from './lib/editor-api'
363
396
  export {
364
397
  clearStructuralElevationGuide,
@@ -378,6 +411,7 @@ export {
378
411
  resolveElevatorSupportLevelId,
379
412
  resolveElevatorSupportY,
380
413
  } from './lib/elevator-support'
414
+ export type { ExportTextureUtils } from './lib/export-texture-utils'
381
415
  export { getFloatingMenuScale } from './lib/floating-menu-scale'
382
416
  // Floor-plan stair helpers — the cumulative-transform walk
383
417
  // (`computeFloorplanStairSegmentTransforms`) and the rich segment-entry
@@ -432,6 +466,7 @@ export {
432
466
  type FloorplanMode,
433
467
  isFloorplanToolAvailableInMode,
434
468
  } from './lib/floorplan/floorplan-mode'
469
+ export { clientToPlan } from './lib/floorplan/plan-coords'
435
470
  export {
436
471
  commitFreshPlacementSubtree,
437
472
  createFreshPlacementSubtree,
@@ -471,6 +506,8 @@ export {
471
506
  movingNodeOf,
472
507
  scopeNodeId,
473
508
  } from './lib/interaction/scope'
509
+ export { isEditableKeyboardTarget } from './lib/keyboard-pan'
510
+ export { useMaterialCatalogModel } from './lib/material-catalog-model'
474
511
  export {
475
512
  type ActivePaintMaterial,
476
513
  buildResetSurfaceMaterialUpdates,
@@ -480,6 +517,7 @@ export {
480
517
  getActivePaintMaterialLabel,
481
518
  hasActivePaintMaterial,
482
519
  } from './lib/material-paint'
520
+ export { useMaterialPaintPanelModel } from './lib/material-paint-panel-model'
483
521
  export {
484
522
  CREATABLE_MEASUREMENT_KINDS,
485
523
  type CreatableMeasurementKind,
@@ -526,6 +564,21 @@ export type {
526
564
  ModelExportFormat,
527
565
  ModelExportOptions,
528
566
  } from './lib/model-export'
567
+ export {
568
+ cyclePaintScope,
569
+ type PaintHoverInfo,
570
+ type PaintScope,
571
+ paintScopeLabel,
572
+ } from './lib/paint-scope'
573
+ export {
574
+ getNodePanelModel,
575
+ type NodePanelModel,
576
+ PANEL_MODEL_EXTENSION,
577
+ type PanelAction,
578
+ type PanelRow,
579
+ } from './lib/panel-rows'
580
+ export { type PanelToolOption, usePanelToolHints } from './lib/panel-tool-options'
581
+ export { commitParametricNodeFields } from './lib/parametric-node-update'
529
582
  export { consumePlacementDragRelease } from './lib/placement-drag-release'
530
583
  export {
531
584
  addFreshPlacementMetadata,
@@ -544,6 +597,8 @@ export {
544
597
  editorHostPanelRegistry,
545
598
  registerEditorHostPanel,
546
599
  } from './lib/plugin-panels'
600
+ export { configureManifoldRuntime } from './lib/print-shell-compiler-manifold-worker'
601
+ export type { ManifoldRuntimeOptions } from './lib/print-shell-compiler-protocol'
547
602
  export {
548
603
  createQuickMeasurementPointerScheduler,
549
604
  quickMeasurementContext,
@@ -557,7 +612,7 @@ export { hasRoofFaceChildOverlap, type RoofWallHit, resolveRoofWallHit } from '.
557
612
  export type { SceneGraph } from './lib/scene'
558
613
  export { applySceneGraphToEditor } from './lib/scene'
559
614
  export { movementSfxStepKey } from './lib/sfx/movement-tick'
560
- export { triggerSFX } from './lib/sfx-bus'
615
+ export { emitDeleteSFX, triggerSFX } from './lib/sfx-bus'
561
616
  export { playSFX, type SFXName, type SFXPlaybackOptions } from './lib/sfx-player'
562
617
  export {
563
618
  clearSlabSnapFeedback,
@@ -570,12 +625,14 @@ export {
570
625
  type SlabPlanSnapResult,
571
626
  } from './lib/slab-plan-snap'
572
627
  export {
628
+ cycleSnappingModeIn,
573
629
  getSnappingModeLabel,
574
630
  resolveSnapFlags,
575
631
  type SnapContext,
576
632
  type SnapFlags,
577
633
  type SnappingMode,
578
634
  } from './lib/snapping-mode'
635
+ export { getSpatialPointerId, spatialPointerInput } from './lib/spatial-pointer-input'
579
636
  export { duplicateStairSubtree } from './lib/stair-duplication'
580
637
  export {
581
638
  getBuildingLevelsForLevel,
@@ -585,6 +642,7 @@ export {
585
642
  resolveStairPlacementLevelId,
586
643
  resolveStairToLevelId,
587
644
  } from './lib/stair-levels'
645
+ export { surfaceAttachmentId, surfaceFramePose, updateSurfaceNode } from './lib/surface-attachment'
588
646
  export {
589
647
  clearSurfacePlanSnapFeedback,
590
648
  resolveSurfacePlanPointSnap,
@@ -592,14 +650,20 @@ export {
592
650
  type SurfacePlanSnapInput,
593
651
  type SurfacePlanSnapResult,
594
652
  } from './lib/surface-plan-snap'
653
+ export { useTerrainPanelRows } from './lib/terrain-panel-model'
595
654
  export {
655
+ brushRadiusRange,
656
+ clipTerrainPatchToSite,
657
+ commitStroke,
596
658
  fieldExtentForSite,
597
659
  flattenSite,
598
660
  resetSiteTerrain,
599
661
  resolveFlattenTarget,
600
662
  sculptFieldForSite,
663
+ terrainPointInsideSite,
601
664
  } from './lib/terrain-sculpt'
602
665
  export { exportSceneToUsdz, type UsdzExportOptions } from './lib/usdz-export'
666
+ export { useLinearDisplay } from './lib/use-linear-display'
603
667
  // `cn` (twMerge + clsx) — used by kind-owned panels in `@pascal-app/
604
668
  // nodes` so they don't need their own copy / their own tailwind-merge
605
669
  // dependency.
@@ -647,6 +711,7 @@ export {
647
711
  isAngleSnapActive,
648
712
  isGridSnapActive,
649
713
  isMagneticSnapActive,
714
+ selectDefaultBuildingAndLevel,
650
715
  } from './store/use-editor'
651
716
  export { default as useFacingPose, type FacingPose } from './store/use-facing-pose'
652
717
  export { default as useFenceCurveDraft } from './store/use-fence-curve-draft'
@@ -0,0 +1,39 @@
1
+ import { useViewer } from '@pascal-app/viewer'
2
+ import { CATALOG_ITEMS, type CatalogItem } from '../components/ui/item-catalog/catalog-items'
3
+ import useEditor, { type CatalogCategory } from '../store/use-editor'
4
+
5
+ export function filterCatalogItems({
6
+ category,
7
+ items = CATALOG_ITEMS,
8
+ search = '',
9
+ activePlacementTag,
10
+ activeFunctionalTag,
11
+ overrideItems,
12
+ }: {
13
+ category?: CatalogCategory
14
+ items?: readonly CatalogItem[]
15
+ search?: string
16
+ activePlacementTag?: string | null
17
+ activeFunctionalTag?: string | null
18
+ overrideItems?: readonly CatalogItem[]
19
+ }): readonly CatalogItem[] {
20
+ return (
21
+ overrideItems ??
22
+ items.filter((item) => {
23
+ if (!search && category && item.category !== category) return false
24
+ if (activePlacementTag && !item.tags?.includes(activePlacementTag)) return false
25
+ if (activeFunctionalTag && !item.tags?.includes(activeFunctionalTag)) return false
26
+ return !search || item.name.toLowerCase().includes(search.toLowerCase())
27
+ })
28
+ )
29
+ }
30
+
31
+ export function activateCatalogItem(item: CatalogItem) {
32
+ useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
33
+ const editor = useEditor.getState()
34
+ editor.setSelectedItem(item)
35
+ editor.setTool(item.tool ?? 'item')
36
+ editor.setMode('build')
37
+ }
38
+
39
+ export { isCatalogItemSelected } from './catalog-selection'
@@ -0,0 +1,27 @@
1
+ import { test } from 'bun:test'
2
+ import assert from 'node:assert/strict'
3
+ import { isCatalogItemSelected } from './catalog-selection'
4
+
5
+ test('no selection leaves every tile unhighlighted, including items without model URLs', () => {
6
+ for (const item of [{ id: 'chair' }, { id: 'cabinet' }]) {
7
+ assert.equal(isCatalogItemSelected(item, null), false)
8
+ }
9
+ })
10
+
11
+ test('only the chosen item is highlighted when model URLs are missing or shared', () => {
12
+ for (const src of [undefined, '/shared-model.glb']) {
13
+ const items = [
14
+ { id: 'first', src },
15
+ { id: 'second', src },
16
+ ]
17
+ assert.deepEqual(
18
+ items.map((item) => isCatalogItemSelected(item, items[1]!)),
19
+ [false, true],
20
+ )
21
+ }
22
+ })
23
+
24
+ test('selection follows identity across reloaded catalog objects', () => {
25
+ assert.equal(isCatalogItemSelected({ id: 'chair' }, { id: 'chair' }), true)
26
+ assert.equal(isCatalogItemSelected({ id: 'chair' }, { id: 'table' }), false)
27
+ })
@@ -0,0 +1,8 @@
1
+ import type { CatalogItem } from '../components/ui/item-catalog/catalog-items'
2
+
3
+ export function isCatalogItemSelected(
4
+ item: Pick<CatalogItem, 'id'>,
5
+ selected: Pick<CatalogItem, 'id'> | null,
6
+ ) {
7
+ return selected != null && item.id === selected.id
8
+ }
@@ -1,5 +1,14 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { resolveSelectModeHelpHints } from './contextual-help'
2
+ import { resolveRotateHandleHelpHints, resolveSelectModeHelpHints } from './contextual-help'
3
+
4
+ test('rotation advertises Alt as the temporary free-rotation override', () => {
5
+ expect(resolveRotateHandleHelpHints(false)).toEqual([
6
+ { keys: ['Alt'], label: 'Hold to rotate freely', active: false },
7
+ ])
8
+ expect(resolveRotateHandleHelpHints(true)).toEqual([
9
+ { keys: ['Alt'], label: 'Rotating freely (no angle step)', active: true },
10
+ ])
11
+ })
3
12
 
4
13
  describe('resolveSelectModeHelpHints', () => {
5
14
  test('stays hidden in idle select mode with no selection', () => {
@@ -117,4 +126,21 @@ describe('resolveSelectModeHelpHints', () => {
117
126
  },
118
127
  ])
119
128
  })
129
+
130
+ test('shows the single-side radius hint for rounded openings', () => {
131
+ expect(
132
+ resolveSelectModeHelpHints({
133
+ selectedCount: 1,
134
+ hasMovableSelection: true,
135
+ hasRotatableSelection: false,
136
+ hasOpeningRadiusSelection: true,
137
+ commandPressed: false,
138
+ shiftPressed: false,
139
+ }),
140
+ ).toContainEqual({
141
+ keys: ['Shift'],
142
+ label: 'Adjust one side’s corner radius',
143
+ active: false,
144
+ })
145
+ })
120
146
  })
@@ -11,8 +11,8 @@ export type ContextualShortcutHint = {
11
11
  }
12
12
 
13
13
  // `activeHandleDrag.label` value a rotate gizmo sets while dragging, so the
14
- // contextual HUD can surface the Shift = free-rotation toggle for the duration
15
- // (mirrors how wall drafting advertises Shift). Distinct from resize handles,
14
+ // contextual HUD can surface the rotation snapping mode for the duration.
15
+ // Distinct from resize handles,
16
16
  // which route their own measurement label here.
17
17
  export const ROTATE_HANDLE_DRAG_LABEL = 'rotate-handle'
18
18
 
@@ -29,15 +29,12 @@ export const RESIZE_HANDLE_DRAG_LABEL = 'resize-handle'
29
29
  export const GROUP_MOVE_DRAG_LABEL = 'group-move-handle'
30
30
  export const GROUP_ROTATE_DRAG_LABEL = 'group-rotate-handle'
31
31
 
32
- // Hints shown while a rotate gizmo is mid-drag: Shift bypasses the angle step
33
- // (free rotation), the same toggle wall drafting exposes. `active` lights the
34
- // pill while Shift is held.
35
- export function resolveRotateHandleHelpHints(shiftPressed: boolean): ContextualShortcutHint[] {
32
+ export function resolveRotateHandleHelpHints(altPressed: boolean): ContextualShortcutHint[] {
36
33
  return [
37
34
  {
38
- keys: [SHIFT_KEY],
39
- label: shiftPressed ? 'Rotating freely (no angle step)' : 'Hold to rotate freely',
40
- active: shiftPressed,
35
+ keys: [ALT_KEY],
36
+ label: altPressed ? 'Rotating freely (no angle step)' : 'Hold to rotate freely',
37
+ active: altPressed,
41
38
  },
42
39
  ]
43
40
  }
@@ -46,6 +43,7 @@ export type SelectModeHelpContext = {
46
43
  selectedCount: number
47
44
  hasMovableSelection: boolean
48
45
  hasRotatableSelection: boolean
46
+ hasOpeningRadiusSelection?: boolean
49
47
  commandPressed: boolean
50
48
  shiftPressed: boolean
51
49
  // When a single MEP node is selected its in-world handle rig (click a dot to
@@ -67,6 +65,7 @@ export function resolveSelectModeHelpHints({
67
65
  selectedCount,
68
66
  hasMovableSelection,
69
67
  hasRotatableSelection,
68
+ hasOpeningRadiusSelection = false,
70
69
  commandPressed,
71
70
  shiftPressed,
72
71
  mepSelection = null,
@@ -142,6 +141,14 @@ export function resolveSelectModeHelpHints({
142
141
  })
143
142
  }
144
143
 
144
+ if (hasOpeningRadiusSelection) {
145
+ hints.push({
146
+ keys: [SHIFT_KEY],
147
+ label: 'Adjust one side’s corner radius',
148
+ active: shiftPressed,
149
+ })
150
+ }
151
+
145
152
  hints.push({
146
153
  keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
147
154
  label: 'Add or remove objects from the selection',
@@ -0,0 +1,167 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import * as THREE from 'three'
3
+ import type { WebGPURenderer } from 'three/webgpu'
4
+ import { createExportTextureUtils } from './export-texture-utils'
5
+
6
+ const globals = globalThis as unknown as { document?: unknown }
7
+ const previousDocument = globals.document
8
+
9
+ type FakeRenderer = {
10
+ renderer: WebGPURenderer
11
+ calls: { render: number; dispose: number; destroy: number; sizes: [number, number][] }
12
+ domElement: object
13
+ }
14
+
15
+ function fakeRenderer(): FakeRenderer {
16
+ const calls = { render: 0, dispose: 0, destroy: 0, sizes: [] as [number, number][] }
17
+ const domElement = { kind: 'renderer-canvas' }
18
+ const renderer = {
19
+ domElement,
20
+ outputColorSpace: THREE.SRGBColorSpace,
21
+ backend: {
22
+ device: {
23
+ destroy: () => {
24
+ calls.destroy += 1
25
+ },
26
+ },
27
+ },
28
+ setSize: (width: number, height: number) => {
29
+ calls.sizes.push([width, height])
30
+ },
31
+ render: () => {
32
+ calls.render += 1
33
+ },
34
+ dispose: async () => {
35
+ calls.dispose += 1
36
+ },
37
+ }
38
+ return { renderer: renderer as unknown as WebGPURenderer, calls, domElement }
39
+ }
40
+
41
+ function installCanvasDocument(drawn: object[]) {
42
+ globals.document = {
43
+ createElement: (tagName: string) => {
44
+ if (tagName !== 'canvas') throw new Error(`Unexpected element request: ${tagName}`)
45
+ return {
46
+ width: 0,
47
+ height: 0,
48
+ getContext: () => ({
49
+ drawImage: (source: object) => {
50
+ drawn.push(source)
51
+ },
52
+ }),
53
+ }
54
+ },
55
+ }
56
+ }
57
+
58
+ function compressedTexture(name: string, width = 8, height = 4): THREE.CompressedTexture {
59
+ const texture = new THREE.CompressedTexture([], width, height)
60
+ texture.name = name
61
+ texture.colorSpace = THREE.NoColorSpace
62
+ texture.wrapS = THREE.RepeatWrapping
63
+ texture.wrapT = THREE.MirroredRepeatWrapping
64
+ texture.minFilter = THREE.NearestFilter
65
+ texture.magFilter = THREE.NearestFilter
66
+ return texture
67
+ }
68
+
69
+ afterEach(() => {
70
+ globals.document = previousDocument
71
+ })
72
+
73
+ describe('createExportTextureUtils', () => {
74
+ test('starts one renderer for every compressed texture in the export and releases it once', async () => {
75
+ const drawn: object[] = []
76
+ installCanvasDocument(drawn)
77
+ const fake = fakeRenderer()
78
+ let starts = 0
79
+ const utils = createExportTextureUtils(async () => {
80
+ starts += 1
81
+ return fake.renderer
82
+ })
83
+
84
+ const readable = await Promise.all([
85
+ utils.decompress(compressedTexture('NormalGL_a')),
86
+ utils.decompress(compressedTexture('Color_b', 16, 16)),
87
+ utils.decompress(compressedTexture('Rough_c', 2, 8)),
88
+ ])
89
+
90
+ expect(starts).toBe(1)
91
+ expect(fake.calls.render).toBe(3)
92
+ expect(fake.calls.sizes).toEqual([
93
+ [8, 4],
94
+ [16, 16],
95
+ [2, 8],
96
+ ])
97
+ expect(drawn).toEqual([fake.domElement, fake.domElement, fake.domElement])
98
+ expect(fake.renderer.outputColorSpace).toBe(THREE.SRGBColorSpace)
99
+
100
+ const first = readable[0] as THREE.CanvasTexture
101
+ expect(first.isCanvasTexture).toBe(true)
102
+ expect(first.name).toBe('NormalGL_a')
103
+ expect(first.colorSpace).toBe(THREE.NoColorSpace)
104
+ expect(first.wrapS).toBe(THREE.RepeatWrapping)
105
+ expect(first.wrapT).toBe(THREE.MirroredRepeatWrapping)
106
+ expect(first.minFilter).toBe(THREE.NearestFilter)
107
+ expect(first.magFilter).toBe(THREE.NearestFilter)
108
+
109
+ await utils.dispose()
110
+ await utils.dispose()
111
+ expect(fake.calls.dispose).toBe(1)
112
+ expect(fake.calls.destroy).toBe(1)
113
+ })
114
+
115
+ test('clamps the readback to maxTextureSize', async () => {
116
+ installCanvasDocument([])
117
+ const fake = fakeRenderer()
118
+ const utils = createExportTextureUtils(async () => fake.renderer)
119
+
120
+ const readable = await utils.decompress(compressedTexture('Color', 4096, 1024), 2048)
121
+
122
+ expect(fake.calls.sizes).toEqual([[2048, 1024]])
123
+ expect((readable.image as { width: number; height: number }).width).toBe(2048)
124
+ await utils.dispose()
125
+ })
126
+
127
+ test('rejects when no GPU renderer can start instead of leaving the export pending', async () => {
128
+ installCanvasDocument([])
129
+ const utils = createExportTextureUtils(async () => {
130
+ throw new Error('no adapter')
131
+ })
132
+
133
+ await expect(utils.decompress(compressedTexture('Color'))).rejects.toThrow('no adapter')
134
+ await expect(utils.decompress(compressedTexture('Color'))).rejects.toThrow('no adapter')
135
+ await expect(utils.dispose()).resolves.toBeUndefined()
136
+ })
137
+
138
+ test('disposing without decompressing never starts a renderer', async () => {
139
+ let starts = 0
140
+ const utils = createExportTextureUtils(async () => {
141
+ starts += 1
142
+ return fakeRenderer().renderer
143
+ })
144
+
145
+ await utils.dispose()
146
+ expect(starts).toBe(0)
147
+ })
148
+
149
+ test('a decompress after dispose rejects instead of starting an unowned renderer', async () => {
150
+ installCanvasDocument([])
151
+ const fake = fakeRenderer()
152
+ let starts = 0
153
+ const utils = createExportTextureUtils(async () => {
154
+ starts += 1
155
+ return fake.renderer
156
+ })
157
+
158
+ await utils.decompress(compressedTexture('Color'))
159
+ await utils.dispose()
160
+ await expect(utils.decompress(compressedTexture('Color'))).rejects.toThrow('abandoned')
161
+ await utils.dispose()
162
+
163
+ expect(starts).toBe(1)
164
+ expect(fake.calls.dispose).toBe(1)
165
+ expect(fake.calls.destroy).toBe(1)
166
+ })
167
+ })