@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,254 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
BaseNode,
|
|
7
|
+
clearSceneHistory,
|
|
8
|
+
type FloorplanGeometry,
|
|
9
|
+
LevelNode,
|
|
10
|
+
loadPlugin,
|
|
11
|
+
nodeRegistry,
|
|
12
|
+
nodeType,
|
|
13
|
+
objectId,
|
|
14
|
+
type RevealConfig,
|
|
15
|
+
runAsSceneCommitAuthor,
|
|
16
|
+
useScene,
|
|
17
|
+
} from '@pascal-app/core'
|
|
18
|
+
import {
|
|
19
|
+
type ConstructionRevealDriver,
|
|
20
|
+
getRevealPlanState,
|
|
21
|
+
startConstructionReveal,
|
|
22
|
+
useViewer,
|
|
23
|
+
} from '@pascal-app/viewer'
|
|
24
|
+
import React from 'react'
|
|
25
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
26
|
+
import useEditor from '../../../store/use-editor'
|
|
27
|
+
import { FloorplanRegistryLayer } from './floorplan-registry-layer'
|
|
28
|
+
import { floorplanRevealAxis, floorplanRevealLook } from './floorplan-reveal'
|
|
29
|
+
|
|
30
|
+
// The 2D plan follows the construction reveal (E-006): a node still waiting
|
|
31
|
+
// for its turn is not drawn, then a wall grows along its centreline and the
|
|
32
|
+
// rest fades in, on the reveal's own clock.
|
|
33
|
+
|
|
34
|
+
const PLUGIN_ID = 'fixture:plan-reveal'
|
|
35
|
+
const base = { object: 'node', parentId: null, visible: true, metadata: {} } as const
|
|
36
|
+
|
|
37
|
+
function planKind(
|
|
38
|
+
kind: string,
|
|
39
|
+
prefix: string,
|
|
40
|
+
reveal: RevealConfig,
|
|
41
|
+
floorplan: () => FloorplanGeometry,
|
|
42
|
+
): { schema: typeof BaseNode; definition: AnyNodeDefinition } {
|
|
43
|
+
const schema = BaseNode.extend({
|
|
44
|
+
id: objectId(prefix),
|
|
45
|
+
type: nodeType(kind),
|
|
46
|
+
children: LevelNode.shape.children,
|
|
47
|
+
})
|
|
48
|
+
const definition = {
|
|
49
|
+
kind,
|
|
50
|
+
schemaVersion: 1,
|
|
51
|
+
schema,
|
|
52
|
+
category: 'structure',
|
|
53
|
+
defaults: () => ({ ...base, children: [] }),
|
|
54
|
+
capabilities: { reveal },
|
|
55
|
+
floorplan,
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
schema: schema as unknown as typeof BaseNode,
|
|
59
|
+
definition: definition as unknown as AnyNodeDefinition,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const WALL_LINE = { x1: 0, y1: 0, x2: 4, y2: 0 }
|
|
64
|
+
const Wall = planKind(
|
|
65
|
+
'fixture:plan-wall',
|
|
66
|
+
'fxpwall',
|
|
67
|
+
{ phase: 'structure', style: 'rise' },
|
|
68
|
+
() => ({
|
|
69
|
+
kind: 'group',
|
|
70
|
+
children: [
|
|
71
|
+
{
|
|
72
|
+
kind: 'polygon',
|
|
73
|
+
points: [
|
|
74
|
+
[0, -0.1],
|
|
75
|
+
[4, -0.1],
|
|
76
|
+
[4, 0.1],
|
|
77
|
+
[0, 0.1],
|
|
78
|
+
],
|
|
79
|
+
fill: '#222',
|
|
80
|
+
},
|
|
81
|
+
{ kind: 'hit-line', ...WALL_LINE, strokeWidthPx: 18 },
|
|
82
|
+
],
|
|
83
|
+
}),
|
|
84
|
+
)
|
|
85
|
+
const Crate = planKind(
|
|
86
|
+
'fixture:plan-crate',
|
|
87
|
+
'fxpcrate',
|
|
88
|
+
{ phase: 'furnishing', style: 'drop', height: 0.3 },
|
|
89
|
+
() => ({ kind: 'rect', x: 1, y: 1, width: 0.5, height: 0.5, fill: '#888' }),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const make = (kind: { schema: typeof BaseNode }) => kind.schema.parse({}) as unknown as AnyNode
|
|
93
|
+
|
|
94
|
+
let driver: ConstructionRevealDriver | null = null
|
|
95
|
+
let restoreRegistry: () => void = () => {}
|
|
96
|
+
const savedScene = useScene.getState()
|
|
97
|
+
const savedViewer = useViewer.getState()
|
|
98
|
+
const savedEditor = useEditor.getState()
|
|
99
|
+
|
|
100
|
+
function plan(): string {
|
|
101
|
+
// Server rendering reads Zustand's startup state; render the current scene instead.
|
|
102
|
+
const useSyncExternalStore = React.useSyncExternalStore
|
|
103
|
+
const serverSnapshot = spyOn(React, 'useSyncExternalStore').mockImplementation(
|
|
104
|
+
(subscribe, getSnapshot) => useSyncExternalStore(subscribe, getSnapshot, getSnapshot),
|
|
105
|
+
)
|
|
106
|
+
try {
|
|
107
|
+
return renderToStaticMarkup(
|
|
108
|
+
<svg>
|
|
109
|
+
<FloorplanRegistryLayer />
|
|
110
|
+
</svg>,
|
|
111
|
+
)
|
|
112
|
+
} finally {
|
|
113
|
+
serverSnapshot.mockRestore()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function openLevel(): AnyNodeId {
|
|
118
|
+
const level = LevelNode.parse({ level: 0, children: [] })
|
|
119
|
+
useScene.getState().setScene({ [level.id]: level as AnyNode }, [level.id as AnyNodeId], {
|
|
120
|
+
installedPlugins: [PLUGIN_ID],
|
|
121
|
+
hasExplicitPluginInstallState: true,
|
|
122
|
+
})
|
|
123
|
+
clearSceneHistory()
|
|
124
|
+
useViewer.setState({
|
|
125
|
+
selection: { ...useViewer.getState().selection, levelId: level.id, selectedIds: [] },
|
|
126
|
+
} as never)
|
|
127
|
+
useEditor.setState({ viewMode: '2d' })
|
|
128
|
+
driver = startConstructionReveal({
|
|
129
|
+
reveals: (commit) => commit.author === 'agent',
|
|
130
|
+
prefersReducedMotion: () => false,
|
|
131
|
+
})
|
|
132
|
+
return level.id as AnyNodeId
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
beforeEach(async () => {
|
|
136
|
+
restoreRegistry = nodeRegistry._snapshot()
|
|
137
|
+
nodeRegistry._reset()
|
|
138
|
+
await loadPlugin({ id: PLUGIN_ID, apiVersion: 1, nodes: [Wall.definition, Crate.definition] })
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
afterEach(() => {
|
|
142
|
+
driver?.stop()
|
|
143
|
+
driver = null
|
|
144
|
+
restoreRegistry()
|
|
145
|
+
useScene.setState(savedScene)
|
|
146
|
+
useViewer.setState(savedViewer)
|
|
147
|
+
useEditor.setState(savedEditor)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('the plan during a construction reveal', () => {
|
|
151
|
+
test('a node waiting for its turn is not drawn; it is once its turn comes', () => {
|
|
152
|
+
const levelId = openLevel()
|
|
153
|
+
const wall = make(Wall)
|
|
154
|
+
const crate = make(Crate)
|
|
155
|
+
runAsSceneCommitAuthor('agent', () =>
|
|
156
|
+
useScene.getState().createNodes([
|
|
157
|
+
{ node: wall, parentId: levelId },
|
|
158
|
+
{ node: crate, parentId: levelId },
|
|
159
|
+
]),
|
|
160
|
+
)
|
|
161
|
+
let markup = plan()
|
|
162
|
+
expect(markup).not.toContain(`data-node-id="${wall.id}"`)
|
|
163
|
+
expect(markup).not.toContain(`data-node-id="${crate.id}"`)
|
|
164
|
+
|
|
165
|
+
driver!.tick(0)
|
|
166
|
+
expect(getRevealPlanState(wall.id)).not.toBeNull()
|
|
167
|
+
// Nothing draws in a test with no canvas, so the build does not wait for a first draw to settle.
|
|
168
|
+
let now = 0
|
|
169
|
+
while (getRevealPlanState(wall.id)?.progress === 0 && now < 3000) {
|
|
170
|
+
now += 20
|
|
171
|
+
driver!.tick(now)
|
|
172
|
+
}
|
|
173
|
+
markup = plan()
|
|
174
|
+
expect(markup).toContain(`data-node-id="${wall.id}"`)
|
|
175
|
+
expect(markup).not.toContain(`data-node-id="${crate.id}"`)
|
|
176
|
+
|
|
177
|
+
while (getRevealPlanState(crate.id)?.progress === 0 && now < 6000) {
|
|
178
|
+
now += 20
|
|
179
|
+
driver!.tick(now)
|
|
180
|
+
}
|
|
181
|
+
expect(plan()).toContain(`data-node-id="${crate.id}"`)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test("a person's own edit is drawn at once", () => {
|
|
185
|
+
const levelId = openLevel()
|
|
186
|
+
const wall = make(Wall)
|
|
187
|
+
useScene.getState().createNode(wall, levelId)
|
|
188
|
+
expect(plan()).toContain(`data-node-id="${wall.id}"`)
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe('how a revealing entry draws', () => {
|
|
193
|
+
test('a wall grows along its centreline from its start', () => {
|
|
194
|
+
const axis = floorplanRevealAxis([Wall.definition.floorplan!(make(Wall), {} as never)])
|
|
195
|
+
expect(axis).toEqual(WALL_LINE)
|
|
196
|
+
|
|
197
|
+
const start = floorplanRevealLook({ style: 'rise', progress: 0 }, axis)
|
|
198
|
+
const middle = floorplanRevealLook({ style: 'rise', progress: 0.5 }, axis)
|
|
199
|
+
const end = floorplanRevealLook({ style: 'rise', progress: 1 }, axis)
|
|
200
|
+
expect(start.opacity).toBe(1)
|
|
201
|
+
expect(start.clip!.width).toBeLessThan(middle.clip!.width)
|
|
202
|
+
expect(middle.clip!.width).toBeLessThan(end.clip!.width)
|
|
203
|
+
// Grown, it covers the whole wall and its mitred corners.
|
|
204
|
+
expect(start.clip!.x + end.clip!.width).toBeGreaterThan(WALL_LINE.x2)
|
|
205
|
+
expect(start.clip!.x).toBeLessThan(WALL_LINE.x1)
|
|
206
|
+
expect(end.clip!.rotateDeg).toBe(0)
|
|
207
|
+
expect(
|
|
208
|
+
floorplanRevealLook({ style: 'rise', progress: 0.5 }, { x1: 0, y1: 0, x2: 0, y2: 3 }).clip!
|
|
209
|
+
.rotateDeg,
|
|
210
|
+
).toBe(90)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test("an undo's reverse play is the same look read backwards: a wall's clip shrinks, the rest fades out", () => {
|
|
214
|
+
const axis = floorplanRevealAxis([Wall.definition.floorplan!(make(Wall), {} as never)])
|
|
215
|
+
const whole = floorplanRevealLook({ style: 'rise', progress: 1 }, axis)
|
|
216
|
+
const half = floorplanRevealLook({ style: 'rise', progress: 0.5 }, axis)
|
|
217
|
+
const gone = floorplanRevealLook({ style: 'rise', progress: 0 }, axis)
|
|
218
|
+
expect(whole.clip!.width).toBeGreaterThan(half.clip!.width)
|
|
219
|
+
expect(half.clip!.width).toBeGreaterThan(gone.clip!.width)
|
|
220
|
+
// Gone, the clip covers nothing of the wall (only the pad before its start).
|
|
221
|
+
expect(gone.clip!.width).toBeLessThanOrEqual(1)
|
|
222
|
+
expect(floorplanRevealLook({ style: 'drop', progress: 1 }, null).opacity).toBe(1)
|
|
223
|
+
expect(floorplanRevealLook({ style: 'drop', progress: 0.4 }, null).opacity).toBeLessThan(1)
|
|
224
|
+
expect(floorplanRevealLook({ style: 'drop', progress: 0 }, null).opacity).toBe(0)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('everything else fades in', () => {
|
|
228
|
+
for (const style of ['drop', 'settle', 'cut', 'scale', 'assemble'] as const) {
|
|
229
|
+
const faint = floorplanRevealLook({ style, progress: 0.1 }, null)
|
|
230
|
+
const strong = floorplanRevealLook({ style, progress: 0.8 }, null)
|
|
231
|
+
expect(faint.clip).toBeNull()
|
|
232
|
+
expect(faint.opacity).toBeLessThan(strong.opacity)
|
|
233
|
+
expect(floorplanRevealLook({ style, progress: 1 }, null).opacity).toBe(1)
|
|
234
|
+
}
|
|
235
|
+
// A rising kind with no centreline fades too.
|
|
236
|
+
expect(floorplanRevealLook({ style: 'rise', progress: 0.1 }, null).clip).toBeNull()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('only a centreline outside any transformed group is an axis', () => {
|
|
240
|
+
expect(floorplanRevealAxis([{ kind: 'rect', x: 0, y: 0, width: 1, height: 1 }])).toBeNull()
|
|
241
|
+
expect(
|
|
242
|
+
floorplanRevealAxis([
|
|
243
|
+
{
|
|
244
|
+
kind: 'group',
|
|
245
|
+
transform: { rotate: 1 },
|
|
246
|
+
children: [{ kind: 'hit-line', ...WALL_LINE, strokeWidthPx: 18 }],
|
|
247
|
+
},
|
|
248
|
+
]),
|
|
249
|
+
).toBeNull()
|
|
250
|
+
expect(
|
|
251
|
+
floorplanRevealAxis([{ kind: 'hit-line', x1: 1, y1: 1, x2: 1, y2: 1, strokeWidthPx: 18 }]),
|
|
252
|
+
).toBeNull()
|
|
253
|
+
})
|
|
254
|
+
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { FloorplanGeometry } from '@pascal-app/core'
|
|
4
|
+
import { getRevealPlanState, type RevealPlanState, subscribeRevealTicks } from '@pascal-app/viewer'
|
|
5
|
+
import { type RefObject, useEffect } from 'react'
|
|
6
|
+
|
|
7
|
+
/** A node's plan centreline, from its start: what a growing reveal follows. */
|
|
8
|
+
export type FloorplanRevealAxis = { x1: number; y1: number; x2: number; y2: number }
|
|
9
|
+
|
|
10
|
+
/** An entry's plan geometry, by pass. */
|
|
11
|
+
export type FloorplanRevealEntry = {
|
|
12
|
+
base: FloorplanGeometry | null
|
|
13
|
+
overlay: FloorplanGeometry | null
|
|
14
|
+
handles: FloorplanGeometry | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** How a revealing entry draws: its opacity, and for a growing one the clip that uncovers it. */
|
|
18
|
+
export type FloorplanRevealLook = {
|
|
19
|
+
opacity: number
|
|
20
|
+
clip: { x: number; y: number; width: number; height: number; rotateDeg: number } | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The clip reaches this far before the start and past the end, so mitred corners show whole. */
|
|
24
|
+
const CLIP_PAD_M = 1
|
|
25
|
+
/** And this far to either side of the centreline: the wall, its layers and its labels. */
|
|
26
|
+
const CLIP_HALF_WIDTH_M = 50
|
|
27
|
+
|
|
28
|
+
const easeOutCubic = (t: number) => 1 - (1 - t) ** 3
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The centreline a linear kind (a wall, a fence) draws as its `hit-line`, if
|
|
32
|
+
* its plan geometry has one outside any transformed group.
|
|
33
|
+
*/
|
|
34
|
+
export function floorplanRevealAxis(
|
|
35
|
+
geometries: readonly (FloorplanGeometry | null | undefined)[],
|
|
36
|
+
): FloorplanRevealAxis | null {
|
|
37
|
+
const visit = (geometry: FloorplanGeometry): FloorplanRevealAxis | null => {
|
|
38
|
+
if (geometry.kind === 'hit-line') {
|
|
39
|
+
const { x1, y1, x2, y2 } = geometry
|
|
40
|
+
return Math.hypot(x2 - x1, y2 - y1) > 1e-6 ? { x1, y1, x2, y2 } : null
|
|
41
|
+
}
|
|
42
|
+
if (geometry.kind !== 'group' || geometry.transform) return null
|
|
43
|
+
for (const child of geometry.children) {
|
|
44
|
+
const axis = visit(child)
|
|
45
|
+
if (axis) return axis
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
for (const geometry of geometries) {
|
|
50
|
+
const axis = geometry ? visit(geometry) : null
|
|
51
|
+
if (axis) return axis
|
|
52
|
+
}
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The plan's twin of a 3D reveal: a node that rises in 3D and has a
|
|
58
|
+
* centreline grows along it from its start; everything else fades in. An undo's
|
|
59
|
+
* reverse play reads the same look backwards: a wall's clip shrinks toward its
|
|
60
|
+
* start, everything else fades out.
|
|
61
|
+
*/
|
|
62
|
+
export function floorplanRevealLook(
|
|
63
|
+
state: RevealPlanState,
|
|
64
|
+
axis: FloorplanRevealAxis | null,
|
|
65
|
+
): FloorplanRevealLook {
|
|
66
|
+
const eased = easeOutCubic(Math.min(1, Math.max(0, state.progress)))
|
|
67
|
+
if (state.style !== 'rise' || !axis) return { opacity: eased, clip: null }
|
|
68
|
+
const length = Math.hypot(axis.x2 - axis.x1, axis.y2 - axis.y1)
|
|
69
|
+
return {
|
|
70
|
+
opacity: 1,
|
|
71
|
+
clip: {
|
|
72
|
+
x: axis.x1 - CLIP_PAD_M,
|
|
73
|
+
y: axis.y1 - CLIP_HALF_WIDTH_M,
|
|
74
|
+
width: CLIP_PAD_M + eased * (length + CLIP_PAD_M),
|
|
75
|
+
height: CLIP_HALF_WIDTH_M * 2,
|
|
76
|
+
rotateDeg: (Math.atan2(axis.y2 - axis.y1, axis.x2 - axis.x1) * 180) / Math.PI,
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
82
|
+
let clipIds = 0
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Plays a node's construction reveal on its plan entry: while the reveal
|
|
86
|
+
* runs, each tick sets the entry group's opacity or its growing clip, then
|
|
87
|
+
* clears them when the node is done. Imperative, so a reveal re-renders no
|
|
88
|
+
* React tree; an entry with nothing revealing subscribes to nothing.
|
|
89
|
+
*/
|
|
90
|
+
export function useFloorplanReveal(
|
|
91
|
+
nodeId: string,
|
|
92
|
+
groupRef: RefObject<SVGGElement | null>,
|
|
93
|
+
entry: FloorplanRevealEntry | null,
|
|
94
|
+
waiting: boolean,
|
|
95
|
+
/** The reveal has something to play for the node (it moves in, or an undo takes it back): the effect runs again. */
|
|
96
|
+
active = false,
|
|
97
|
+
) {
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const group = groupRef.current
|
|
100
|
+
if (waiting || !group || !getRevealPlanState(nodeId)) return
|
|
101
|
+
const axis = entry ? floorplanRevealAxis([entry.base, entry.overlay, entry.handles]) : null
|
|
102
|
+
let clip: SVGClipPathElement | null = null
|
|
103
|
+
let rect: SVGRectElement | null = null
|
|
104
|
+
let stop = () => {}
|
|
105
|
+
const clear = () => {
|
|
106
|
+
stop()
|
|
107
|
+
group.removeAttribute('opacity')
|
|
108
|
+
group.removeAttribute('clip-path')
|
|
109
|
+
clip?.remove()
|
|
110
|
+
clip = null
|
|
111
|
+
}
|
|
112
|
+
const apply = () => {
|
|
113
|
+
const state = getRevealPlanState(nodeId)
|
|
114
|
+
if (!state) return clear()
|
|
115
|
+
const look = floorplanRevealLook(state, axis)
|
|
116
|
+
group.setAttribute('opacity', String(look.opacity))
|
|
117
|
+
if (!look.clip) return
|
|
118
|
+
if (!(clip && rect)) {
|
|
119
|
+
clipIds += 1
|
|
120
|
+
const id = `floorplan-reveal-${clipIds}`
|
|
121
|
+
clip = document.createElementNS(SVG_NS, 'clipPath')
|
|
122
|
+
clip.setAttribute('id', id)
|
|
123
|
+
clip.setAttribute('clipPathUnits', 'userSpaceOnUse')
|
|
124
|
+
rect = document.createElementNS(SVG_NS, 'rect')
|
|
125
|
+
clip.appendChild(rect)
|
|
126
|
+
group.appendChild(clip)
|
|
127
|
+
group.setAttribute('clip-path', `url(#${id})`)
|
|
128
|
+
}
|
|
129
|
+
const { x, y, width, height, rotateDeg } = look.clip
|
|
130
|
+
rect.setAttribute('x', String(x))
|
|
131
|
+
rect.setAttribute('y', String(y))
|
|
132
|
+
rect.setAttribute('width', String(width))
|
|
133
|
+
rect.setAttribute('height', String(height))
|
|
134
|
+
rect.setAttribute('transform', `rotate(${rotateDeg} ${axis?.x1 ?? 0} ${axis?.y1 ?? 0})`)
|
|
135
|
+
}
|
|
136
|
+
stop = subscribeRevealTicks(apply)
|
|
137
|
+
apply()
|
|
138
|
+
return clear
|
|
139
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: `active` only says the reveal's state may have changed
|
|
140
|
+
}, [nodeId, groupRef, entry, waiting, active])
|
|
141
|
+
}
|