@pascal-app/editor 1.0.4 → 1.0.5
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 +39 -37
- 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
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { rankedGroups } from '.'
|
|
3
|
+
|
|
4
|
+
const action = (group: string, label: string, keywords?: string[]) => ({
|
|
5
|
+
id: label,
|
|
6
|
+
group,
|
|
7
|
+
label,
|
|
8
|
+
keywords,
|
|
9
|
+
execute: () => {},
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
// What a host's palette holds: weak fuzzy matches for "theme" registered before the strong one.
|
|
13
|
+
const actions = [
|
|
14
|
+
action('Generate', 'Generate house from template: The Poppy', ['template', 'adu', 'poppy']),
|
|
15
|
+
action('Viewer Controls', 'Switch to Solid', ['solid', 'shading', 'render', 'mode']),
|
|
16
|
+
action('View', 'Use dark theme', ['theme', 'appearance', 'interface']),
|
|
17
|
+
action('Export & Share', 'Take Screenshot', ['screenshot', 'capture', 'image']),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const order = (search: string) => rankedGroups(actions, search).map(([group]) => group)
|
|
21
|
+
|
|
22
|
+
describe('command palette groups', () => {
|
|
23
|
+
test('a search lists the group with the best match first', () => {
|
|
24
|
+
expect(order('theme')[0]).toBe('View')
|
|
25
|
+
expect(order('solid')[0]).toBe('Viewer Controls')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('with no search the groups keep the order they were registered in', () => {
|
|
29
|
+
expect(order('')).toEqual(['Generate', 'Viewer Controls', 'View', 'Export & Share'])
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -12,12 +12,15 @@ export function ActionButton({ icon, label, className, ...props }: ActionButtonP
|
|
|
12
12
|
<button
|
|
13
13
|
{...props}
|
|
14
14
|
className={cn(
|
|
15
|
-
|
|
15
|
+
// The fill runs through custom properties so a caller's `bg-*` or
|
|
16
|
+
// `hover:bg-*` replaces it in both themes; tailwind-merge would keep a
|
|
17
|
+
// `dark:bg-*` fill alive under the caller's class.
|
|
18
|
+
'flex min-h-9 min-w-0 flex-1 shrink-0 items-center justify-center gap-1.5 rounded-lg border border-border/50 bg-(--action-fill) px-3 py-2 font-medium text-foreground text-xs leading-4 transition-colors [&>svg]:shrink-0 [--action-fill-hover:rgb(0_0_0/0.08)] [--action-fill:rgb(0_0_0/0.04)] hover:bg-(--action-fill-hover) active:bg-(--action-fill-hover) dark:[--action-fill-hover:#3e3e3e] dark:[--action-fill:#2C2C2E]',
|
|
16
19
|
className,
|
|
17
20
|
)}
|
|
18
21
|
>
|
|
19
22
|
{icon}
|
|
20
|
-
<span>{label}</span>
|
|
23
|
+
<span className="min-w-0 whitespace-normal text-center">{label}</span>
|
|
21
24
|
</button>
|
|
22
25
|
)
|
|
23
26
|
}
|
|
@@ -310,7 +310,7 @@ export function MetricControl({
|
|
|
310
310
|
<div
|
|
311
311
|
className={cn(
|
|
312
312
|
'group flex h-10 w-full items-center justify-between rounded-lg border border-border/50 px-3 text-sm transition-colors',
|
|
313
|
-
isDragging ? 'bg-[#3e3e3e]' : 'bg-[#2C2C2E] hover:bg-[#3e3e3e]',
|
|
313
|
+
isDragging ? 'bg-black/8 dark:bg-[#3e3e3e]' : 'bg-black/4 dark:bg-[#2C2C2E] hover:bg-black/8 dark:hover:bg-[#3e3e3e]',
|
|
314
314
|
className,
|
|
315
315
|
)}
|
|
316
316
|
onMouseEnter={() => setIsHovered(true)}
|
|
@@ -5,7 +5,11 @@ import { cn } from '../../../lib/utils'
|
|
|
5
5
|
interface SegmentedControlProps<T extends string> {
|
|
6
6
|
value: T
|
|
7
7
|
onChange: (value: T) => void
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* `disabled` greys one option and ignores its click; it stays hoverable, so a tooltip in its
|
|
10
|
+
* `label` can say why.
|
|
11
|
+
*/
|
|
12
|
+
options: { label: React.ReactNode; value: T; disabled?: boolean }[]
|
|
9
13
|
className?: string
|
|
10
14
|
disabled?: boolean
|
|
11
15
|
mixed?: boolean
|
|
@@ -22,28 +26,37 @@ export function SegmentedControl<T extends string>({
|
|
|
22
26
|
return (
|
|
23
27
|
<div
|
|
24
28
|
className={cn(
|
|
25
|
-
'flex h-9 w-full items-
|
|
29
|
+
'flex min-h-9 w-full items-stretch rounded-lg border border-border/50 bg-black/4 dark:bg-[#2C2C2E] p-[3px]',
|
|
26
30
|
disabled && 'opacity-60',
|
|
27
31
|
className,
|
|
28
32
|
)}
|
|
29
33
|
>
|
|
30
34
|
{options.map((option) => {
|
|
31
35
|
const isSelected = !mixed && value === option.value
|
|
36
|
+
const isDisabled = disabled || option.disabled === true
|
|
32
37
|
return (
|
|
33
38
|
<button
|
|
39
|
+
aria-disabled={option.disabled || undefined}
|
|
40
|
+
aria-pressed={isSelected}
|
|
34
41
|
className={cn(
|
|
35
|
-
'relative flex h-
|
|
42
|
+
'relative flex min-h-7 min-w-0 flex-1 items-center justify-center rounded-md px-1.5 py-1 font-medium text-xs leading-4 transition-colors duration-200',
|
|
36
43
|
isSelected
|
|
37
|
-
? 'bg-[#3e3e3e] text-foreground shadow-sm ring-1 ring-border/50'
|
|
38
|
-
: 'text-muted-foreground hover:bg-white/5 hover:text-foreground',
|
|
39
|
-
|
|
44
|
+
? 'bg-white dark:bg-[#3e3e3e] text-foreground shadow-sm ring-1 ring-border/50'
|
|
45
|
+
: 'text-muted-foreground hover:bg-black/4 dark:hover:bg-white/5 hover:text-foreground',
|
|
46
|
+
isDisabled &&
|
|
47
|
+
'cursor-not-allowed hover:bg-transparent hover:text-muted-foreground dark:hover:bg-transparent',
|
|
48
|
+
option.disabled && 'opacity-40',
|
|
40
49
|
)}
|
|
41
50
|
disabled={disabled}
|
|
42
51
|
key={option.value}
|
|
43
|
-
onClick={() =>
|
|
52
|
+
onClick={() => {
|
|
53
|
+
if (!option.disabled) onChange(option.value)
|
|
54
|
+
}}
|
|
44
55
|
type="button"
|
|
45
56
|
>
|
|
46
|
-
<span className="relative z-10 flex items-center gap-1.5">
|
|
57
|
+
<span className="relative z-10 flex min-w-0 items-center justify-center gap-1.5 whitespace-normal text-center [&>svg]:shrink-0">
|
|
58
|
+
{option.label}
|
|
59
|
+
</span>
|
|
47
60
|
</button>
|
|
48
61
|
)
|
|
49
62
|
})}
|
|
@@ -34,7 +34,7 @@ export function SelectControl<T extends string>({
|
|
|
34
34
|
<button
|
|
35
35
|
aria-label={label}
|
|
36
36
|
className={cn(
|
|
37
|
-
'group flex h-10 w-full items-center justify-between gap-2 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm transition-colors hover:bg-[#3e3e3e] data-[state=open]:bg-[#3e3e3e]',
|
|
37
|
+
'group flex h-10 w-full items-center justify-between gap-2 rounded-lg border border-border/50 bg-black/4 dark:bg-[#2C2C2E] px-3 text-sm transition-colors hover:bg-black/8 dark:hover:bg-[#3e3e3e] data-[state=open]:bg-black/8 dark:data-[state=open]:bg-[#3e3e3e]',
|
|
38
38
|
className,
|
|
39
39
|
)}
|
|
40
40
|
type="button"
|
|
@@ -329,7 +329,7 @@ export function SliderControl({
|
|
|
329
329
|
<div
|
|
330
330
|
className={cn(
|
|
331
331
|
'group flex h-7 w-full select-none items-center rounded-lg px-2 transition-colors',
|
|
332
|
-
isDragging ? 'bg-white/5' : 'hover:bg-white/5',
|
|
332
|
+
isDragging ? 'bg-black/4 dark:bg-white/5' : 'hover:bg-black/4 dark:hover:bg-white/5',
|
|
333
333
|
className,
|
|
334
334
|
)}
|
|
335
335
|
onMouseEnter={() => setIsHovered(true)}
|
|
@@ -21,7 +21,7 @@ export function ToggleControl({
|
|
|
21
21
|
return (
|
|
22
22
|
<div
|
|
23
23
|
className={cn(
|
|
24
|
-
'group flex h-10 w-full cursor-pointer items-center justify-between rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm transition-colors hover:bg-[#3e3e3e]',
|
|
24
|
+
'group flex h-10 w-full cursor-pointer items-center justify-between rounded-lg border border-border/50 bg-black/4 dark:bg-[#2C2C2E] px-3 text-sm transition-colors hover:bg-black/8 dark:hover:bg-[#3e3e3e]',
|
|
25
25
|
className,
|
|
26
26
|
)}
|
|
27
27
|
onClick={() => onChange(!checked)}
|
|
@@ -34,10 +34,10 @@ export function ToggleControl({
|
|
|
34
34
|
className={cn(
|
|
35
35
|
'flex h-5 w-5 items-center justify-center rounded-[4px] border transition-all duration-200',
|
|
36
36
|
mixed
|
|
37
|
-
? 'border-muted-foreground/70 bg-black/20 text-muted-foreground'
|
|
37
|
+
? 'border-muted-foreground/70 bg-black/5 dark:bg-black/20 text-muted-foreground'
|
|
38
38
|
: checked
|
|
39
39
|
? 'border-primary bg-primary text-primary-foreground'
|
|
40
|
-
: 'border-border bg-black/20 text-transparent group-hover:border-muted-foreground',
|
|
40
|
+
: 'border-border bg-black/5 dark:bg-black/20 text-transparent group-hover:border-muted-foreground',
|
|
41
41
|
)}
|
|
42
42
|
>
|
|
43
43
|
{mixed ? (
|
|
@@ -50,7 +50,7 @@ function ToolOptionRow({
|
|
|
50
50
|
'rounded-lg px-2 py-2 text-center font-medium text-xs transition-colors',
|
|
51
51
|
selected
|
|
52
52
|
? 'bg-primary/10 text-primary ring-1 ring-primary/50'
|
|
53
|
-
: 'bg-
|
|
53
|
+
: 'bg-black/4 text-muted-foreground hover:bg-black/8 hover:text-foreground dark:bg-muted/40 dark:hover:bg-muted',
|
|
54
54
|
)}
|
|
55
55
|
key={choice.value}
|
|
56
56
|
onClick={() => {
|
|
@@ -24,11 +24,13 @@ import {
|
|
|
24
24
|
type BuildingNode,
|
|
25
25
|
DEFAULT_LEVEL_HEIGHT,
|
|
26
26
|
getStoredLevelHeight,
|
|
27
|
+
groundSeat,
|
|
27
28
|
LevelNode,
|
|
28
29
|
useScene,
|
|
29
30
|
} from '@pascal-app/core'
|
|
30
31
|
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
31
32
|
import {
|
|
33
|
+
ArrowDownToLine,
|
|
32
34
|
ClipboardPaste,
|
|
33
35
|
Copy,
|
|
34
36
|
GripVertical,
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
import {
|
|
41
43
|
type ButtonHTMLAttributes,
|
|
42
44
|
type CSSProperties,
|
|
45
|
+
Fragment,
|
|
43
46
|
useCallback,
|
|
44
47
|
useEffect,
|
|
45
48
|
useRef,
|
|
@@ -51,7 +54,13 @@ import {
|
|
|
51
54
|
buildLevelDuplicateCreateOps,
|
|
52
55
|
type LevelDuplicatePreset,
|
|
53
56
|
} from '@pascal-app/core/building'
|
|
54
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
getDefaultLevelName,
|
|
59
|
+
getLevelDisplayName,
|
|
60
|
+
roofLevelOrderBreach,
|
|
61
|
+
servesAsRoofLevel,
|
|
62
|
+
} from '@pascal-app/core'
|
|
63
|
+
import { addStoreyBelow, addStoreyOnTop, setGroundFloor } from '../../lib/add-storey'
|
|
55
64
|
import { deleteLevelWithFallbackSelection } from '../../lib/level-selection'
|
|
56
65
|
import { unitMemberLevels, leaveUnitFocus } from '../../lib/units'
|
|
57
66
|
import { useLinearDisplay } from '../../lib/use-linear-display'
|
|
@@ -141,6 +150,8 @@ function LevelRow({
|
|
|
141
150
|
onDuplicate,
|
|
142
151
|
onPaste,
|
|
143
152
|
onRequestDelete,
|
|
153
|
+
onSetGround,
|
|
154
|
+
isGround,
|
|
144
155
|
unitDotColor,
|
|
145
156
|
}: {
|
|
146
157
|
level: LevelNode
|
|
@@ -154,6 +165,10 @@ function LevelRow({
|
|
|
154
165
|
onDuplicate: (preset?: LevelDuplicatePreset) => void
|
|
155
166
|
onPaste?: () => void
|
|
156
167
|
onRequestDelete: () => void
|
|
168
|
+
/** Makes this level the ground floor: the line moves under it. */
|
|
169
|
+
onSetGround: () => void
|
|
170
|
+
/** This level is the ground floor, seated at grade: nothing left to set. */
|
|
171
|
+
isGround: boolean
|
|
157
172
|
}) {
|
|
158
173
|
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
|
|
159
174
|
const [isEditing, setIsEditing] = useState(false)
|
|
@@ -196,17 +211,17 @@ function LevelRow({
|
|
|
196
211
|
<div
|
|
197
212
|
className={cn(
|
|
198
213
|
'flex items-center rounded-lg transition-colors',
|
|
199
|
-
isDragging && 'bg-white/10 text-foreground shadow-lg',
|
|
214
|
+
isDragging && 'bg-black/6 dark:bg-white/10 text-foreground shadow-lg',
|
|
200
215
|
isSelected
|
|
201
|
-
? 'bg-white/10 text-foreground'
|
|
202
|
-
: 'text-muted-foreground/70 hover:bg-white/5 hover:text-muted-foreground',
|
|
216
|
+
? 'bg-black/6 dark:bg-white/10 text-foreground'
|
|
217
|
+
: 'text-muted-foreground/70 hover:bg-black/4 dark:hover:bg-white/5 hover:text-muted-foreground',
|
|
203
218
|
)}
|
|
204
219
|
>
|
|
205
220
|
<button
|
|
206
221
|
{...dragHandleProps}
|
|
207
222
|
aria-label={`Reorder ${getLevelDisplayName(level)}`}
|
|
208
223
|
className={cn(
|
|
209
|
-
'ml-0.5 flex h-6 w-4 shrink-0 cursor-grab touch-none items-center justify-center rounded-md text-muted-foreground/35 opacity-0 transition-colors hover:bg-white/5 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 group-hover/level:opacity-100',
|
|
224
|
+
'ml-0.5 flex h-6 w-4 shrink-0 cursor-grab touch-none items-center justify-center rounded-md text-muted-foreground/35 opacity-0 transition-colors hover:bg-black/4 dark:hover:bg-white/5 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 group-hover/level:opacity-100',
|
|
210
225
|
isDragging && 'cursor-grabbing opacity-100',
|
|
211
226
|
)}
|
|
212
227
|
onClick={(e) => {
|
|
@@ -244,7 +259,7 @@ function LevelRow({
|
|
|
244
259
|
<Popover>
|
|
245
260
|
<PopoverTrigger asChild>
|
|
246
261
|
<button
|
|
247
|
-
className="mr-0.5 shrink-0 whitespace-nowrap rounded px-1 py-0.5 font-mono text-[10px] text-muted-foreground
|
|
262
|
+
className="mr-0.5 shrink-0 whitespace-nowrap rounded px-1 py-0.5 font-mono text-[10px] text-muted-foreground tabular-nums transition-colors hover:bg-black/4 hover:text-foreground dark:text-muted-foreground/50 dark:hover:bg-white/5 dark:hover:text-foreground"
|
|
248
263
|
onClick={(e) => e.stopPropagation()}
|
|
249
264
|
title="Level height"
|
|
250
265
|
type="button"
|
|
@@ -295,7 +310,7 @@ function LevelRow({
|
|
|
295
310
|
</PopoverTrigger>
|
|
296
311
|
<PopoverContent align="start" className="w-44 p-1" side="right" sideOffset={8}>
|
|
297
312
|
<button
|
|
298
|
-
className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors hover:bg-white/10 hover:text-foreground"
|
|
313
|
+
className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
|
|
299
314
|
onClick={(e) => {
|
|
300
315
|
e.stopPropagation()
|
|
301
316
|
onDuplicate()
|
|
@@ -306,7 +321,7 @@ function LevelRow({
|
|
|
306
321
|
Duplicate level
|
|
307
322
|
</button>
|
|
308
323
|
<button
|
|
309
|
-
className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors hover:bg-white/10 hover:text-foreground"
|
|
324
|
+
className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
|
|
310
325
|
onClick={(e) => {
|
|
311
326
|
e.stopPropagation()
|
|
312
327
|
setDuplicateDialogOpen(true)
|
|
@@ -316,9 +331,26 @@ function LevelRow({
|
|
|
316
331
|
<Copy className="h-3 w-3" />
|
|
317
332
|
Duplicate with options...
|
|
318
333
|
</button>
|
|
334
|
+
<button
|
|
335
|
+
className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors enabled:hover:bg-black/6 dark:enabled:hover:bg-white/10 enabled:hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
336
|
+
disabled={isGround}
|
|
337
|
+
onClick={(e) => {
|
|
338
|
+
e.stopPropagation()
|
|
339
|
+
onSetGround()
|
|
340
|
+
}}
|
|
341
|
+
title={
|
|
342
|
+
isGround
|
|
343
|
+
? 'This floor stands at grade'
|
|
344
|
+
: 'Seat the building so this floor stands at grade; the floors under it are below grade'
|
|
345
|
+
}
|
|
346
|
+
type="button"
|
|
347
|
+
>
|
|
348
|
+
<ArrowDownToLine className="h-3 w-3" />
|
|
349
|
+
Set as ground floor
|
|
350
|
+
</button>
|
|
319
351
|
{onPaste && (
|
|
320
352
|
<button
|
|
321
|
-
className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors hover:bg-white/10 hover:text-foreground"
|
|
353
|
+
className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
|
|
322
354
|
onClick={(e) => {
|
|
323
355
|
e.stopPropagation()
|
|
324
356
|
onPaste()
|
|
@@ -330,7 +362,7 @@ function LevelRow({
|
|
|
330
362
|
</button>
|
|
331
363
|
)}
|
|
332
364
|
<button
|
|
333
|
-
className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors enabled:hover:bg-white/10 enabled:hover:text-red-400 disabled:cursor-not-allowed disabled:opacity-50"
|
|
365
|
+
className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors enabled:hover:bg-black/6 dark:enabled:hover:bg-white/10 enabled:hover:text-red-600 dark:enabled:hover:text-red-400 disabled:cursor-not-allowed disabled:opacity-50"
|
|
334
366
|
disabled={!canDeleteLevel}
|
|
335
367
|
onClick={(e) => {
|
|
336
368
|
e.stopPropagation()
|
|
@@ -366,6 +398,8 @@ function SortableLevelRow({
|
|
|
366
398
|
onDuplicate,
|
|
367
399
|
onPaste,
|
|
368
400
|
onRequestDelete,
|
|
401
|
+
onSetGround,
|
|
402
|
+
isGround,
|
|
369
403
|
unitDotColor,
|
|
370
404
|
}: {
|
|
371
405
|
level: LevelNode
|
|
@@ -375,6 +409,8 @@ function SortableLevelRow({
|
|
|
375
409
|
onDuplicate: (preset?: LevelDuplicatePreset) => void
|
|
376
410
|
onPaste?: () => void
|
|
377
411
|
onRequestDelete: () => void
|
|
412
|
+
onSetGround: () => void
|
|
413
|
+
isGround: boolean
|
|
378
414
|
}) {
|
|
379
415
|
const {
|
|
380
416
|
attributes,
|
|
@@ -404,14 +440,67 @@ function SortableLevelRow({
|
|
|
404
440
|
level={level}
|
|
405
441
|
onDuplicate={onDuplicate}
|
|
406
442
|
onPaste={onPaste}
|
|
443
|
+
isGround={isGround}
|
|
407
444
|
onRequestDelete={onRequestDelete}
|
|
408
445
|
onSelect={onSelect}
|
|
446
|
+
onSetGround={onSetGround}
|
|
409
447
|
unitDotColor={unitDotColor}
|
|
410
448
|
/>
|
|
411
449
|
</div>
|
|
412
450
|
)
|
|
413
451
|
}
|
|
414
452
|
|
|
453
|
+
// ── The ground line: between the level at grade and the ones under it ───────
|
|
454
|
+
|
|
455
|
+
type GroundLineState = {
|
|
456
|
+
levelId: string
|
|
457
|
+
seated: boolean
|
|
458
|
+
atFoundationTop: boolean
|
|
459
|
+
offset: number
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* "Ground · +0.20": where the stack meets grade, read as the ground floor's datum over the ground
|
|
464
|
+
* (the top of the foundation, 8 in over it once seated). The levels under it are below grade. When
|
|
465
|
+
* the datum stands outside the seat (a stack that lifted it, or sank it) the line turns amber and
|
|
466
|
+
* a click seats the building.
|
|
467
|
+
*/
|
|
468
|
+
function GroundLine({ ground, onSeat }: { ground: GroundLineState; onSeat: () => void }) {
|
|
469
|
+
const { toDisplay, displayUnit, precision } = useLinearDisplay('m', 2)
|
|
470
|
+
const over = toDisplay(ground.offset)
|
|
471
|
+
const label = `Ground · ${over > 0 ? '+' : ''}${over.toFixed(precision)}`
|
|
472
|
+
return (
|
|
473
|
+
<div
|
|
474
|
+
className="relative flex items-center gap-1.5 px-2 py-0.5"
|
|
475
|
+
data-guide-target="level-ground-line"
|
|
476
|
+
>
|
|
477
|
+
{ground.seated ? (
|
|
478
|
+
<span
|
|
479
|
+
className="shrink-0 font-mono text-[10px] text-muted-foreground/70 tabular-nums"
|
|
480
|
+
title="Ground line: the levels under it are below grade"
|
|
481
|
+
>
|
|
482
|
+
{label}
|
|
483
|
+
</span>
|
|
484
|
+
) : (
|
|
485
|
+
<button
|
|
486
|
+
className="shrink-0 rounded-full px-1.5 font-mono text-[10px] text-amber-400 tabular-nums transition-colors hover:bg-amber-400/10"
|
|
487
|
+
onClick={onSeat}
|
|
488
|
+
title={`The ground floor stands ${Math.abs(toDisplay(ground.offset)).toFixed(precision)} ${displayUnit} ${ground.offset > 0 ? 'over' : 'under'} grade. Seat the ground floor 8 in over grade`}
|
|
489
|
+
type="button"
|
|
490
|
+
>
|
|
491
|
+
{label}
|
|
492
|
+
</button>
|
|
493
|
+
)}
|
|
494
|
+
<span
|
|
495
|
+
className={cn(
|
|
496
|
+
'h-px flex-1',
|
|
497
|
+
ground.seated ? 'bg-foreground/25' : 'bg-amber-400/60',
|
|
498
|
+
)}
|
|
499
|
+
/>
|
|
500
|
+
</div>
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
|
|
415
504
|
// ── Unit focus chip: name + × to end focus, only while a unit is focused ────
|
|
416
505
|
|
|
417
506
|
function UnitFocusChip() {
|
|
@@ -435,7 +524,7 @@ function UnitFocusChip() {
|
|
|
435
524
|
<span className="truncate">{focusedUnit.name || 'Unit'}</span>
|
|
436
525
|
<button
|
|
437
526
|
aria-label="Exit unit focus"
|
|
438
|
-
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-white/10 hover:text-foreground"
|
|
527
|
+
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
|
|
439
528
|
onClick={() => leaveUnitFocus()}
|
|
440
529
|
title="Exit unit focus"
|
|
441
530
|
type="button"
|
|
@@ -500,31 +589,40 @@ export function FloatingLevelSelector() {
|
|
|
500
589
|
}),
|
|
501
590
|
)
|
|
502
591
|
|
|
592
|
+
const ground = useScene(
|
|
593
|
+
useShallow((state): GroundLineState | null => {
|
|
594
|
+
const seat = resolvedBuildingId ? groundSeat(state.nodes, resolvedBuildingId) : null
|
|
595
|
+
return seat
|
|
596
|
+
? {
|
|
597
|
+
levelId: seat.levelId,
|
|
598
|
+
seated: seat.seated,
|
|
599
|
+
atFoundationTop: seat.atFoundationTop,
|
|
600
|
+
offset: seat.offset,
|
|
601
|
+
}
|
|
602
|
+
: null
|
|
603
|
+
}),
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
const handleSetGround = useCallback((levelId: string) => {
|
|
607
|
+
setGroundFloor(levelId as LevelNode['id'])
|
|
608
|
+
}, [])
|
|
609
|
+
|
|
610
|
+
const topIsRoofLevel = useScene((state) => {
|
|
611
|
+
const top = levels.at(-1)
|
|
612
|
+
return top ? servesAsRoofLevel(state.nodes, top) : false
|
|
613
|
+
})
|
|
614
|
+
|
|
503
615
|
const handleAddAbove = useCallback(() => {
|
|
504
616
|
if (!resolvedBuildingId) return
|
|
505
|
-
const
|
|
506
|
-
const newLevel = LevelNode.parse({
|
|
507
|
-
level: maxLevel + 1,
|
|
508
|
-
height: DEFAULT_LEVEL_HEIGHT,
|
|
509
|
-
children: [],
|
|
510
|
-
parentId: resolvedBuildingId,
|
|
511
|
-
})
|
|
512
|
-
createNode(newLevel, resolvedBuildingId)
|
|
617
|
+
const newLevel = addStoreyOnTop(resolvedBuildingId)
|
|
513
618
|
setSelection({ buildingId: resolvedBuildingId, levelId: newLevel.id })
|
|
514
|
-
}, [resolvedBuildingId,
|
|
619
|
+
}, [resolvedBuildingId, setSelection])
|
|
515
620
|
|
|
516
621
|
const handleAddBelow = useCallback(() => {
|
|
517
622
|
if (!resolvedBuildingId) return
|
|
518
|
-
const
|
|
519
|
-
const newLevel = LevelNode.parse({
|
|
520
|
-
level: minLevel - 1,
|
|
521
|
-
height: DEFAULT_LEVEL_HEIGHT,
|
|
522
|
-
children: [],
|
|
523
|
-
parentId: resolvedBuildingId,
|
|
524
|
-
})
|
|
525
|
-
createNode(newLevel, resolvedBuildingId)
|
|
623
|
+
const newLevel = addStoreyBelow(resolvedBuildingId)
|
|
526
624
|
setSelection({ buildingId: resolvedBuildingId, levelId: newLevel.id })
|
|
527
|
-
}, [resolvedBuildingId,
|
|
625
|
+
}, [resolvedBuildingId, setSelection])
|
|
528
626
|
|
|
529
627
|
const handleInsertBetween = useCallback(
|
|
530
628
|
(lowerIndex: number) => {
|
|
@@ -533,26 +631,22 @@ export function FloatingLevelSelector() {
|
|
|
533
631
|
if (!lower) return
|
|
534
632
|
|
|
535
633
|
const newLevelNumber = lower.level + 1
|
|
536
|
-
const toShift = levels.filter((l) => l.level >= newLevelNumber)
|
|
537
|
-
if (toShift.length > 0) {
|
|
538
|
-
updateNodes(
|
|
539
|
-
toShift.map((l) => ({
|
|
540
|
-
id: l.id as AnyNodeId,
|
|
541
|
-
data: { level: l.level + 1 } as Partial<AnyNode>,
|
|
542
|
-
})),
|
|
543
|
-
)
|
|
544
|
-
}
|
|
545
|
-
|
|
546
634
|
const newLevel = LevelNode.parse({
|
|
547
635
|
level: newLevelNumber,
|
|
548
636
|
height: DEFAULT_LEVEL_HEIGHT,
|
|
549
637
|
children: [],
|
|
550
638
|
parentId: resolvedBuildingId,
|
|
551
639
|
})
|
|
552
|
-
|
|
640
|
+
// One undo step: undoing the floor also moves the levels above back down.
|
|
641
|
+
applyNodeChanges({
|
|
642
|
+
create: [{ node: newLevel, parentId: resolvedBuildingId }],
|
|
643
|
+
update: levels
|
|
644
|
+
.filter((l) => l.level >= newLevelNumber)
|
|
645
|
+
.map((l) => ({ id: l.id as AnyNodeId, data: { level: l.level + 1 } as Partial<AnyNode> })),
|
|
646
|
+
})
|
|
553
647
|
setSelection({ buildingId: resolvedBuildingId, levelId: newLevel.id })
|
|
554
648
|
},
|
|
555
|
-
[resolvedBuildingId, levels,
|
|
649
|
+
[resolvedBuildingId, levels, applyNodeChanges, setSelection],
|
|
556
650
|
)
|
|
557
651
|
|
|
558
652
|
const handleConfirmDelete = useCallback(() => {
|
|
@@ -602,6 +696,11 @@ export function FloatingLevelSelector() {
|
|
|
602
696
|
|
|
603
697
|
const reorderedVisualLevels = arrayMove(visualLevels, oldIndex, newIndex)
|
|
604
698
|
const levelNumbersDescending = levels.map((level) => level.level).sort((a, b) => b - a)
|
|
699
|
+
// The roof level stays above every storey: a drop putting it under one it stood over is refused.
|
|
700
|
+
const floors = new Map(
|
|
701
|
+
reorderedVisualLevels.map((level, index) => [level.id, levelNumbersDescending[index]!]),
|
|
702
|
+
)
|
|
703
|
+
if (roofLevelOrderBreach(useScene.getState().nodes, floors)) return
|
|
605
704
|
|
|
606
705
|
const updates = reorderedVisualLevels
|
|
607
706
|
.map((level, index) => ({
|
|
@@ -632,13 +731,13 @@ export function FloatingLevelSelector() {
|
|
|
632
731
|
const sortableLevelIds = reversedLevels.map((level) => level.id)
|
|
633
732
|
|
|
634
733
|
const addButtonClass =
|
|
635
|
-
'absolute left-1/2 z-10 flex h-4 w-4 -translate-x-1/2 items-center justify-center rounded-full border border-border/80 bg-
|
|
734
|
+
'absolute left-1/2 z-10 flex h-4 w-4 -translate-x-1/2 items-center justify-center rounded-full border border-border/80 bg-white text-muted-foreground/60 shadow-md transition-colors hover:bg-neutral-100 hover:text-foreground dark:bg-neutral-800 dark:hover:bg-neutral-700'
|
|
636
735
|
|
|
637
736
|
return (
|
|
638
737
|
<>
|
|
639
738
|
<div className={cn('pointer-events-auto absolute left-3 z-20', isMobile ? 'top-14' : 'top-3')}>
|
|
640
739
|
<div className="relative">
|
|
641
|
-
{/* Floating + at top edge */}
|
|
740
|
+
{/* Floating + at top edge: a floor on top, under the roof level when one tops the stack */}
|
|
642
741
|
{!draggingLevelId && (
|
|
643
742
|
<button
|
|
644
743
|
className={cn(addButtonClass, 'top-0 -translate-y-1/2')}
|
|
@@ -646,7 +745,7 @@ export function FloatingLevelSelector() {
|
|
|
646
745
|
// read only from outside: nothing here depends on it.
|
|
647
746
|
data-guide-target="level-add"
|
|
648
747
|
onClick={handleAddAbove}
|
|
649
|
-
title=
|
|
748
|
+
title={topIsRoofLevel ? 'Add a floor under the roof' : 'Add level above'}
|
|
650
749
|
type="button"
|
|
651
750
|
>
|
|
652
751
|
<Plus className="h-2.5 w-2.5" />
|
|
@@ -681,6 +780,7 @@ export function FloatingLevelSelector() {
|
|
|
681
780
|
const showGapBelow = i < reversedLevels.length - 1
|
|
682
781
|
|
|
683
782
|
return (
|
|
783
|
+
<Fragment key={level.id}>
|
|
684
784
|
<div
|
|
685
785
|
className="relative"
|
|
686
786
|
// A stable hook for host-app onboarding to point at, on
|
|
@@ -688,7 +788,6 @@ export function FloatingLevelSelector() {
|
|
|
688
788
|
// without knowing the building. Static, and read only
|
|
689
789
|
// from outside: nothing here depends on it.
|
|
690
790
|
data-guide-target={level.level === 0 ? 'level-ground' : undefined}
|
|
691
|
-
key={level.id}
|
|
692
791
|
>
|
|
693
792
|
<SortableLevelRow
|
|
694
793
|
isSelected={isSelected}
|
|
@@ -700,7 +799,9 @@ export function FloatingLevelSelector() {
|
|
|
700
799
|
}
|
|
701
800
|
onDuplicate={(preset) => handleDuplicateLevel(level, preset)}
|
|
702
801
|
onPaste={() => handlePasteToLevel(level)}
|
|
802
|
+
isGround={ground?.levelId === level.id && ground.atFoundationTop}
|
|
703
803
|
onRequestDelete={() => setDeletingLevel(level)}
|
|
804
|
+
onSetGround={() => handleSetGround(level.id)}
|
|
704
805
|
onSelect={() => {
|
|
705
806
|
if (!isSelected) markPerfAction('level-switch', level.id)
|
|
706
807
|
setSelection(
|
|
@@ -722,6 +823,10 @@ export function FloatingLevelSelector() {
|
|
|
722
823
|
</button>
|
|
723
824
|
)}
|
|
724
825
|
</div>
|
|
826
|
+
{ground?.levelId === level.id && !draggingLevelId && (
|
|
827
|
+
<GroundLine ground={ground} onSeat={() => handleSetGround(level.id)} />
|
|
828
|
+
)}
|
|
829
|
+
</Fragment>
|
|
725
830
|
)
|
|
726
831
|
})}
|
|
727
832
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Icon } from '@iconify/react'
|
|
2
2
|
import type { ToolHint } from '@pascal-app/core'
|
|
3
|
-
import { Fragment, useEffect, useSyncExternalStore } from 'react'
|
|
3
|
+
import { Fragment, useEffect, useId, useState, useSyncExternalStore } from 'react'
|
|
4
4
|
import {
|
|
5
5
|
CONTINUATION_PROFILES,
|
|
6
6
|
type ContinuationContext,
|
|
@@ -73,14 +73,16 @@ function ShortcutSequence({
|
|
|
73
73
|
<span className="text-[9px] text-muted-foreground/70">/</span>
|
|
74
74
|
) : null}
|
|
75
75
|
<ShortcutToken
|
|
76
|
-
className={cn(TOKEN_CLASS, active &&
|
|
76
|
+
className={cn(TOKEN_CLASS, active &&
|
|
77
|
+
'border-foreground bg-foreground text-background shadow-sm dark:border-white dark:bg-white dark:text-black')}
|
|
77
78
|
value={alternative}
|
|
78
79
|
/>
|
|
79
80
|
</Fragment>
|
|
80
81
|
))
|
|
81
82
|
) : (
|
|
82
83
|
<ShortcutToken
|
|
83
|
-
className={cn(TOKEN_CLASS, active &&
|
|
84
|
+
className={cn(TOKEN_CLASS, active &&
|
|
85
|
+
'border-foreground bg-foreground text-background shadow-sm dark:border-white dark:bg-white dark:text-black')}
|
|
84
86
|
value={entry}
|
|
85
87
|
/>
|
|
86
88
|
)}
|
|
@@ -423,6 +425,10 @@ function HudHeader({ title }: { title: HudTitle }) {
|
|
|
423
425
|
const isEscHint = (hint: ContextualShortcutHint) =>
|
|
424
426
|
hint.keys.length === 1 && hint.keys[0] === 'Esc'
|
|
425
427
|
|
|
428
|
+
// A "!" row says why something won't happen here (a wall end a window can't wrap), not a key to
|
|
429
|
+
// press, so it is never folded under More shortcuts.
|
|
430
|
+
const isNoteHint = (hint: ContextualShortcutHint) => hint.keys.length === 1 && hint.keys[0] === '!'
|
|
431
|
+
|
|
426
432
|
function HintRow({ hint }: { hint: ContextualShortcutHint }) {
|
|
427
433
|
return (
|
|
428
434
|
<div className={cn(ROW_CLASS, 'items-start')}>
|
|
@@ -431,7 +437,7 @@ function HintRow({ hint }: { hint: ContextualShortcutHint }) {
|
|
|
431
437
|
<div
|
|
432
438
|
className={cn(
|
|
433
439
|
'text-xs leading-5',
|
|
434
|
-
hint.active ? 'font-medium text-white' : 'text-muted-foreground',
|
|
440
|
+
hint.active ? 'font-medium text-foreground dark:text-white' : 'text-muted-foreground',
|
|
435
441
|
)}
|
|
436
442
|
>
|
|
437
443
|
{hint.label}
|
|
@@ -470,6 +476,8 @@ export function ContextualHelperPanel({
|
|
|
470
476
|
notice?: string | null
|
|
471
477
|
}) {
|
|
472
478
|
const inStack = useInRightStack()
|
|
479
|
+
const [showAllHints, setShowAllHints] = useState(false)
|
|
480
|
+
const hintsId = useId()
|
|
473
481
|
const modeChips = chipHints.filter((hint) => hint.chip)
|
|
474
482
|
const hasChips = !!snapContext || !!continuationContext || modeChips.length > 0 || showPaintScope
|
|
475
483
|
const fenceFeature = useEditor((state) =>
|
|
@@ -486,6 +494,10 @@ export function ContextualHelperPanel({
|
|
|
486
494
|
|
|
487
495
|
const actionHints = hints.filter((hint) => !isEscHint(hint))
|
|
488
496
|
const escHints = hints.filter(isEscHint)
|
|
497
|
+
const collapsedHints = actionHints.filter(
|
|
498
|
+
(hint, index) => index === 0 || hint.active || isNoteHint(hint),
|
|
499
|
+
)
|
|
500
|
+
const visibleHints = showAllHints ? actionHints : collapsedHints
|
|
489
501
|
|
|
490
502
|
return (
|
|
491
503
|
<div
|
|
@@ -493,9 +505,22 @@ export function ContextualHelperPanel({
|
|
|
493
505
|
data-hud-card
|
|
494
506
|
>
|
|
495
507
|
{title ? <HudHeader title={title} /> : null}
|
|
496
|
-
{
|
|
497
|
-
|
|
498
|
-
|
|
508
|
+
<div id={hintsId} className="col-span-2 grid grid-cols-subgrid gap-y-1.5">
|
|
509
|
+
{visibleHints.map((hint) => (
|
|
510
|
+
<HintRow hint={hint} key={hintKey(hint)} />
|
|
511
|
+
))}
|
|
512
|
+
</div>
|
|
513
|
+
{collapsedHints.length < actionHints.length ? (
|
|
514
|
+
<button
|
|
515
|
+
aria-controls={hintsId}
|
|
516
|
+
aria-expanded={showAllHints}
|
|
517
|
+
className="pointer-events-auto col-span-2 min-h-7 rounded-md px-1 text-left text-[11px] text-muted-foreground hover:bg-muted/60 hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring"
|
|
518
|
+
onClick={() => setShowAllHints((value) => !value)}
|
|
519
|
+
type="button"
|
|
520
|
+
>
|
|
521
|
+
{showAllHints ? 'Fewer shortcuts' : 'More shortcuts'}
|
|
522
|
+
</button>
|
|
523
|
+
) : null}
|
|
499
524
|
{notice ? <HintRow hint={{ keys: ['!'], label: notice, active: true }} /> : null}
|
|
500
525
|
{actionHints.length > 0 && (hasChips || escHints.length > 0) ? (
|
|
501
526
|
<div className="col-span-2 my-0.5 h-px bg-border" />
|