@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
|
@@ -5,14 +5,17 @@ import {
|
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
area,
|
|
7
7
|
getLevelDisplayName,
|
|
8
|
+
runAsSingleSceneHistoryStep,
|
|
8
9
|
useScene,
|
|
9
10
|
type ZoneNode,
|
|
10
11
|
} from '@pascal-app/core'
|
|
12
|
+
import { setRoomFloorScript } from '@pascal-app/core/agent-operations'
|
|
11
13
|
import { useViewer } from '@pascal-app/viewer'
|
|
12
14
|
import { Check, ChevronDown, Plus } from 'lucide-react'
|
|
13
15
|
import { type KeyboardEvent, useEffect, useId, useMemo, useRef, useState } from 'react'
|
|
14
16
|
import { startCeilingEdit } from '../../../lib/ceiling-edit-session'
|
|
15
17
|
import { resolveOverlayPolicy } from '../../../lib/interaction/overlay-policy'
|
|
18
|
+
import { finishScriptCode } from '../../../lib/geometry-script/finish-scripts'
|
|
16
19
|
import { formatAreaLabel, formatLinearMeasurement } from '../../../lib/measurements'
|
|
17
20
|
import { addMezzanineStairs } from '../../../lib/mezzanine-stairs'
|
|
18
21
|
import { roomFloorBase, roomRelativeFloorHeight } from '../../../lib/room-construction-commands'
|
|
@@ -111,6 +114,7 @@ export function RoomPanel({
|
|
|
111
114
|
) : (
|
|
112
115
|
<RoomConstructionRows key={room.zoneId} room={room} />
|
|
113
116
|
)}
|
|
117
|
+
<RoomFloorScriptRow key={`floor-script-${room.zoneId}`} zoneId={room.zoneId} />
|
|
114
118
|
</div>
|
|
115
119
|
</PanelWrapper>
|
|
116
120
|
)
|
|
@@ -222,6 +226,67 @@ export function RoomUnitRow({ zoneId }: { zoneId: string }) {
|
|
|
222
226
|
)
|
|
223
227
|
}
|
|
224
228
|
|
|
229
|
+
/**
|
|
230
|
+
* "Scripted finish": a floor a script builds (it owns the floor's colours, so Paint leaves it),
|
|
231
|
+
* with its code to read and a way to take it off, back to the floor's own finish.
|
|
232
|
+
*/
|
|
233
|
+
function RoomFloorScriptRow({ zoneId }: { zoneId: string }) {
|
|
234
|
+
const sha = useScene((scene) => {
|
|
235
|
+
const zone = scene.nodes[zoneId as AnyNodeId]
|
|
236
|
+
return zone?.type === 'zone' ? zone.floor?.script?.script : undefined
|
|
237
|
+
})
|
|
238
|
+
const [code, setCode] = useState<{ sha: string; text: string } | null>(null)
|
|
239
|
+
const [open, setOpen] = useState(false)
|
|
240
|
+
if (!sha) return null
|
|
241
|
+
const toggleCode = () => {
|
|
242
|
+
setOpen((shown) => !shown)
|
|
243
|
+
if (code?.sha === sha) return
|
|
244
|
+
finishScriptCode(sha).then(
|
|
245
|
+
(text) => setCode({ sha, text: text ?? 'The script could not be read.' }),
|
|
246
|
+
() => setCode({ sha, text: 'The script could not be read.' }),
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
const remove = () => {
|
|
250
|
+
const { changes } = setRoomFloorScript(
|
|
251
|
+
useScene.getState().nodes,
|
|
252
|
+
{ zoneId, remove: true },
|
|
253
|
+
{ activeLevelId: null },
|
|
254
|
+
)
|
|
255
|
+
runAsSingleSceneHistoryStep(useScene, () => {
|
|
256
|
+
for (const { id, data } of changes?.update ?? [])
|
|
257
|
+
useScene.getState().updateNode(id as AnyNodeId, data)
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
return (
|
|
261
|
+
<div className="mt-3 flex flex-col gap-2" data-room-floor-script={zoneId}>
|
|
262
|
+
<div className="flex min-h-8 items-center justify-between gap-3">
|
|
263
|
+
<span className="text-muted-foreground text-xs">Scripted finish</span>
|
|
264
|
+
<div className="flex items-center gap-1.5">
|
|
265
|
+
<button
|
|
266
|
+
className="rounded-full border border-border/50 px-2.5 py-0.5 text-xs transition-colors hover:bg-accent"
|
|
267
|
+
onClick={toggleCode}
|
|
268
|
+
type="button"
|
|
269
|
+
>
|
|
270
|
+
{open ? 'Hide code' : 'View code'}
|
|
271
|
+
</button>
|
|
272
|
+
<button
|
|
273
|
+
className="rounded-full border border-border/50 px-2.5 py-0.5 text-xs transition-colors hover:bg-accent"
|
|
274
|
+
onClick={remove}
|
|
275
|
+
type="button"
|
|
276
|
+
>
|
|
277
|
+
Remove
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
{open && (
|
|
282
|
+
<pre className="max-h-60 overflow-auto whitespace-pre rounded-lg border border-border/50 p-2 font-mono text-[11px] text-foreground">
|
|
283
|
+
{code?.sha === sha ? code.text : 'Loading…'}
|
|
284
|
+
</pre>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
225
290
|
const count = (n: number, one: string, many = `${one}s`) => `${n} ${n === 1 ? one : many}`
|
|
226
291
|
|
|
227
292
|
/** "2 doors · 3 windows" (the level is in the kicker). */
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** A small mark beside a name: the plan a plugin needs ("Pro"), as the plugin manager shows it. */
|
|
2
|
+
export function Badge({ label }: { label: string }) {
|
|
3
|
+
return (
|
|
4
|
+
<span className="rounded-[5px] border border-border px-1.5 py-px font-mono font-medium text-[10px] text-muted-foreground uppercase tracking-wide">
|
|
5
|
+
{label}
|
|
6
|
+
</span>
|
|
7
|
+
)
|
|
8
|
+
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { createContext, type ReactNode, useContext, useEffect, useRef } from 'react'
|
|
4
4
|
import { useIsMobile } from '../../hooks/use-mobile'
|
|
5
|
+
import { cn } from '../../lib/utils'
|
|
6
|
+
import { TOP_MENU_CLEARANCE } from './action-menu/placement'
|
|
5
7
|
|
|
6
8
|
const RightStackContext = createContext(false)
|
|
7
9
|
|
|
@@ -56,7 +58,19 @@ function inspectorHeights(slot: HTMLElement): { natural: number; header: number
|
|
|
56
58
|
* and the card scrolls instead. Neither hides the other. On a phone the two
|
|
57
59
|
* keep their own layouts (sheet / no card).
|
|
58
60
|
*/
|
|
59
|
-
export function RightStack({
|
|
61
|
+
export function RightStack({
|
|
62
|
+
inspector,
|
|
63
|
+
helper,
|
|
64
|
+
reserveBottomMenu = true,
|
|
65
|
+
belowTopMenu = false,
|
|
66
|
+
}: {
|
|
67
|
+
inspector: ReactNode
|
|
68
|
+
helper: ReactNode
|
|
69
|
+
// Keep clear of an action menu docked at the bottom; off when it sits at the top.
|
|
70
|
+
reserveBottomMenu?: boolean
|
|
71
|
+
// Start below an action menu docked at the top of the stage.
|
|
72
|
+
belowTopMenu?: boolean
|
|
73
|
+
}) {
|
|
60
74
|
const isMobile = useIsMobile()
|
|
61
75
|
const stackRef = useRef<HTMLDivElement>(null)
|
|
62
76
|
const inspectorRef = useRef<HTMLDivElement>(null)
|
|
@@ -125,7 +139,12 @@ export function RightStack({ inspector, helper }: { inspector: ReactNode; helper
|
|
|
125
139
|
return (
|
|
126
140
|
<RightStackContext.Provider value={true}>
|
|
127
141
|
<div
|
|
128
|
-
className=
|
|
142
|
+
className={cn(
|
|
143
|
+
'pointer-events-none fixed right-4 z-40 flex flex-col items-end gap-2',
|
|
144
|
+
reserveBottomMenu ? 'bottom-[74px]' : 'bottom-4',
|
|
145
|
+
!belowTopMenu && 'top-20',
|
|
146
|
+
)}
|
|
147
|
+
style={belowTopMenu ? { top: TOP_MENU_CLEARANCE } : undefined}
|
|
129
148
|
data-right-stack
|
|
130
149
|
ref={stackRef}
|
|
131
150
|
>
|
|
@@ -78,7 +78,7 @@ export function AppSidebar({
|
|
|
78
78
|
|
|
79
79
|
return (
|
|
80
80
|
<>
|
|
81
|
-
<div className=
|
|
81
|
+
<div className="flex h-full w-full bg-sidebar text-sidebar-foreground">
|
|
82
82
|
{/* Icon Rail */}
|
|
83
83
|
<IconRail
|
|
84
84
|
activePanel={activePanel}
|
|
@@ -65,7 +65,7 @@ export function IconRail({
|
|
|
65
65
|
<button
|
|
66
66
|
className={cn(
|
|
67
67
|
'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
|
|
68
|
-
isActive ? 'bg-accent' : 'hover:bg-accent',
|
|
68
|
+
isActive ? 'bg-sidebar-accent' : 'hover:bg-sidebar-accent',
|
|
69
69
|
)}
|
|
70
70
|
onClick={() => onPanelChange(panel.id)}
|
|
71
71
|
type="button"
|
|
@@ -107,7 +107,7 @@ export function IconRail({
|
|
|
107
107
|
<button
|
|
108
108
|
className={cn(
|
|
109
109
|
'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
|
|
110
|
-
isActive ? 'bg-accent' : 'hover:bg-accent',
|
|
110
|
+
isActive ? 'bg-sidebar-accent' : 'hover:bg-sidebar-accent',
|
|
111
111
|
)}
|
|
112
112
|
onClick={() => onPanelChange(panel.id)}
|
|
113
113
|
type="button"
|
|
@@ -138,7 +138,7 @@ export function IconRail({
|
|
|
138
138
|
<button
|
|
139
139
|
className={cn(
|
|
140
140
|
'flex h-9 w-9 items-center justify-center rounded-lg transition-all',
|
|
141
|
-
isActive ? 'bg-accent' : 'hover:bg-accent',
|
|
141
|
+
isActive ? 'bg-sidebar-accent' : 'hover:bg-sidebar-accent',
|
|
142
142
|
)}
|
|
143
143
|
onClick={() => onPanelChange(panel.id)}
|
|
144
144
|
type="button"
|
|
@@ -136,7 +136,7 @@ export function FunctionTreePanel({
|
|
|
136
136
|
'relative flex aspect-square items-center justify-center rounded-xl transition-all duration-200',
|
|
137
137
|
isActive
|
|
138
138
|
? 'bg-primary/10 ring-1 ring-primary/50'
|
|
139
|
-
: 'bg-
|
|
139
|
+
: 'bg-black/4 opacity-70 grayscale hover:bg-black/8 hover:opacity-100 hover:grayscale-0 dark:bg-muted/40 dark:hover:bg-muted',
|
|
140
140
|
)}
|
|
141
141
|
onClick={() => {
|
|
142
142
|
triggerSFX('sfx:menu-click')
|
|
@@ -279,7 +279,7 @@ function LegacyItemsPanel({
|
|
|
279
279
|
'cursor-pointer rounded-md px-2 py-0.5 font-medium text-xs transition-colors',
|
|
280
280
|
activePlacementTag === null
|
|
281
281
|
? 'bg-blue-500 text-white'
|
|
282
|
-
: 'bg-blue-950/50 text-blue-300 hover:bg-blue-900/60 hover:text-blue-200',
|
|
282
|
+
: 'bg-blue-50 dark:bg-blue-950/50 text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/60 hover:text-blue-800 dark:hover:text-blue-200',
|
|
283
283
|
)}
|
|
284
284
|
onClick={() => setActivePlacementTag(null)}
|
|
285
285
|
type="button"
|
|
@@ -297,8 +297,8 @@ function LegacyItemsPanel({
|
|
|
297
297
|
isActive
|
|
298
298
|
? 'bg-blue-500 text-white'
|
|
299
299
|
: isEmpty
|
|
300
|
-
? 'cursor-not-allowed bg-zinc-800 text-zinc-500'
|
|
301
|
-
: 'bg-blue-950/50 text-blue-300 hover:bg-blue-900/60 hover:text-blue-200',
|
|
300
|
+
? 'cursor-not-allowed bg-zinc-100 dark:bg-zinc-800 text-zinc-400 dark:text-zinc-500'
|
|
301
|
+
: 'bg-blue-50 dark:bg-blue-950/50 text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/60 hover:text-blue-800 dark:hover:text-blue-200',
|
|
302
302
|
)}
|
|
303
303
|
disabled={isEmpty}
|
|
304
304
|
key={tag}
|
|
@@ -312,7 +312,7 @@ function LegacyItemsPanel({
|
|
|
312
312
|
isActive
|
|
313
313
|
? 'text-blue-200'
|
|
314
314
|
: isEmpty
|
|
315
|
-
? 'text-zinc-600'
|
|
315
|
+
? 'text-zinc-400 dark:text-zinc-600'
|
|
316
316
|
: 'text-blue-500/70',
|
|
317
317
|
)}
|
|
318
318
|
>
|
|
@@ -337,7 +337,7 @@ function LegacyItemsPanel({
|
|
|
337
337
|
isActive
|
|
338
338
|
? 'bg-violet-500 text-white'
|
|
339
339
|
: isEmpty
|
|
340
|
-
? 'cursor-not-allowed bg-zinc-800 text-zinc-500'
|
|
340
|
+
? 'cursor-not-allowed bg-zinc-100 dark:bg-zinc-800 text-zinc-400 dark:text-zinc-500'
|
|
341
341
|
: 'bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground',
|
|
342
342
|
)}
|
|
343
343
|
disabled={isEmpty}
|
|
@@ -352,7 +352,7 @@ function LegacyItemsPanel({
|
|
|
352
352
|
isActive
|
|
353
353
|
? 'text-violet-200'
|
|
354
354
|
: isEmpty
|
|
355
|
-
? 'text-zinc-600'
|
|
355
|
+
? 'text-zinc-400 dark:text-zinc-600'
|
|
356
356
|
: 'text-zinc-500/70',
|
|
357
357
|
)}
|
|
358
358
|
>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { ANIMATION_SPEEDS, type AnimationSpeed, normalizeAnimationSpeed } from '@pascal-app/viewer'
|
|
4
|
+
import useConstructionReveal from '../../../../../store/use-construction-reveal'
|
|
5
|
+
import { SegmentedControl } from '../../../controls/segmented-control'
|
|
6
|
+
|
|
7
|
+
const label = (speed: AnimationSpeed) => `×${speed}`
|
|
8
|
+
|
|
9
|
+
// The control's values are strings; a step is its number as text.
|
|
10
|
+
const OPTIONS = ANIMATION_SPEEDS.map((speed) => ({
|
|
11
|
+
label: label(speed),
|
|
12
|
+
value: String(speed),
|
|
13
|
+
title: speed === 1 ? 'The pace builds were made at.' : `Builds play ×${speed} as fast.`,
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
/** How fast what the AI builds plays: ×0.5 to ×2, for the Full and Framing animations and their undo. */
|
|
17
|
+
export function AnimationSpeedSetting() {
|
|
18
|
+
const level = useConstructionReveal((state) => state.level)
|
|
19
|
+
const speed = useConstructionReveal((state) => state.speed)
|
|
20
|
+
const setSpeed = useConstructionReveal((state) => state.setSpeed)
|
|
21
|
+
const off = level === 'off'
|
|
22
|
+
return (
|
|
23
|
+
<div className="space-y-2 py-1">
|
|
24
|
+
<div className="space-y-0.5">
|
|
25
|
+
<p className="font-medium text-sm">Animation speed</p>
|
|
26
|
+
<p className="text-muted-foreground text-xs">
|
|
27
|
+
{off
|
|
28
|
+
? 'Turn the construction animation on to use this.'
|
|
29
|
+
: 'Higher plays a build faster; it applies to the next build.'}
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
<SegmentedControl
|
|
33
|
+
aria-label="Animation speed"
|
|
34
|
+
onChange={(value) => setSpeed(normalizeAnimationSpeed(Number(value)))}
|
|
35
|
+
options={OPTIONS}
|
|
36
|
+
value={String(speed)}
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -34,17 +34,18 @@ export function AudioSettingsDialog() {
|
|
|
34
34
|
<DialogContent className="sm:max-w-[425px]">
|
|
35
35
|
<DialogHeader>
|
|
36
36
|
<DialogTitle>Audio settings</DialogTitle>
|
|
37
|
-
<DialogDescription>
|
|
37
|
+
<DialogDescription>
|
|
38
|
+
Volume levels apply to every tab. Muting applies to this tab only.
|
|
39
|
+
</DialogDescription>
|
|
38
40
|
</DialogHeader>
|
|
39
41
|
<div className="space-y-6 py-4">
|
|
40
42
|
{/* Master Volume */}
|
|
41
43
|
<div className="space-y-2">
|
|
42
44
|
<div className="flex items-center justify-between">
|
|
43
|
-
<label className="font-medium text-sm">Master
|
|
45
|
+
<label className="font-medium text-sm">Master volume</label>
|
|
44
46
|
<span className="text-muted-foreground text-sm">{masterVolume}%</span>
|
|
45
47
|
</div>
|
|
46
48
|
<Slider
|
|
47
|
-
disabled={muted}
|
|
48
49
|
max={100}
|
|
49
50
|
onValueChange={(value) => value[0] !== undefined && setMasterVolume(value[0])}
|
|
50
51
|
step={1}
|
|
@@ -55,11 +56,10 @@ export function AudioSettingsDialog() {
|
|
|
55
56
|
{/* Radio Volume */}
|
|
56
57
|
<div className="space-y-2">
|
|
57
58
|
<div className="flex items-center justify-between">
|
|
58
|
-
<label className="font-medium text-sm">Radio
|
|
59
|
+
<label className="font-medium text-sm">Radio volume</label>
|
|
59
60
|
<span className="text-muted-foreground text-sm">{radioVolume}%</span>
|
|
60
61
|
</div>
|
|
61
62
|
<Slider
|
|
62
|
-
disabled={muted}
|
|
63
63
|
max={100}
|
|
64
64
|
onValueChange={(value) => value[0] !== undefined && setRadioVolume(value[0])}
|
|
65
65
|
step={1}
|
|
@@ -70,11 +70,10 @@ export function AudioSettingsDialog() {
|
|
|
70
70
|
{/* SFX Volume */}
|
|
71
71
|
<div className="space-y-2">
|
|
72
72
|
<div className="flex items-center justify-between">
|
|
73
|
-
<label className="font-medium text-sm">Sound
|
|
73
|
+
<label className="font-medium text-sm">Sound effects</label>
|
|
74
74
|
<span className="text-muted-foreground text-sm">{sfxVolume}%</span>
|
|
75
75
|
</div>
|
|
76
76
|
<Slider
|
|
77
|
-
disabled={muted}
|
|
78
77
|
max={100}
|
|
79
78
|
onValueChange={(value) => value[0] !== undefined && setSfxVolume(value[0])}
|
|
80
79
|
step={1}
|
|
@@ -90,7 +89,7 @@ export function AudioSettingsDialog() {
|
|
|
90
89
|
variant={muted ? 'default' : 'outline'}
|
|
91
90
|
>
|
|
92
91
|
{muted ? <VolumeX className="size-4" /> : <Volume2 className="size-4" />}
|
|
93
|
-
{muted ? 'Unmute
|
|
92
|
+
{muted ? 'Unmute this tab' : 'Mute this tab'}
|
|
94
93
|
</Button>
|
|
95
94
|
</div>
|
|
96
95
|
</div>
|
package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNodeDefinition,
|
|
4
|
+
BaseNode,
|
|
5
|
+
loadPlugin,
|
|
6
|
+
nodeRegistry,
|
|
7
|
+
nodeType,
|
|
8
|
+
objectId,
|
|
9
|
+
useScene,
|
|
10
|
+
} from '@pascal-app/core'
|
|
11
|
+
import type { ConstructionRevealLevel } from '@pascal-app/viewer'
|
|
12
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
13
|
+
import useConstructionReveal, {
|
|
14
|
+
chooseConstructionLevel,
|
|
15
|
+
FRAMING_KIND,
|
|
16
|
+
resolveConstructionLevel,
|
|
17
|
+
} from '../../../../../store/use-construction-reveal'
|
|
18
|
+
import { AnimationSpeedSetting } from './animation-speed-setting'
|
|
19
|
+
import {
|
|
20
|
+
ConstructionAnimationSetting,
|
|
21
|
+
constructionLevelOptions,
|
|
22
|
+
FRAMING_NEEDS_ARCHITECT,
|
|
23
|
+
} from './construction-animation-setting'
|
|
24
|
+
|
|
25
|
+
// The framing plugin carries the Framing level: without it in the project the choice is greyed out with the
|
|
26
|
+
// reason as its tip, and the level that plays is Full.
|
|
27
|
+
|
|
28
|
+
const framing = {
|
|
29
|
+
kind: FRAMING_KIND,
|
|
30
|
+
schemaVersion: 1,
|
|
31
|
+
schema: BaseNode.extend({ id: objectId('framing'), type: nodeType(FRAMING_KIND) }),
|
|
32
|
+
category: 'structure',
|
|
33
|
+
defaults: () => ({}),
|
|
34
|
+
capabilities: {},
|
|
35
|
+
geometry: () => null,
|
|
36
|
+
} as unknown as AnyNodeDefinition
|
|
37
|
+
|
|
38
|
+
let restore: () => void = () => {}
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
restore = nodeRegistry._snapshot()
|
|
41
|
+
nodeRegistry._reset()
|
|
42
|
+
await loadPlugin({ id: 'fixture:framing', apiVersion: 1, nodes: [framing] })
|
|
43
|
+
})
|
|
44
|
+
afterAll(() => restore())
|
|
45
|
+
|
|
46
|
+
// A server render reads a store's initial state, so the state to draw is put there.
|
|
47
|
+
const sceneInitial = useScene.getInitialState() as { installedPlugins: string[] }
|
|
48
|
+
const revealInitial = useConstructionReveal.getInitialState() as {
|
|
49
|
+
level: string
|
|
50
|
+
chosen: boolean
|
|
51
|
+
speed: number
|
|
52
|
+
}
|
|
53
|
+
const was = { plugins: sceneInitial.installedPlugins, ...revealInitial }
|
|
54
|
+
|
|
55
|
+
function project(options: { installed: boolean; level?: string; chosen?: boolean; speed?: number }) {
|
|
56
|
+
sceneInitial.installedPlugins = options.installed ? ['fixture:framing'] : []
|
|
57
|
+
revealInitial.level = options.level ?? 'full'
|
|
58
|
+
revealInitial.chosen = options.chosen ?? false
|
|
59
|
+
revealInitial.speed = options.speed ?? 1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
beforeEach(() => project({ installed: false }))
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
sceneInitial.installedPlugins = was.plugins
|
|
65
|
+
Object.assign(revealInitial, { level: was.level, chosen: was.chosen, speed: was.speed })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const optionOf = (markup: string, label: string) => {
|
|
69
|
+
const match = new RegExp(`<button[^>]*>(?:(?!</button>).)*${label}(?:(?!</button>).)*</button>`).exec(
|
|
70
|
+
markup,
|
|
71
|
+
)
|
|
72
|
+
return match?.[0] ?? ''
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('the levels the control offers', () => {
|
|
76
|
+
test('without the plugin, Framing is disabled and its tip says it needs the Architect plugin', () => {
|
|
77
|
+
const framingOption = constructionLevelOptions(false).find((o) => o.value === 'framing')!
|
|
78
|
+
expect(framingOption.disabled).toBe(true)
|
|
79
|
+
expect(framingOption.title).toBe(FRAMING_NEEDS_ARCHITECT)
|
|
80
|
+
expect(FRAMING_NEEDS_ARCHITECT).toBe('Framing needs the Architect plugin.')
|
|
81
|
+
for (const option of constructionLevelOptions(false).filter((o) => o.value !== 'framing'))
|
|
82
|
+
expect(option.disabled).toBe(false)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('with the plugin, every level is on offer', () => {
|
|
86
|
+
expect(constructionLevelOptions(true).every((option) => option.disabled === false)).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('the construction animation control, drawn', () => {
|
|
91
|
+
test('without the plugin: Framing is greyed out and Full is the level that plays', () => {
|
|
92
|
+
project({ installed: false })
|
|
93
|
+
const markup = renderToStaticMarkup(<ConstructionAnimationSetting />)
|
|
94
|
+
expect(optionOf(markup, 'Framing')).toContain('aria-disabled="true"')
|
|
95
|
+
expect(optionOf(markup, 'Full')).toContain('aria-pressed="true"')
|
|
96
|
+
expect(optionOf(markup, 'Full')).not.toContain('aria-disabled')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('with the plugin and no choice of the person’s own: Framing plays, nothing is greyed', () => {
|
|
100
|
+
project({ installed: true })
|
|
101
|
+
const markup = renderToStaticMarkup(<ConstructionAnimationSetting />)
|
|
102
|
+
expect(optionOf(markup, 'Framing')).toContain('aria-pressed="true"')
|
|
103
|
+
expect(markup).not.toContain('aria-disabled')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('with the plugin and a choice of Full: Full is kept', () => {
|
|
107
|
+
project({ installed: true, level: 'full', chosen: true })
|
|
108
|
+
const markup = renderToStaticMarkup(<ConstructionAnimationSetting />)
|
|
109
|
+
expect(optionOf(markup, 'Full')).toContain('aria-pressed="true"')
|
|
110
|
+
expect(optionOf(markup, 'Framing')).toContain('aria-pressed="false"')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('a saved choice of Framing falls back to Full while the plugin is gone, greyed out', () => {
|
|
114
|
+
project({ installed: false, level: 'framing', chosen: true })
|
|
115
|
+
const markup = renderToStaticMarkup(<ConstructionAnimationSetting />)
|
|
116
|
+
expect(optionOf(markup, 'Full')).toContain('aria-pressed="true"')
|
|
117
|
+
expect(optionOf(markup, 'Framing')).toContain('aria-disabled="true"')
|
|
118
|
+
expect(optionOf(markup, 'Framing')).toContain('aria-pressed="false"')
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('clicking the level that plays', () => {
|
|
123
|
+
// A saved Framing plays as Full while the plugin is gone; the control shows Full pressed, and a click on
|
|
124
|
+
// the pressed option is still a click. It must not turn the kept Framing into a chosen Full.
|
|
125
|
+
const click = (level: ConstructionRevealLevel, installed: boolean) =>
|
|
126
|
+
chooseConstructionLevel(level, installed)
|
|
127
|
+
|
|
128
|
+
beforeEach(() => useConstructionReveal.setState({ level: 'framing', chosen: true }))
|
|
129
|
+
afterEach(() => useConstructionReveal.setState({ level: 'full', chosen: false }))
|
|
130
|
+
|
|
131
|
+
test('the Full shown for a saved Framing keeps the Framing when it is clicked, and it plays again with the plugin', () => {
|
|
132
|
+
click('full', false)
|
|
133
|
+
expect(useConstructionReveal.getState()).toMatchObject({ level: 'framing', chosen: true })
|
|
134
|
+
expect(resolveConstructionLevel(useConstructionReveal.getState(), true)).toBe('framing')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('another level is a choice and replaces it', () => {
|
|
138
|
+
click('simple', false)
|
|
139
|
+
expect(useConstructionReveal.getState()).toMatchObject({ level: 'simple', chosen: true })
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('with no choice of the person’s own, a click on the level that plays is a choice of it', () => {
|
|
143
|
+
useConstructionReveal.setState({ level: 'full', chosen: false })
|
|
144
|
+
click('full', false)
|
|
145
|
+
expect(useConstructionReveal.getState()).toMatchObject({ level: 'full', chosen: true })
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('the Pro badge', () => {
|
|
150
|
+
// The plugin that draws the framing is a Pro one: the option carries the plugin manager's mark,
|
|
151
|
+
// greyed out or not, and no other level does.
|
|
152
|
+
test('Framing carries it, greyed out or on offer; the other levels do not', () => {
|
|
153
|
+
for (const installed of [false, true]) {
|
|
154
|
+
project({ installed })
|
|
155
|
+
const markup = renderToStaticMarkup(<ConstructionAnimationSetting />)
|
|
156
|
+
expect(optionOf(markup, 'Framing')).toMatch(/>Pro</)
|
|
157
|
+
for (const label of ['Off', 'Simple', 'Full']) expect(optionOf(markup, label)).not.toMatch(/>Pro</)
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('the animation speed control, drawn', () => {
|
|
163
|
+
test('offers the five steps and shows the saved one', () => {
|
|
164
|
+
project({ installed: true, speed: 1.5 })
|
|
165
|
+
const markup = renderToStaticMarkup(<AnimationSpeedSetting />)
|
|
166
|
+
for (const label of ['×0.5', '×0.75', '×1', '×1.5', '×2']) expect(markup).toContain(label)
|
|
167
|
+
expect(optionOf(markup, '×1.5')).toContain('aria-pressed="true"')
|
|
168
|
+
expect(optionOf(markup, '×0.5')).toContain('aria-pressed="false"')
|
|
169
|
+
})
|
|
170
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { ConstructionRevealLevel } from '@pascal-app/viewer'
|
|
4
|
+
import {
|
|
5
|
+
chooseConstructionLevel,
|
|
6
|
+
useConstructionRevealLevel,
|
|
7
|
+
useFramingAvailable,
|
|
8
|
+
} from '../../../../../store/use-construction-reveal'
|
|
9
|
+
import { SegmentedControl } from '../../../controls/segmented-control'
|
|
10
|
+
import { Badge } from '../../../primitives/badge'
|
|
11
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../../../primitives/tooltip'
|
|
12
|
+
|
|
13
|
+
/** Why Framing is greyed out. */
|
|
14
|
+
export const FRAMING_NEEDS_ARCHITECT = 'Framing needs the Architect plugin.'
|
|
15
|
+
|
|
16
|
+
const OPTIONS: { label: string; value: ConstructionRevealLevel; title: string; badge?: string }[] = [
|
|
17
|
+
{ label: 'Off', value: 'off', title: 'What the AI builds appears at once.' },
|
|
18
|
+
{
|
|
19
|
+
label: 'Simple',
|
|
20
|
+
value: 'simple',
|
|
21
|
+
title: 'Walls rise and the rest grows in, piece by piece. No falls, no dust, no sound.',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: 'Full',
|
|
25
|
+
value: 'full',
|
|
26
|
+
title: 'Pieces drop into place with dust, the roof assembles, one sound per phase.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
label: 'Framing',
|
|
30
|
+
value: 'framing',
|
|
31
|
+
title: 'Full, plus the framing members dropping in first, where the framing plugin draws them.',
|
|
32
|
+
// The plugin that draws the framing is a Pro one.
|
|
33
|
+
badge: 'Pro',
|
|
34
|
+
},
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The four levels as the control offers them: Framing is greyed out, with the reason as its tip,
|
|
39
|
+
* where the project has no plugin that draws the framing.
|
|
40
|
+
*/
|
|
41
|
+
export function constructionLevelOptions(framingAvailable: boolean) {
|
|
42
|
+
return OPTIONS.map((option) =>
|
|
43
|
+
option.value === 'framing' && !framingAvailable
|
|
44
|
+
? { ...option, title: FRAMING_NEEDS_ARCHITECT, disabled: true }
|
|
45
|
+
: { ...option, disabled: false },
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** How much of an AI build plays as a construction. */
|
|
50
|
+
export function ConstructionAnimationSetting() {
|
|
51
|
+
// What plays: the person's choice, or Framing where the framing plugin is installed and Full where it is not.
|
|
52
|
+
const level = useConstructionRevealLevel()
|
|
53
|
+
const framingAvailable = useFramingAvailable()
|
|
54
|
+
const current = OPTIONS.find((option) => option.value === level)
|
|
55
|
+
const options = constructionLevelOptions(framingAvailable).map((option) => {
|
|
56
|
+
const label = option.badge ? (
|
|
57
|
+
<span className="inline-flex items-center gap-1.5">
|
|
58
|
+
{option.label}
|
|
59
|
+
<Badge label={option.badge} />
|
|
60
|
+
</span>
|
|
61
|
+
) : (
|
|
62
|
+
option.label
|
|
63
|
+
)
|
|
64
|
+
if (!option.disabled) return { ...option, label }
|
|
65
|
+
return {
|
|
66
|
+
...option,
|
|
67
|
+
title: undefined,
|
|
68
|
+
label: (
|
|
69
|
+
<Tooltip>
|
|
70
|
+
<TooltipTrigger asChild>
|
|
71
|
+
<span>{label}</span>
|
|
72
|
+
</TooltipTrigger>
|
|
73
|
+
<TooltipContent>{option.title}</TooltipContent>
|
|
74
|
+
</Tooltip>
|
|
75
|
+
),
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
return (
|
|
79
|
+
<div className="space-y-2 py-1">
|
|
80
|
+
<div className="space-y-0.5">
|
|
81
|
+
<p className="font-medium text-sm">Construction animation</p>
|
|
82
|
+
<p className="text-muted-foreground text-xs">{current?.title}</p>
|
|
83
|
+
</div>
|
|
84
|
+
<SegmentedControl
|
|
85
|
+
aria-label="Construction animation"
|
|
86
|
+
onChange={(next) => chooseConstructionLevel(next, framingAvailable)}
|
|
87
|
+
options={options}
|
|
88
|
+
value={level}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|