@pascal-app/editor 1.0.4 → 1.0.6
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.
- package/README.md +106 -0
- package/package.json +40 -38
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/bake-thumbnail.tsx +6 -15
- package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
- package/src/components/editor/construction-reveal-audio.tsx +40 -0
- package/src/components/editor/custom-camera-controls.tsx +81 -8
- package/src/components/editor/editor-layout-mobile.tsx +1 -1
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/export-manager.tsx +14 -19
- package/src/components/editor/first-person/build-collider-world.test.ts +269 -0
- package/src/components/editor/first-person/build-collider-world.ts +22 -5
- package/src/components/editor/first-person/ground-holes.test.ts +128 -0
- package/src/components/editor/first-person/ground-holes.ts +130 -0
- package/src/components/editor/first-person/mechanism-targets.test.ts +89 -2
- package/src/components/editor/first-person/mechanism-targets.ts +52 -0
- package/src/components/editor/first-person-controls.tsx +19 -11
- package/src/components/editor/floor-aware-camera-controls.test.ts +24 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +72 -23
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/group-actions.ts +2 -0
- package/src/components/editor/group-move-3d.ts +2 -0
- package/src/components/editor/group-rotate-handle.tsx +2 -0
- package/src/components/editor/group-transform-shared.test.ts +460 -0
- package/src/components/editor/index.tsx +166 -79
- package/src/components/editor/riser-diagram-panel.tsx +14 -7
- package/src/components/editor/room-highlight.tsx +13 -5
- package/src/components/editor/room-labels.test.ts +621 -128
- package/src/components/editor/room-labels.tsx +1090 -190
- package/src/components/editor/selection-manager.tsx +38 -117
- package/src/components/editor/slab-hole-highlights.tsx +2 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +3 -2
- package/src/components/editor/snapshot-capture.test.ts +69 -58
- package/src/components/editor/snapshot-capture.ts +7 -1
- package/src/components/editor/thumbnail-generator.test.ts +350 -4
- package/src/components/editor/thumbnail-generator.tsx +128 -54
- package/src/components/editor/use-floorplan-background-placement.ts +3 -1
- package/src/components/editor/views/plugins-view.tsx +1 -8
- package/src/components/editor/views/view-bar.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +140 -17
- package/src/components/editor/wall-snap-beacon-layer.tsx +5 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +3 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +1 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +29 -7
- package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
- package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
- package/src/components/point-ask/bubble.tsx +538 -0
- package/src/components/point-ask/hover-chip.tsx +91 -0
- package/src/components/point-ask/mode-pill.tsx +92 -0
- package/src/components/point-ask/optional-motion.ts +22 -0
- package/src/components/point-ask/outline-corners.tsx +93 -0
- package/src/components/point-ask/pins.tsx +711 -0
- package/src/components/point-ask/point-ask-layer.tsx +83 -0
- package/src/components/point-ask/point-ask-styles.ts +105 -0
- package/src/components/point-ask/region-marquee-styles.ts +15 -0
- package/src/components/point-ask/region-marquee.tsx +64 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- package/src/components/systems/roof/roof-edit-system.tsx +1 -0
- package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
- package/src/components/tools/distribution/connection-compatibility.ts +43 -0
- package/src/components/tools/distribution/distribution-public.ts +9 -0
- package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
- package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
- package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
- package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
- package/src/components/tools/distribution/draw-alignment.ts +35 -0
- package/src/components/tools/distribution/ports.test.ts +56 -0
- package/src/components/tools/distribution/ports.ts +378 -0
- package/src/components/tools/distribution/run-cursor.test.ts +135 -0
- package/src/components/tools/distribution/run-cursor.ts +36 -0
- package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
- package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
- package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
- package/src/components/tools/distribution/run-port-snap.ts +25 -0
- package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
- package/src/components/tools/distribution/run-screen-direction.ts +30 -0
- package/src/components/tools/elevator/elevator-tool.tsx +3 -6
- package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
- package/src/components/tools/item/placement-history.test.tsx +38 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +3 -6
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- package/src/components/tools/item/use-draft-node.test.tsx +94 -94
- package/src/components/tools/item/use-placement-coordinator.tsx +55 -49
- package/src/components/tools/registry/item-surface-move.test.ts +62 -63
- package/src/components/tools/registry/item-surface-move.ts +2 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
- package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +63 -29
- package/src/components/tools/tool-manager.tsx +14 -5
- package/src/components/tools/wall/wall-drafting.test.ts +170 -57
- package/src/components/ui/action-menu/action-button.tsx +6 -1
- package/src/components/ui/action-menu/camera-actions.tsx +7 -3
- package/src/components/ui/action-menu/control-modes.tsx +13 -7
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
- package/src/components/ui/action-menu/index.tsx +56 -33
- package/src/components/ui/action-menu/measurement-control.tsx +11 -9
- package/src/components/ui/action-menu/placement.ts +20 -0
- package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
- package/src/components/ui/action-menu/view-toggles.tsx +47 -42
- package/src/components/ui/build-panel/build-panel.tsx +10 -3
- package/src/components/ui/command-palette/editor-commands.tsx +3 -12
- package/src/components/ui/command-palette/index.tsx +45 -15
- package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
- package/src/components/ui/controls/action-button.tsx +5 -2
- package/src/components/ui/controls/metric-control.tsx +1 -1
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/slider-control.tsx +1 -1
- package/src/components/ui/controls/toggle-control.tsx +3 -3
- package/src/components/ui/controls/tool-options-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +150 -45
- package/src/components/ui/helpers/contextual-helper-panel.tsx +32 -7
- package/src/components/ui/helpers/helper-manager.test.tsx +54 -2
- package/src/components/ui/helpers/helper-manager.tsx +7 -1
- package/src/components/ui/helpers/item-helper.tsx +9 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +8 -8
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +6 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +120 -3
- package/src/components/ui/panels/panel-wrapper.tsx +53 -27
- package/src/components/ui/panels/parametric-field-control.tsx +2 -2
- package/src/components/ui/panels/parametric-inspector.tsx +3 -3
- package/src/components/ui/panels/room-panel.tsx +65 -0
- package/src/components/ui/primitives/badge.tsx +8 -0
- package/src/components/ui/primitives/sidebar.tsx +1 -1
- package/src/components/ui/right-stack.tsx +21 -2
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +3 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
- package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +107 -23
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +5 -12
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -10
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/tab-bar.tsx +4 -4
- package/src/components/viewer/viewer-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-rooms.tsx +16 -0
- package/src/hooks/use-auto-save.integration.test.tsx +96 -38
- package/src/hooks/use-auto-save.ts +26 -1
- package/src/hooks/use-ceiling-events.test.ts +123 -118
- package/src/hooks/use-grid-events.test.ts +105 -105
- package/src/hooks/use-keyboard.test.ts +65 -16
- package/src/hooks/use-keyboard.ts +29 -12
- package/src/hooks/use-placement-notice.test.ts +24 -1
- package/src/hooks/use-placement-notice.ts +37 -6
- package/src/index.tsx +71 -24
- package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
- package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
- package/src/lib/add-storey.test.ts +177 -0
- package/src/lib/add-storey.ts +59 -0
- package/src/lib/below-grade.test.tsx +171 -0
- package/src/lib/below-grade.ts +133 -0
- package/src/lib/camera-facing.test.ts +24 -0
- package/src/lib/camera-facing.ts +24 -0
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.ts +9 -2
- package/src/lib/editor-views.ts +35 -1
- package/src/lib/export-texture-utils.ts +2 -4
- package/src/lib/floor-foundation-support.test.ts +1 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +31 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +39 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +4 -0
- package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
- package/src/lib/floorplan/sheet-export.test.ts +24 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +34 -45
- package/src/lib/floorplan/site-plan/coverage.ts +2 -4
- package/src/lib/floorplan/site-plan/front-edge.ts +2 -1
- package/src/lib/floorplan/site-plan/geometry.test.ts +9 -15
- package/src/lib/floorplan/site-plan/geometry.ts +1 -416
- package/src/lib/floorplan/site-plan/index.ts +0 -16
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +117 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -2
- package/src/lib/floorplan/site-plan/site-parts.ts +14 -106
- package/src/lib/floorplan/site-plan/yards.test.ts +2 -1
- package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
- package/src/lib/follow-pascal-exterior.test.ts +171 -0
- package/src/lib/follow-pascal-exterior.ts +39 -0
- package/src/lib/follow-pascal-presentation.test.ts +203 -0
- package/src/lib/follow-pascal-presentation.ts +129 -0
- package/src/lib/follow-pascal-sight.ts +84 -0
- package/src/lib/follow-pascal-targets.test.ts +406 -0
- package/src/lib/follow-pascal-targets.ts +198 -0
- package/src/lib/follow-pascal.test.ts +350 -0
- package/src/lib/follow-pascal.ts +284 -0
- package/src/lib/fresh-planar-placement.test.ts +112 -0
- package/src/lib/fresh-planar-placement.ts +57 -3
- package/src/lib/geometry-script/author.ts +6 -1
- package/src/lib/geometry-script/client.ts +8 -1
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +1 -0
- package/src/lib/glb-export.test.ts +187 -162
- package/src/lib/glb-export.ts +4 -1
- package/src/lib/history.test.ts +12 -7
- package/src/lib/interaction/scope.ts +4 -0
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- package/src/lib/lot/clean-ring.test.ts +2 -1
- package/src/lib/lot/drop-in.ts +9 -24
- package/src/lib/lot/lot-patch.test.ts +1 -1
- package/src/lib/lot/lot-patch.ts +2 -2
- package/src/lib/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -0
- package/src/lib/orbit-floor-clearance.test.ts +36 -0
- package/src/lib/orbit-floor-clearance.ts +48 -0
- package/src/lib/paint-pass-through.test.ts +11 -0
- package/src/lib/paint-pass-through.ts +4 -2
- package/src/lib/paint-scope.ts +1 -0
- package/src/lib/parametric-node-update.ts +16 -7
- package/src/lib/plugin-chat.ts +15 -0
- package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
- package/src/lib/point-ask/__fixtures__/house.ts +81 -0
- package/src/lib/point-ask/ask-created.test.ts +195 -0
- package/src/lib/point-ask/ask-flow.test.ts +380 -0
- package/src/lib/point-ask/build-context.test.ts +173 -0
- package/src/lib/point-ask/build-context.ts +94 -0
- package/src/lib/point-ask/capture-crop.ts +129 -0
- package/src/lib/point-ask/choreography.ts +69 -0
- package/src/lib/point-ask/compare.test.ts +145 -0
- package/src/lib/point-ask/compare.ts +73 -0
- package/src/lib/point-ask/describe-target.test.ts +165 -0
- package/src/lib/point-ask/describe-target.ts +234 -0
- package/src/lib/point-ask/dictation.ts +79 -0
- package/src/lib/point-ask/edge-marker.test.ts +61 -0
- package/src/lib/point-ask/edge-marker.ts +27 -0
- package/src/lib/point-ask/frame-loop.ts +25 -0
- package/src/lib/point-ask/history-marks.test.ts +94 -0
- package/src/lib/point-ask/history-marks.ts +39 -0
- package/src/lib/point-ask/host-api.ts +32 -0
- package/src/lib/point-ask/outline-box.test.ts +30 -0
- package/src/lib/point-ask/outline-box.ts +11 -0
- package/src/lib/point-ask/overlay-metrics.ts +6 -0
- package/src/lib/point-ask/pick.test.ts +220 -0
- package/src/lib/point-ask/pick.ts +121 -0
- package/src/lib/point-ask/pin-machine.test.ts +494 -0
- package/src/lib/point-ask/pin-machine.ts +217 -0
- package/src/lib/point-ask/placement.test.ts +203 -0
- package/src/lib/point-ask/placement.ts +141 -0
- package/src/lib/point-ask/pointer-tracker.ts +34 -0
- package/src/lib/point-ask/projector.ts +83 -0
- package/src/lib/point-ask/region.test.ts +157 -0
- package/src/lib/point-ask/region.ts +136 -0
- package/src/lib/point-ask/retry.test.ts +217 -0
- package/src/lib/point-ask/retry.ts +37 -0
- package/src/lib/point-ask/scene-commit.test.ts +312 -0
- package/src/lib/point-ask/scene-commit.ts +32 -0
- package/src/lib/point-ask/session.ts +1391 -0
- package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
- package/src/lib/point-ask/spring-keyframes.ts +38 -0
- package/src/lib/point-ask/spring.test.ts +63 -0
- package/src/lib/point-ask/spring.ts +107 -0
- package/src/lib/point-ask/suggestions.test.ts +80 -0
- package/src/lib/point-ask/suggestions.ts +125 -0
- package/src/lib/point-ask/target-scope.test.ts +622 -0
- package/src/lib/point-ask/target-scope.ts +338 -0
- package/src/lib/point-ask/types.ts +48 -0
- package/src/lib/portable-export.test.ts +34 -1
- package/src/lib/portable-export.ts +119 -53
- package/src/lib/print-feature-thickness.ts +10 -1
- package/src/lib/print-roof-solids.test.ts +39 -1
- package/src/lib/print-roof-solids.ts +43 -10
- package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
- package/src/lib/room-assembly-overlay.ts +2 -3
- package/src/lib/room-selection.test.ts +45 -1
- package/src/lib/scene-clipboard.test.ts +246 -1
- package/src/lib/scene-clipboard.ts +44 -14
- package/src/lib/selection-routing.ts +1 -1
- package/src/lib/sfx-bus.ts +13 -0
- package/src/lib/sfx-player.test.ts +70 -9
- package/src/lib/sfx-player.ts +6 -0
- package/src/lib/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.ts +2 -1
- package/src/lib/viewer-selection.ts +22 -11
- package/src/lib/wall-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -0
- package/src/lib/window-changes.ts +35 -0
- package/src/store/use-audio.test.ts +146 -0
- package/src/store/use-audio.tsx +127 -3
- package/src/store/use-construction-reveal.test.ts +173 -0
- package/src/store/use-construction-reveal.ts +145 -0
- package/src/store/use-corner-snap-hint.test.ts +24 -0
- package/src/store/use-corner-snap-hint.ts +47 -0
- package/src/store/use-follow-pascal.test.ts +62 -0
- package/src/store/use-follow-pascal.ts +53 -0
- package/src/store/use-point-ask.ts +247 -0
- package/src/store/use-stair-build-preview.ts +25 -5
- package/src/store/view-layout.test.ts +27 -1
- package/src/store/view-layout.ts +4 -1
- package/src/test-utils/render-current-markup.tsx +16 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
- package/src/components/tools/stair/stair-click-guard.ts +0 -72
- package/src/components/tools/stair/stair-defaults.ts +0 -15
- package/src/components/tools/stair/stair-tool.tsx +0 -682
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from 'bun:test'
|
|
|
2
2
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
3
|
import { SELECT_HUD_TITLE, toolHudTitle } from '../../../lib/hud-title'
|
|
4
4
|
import { snapContextOf } from '../../../lib/snapping-mode'
|
|
5
|
+
import { cornerHintRows } from '../../../store/use-corner-snap-hint'
|
|
5
6
|
import { TooltipProvider } from '../primitives/tooltip'
|
|
6
7
|
import { ContextualHelperPanel } from './contextual-helper-panel'
|
|
7
8
|
import { ROOM_DIVIDE_HINTS } from './helper-manager'
|
|
@@ -18,7 +19,7 @@ describe('Divide HUD', () => {
|
|
|
18
19
|
).toBe('wall')
|
|
19
20
|
})
|
|
20
21
|
|
|
21
|
-
test('
|
|
22
|
+
test('shows snapping controls and the primary gesture with expandable secondary shortcuts', () => {
|
|
22
23
|
// Server rendering reads each store's initial state: the wall context starts
|
|
23
24
|
// on Grid with a 0.5 m step.
|
|
24
25
|
const html = renderToStaticMarkup(
|
|
@@ -28,8 +29,11 @@ describe('Divide HUD', () => {
|
|
|
28
29
|
)
|
|
29
30
|
expect(html).toContain('Snapping: Grid')
|
|
30
31
|
expect(html).toContain('Grid: 0.50 m')
|
|
31
|
-
for (const label of ['Add point', '
|
|
32
|
+
for (const label of ['Add point', 'More shortcuts', 'Cancel'])
|
|
32
33
|
expect(html).toContain(label)
|
|
34
|
+
expect(html).toContain('aria-expanded="false"')
|
|
35
|
+
for (const label of ['Remove last point', 'Finish at the edge'])
|
|
36
|
+
expect(html).not.toContain(label)
|
|
33
37
|
expect(ROOM_DIVIDE_HINTS.map((hint) => hint.keys)).toEqual([
|
|
34
38
|
['Left click'],
|
|
35
39
|
['Backspace'],
|
|
@@ -37,6 +41,54 @@ describe('Divide HUD', () => {
|
|
|
37
41
|
['Esc'],
|
|
38
42
|
])
|
|
39
43
|
})
|
|
44
|
+
|
|
45
|
+
test('keeps active secondary gestures visible while shortcuts are collapsed', () => {
|
|
46
|
+
const html = renderToStaticMarkup(
|
|
47
|
+
<TooltipProvider>
|
|
48
|
+
<ContextualHelperPanel
|
|
49
|
+
hints={ROOM_DIVIDE_HINTS.map((hint) => ({
|
|
50
|
+
...hint,
|
|
51
|
+
active: hint.keys.includes('Enter'),
|
|
52
|
+
}))}
|
|
53
|
+
snapContext="wall"
|
|
54
|
+
/>
|
|
55
|
+
</TooltipProvider>,
|
|
56
|
+
)
|
|
57
|
+
for (const label of ['Add point', 'Finish at the edge', 'Cancel'])
|
|
58
|
+
expect(html).toContain(label)
|
|
59
|
+
expect(html).not.toContain('Remove last point')
|
|
60
|
+
expect(html).toContain('aria-expanded="false"')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('keeps a note row visible while shortcuts are collapsed', () => {
|
|
64
|
+
const html = renderToStaticMarkup(
|
|
65
|
+
<TooltipProvider>
|
|
66
|
+
<ContextualHelperPanel
|
|
67
|
+
hints={[
|
|
68
|
+
{ keys: ['Left click'], label: 'Place' },
|
|
69
|
+
...cornerHintRows('free_end'),
|
|
70
|
+
{ keys: ['R', 'T'], label: 'Rotate' },
|
|
71
|
+
{ keys: ['Esc'], label: 'Cancel' },
|
|
72
|
+
]}
|
|
73
|
+
/>
|
|
74
|
+
</TooltipProvider>,
|
|
75
|
+
)
|
|
76
|
+
for (const label of ['Place', 'No wall meets here', 'More shortcuts', 'Cancel'])
|
|
77
|
+
expect(html).toContain(label)
|
|
78
|
+
expect(html).not.toContain('Rotate')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('offers More shortcuts only when collapsing hides a row', () => {
|
|
82
|
+
const html = renderToStaticMarkup(
|
|
83
|
+
<TooltipProvider>
|
|
84
|
+
<ContextualHelperPanel
|
|
85
|
+
hints={[{ keys: ['Left click'], label: 'Place' }, ...cornerHintRows('free_end')]}
|
|
86
|
+
/>
|
|
87
|
+
</TooltipProvider>,
|
|
88
|
+
)
|
|
89
|
+
expect(html).toContain('No wall meets here')
|
|
90
|
+
expect(html).not.toContain('More shortcuts')
|
|
91
|
+
})
|
|
40
92
|
})
|
|
41
93
|
|
|
42
94
|
describe('HUD header', () => {
|
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
usePaintRegionMode,
|
|
67
67
|
} from '../../../lib/paint-region-mode'
|
|
68
68
|
import { snapContextOf } from '../../../lib/snapping-mode'
|
|
69
|
+
import { cornerHintRows, useCornerSnapHint } from '../../../store/use-corner-snap-hint'
|
|
69
70
|
import useEditor, { getActiveContinuationContext } from '../../../store/use-editor'
|
|
70
71
|
import useInteractionScope, {
|
|
71
72
|
useActiveHandleDrag,
|
|
@@ -309,10 +310,11 @@ export function HelperManager() {
|
|
|
309
310
|
const openingShape = useOpeningDraft((s) => (s.host ? s.shape : null))
|
|
310
311
|
const terraceShape = useTerraceDraft((s) => (s.host ? s.shape : null))
|
|
311
312
|
const movingNode = useMovingNode()
|
|
313
|
+
const cornerHint = useCornerSnapHint((s) => s.hint)
|
|
312
314
|
// A tool whose kind gives placement notices: its transient draft is the node in hand.
|
|
313
315
|
const toolNotices = !!(tool && nodeRegistry.get(tool)?.placementNotice)
|
|
314
316
|
const toolDraftId = useScene((s) => (toolNotices ? transientDraftId(s.nodes) : null))
|
|
315
|
-
const placementNotice = usePlacementNotice(movingNode?.id ?? toolDraftId)
|
|
317
|
+
const placementNotice = usePlacementNotice(movingNode?.id ?? toolDraftId, toolNotices)
|
|
316
318
|
const reshapingNode = useReshapingNode()
|
|
317
319
|
const activeHandleDrag = useActiveHandleDrag()
|
|
318
320
|
const mezzanineHandle = useScene((s) => mezzanineGesture(activeHandleDrag, s.nodes))
|
|
@@ -560,6 +562,10 @@ export function HelperManager() {
|
|
|
560
562
|
continuationContext={movingContinuationContext}
|
|
561
563
|
notice={placementNotice?.line}
|
|
562
564
|
showEsc
|
|
565
|
+
extraHints={movingNode.type === 'window' ? cornerHintRows(cornerHint) : []}
|
|
566
|
+
replacesPlace={
|
|
567
|
+
movingNode.type === 'window' && (cornerHint === 'join' || cornerHint === 'new')
|
|
568
|
+
}
|
|
563
569
|
showForce={collisionValidatesDrop}
|
|
564
570
|
snapContext={snapContext}
|
|
565
571
|
title={movingTitle}
|
|
@@ -15,6 +15,10 @@ interface ItemHelperProps {
|
|
|
15
15
|
title?: HudTitle | null
|
|
16
16
|
// Why the item would not fit where it is (a door's way, a room too small): a warning, not a block.
|
|
17
17
|
notice?: string | null
|
|
18
|
+
// Rows a kind adds while it is in hand (a window near a corner says what dropping it does).
|
|
19
|
+
extraHints?: { keys: string[]; label: string }[]
|
|
20
|
+
// The extra rows say what a click does, so the plain Place row is left out.
|
|
21
|
+
replacesPlace?: boolean
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
// Snapping mode is the chip on the right (Shift cycles it), so it's not repeated
|
|
@@ -26,13 +30,17 @@ export function ItemHelper({
|
|
|
26
30
|
continuationContext = null,
|
|
27
31
|
title = null,
|
|
28
32
|
notice = null,
|
|
33
|
+
extraHints = [],
|
|
34
|
+
replacesPlace = false,
|
|
29
35
|
}: ItemHelperProps) {
|
|
30
36
|
return (
|
|
31
37
|
<ContextualHelperPanel
|
|
32
38
|
continuationContext={continuationContext}
|
|
33
39
|
notice={notice}
|
|
34
40
|
hints={[
|
|
35
|
-
|
|
41
|
+
// Where a click does something else (joining a corner), that row says so instead.
|
|
42
|
+
...(replacesPlace ? [] : [{ keys: ['Left click'], label: 'Place' }]),
|
|
43
|
+
...extraHints,
|
|
36
44
|
{ keys: ['R', 'T'], label: 'Rotate' },
|
|
37
45
|
...(showForce ? [{ keys: ['Alt'], label: 'Force place' }] : []),
|
|
38
46
|
{ keys: [showEsc ? 'Esc' : 'Right click'], label: 'Cancel' },
|
|
@@ -106,7 +106,7 @@ export function CollectionsPopover({ nodeIds, children }: CollectionsPopoverProp
|
|
|
106
106
|
</span>
|
|
107
107
|
</div>
|
|
108
108
|
<button
|
|
109
|
-
className="flex items-center gap-1 rounded-md px-2 py-1 font-medium text-[11px] text-muted-foreground transition-colors hover:bg-white/10 hover:text-foreground"
|
|
109
|
+
className="flex items-center gap-1 rounded-md px-2 py-1 font-medium text-[11px] text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
|
|
110
110
|
onClick={() => {
|
|
111
111
|
setShowCreateInput((v) => !v)
|
|
112
112
|
setCreateName('')
|
|
@@ -120,7 +120,7 @@ export function CollectionsPopover({ nodeIds, children }: CollectionsPopoverProp
|
|
|
120
120
|
|
|
121
121
|
{/* Create input */}
|
|
122
122
|
{showCreateInput && (
|
|
123
|
-
<div className="flex items-center gap-1.5 border-border/50 border-b bg-white/5 px-3 py-2">
|
|
123
|
+
<div className="flex items-center gap-1.5 border-border/50 border-b bg-black/4 dark:bg-white/5 px-3 py-2">
|
|
124
124
|
<input
|
|
125
125
|
autoFocus
|
|
126
126
|
className="min-w-0 flex-1 rounded-md border border-border/50 bg-background/50 px-2 py-1 text-foreground text-xs outline-none placeholder:text-muted-foreground/60 focus:border-ring focus:ring-1 focus:ring-ring/30"
|
|
@@ -144,7 +144,7 @@ export function CollectionsPopover({ nodeIds, children }: CollectionsPopoverProp
|
|
|
144
144
|
<Check className="h-3.5 w-3.5" />
|
|
145
145
|
</button>
|
|
146
146
|
<button
|
|
147
|
-
className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-white/10"
|
|
147
|
+
className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10"
|
|
148
148
|
onClick={() => {
|
|
149
149
|
setShowCreateInput(false)
|
|
150
150
|
setCreateName('')
|
|
@@ -186,7 +186,7 @@ export function CollectionsPopover({ nodeIds, children }: CollectionsPopoverProp
|
|
|
186
186
|
</span>
|
|
187
187
|
<div className="flex shrink-0 items-center gap-1">
|
|
188
188
|
<button
|
|
189
|
-
className="rounded-md bg-red-500/20 px-2 py-0.5 font-medium text-[11px] text-red-400 transition-colors hover:bg-red-500/30"
|
|
189
|
+
className="rounded-md bg-red-500/20 px-2 py-0.5 font-medium text-[11px] text-red-600 dark:text-red-400 transition-colors hover:bg-red-500/30"
|
|
190
190
|
onClick={() => {
|
|
191
191
|
deleteCollection(collection.id)
|
|
192
192
|
setDeletingId(null)
|
|
@@ -196,7 +196,7 @@ export function CollectionsPopover({ nodeIds, children }: CollectionsPopoverProp
|
|
|
196
196
|
Delete
|
|
197
197
|
</button>
|
|
198
198
|
<button
|
|
199
|
-
className="rounded-md px-2 py-0.5 font-medium text-[11px] text-muted-foreground transition-colors hover:bg-white/10"
|
|
199
|
+
className="rounded-md px-2 py-0.5 font-medium text-[11px] text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10"
|
|
200
200
|
onClick={() => setDeletingId(null)}
|
|
201
201
|
type="button"
|
|
202
202
|
>
|
|
@@ -229,7 +229,7 @@ export function CollectionsPopover({ nodeIds, children }: CollectionsPopoverProp
|
|
|
229
229
|
<Check className="h-3.5 w-3.5" />
|
|
230
230
|
</button>
|
|
231
231
|
<button
|
|
232
|
-
className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-white/10"
|
|
232
|
+
className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10"
|
|
233
233
|
onClick={() => setRenamingId(null)}
|
|
234
234
|
type="button"
|
|
235
235
|
>
|
|
@@ -241,7 +241,7 @@ export function CollectionsPopover({ nodeIds, children }: CollectionsPopoverProp
|
|
|
241
241
|
|
|
242
242
|
return (
|
|
243
243
|
<li key={collection.id}>
|
|
244
|
-
<div className="group flex items-center gap-2 px-3 py-2 transition-colors hover:bg-white/5">
|
|
244
|
+
<div className="group flex items-center gap-2 px-3 py-2 transition-colors hover:bg-black/4 dark:hover:bg-white/5">
|
|
245
245
|
{/* Color dot — click to pick color */}
|
|
246
246
|
<ColorDot
|
|
247
247
|
color={collection.color ?? '#6366f1'}
|
|
@@ -299,7 +299,7 @@ export function CollectionsPopover({ nodeIds, children }: CollectionsPopoverProp
|
|
|
299
299
|
<DropdownMenu modal={false}>
|
|
300
300
|
<DropdownMenuTrigger asChild>
|
|
301
301
|
<button
|
|
302
|
-
className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground opacity-0 transition-colors hover:bg-white/10 hover:text-foreground group-hover:opacity-100"
|
|
302
|
+
className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground opacity-0 transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground group-hover:opacity-100"
|
|
303
303
|
type="button"
|
|
304
304
|
>
|
|
305
305
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
|
@@ -56,7 +56,7 @@ export function MobilePanelSheet({ open, onClose, icon, title, children }: Mobil
|
|
|
56
56
|
{open && (
|
|
57
57
|
<motion.div
|
|
58
58
|
animate={{ y: 0 }}
|
|
59
|
-
className="
|
|
59
|
+
className="fixed right-0 bottom-0 left-0 z-[60] flex flex-col overflow-hidden rounded-t-2xl bg-sidebar text-sidebar-foreground shadow-[0_-8px_24px_rgba(0,0,0,0.24)]"
|
|
60
60
|
drag="y"
|
|
61
61
|
dragConstraints={{ top: 0, bottom: 0 }}
|
|
62
62
|
dragElastic={{ top: 0, bottom: 0.4 }}
|
|
@@ -89,7 +89,7 @@ export function MobilePanelSheet({ open, onClose, icon, title, children }: Mobil
|
|
|
89
89
|
</div>
|
|
90
90
|
<button
|
|
91
91
|
aria-label="Close"
|
|
92
|
-
className="flex h-8 w-8 items-center justify-center rounded-md bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
|
|
92
|
+
className="flex h-8 w-8 items-center justify-center rounded-md bg-black/4 dark:bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-black/8 dark:hover:bg-[#3e3e3e] hover:text-foreground"
|
|
93
93
|
onClick={onClose}
|
|
94
94
|
type="button"
|
|
95
95
|
>
|
|
@@ -18,7 +18,7 @@ interface MobileSelectionBarProps {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const ACTION_BTN =
|
|
21
|
-
'flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-white/8 hover:text-foreground'
|
|
21
|
+
'flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-black/5 dark:hover:bg-white/8 hover:text-foreground'
|
|
22
22
|
|
|
23
23
|
export function MobileSelectionBar({
|
|
24
24
|
node,
|
|
@@ -40,7 +40,7 @@ export function MobileSelectionBar({
|
|
|
40
40
|
<button
|
|
41
41
|
aria-label={`Edit ${resolvedLabel}`}
|
|
42
42
|
className={cn(
|
|
43
|
-
'flex min-w-0 flex-1 items-center gap-2 rounded-lg px-2 text-left transition-colors hover:bg-white/8',
|
|
43
|
+
'flex min-w-0 flex-1 items-center gap-2 rounded-lg px-2 text-left transition-colors hover:bg-black/5 dark:hover:bg-white/8',
|
|
44
44
|
)}
|
|
45
45
|
onClick={onEdit}
|
|
46
46
|
type="button"
|
|
@@ -80,7 +80,10 @@ export function MobileSelectionBar({
|
|
|
80
80
|
</button>
|
|
81
81
|
<button
|
|
82
82
|
aria-label="Delete"
|
|
83
|
-
className={cn(
|
|
83
|
+
className={cn(
|
|
84
|
+
ACTION_BTN,
|
|
85
|
+
'hover:bg-red-500/15 hover:text-red-600 dark:hover:bg-red-500/15 dark:hover:text-red-400',
|
|
86
|
+
)}
|
|
84
87
|
onClick={(e) => {
|
|
85
88
|
stop(e)
|
|
86
89
|
onDelete()
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
type AnyNode,
|
|
4
5
|
type AnyNodeId,
|
|
5
6
|
type BuildingNode,
|
|
6
7
|
resolveBuildingForLevel,
|
|
8
|
+
runAsSingleSceneHistoryStep,
|
|
7
9
|
useScene,
|
|
8
10
|
} from '@pascal-app/core'
|
|
11
|
+
import { unwrapCorner, windowMergeOffer } from '@pascal-app/core/building'
|
|
9
12
|
import { useViewer } from '@pascal-app/viewer'
|
|
10
|
-
import { Building2, Copy, Group, Trash2, Ungroup } from 'lucide-react'
|
|
13
|
+
import { Building2, Copy, Group, Merge, Split, Trash2, Ungroup } from 'lucide-react'
|
|
11
14
|
import { useMemo } from 'react'
|
|
12
15
|
import { deleteSelection, duplicateSelectionAndPickUp } from '../../editor/group-actions'
|
|
16
|
+
import { formatLinearMeasurement } from '../../../lib/measurements'
|
|
17
|
+
import { triggerSFX } from '../../../lib/sfx-bus'
|
|
18
|
+
import { writeWindowChanges } from '../../../lib/window-changes'
|
|
13
19
|
import { collectSelectableCandidateIds } from '../../tools/select/select-candidates'
|
|
14
20
|
import {
|
|
15
21
|
canCreateSessionGroup,
|
|
@@ -43,6 +49,116 @@ function selectBuilding(buildingId: AnyNodeId) {
|
|
|
43
49
|
useViewer.getState().setSelection({ buildingId: buildingId as BuildingNode['id'] })
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Why two selected windows will not merge, for the person who selected them: the refusal's own
|
|
54
|
+
* message is the agents' (node ids, metres), so the panel words its code, in the editor's units.
|
|
55
|
+
*/
|
|
56
|
+
function mergeRefusalNote(
|
|
57
|
+
refusal: { code: string; details: Record<string, unknown> },
|
|
58
|
+
nodes: Record<string, AnyNode>,
|
|
59
|
+
length: (meters: number) => string,
|
|
60
|
+
): string {
|
|
61
|
+
const { blockingId, limit } = refusal.details as { blockingId?: string; limit?: number }
|
|
62
|
+
const reach = length(limit ?? 0)
|
|
63
|
+
const blocking = nodes[blockingId ?? '']
|
|
64
|
+
const blockingKind = !blocking
|
|
65
|
+
? 'something'
|
|
66
|
+
: blocking.type === 'item'
|
|
67
|
+
? 'an item'
|
|
68
|
+
: `a ${blocking.type}`
|
|
69
|
+
switch (refusal.code) {
|
|
70
|
+
case 'window_already_joined':
|
|
71
|
+
return 'One of them already wraps another corner. Unwrap it first.'
|
|
72
|
+
case 'window_not_on_wall':
|
|
73
|
+
return 'A roof or dormer window cannot join another window.'
|
|
74
|
+
case 'windows_not_adjacent':
|
|
75
|
+
return `They are too far apart. Windows merge with at most ${reach} between them.`
|
|
76
|
+
case 'opening_between':
|
|
77
|
+
return `There is ${blockingKind} between them.`
|
|
78
|
+
case 'walls_not_meeting':
|
|
79
|
+
return 'Their walls do not meet at a corner.'
|
|
80
|
+
case 'corner_angle':
|
|
81
|
+
return 'Their walls meet at too sharp or too flat an angle for a corner window.'
|
|
82
|
+
case 'window_not_at_corner':
|
|
83
|
+
return `One of them is too far from the corner. A window joins one within ${reach} of it.`
|
|
84
|
+
case 'height_exceeds_wall':
|
|
85
|
+
return 'The window selected first is taller than the other wall. Select the other one first.'
|
|
86
|
+
case 'opening_overlap':
|
|
87
|
+
return `Sliding them to the corner would overlap ${blockingKind}.`
|
|
88
|
+
default:
|
|
89
|
+
return 'These windows cannot be merged.'
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Merge windows: with exactly two windows selected, one window across both, or one corner window
|
|
95
|
+
* when they sit on two walls that meet. Windows ride their wall, so they have no floating group
|
|
96
|
+
* menu, and this is where the wall merge's twin lives. Two that already wrap their corner get
|
|
97
|
+
* Unwrap instead; two that cannot join, the reason.
|
|
98
|
+
*/
|
|
99
|
+
function MergeWindowsAction() {
|
|
100
|
+
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
101
|
+
const nodes = useScene((s) => s.nodes)
|
|
102
|
+
const readOnly = useScene((s) => s.readOnly)
|
|
103
|
+
const unit = useViewer((s) => s.unit)
|
|
104
|
+
const metricNotation = useViewer((s) => s.metricNotation)
|
|
105
|
+
const offer = useMemo(() => windowMergeOffer(nodes, selectedIds), [nodes, selectedIds])
|
|
106
|
+
if (!offer) return null
|
|
107
|
+
if (offer.refusal?.code === 'windows_already_joined')
|
|
108
|
+
return (
|
|
109
|
+
<ActionButton
|
|
110
|
+
className="basis-full whitespace-nowrap disabled:cursor-not-allowed disabled:opacity-40"
|
|
111
|
+
disabled={readOnly}
|
|
112
|
+
icon={<Split className="h-4 w-4" />}
|
|
113
|
+
label="Unwrap the corner"
|
|
114
|
+
onClick={() => {
|
|
115
|
+
writeWindowChanges({
|
|
116
|
+
create: null,
|
|
117
|
+
updates: unwrapCorner(useScene.getState().nodes, selectedIds[0]!),
|
|
118
|
+
remove: [],
|
|
119
|
+
})
|
|
120
|
+
triggerSFX('sfx:structure-build')
|
|
121
|
+
}}
|
|
122
|
+
title="Unwrap the corner: both windows stay, each framed at the corner"
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
const { merge } = offer
|
|
126
|
+
const note = offer.refusal
|
|
127
|
+
? mergeRefusalNote(offer.refusal, nodes, (meters) =>
|
|
128
|
+
formatLinearMeasurement(meters, unit, metricNotation),
|
|
129
|
+
)
|
|
130
|
+
: null
|
|
131
|
+
return (
|
|
132
|
+
<>
|
|
133
|
+
<ActionButton
|
|
134
|
+
className="basis-full whitespace-nowrap disabled:cursor-not-allowed disabled:opacity-40"
|
|
135
|
+
disabled={readOnly || !merge}
|
|
136
|
+
icon={<Merge className="h-4 w-4" />}
|
|
137
|
+
label="Merge windows"
|
|
138
|
+
onClick={() => {
|
|
139
|
+
if (!merge) return
|
|
140
|
+
runAsSingleSceneHistoryStep(useScene, () =>
|
|
141
|
+
writeWindowChanges({
|
|
142
|
+
create: null,
|
|
143
|
+
updates: Object.fromEntries(merge.changes.update.map(({ id, data }) => [id, data])),
|
|
144
|
+
remove: merge.changes.delete,
|
|
145
|
+
}),
|
|
146
|
+
)
|
|
147
|
+
triggerSFX('sfx:structure-build')
|
|
148
|
+
useViewer.getState().setSelection({ selectedIds: merge.windowIds as AnyNodeId[] })
|
|
149
|
+
}}
|
|
150
|
+
title={
|
|
151
|
+
note ??
|
|
152
|
+
(merge?.kind === 'corner'
|
|
153
|
+
? 'Merge windows into a corner window: both take the height, sill and type of the one you selected first'
|
|
154
|
+
: 'Merge windows into one across both: it keeps the type and style of the one you selected first')
|
|
155
|
+
}
|
|
156
|
+
/>
|
|
157
|
+
{note ? <p className="basis-full px-1 text-muted-foreground text-xs">{note}</p> : null}
|
|
158
|
+
</>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
46
162
|
export function MultiSelectionActions() {
|
|
47
163
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
48
164
|
const sessionGroups = useSessionGroups((s) => s.groups)
|
|
@@ -61,6 +177,7 @@ export function MultiSelectionActions() {
|
|
|
61
177
|
|
|
62
178
|
return (
|
|
63
179
|
<ActionGroup className="flex-wrap">
|
|
180
|
+
<MergeWindowsAction />
|
|
64
181
|
{buildingId && (
|
|
65
182
|
<ActionButton
|
|
66
183
|
className="basis-full whitespace-nowrap"
|
|
@@ -95,8 +212,8 @@ export function MultiSelectionActions() {
|
|
|
95
212
|
onClick={() => duplicateSelectionAndPickUp()}
|
|
96
213
|
/>
|
|
97
214
|
<ActionButton
|
|
98
|
-
className="min-w-20 px-2 border-red-500/40 text-red-200 hover:bg-red-500/15"
|
|
99
|
-
icon={<Trash2 className="h-4 w-4 text-red-400" />}
|
|
215
|
+
className="min-w-20 px-2 border-red-500/40 text-red-700 dark:text-red-200 hover:bg-red-500/15"
|
|
216
|
+
icon={<Trash2 className="h-4 w-4 text-red-600 dark:text-red-400" />}
|
|
100
217
|
label="Delete"
|
|
101
218
|
onClick={() => deleteSelection()}
|
|
102
219
|
/>
|
|
@@ -138,7 +138,7 @@ export function PanelWrapper({
|
|
|
138
138
|
) as AnyNodeId | undefined
|
|
139
139
|
// Subscribe to the selected node's *type* only — a string primitive that
|
|
140
140
|
// doesn't change as fields are edited (same trick as ParametricInspector).
|
|
141
|
-
const selectedType = useScene((s) => (selectedId ?
|
|
141
|
+
const selectedType = useScene((s) => (selectedId ? s.nodes[selectedId]?.type ?? null : null))
|
|
142
142
|
const installedPlugins = useScene((s) => s.installedPlugins)
|
|
143
143
|
const extensions = useMemo(() => {
|
|
144
144
|
// re-derive when plugin extensions register after mount (async plugin load)
|
|
@@ -155,7 +155,12 @@ export function PanelWrapper({
|
|
|
155
155
|
// See `lib/inspector-card-mode.ts` for the transition table.
|
|
156
156
|
const [activeExtensionId, setActiveExtensionId] = useState<string | null>(null)
|
|
157
157
|
// Stale ids (kind changed, plugin gated off) fall back to regular mode.
|
|
158
|
-
const
|
|
158
|
+
const primaryExtensionId = useScene((state) => {
|
|
159
|
+
const node = selectedId ? state.nodes[selectedId] : undefined
|
|
160
|
+
return node ? extensions.find((extension) => extension.primaryWhen?.(node))?.id : undefined
|
|
161
|
+
})
|
|
162
|
+
const primaryExtension = extensions.find((extension) => extension.id === primaryExtensionId)
|
|
163
|
+
const activeExtension = primaryExtension ?? resolveActiveExtension(activeExtensionId, extensions)
|
|
159
164
|
|
|
160
165
|
// The panel is collapsed to just its header until the user expands it. The
|
|
161
166
|
// choice is one editor preference for every inspector (room, wall, item…),
|
|
@@ -165,6 +170,10 @@ export function PanelWrapper({
|
|
|
165
170
|
useInspectorExpanded.getState().setExpanded(!next)
|
|
166
171
|
}, [])
|
|
167
172
|
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (primaryExtension) setCollapsed(false)
|
|
175
|
+
}, [primaryExtension, selectedId, setCollapsed])
|
|
176
|
+
|
|
168
177
|
const applyMode = useCallback(
|
|
169
178
|
(next: { collapsed: boolean; activeExtensionId: string | null }) => {
|
|
170
179
|
setCollapsed(next.collapsed)
|
|
@@ -176,8 +185,12 @@ export function PanelWrapper({
|
|
|
176
185
|
// Chevron / header press — collapsed → regular, regular → collapsed,
|
|
177
186
|
// extension mode → regular (exit the extension first, stay expanded).
|
|
178
187
|
const handleCardToggle = useCallback(() => {
|
|
188
|
+
if (primaryExtension) {
|
|
189
|
+
setCollapsed(!collapsed)
|
|
190
|
+
return
|
|
191
|
+
}
|
|
179
192
|
applyMode(toggleCard({ collapsed, activeExtensionId }))
|
|
180
|
-
}, [applyMode, collapsed, activeExtensionId])
|
|
193
|
+
}, [applyMode, collapsed, activeExtensionId, primaryExtension, setCollapsed])
|
|
181
194
|
|
|
182
195
|
// Folding the card forgets the active extension — extension mode is a
|
|
183
196
|
// one-shot affordance of the header icon, not sticky panel state.
|
|
@@ -282,6 +295,7 @@ export function PanelWrapper({
|
|
|
282
295
|
|
|
283
296
|
return (
|
|
284
297
|
<div
|
|
298
|
+
data-editor-inspector
|
|
285
299
|
className={cn(
|
|
286
300
|
isMobile
|
|
287
301
|
? 'flex h-full w-full flex-col overflow-hidden bg-transparent dark:text-foreground'
|
|
@@ -327,14 +341,14 @@ export function PanelWrapper({
|
|
|
327
341
|
<div className={cn('flex min-w-0 items-center gap-2', titleContent && 'flex-1 pr-2')}>
|
|
328
342
|
{onBack && (
|
|
329
343
|
<button
|
|
330
|
-
className="mr-1 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
|
|
344
|
+
className="mr-1 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-black/8 dark:hover:bg-[#3e3e3e] hover:text-foreground"
|
|
331
345
|
onClick={onBack}
|
|
332
346
|
type="button"
|
|
333
347
|
>
|
|
334
348
|
<ChevronLeft className="h-4 w-4" />
|
|
335
349
|
</button>
|
|
336
350
|
)}
|
|
337
|
-
{icon &&
|
|
351
|
+
{primaryExtension ? renderExtensionIcon(primaryExtension.icon) : icon &&
|
|
338
352
|
(typeof icon === 'string' ? (
|
|
339
353
|
<NodeIconImage
|
|
340
354
|
alt=""
|
|
@@ -355,13 +369,13 @@ export function PanelWrapper({
|
|
|
355
369
|
)}
|
|
356
370
|
{titleContent ?? (
|
|
357
371
|
<h2 className="truncate font-semibold text-foreground text-sm tracking-tight">
|
|
358
|
-
{title}
|
|
372
|
+
{primaryExtension?.title ?? title}
|
|
359
373
|
</h2>
|
|
360
374
|
)}
|
|
361
375
|
</div>
|
|
362
376
|
) : (
|
|
363
377
|
<h2 className="truncate font-semibold text-foreground text-sm tracking-tight">
|
|
364
|
-
{title}
|
|
378
|
+
{primaryExtension?.title ?? title}
|
|
365
379
|
</h2>
|
|
366
380
|
)}
|
|
367
381
|
</div>
|
|
@@ -374,7 +388,7 @@ export function PanelWrapper({
|
|
|
374
388
|
<div className="flex items-center gap-1">
|
|
375
389
|
{onReset && (
|
|
376
390
|
<button
|
|
377
|
-
className="flex h-7 w-7 items-center justify-center rounded-md bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
|
|
391
|
+
className="flex h-7 w-7 items-center justify-center rounded-md bg-black/4 dark:bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-black/8 dark:hover:bg-[#3e3e3e] hover:text-foreground"
|
|
378
392
|
onClick={onReset}
|
|
379
393
|
type="button"
|
|
380
394
|
>
|
|
@@ -386,7 +400,7 @@ export function PanelWrapper({
|
|
|
386
400
|
ONLY that extension's content (either/or with the regular
|
|
387
401
|
controls); the active icon (highlighted) or the chevron
|
|
388
402
|
returns to the regular controls. */}
|
|
389
|
-
{extensions.map((extension) => {
|
|
403
|
+
{extensions.filter((extension) => extension !== primaryExtension).map((extension) => {
|
|
390
404
|
const isActive = !collapsed && activeExtensionId === extension.id
|
|
391
405
|
return (
|
|
392
406
|
<button
|
|
@@ -395,8 +409,8 @@ export function PanelWrapper({
|
|
|
395
409
|
className={cn(
|
|
396
410
|
'flex h-7 w-7 items-center justify-center rounded-md transition-colors',
|
|
397
411
|
isActive
|
|
398
|
-
? 'bg-cyan-500/20 text-cyan-400 hover:bg-cyan-500/30'
|
|
399
|
-
: 'bg-[#2C2C2E] text-muted-foreground hover:bg-[#3e3e3e] hover:text-foreground',
|
|
412
|
+
? 'bg-cyan-500/20 text-cyan-700 dark:text-cyan-400 hover:bg-cyan-500/30'
|
|
413
|
+
: 'bg-black/4 dark:bg-[#2C2C2E] text-muted-foreground hover:bg-black/8 dark:hover:bg-[#3e3e3e] hover:text-foreground',
|
|
400
414
|
)}
|
|
401
415
|
key={extension.id}
|
|
402
416
|
onClick={() =>
|
|
@@ -412,7 +426,7 @@ export function PanelWrapper({
|
|
|
412
426
|
<button
|
|
413
427
|
aria-expanded={!collapsed}
|
|
414
428
|
aria-label={collapsed ? 'Expand panel' : 'Collapse panel'}
|
|
415
|
-
className="flex h-7 w-7 items-center justify-center rounded-md bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
|
|
429
|
+
className="flex h-7 w-7 items-center justify-center rounded-md bg-black/4 dark:bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-black/8 dark:hover:bg-[#3e3e3e] hover:text-foreground"
|
|
416
430
|
onClick={handleCardToggle}
|
|
417
431
|
type="button"
|
|
418
432
|
>
|
|
@@ -422,7 +436,7 @@ export function PanelWrapper({
|
|
|
422
436
|
</button>
|
|
423
437
|
{onClose && (
|
|
424
438
|
<button
|
|
425
|
-
className="flex h-7 w-7 items-center justify-center rounded-md bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
|
|
439
|
+
className="flex h-7 w-7 items-center justify-center rounded-md bg-black/4 dark:bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-black/8 dark:hover:bg-[#3e3e3e] hover:text-foreground"
|
|
426
440
|
onClick={onClose}
|
|
427
441
|
type="button"
|
|
428
442
|
>
|
|
@@ -440,7 +454,9 @@ export function PanelWrapper({
|
|
|
440
454
|
via `resolveActiveExtension`. */}
|
|
441
455
|
{!(collapsed && !isMobile) && (
|
|
442
456
|
<div className="no-scrollbar flex min-h-0 flex-1 flex-col overflow-y-auto" data-panel-scroll>
|
|
443
|
-
{
|
|
457
|
+
{selectedId && primaryExtension ? (
|
|
458
|
+
<InspectorExtensionContent extension={primaryExtension} nodeId={selectedId} />
|
|
459
|
+
) : !isMobile && selectedId && activeExtension ? (
|
|
444
460
|
<InspectorExtensionSection
|
|
445
461
|
extension={activeExtension}
|
|
446
462
|
key={activeExtension.id}
|
|
@@ -551,23 +567,33 @@ function InspectorExtensionSection({
|
|
|
551
567
|
defaultExpanded?: boolean
|
|
552
568
|
extension: InspectorExtension
|
|
553
569
|
nodeId: AnyNodeId
|
|
570
|
+
}) {
|
|
571
|
+
return (
|
|
572
|
+
<PanelSection defaultExpanded={defaultExpanded} title={extension.title}>
|
|
573
|
+
<InspectorExtensionContent extension={extension} nodeId={nodeId} />
|
|
574
|
+
</PanelSection>
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function InspectorExtensionContent({
|
|
579
|
+
extension,
|
|
580
|
+
nodeId,
|
|
581
|
+
}: {
|
|
582
|
+
extension: InspectorExtension
|
|
583
|
+
nodeId: AnyNodeId
|
|
554
584
|
}) {
|
|
555
585
|
const node = useScene((s) => s.nodes[nodeId])
|
|
556
586
|
if (!node) return null
|
|
557
587
|
const Extension = resolveExtensionComponent(extension)
|
|
558
588
|
return (
|
|
559
|
-
<
|
|
560
|
-
<
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
>
|
|
567
|
-
|
|
568
|
-
<Extension node={node} />
|
|
569
|
-
</Suspense>
|
|
570
|
-
</ErrorBoundary>
|
|
571
|
-
</PanelSection>
|
|
589
|
+
<ErrorBoundary fallback={
|
|
590
|
+
<p className="p-1 text-muted-foreground text-xs">
|
|
591
|
+
“{extension.title}” hit an error and was unloaded for this session.
|
|
592
|
+
</p>
|
|
593
|
+
}>
|
|
594
|
+
<Suspense fallback={null}>
|
|
595
|
+
<Extension node={node} />
|
|
596
|
+
</Suspense>
|
|
597
|
+
</ErrorBoundary>
|
|
572
598
|
)
|
|
573
599
|
}
|
|
@@ -82,7 +82,7 @@ export function ParametricFieldControl({
|
|
|
82
82
|
<div className="flex items-center justify-between px-3 py-2">
|
|
83
83
|
<span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
|
|
84
84
|
<select
|
|
85
|
-
className="rounded-md border border-border/50 bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
|
|
85
|
+
className="rounded-md border border-border/50 bg-black/4 dark:bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
|
|
86
86
|
onChange={(e) => apply(e.target.value)}
|
|
87
87
|
value={mixed ? '' : str}
|
|
88
88
|
>
|
|
@@ -119,7 +119,7 @@ export function ParametricFieldControl({
|
|
|
119
119
|
value={str}
|
|
120
120
|
/>
|
|
121
121
|
<input
|
|
122
|
-
className="w-20 rounded-md border border-border/50 bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
|
|
122
|
+
className="w-20 rounded-md border border-border/50 bg-black/4 dark:bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
|
|
123
123
|
onChange={(e) => apply(e.target.value)}
|
|
124
124
|
type="text"
|
|
125
125
|
value={mixed ? 'Mixed' : str}
|
|
@@ -180,20 +180,20 @@ export function ParametricInspector({
|
|
|
180
180
|
<>
|
|
181
181
|
<ActionButton
|
|
182
182
|
className="w-full flex-none"
|
|
183
|
-
icon={<Trash2 className="h-4 w-4 text-red-400" />}
|
|
183
|
+
icon={<Trash2 className="h-4 w-4 text-red-600 dark:text-red-400" />}
|
|
184
184
|
label="Delete"
|
|
185
185
|
onClick={() => handleDelete(false)}
|
|
186
186
|
/>
|
|
187
187
|
<ActionButton
|
|
188
188
|
className="w-full flex-none"
|
|
189
|
-
icon={<Trash2 className="h-4 w-4 text-red-400" />}
|
|
189
|
+
icon={<Trash2 className="h-4 w-4 text-red-600 dark:text-red-400" />}
|
|
190
190
|
label="Delete with contents"
|
|
191
191
|
onClick={() => handleDelete(true)}
|
|
192
192
|
/>
|
|
193
193
|
</>
|
|
194
194
|
) : (
|
|
195
195
|
<ActionButton
|
|
196
|
-
className="border-red-500/40 text-red-200 hover:bg-red-500/15"
|
|
196
|
+
className="border-red-500/40 text-red-700 dark:text-red-200 hover:bg-red-500/15"
|
|
197
197
|
icon={<Trash2 className="h-4 w-4" />}
|
|
198
198
|
label="Delete"
|
|
199
199
|
onClick={() => handleDelete()}
|