@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
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @pascal-app/editor
|
|
2
|
+
|
|
3
|
+
The Pascal building editor as a React component: tools, panels, the floor plan, views and the agent chat surfaces, composed over `@pascal-app/viewer`.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @pascal-app/core @pascal-app/viewer @pascal-app/editor @pascal-app/nodes
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Load the built-in plugin once before mounting, as for the viewer (see the [viewer quick start](../viewer/README.md#usage)).
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Editor } from '@pascal-app/editor'
|
|
17
|
+
|
|
18
|
+
export function ProjectPage({ projectId }: { projectId: string }) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="dark" style={{ width: '100vw', height: '100vh' }}>
|
|
21
|
+
<Editor
|
|
22
|
+
layoutVersion="v2"
|
|
23
|
+
projectId={projectId}
|
|
24
|
+
onLoad={() => loadScene(projectId)}
|
|
25
|
+
onSave={(scene) => saveScene(projectId, scene)}
|
|
26
|
+
actionMenuPlacement="top"
|
|
27
|
+
loadingSlot={<MyProjectSpinner />}
|
|
28
|
+
onLoaderChange={(visible) => track('editor-loading', visible)}
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`apps/editor/components/scene-loader.tsx` is a complete host: navbar, sidebar tabs, toolbars and persistence.
|
|
36
|
+
|
|
37
|
+
## Host props
|
|
38
|
+
|
|
39
|
+
| Prop | What it does |
|
|
40
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
41
|
+
| `loadingSlot` | Your loading surface, shown in the canvas panes of both layouts and in 2D and 3D while the scene loads. The editor still decides when loading is over and fires `onLoaderChange`; a failed load always shows the editor's error and retry screen. Omit it or pass `null` for the default loader. |
|
|
42
|
+
| `actionMenuPlacement` | `'bottom'` (default) or `'top'`. With `'top'`, the v2 layout gives the tool menu its own row under the toolbar, centred on the scene panes and following them in split, so the bottom strip is free; v1 floats it at the top edge. Mobile keeps the bottom rail. |
|
|
43
|
+
| `viewBar` | Each pane's view bar (v2, default `true`). Turn it off for a host that shows one fixed view. |
|
|
44
|
+
| `sceneOverlay` | Chrome over the 3D and 2D panes with the level selector and tool dock, hidden while no scene view is on screen. |
|
|
45
|
+
| `stageOverlay` | A full-bleed surface over the whole stage; the scene stays mounted underneath. To show something beside the scene, register a view instead. |
|
|
46
|
+
|
|
47
|
+
## Theme
|
|
48
|
+
|
|
49
|
+
The editor sets no theme of its own: its chrome follows the nearest `.dark` ancestor, so leave the class off for a light editor. Menus and tooltips portal to `<body>`, so put `dark` there too (or on `<html>`) for a dark one. Hand the person a theme switch with `InterfaceThemeRow` in the settings panel's `preferences` slot:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { InterfaceThemeRow, SettingsPanel, type InterfaceTheme } from '@pascal-app/editor'
|
|
53
|
+
|
|
54
|
+
<SettingsPanel
|
|
55
|
+
preferences={<InterfaceThemeRow value={theme} onChange={(next: InterfaceTheme) => setTheme(next)} />}
|
|
56
|
+
/>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`theme` is `'system' | 'light' | 'dark'`; applying it (toggling `.dark`) is the host's. A host that passes no row gets none. The Preview stage, the walkthrough HUD and the controls bar stay dark in either theme. The viewer's own "Scene theme" is separate (see [materials and themes](../../wiki/architecture/materials-and-themes.md)).
|
|
60
|
+
|
|
61
|
+
## Views
|
|
62
|
+
|
|
63
|
+
The stage is one or two panes, each showing a view: 3D, 2D, or one a plugin registers.
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { openEditorView, PLUGINS_VIEW_ID, registerEditorHostView } from '@pascal-app/editor'
|
|
67
|
+
|
|
68
|
+
registerEditorHostView({
|
|
69
|
+
id: 'acme:signage:schedule',
|
|
70
|
+
label: 'Sign schedule',
|
|
71
|
+
icon: { kind: 'url', src: '/icons/signs.webp' },
|
|
72
|
+
component: () => import('./schedule-view'),
|
|
73
|
+
pluginId: 'acme:signage',
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
openEditorView('acme:signage:schedule', { beside: true }) // split, next to the scene
|
|
77
|
+
openEditorView(PLUGINS_VIEW_ID) // the plugin directory
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Each workspace keeps its own layout, and swapping panes never remounts the 3D canvas or the plan. `\` toggles split. The full contract is in [plugin authoring](../../wiki/architecture/plugin-authoring.md).
|
|
81
|
+
|
|
82
|
+
## Selecting and showing a node
|
|
83
|
+
|
|
84
|
+
The helpers the editor's own lists use:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { enterUnitFocus, frameViewerNode, selectZoneOrRoom } from '@pascal-app/editor'
|
|
88
|
+
|
|
89
|
+
selectZoneOrRoom(zoneId) // 'room' for an enclosed room, else 'zone'
|
|
90
|
+
frameViewerNode(nodeId, source) // the node's saved view, else its box; the plan frames it when no 3D view is on screen
|
|
91
|
+
enterUnitFocus(unitId) // focus a unit, as the site panel does
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
What a host's chat points at (`useViewer.pointedIds`) lights in the plan and as a room outline, as well as in 3D. See [selection managers](../../wiki/architecture/selection-managers.md) and [point and ask](../../wiki/architecture/point-and-ask.md).
|
|
95
|
+
|
|
96
|
+
## Plugin chat surfaces
|
|
97
|
+
|
|
98
|
+
A plugin's `PluginChatExtension` can give a host's chat a start screen (`start`), shown while the thread is empty and acting through `ChatStartProps` (`send`, `attach`, `requirePro`). `IconRefGlyph` and `IconRefImage` render registry icons the way the editor does:
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { IconRefGlyph } from '@pascal-app/editor'
|
|
102
|
+
|
|
103
|
+
<IconRefGlyph icon={view.icon} />
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
See [plugin authoring, agent tools and chat extensions](../../wiki/architecture/plugin-authoring.md#agent-tools-and-chat-extensions).
|
package/package.json
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pascal-app/editor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Pascal building editor component",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.tsx",
|
|
9
9
|
"./catalog": "./src/components/ui/item-catalog/catalog-items.tsx",
|
|
10
|
-
"./procedural-items": "./src/components/procedural-items/inspector.tsx"
|
|
10
|
+
"./procedural-items": "./src/components/procedural-items/inspector.tsx",
|
|
11
|
+
"./distribution": "./src/components/tools/distribution/distribution-public.ts"
|
|
11
12
|
},
|
|
12
13
|
"scripts": {
|
|
14
|
+
"bench:room-labels": "bun run src/components/editor/__bench__/room-label-placement.bench.ts",
|
|
13
15
|
"check-types": "tsgo --noEmit",
|
|
14
16
|
"test": "bun test src"
|
|
15
17
|
},
|
|
16
18
|
"peerDependencies": {
|
|
17
|
-
"@pascal-app/core": "^1.0.
|
|
18
|
-
"@pascal-app/viewer": "^1.0.
|
|
19
|
+
"@pascal-app/core": "^1.0.6",
|
|
20
|
+
"@pascal-app/viewer": "^1.0.6",
|
|
19
21
|
"@react-three/drei": "^10",
|
|
20
22
|
"@react-three/fiber": "^9",
|
|
21
23
|
"next": ">=15",
|
|
@@ -27,23 +29,23 @@
|
|
|
27
29
|
"@dnd-kit/core": "^6.3.1",
|
|
28
30
|
"@dnd-kit/sortable": "^10.0.0",
|
|
29
31
|
"@dnd-kit/utilities": "^3.2.2",
|
|
30
|
-
"@iconify/react": "^6.0.
|
|
31
|
-
"@number-flow/react": "^0.6.
|
|
32
|
-
"@pascal-app/geometry-script": "^1.0.
|
|
33
|
-
"@pascal-app/ifc-converter": "^1.0.
|
|
34
|
-
"@pascal-app/lingo": "^0.
|
|
35
|
-
"@radix-ui/react-alert-dialog": "^1.1
|
|
36
|
-
"@radix-ui/react-context-menu": "^2.
|
|
37
|
-
"@radix-ui/react-dialog": "^1.1
|
|
38
|
-
"@radix-ui/react-dropdown-menu": "^2.1
|
|
39
|
-
"@radix-ui/react-popover": "^1.1
|
|
40
|
-
"@radix-ui/react-select": "^2.
|
|
41
|
-
"@radix-ui/react-separator": "^1.
|
|
42
|
-
"@radix-ui/react-slider": "^1.
|
|
43
|
-
"@radix-ui/react-slot": "^1.
|
|
44
|
-
"@radix-ui/react-switch": "^1.
|
|
45
|
-
"@radix-ui/react-tabs": "^1.1
|
|
46
|
-
"@radix-ui/react-tooltip": "^1.
|
|
32
|
+
"@iconify/react": "^6.0.3",
|
|
33
|
+
"@number-flow/react": "^0.6.2",
|
|
34
|
+
"@pascal-app/geometry-script": "^1.0.6",
|
|
35
|
+
"@pascal-app/ifc-converter": "^1.0.6",
|
|
36
|
+
"@pascal-app/lingo": "^0.5.0",
|
|
37
|
+
"@radix-ui/react-alert-dialog": "^1.2.1",
|
|
38
|
+
"@radix-ui/react-context-menu": "^2.4.1",
|
|
39
|
+
"@radix-ui/react-dialog": "^1.3.1",
|
|
40
|
+
"@radix-ui/react-dropdown-menu": "^2.2.1",
|
|
41
|
+
"@radix-ui/react-popover": "^1.3.1",
|
|
42
|
+
"@radix-ui/react-select": "^2.4.1",
|
|
43
|
+
"@radix-ui/react-separator": "^1.2.0",
|
|
44
|
+
"@radix-ui/react-slider": "^1.6.1",
|
|
45
|
+
"@radix-ui/react-slot": "^1.5.0",
|
|
46
|
+
"@radix-ui/react-switch": "^1.4.1",
|
|
47
|
+
"@radix-ui/react-tabs": "^1.2.1",
|
|
48
|
+
"@radix-ui/react-tooltip": "^1.4.1",
|
|
47
49
|
"@visual-json/react": "^0.4.0",
|
|
48
50
|
"blob-stream": "^0.1.3",
|
|
49
51
|
"class-variance-authority": "^0.7.1",
|
|
@@ -51,32 +53,32 @@
|
|
|
51
53
|
"cmdk": "^1.1.1",
|
|
52
54
|
"fflate": "^0.8.3",
|
|
53
55
|
"howler": "^2.2.4",
|
|
54
|
-
"lucide-react": "^1.
|
|
55
|
-
"manifold-3d": "3.5.
|
|
56
|
+
"lucide-react": "^1.61.0",
|
|
57
|
+
"manifold-3d": "3.5.4",
|
|
56
58
|
"mitt": "^3.0.1",
|
|
57
|
-
"motion": "^
|
|
58
|
-
"nanoid": "^
|
|
59
|
-
"pdfkit": "^0.
|
|
60
|
-
"tailwind-merge": "^3.
|
|
59
|
+
"motion": "^14.6.0",
|
|
60
|
+
"nanoid": "^6.0.2",
|
|
61
|
+
"pdfkit": "^0.20.2",
|
|
62
|
+
"tailwind-merge": "^3.7.0",
|
|
61
63
|
"three-bvh-csg": "^0.0.18",
|
|
62
|
-
"three-mesh-bvh": "~0.9.
|
|
63
|
-
"zod": ">=4.
|
|
64
|
-
"zustand": "^5.0.
|
|
64
|
+
"three-mesh-bvh": "~0.9.16",
|
|
65
|
+
"zod": ">=4.6.7 <4.7",
|
|
66
|
+
"zustand": "^5.0.16"
|
|
65
67
|
},
|
|
66
68
|
"devDependencies": {
|
|
67
|
-
"@pascal-app/core": "^1.0.
|
|
68
|
-
"@pascal-app/viewer": "^1.0.
|
|
69
|
+
"@pascal-app/core": "^1.0.6",
|
|
70
|
+
"@pascal-app/viewer": "^1.0.6",
|
|
69
71
|
"@pascal/typescript-config": "*",
|
|
70
|
-
"@react-three/test-renderer": "^9.1.
|
|
72
|
+
"@react-three/test-renderer": "^9.1.1",
|
|
71
73
|
"@types/blob-stream": "^0.1.33",
|
|
72
|
-
"@types/bun": "^1.4.
|
|
73
|
-
"@types/howler": "^2.2.
|
|
74
|
-
"@types/pdfkit": "^0.17.
|
|
74
|
+
"@types/bun": "^1.4.3",
|
|
75
|
+
"@types/howler": "^2.2.13",
|
|
76
|
+
"@types/pdfkit": "^0.17.7",
|
|
75
77
|
"@types/react": "19.3.0",
|
|
76
78
|
"@types/react-dom": "19.3.0",
|
|
77
|
-
"@types/three": "^0.
|
|
79
|
+
"@types/three": "^0.186.0",
|
|
78
80
|
"fast-xml-parser": "^5.11.2",
|
|
79
|
-
"typescript": "
|
|
81
|
+
"typescript": "7.0.2"
|
|
80
82
|
},
|
|
81
83
|
"repository": {
|
|
82
84
|
"type": "git",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Polygon } from '@pascal-app/core'
|
|
2
|
+
import {
|
|
3
|
+
type PillCandidate,
|
|
4
|
+
placePills,
|
|
5
|
+
type RoomHit,
|
|
6
|
+
type RoomLabelEntry,
|
|
7
|
+
roomAt,
|
|
8
|
+
} from '../room-labels'
|
|
9
|
+
|
|
10
|
+
// One room-label placement pass and one pointer pick, for a 900 × 600 px house of 31, 60 and 120
|
|
11
|
+
// rooms (every third an L), printed as JSON medians in ms. Run with `bun run <this file>`.
|
|
12
|
+
let seed = 11
|
|
13
|
+
const random = () => {
|
|
14
|
+
seed = (seed * 1_103_515_245 + 12_345) % 2 ** 31
|
|
15
|
+
return seed / 2 ** 31
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function house(count: number) {
|
|
19
|
+
const columns = Math.ceil(Math.sqrt(count * 1.5))
|
|
20
|
+
const rows = Math.ceil(count / columns)
|
|
21
|
+
const width = 900 / columns
|
|
22
|
+
const depth = 600 / rows
|
|
23
|
+
return Array.from({ length: count }, (_, i) => {
|
|
24
|
+
const left = 270 + (i % columns) * width
|
|
25
|
+
const top = 150 + Math.floor(i / columns) * depth
|
|
26
|
+
const right = left + width
|
|
27
|
+
const bottom = top + depth
|
|
28
|
+
const outer: [number, number][] =
|
|
29
|
+
i % 3 === 0
|
|
30
|
+
? [
|
|
31
|
+
[left, top],
|
|
32
|
+
[right, top],
|
|
33
|
+
[right, top + depth / 2],
|
|
34
|
+
[left + width / 2, top + depth / 2],
|
|
35
|
+
[left + width / 2, bottom],
|
|
36
|
+
[left, bottom],
|
|
37
|
+
]
|
|
38
|
+
: [
|
|
39
|
+
[left, top],
|
|
40
|
+
[right, top],
|
|
41
|
+
[right, bottom],
|
|
42
|
+
[left, bottom],
|
|
43
|
+
]
|
|
44
|
+
const outline: Polygon = { outer, holes: [] }
|
|
45
|
+
const pill = 60 + random() * 110
|
|
46
|
+
const candidate: PillCandidate = {
|
|
47
|
+
id: `room_${i}`,
|
|
48
|
+
x: left + width / 4,
|
|
49
|
+
y: top + depth / 4,
|
|
50
|
+
size: [pill, 24, pill * 0.6],
|
|
51
|
+
area: width * depth * (i % 3 === 0 ? 0.75 : 1),
|
|
52
|
+
outline,
|
|
53
|
+
}
|
|
54
|
+
const entry: RoomLabelEntry = {
|
|
55
|
+
zoneId: candidate.id,
|
|
56
|
+
polygon: outer,
|
|
57
|
+
holes: [],
|
|
58
|
+
area: candidate.area,
|
|
59
|
+
}
|
|
60
|
+
return { candidate, entry }
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const median = (samples: number[]) => samples.sort((a, b) => a - b)[samples.length >> 1]!
|
|
65
|
+
|
|
66
|
+
const pane = { width: 1440, height: 900 }
|
|
67
|
+
const results: Record<string, unknown> = {}
|
|
68
|
+
for (const count of [31, 60, 120]) {
|
|
69
|
+
const rooms = house(count)
|
|
70
|
+
const candidates = rooms.map((room) => room.candidate)
|
|
71
|
+
const entries = rooms.map((room) => room.entry)
|
|
72
|
+
for (let i = 0; i < 20; i++) placePills(candidates, pane)
|
|
73
|
+
const cold: number[] = []
|
|
74
|
+
const warm: number[] = []
|
|
75
|
+
let previous = placePills(candidates, pane)
|
|
76
|
+
for (let i = 0; i < 100; i++) {
|
|
77
|
+
let started = performance.now()
|
|
78
|
+
placePills(candidates, pane)
|
|
79
|
+
cold.push(performance.now() - started)
|
|
80
|
+
started = performance.now()
|
|
81
|
+
previous = placePills(candidates, pane, previous)
|
|
82
|
+
warm.push(performance.now() - started)
|
|
83
|
+
}
|
|
84
|
+
const points = Array.from(
|
|
85
|
+
{ length: 1000 },
|
|
86
|
+
(): RoomHit => [270 + random() * 900, 150 + random() * 600, 0],
|
|
87
|
+
)
|
|
88
|
+
for (const point of points) roomAt(entries, () => point)
|
|
89
|
+
const picks: number[] = []
|
|
90
|
+
for (let i = 0; i < 20; i++) {
|
|
91
|
+
const started = performance.now()
|
|
92
|
+
for (const point of points) roomAt(entries, () => point)
|
|
93
|
+
picks.push((performance.now() - started) / points.length)
|
|
94
|
+
}
|
|
95
|
+
const tiers = Object.fromEntries(
|
|
96
|
+
['full', 'name', 'dot', 'hidden'].map((tier) => [
|
|
97
|
+
tier,
|
|
98
|
+
[...previous.values()].filter((placement) => placement.tier === tier).length,
|
|
99
|
+
]),
|
|
100
|
+
)
|
|
101
|
+
results[count] = {
|
|
102
|
+
placeMs: median(cold),
|
|
103
|
+
placeWithPreviousMs: median(warm),
|
|
104
|
+
pickMs: median(picks),
|
|
105
|
+
tiers,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
console.log(JSON.stringify(results, null, 2))
|
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
setKeyLightDirectionOverride,
|
|
13
13
|
setProceduralEmission,
|
|
14
14
|
setSlotDefaultOverrides,
|
|
15
|
+
siteSurfaces,
|
|
15
16
|
temporarilyHideNodeTypes,
|
|
17
|
+
temporarilyWithoutAmbiance,
|
|
16
18
|
useSceneAtmosphere,
|
|
17
19
|
useViewer,
|
|
18
20
|
} from '@pascal-app/viewer'
|
|
@@ -92,20 +94,6 @@ async function settledHeroFraming() {
|
|
|
92
94
|
return framing
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
/** The site's own surfaces (ground fill, horizon disc, property ribbons) —
|
|
96
|
-
* not its registered children (buildings, items), which are groups. */
|
|
97
|
-
function siteSurfaces(): Object3D[] {
|
|
98
|
-
const surfaces: Object3D[] = []
|
|
99
|
-
for (const siteId of sceneRegistry.byType.site ?? []) {
|
|
100
|
-
for (const child of sceneRegistry.nodes.get(siteId)?.children ?? []) {
|
|
101
|
-
if (child.visible && !(child.type === 'Group' && child.children.length > 0)) {
|
|
102
|
-
surfaces.push(child)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return surfaces
|
|
107
|
-
}
|
|
108
|
-
|
|
109
97
|
/**
|
|
110
98
|
* A soft contact shade on the ground along every ground-floor wall: the wall
|
|
111
99
|
* centrelines stroked onto a canvas in the building's facade axes, then
|
|
@@ -221,6 +209,8 @@ export function BakeThumbnail({
|
|
|
221
209
|
|
|
222
210
|
const run = async () => {
|
|
223
211
|
const restoreNodeVisibility = temporarilyHideNodeTypes(['scan', 'guide', 'spawn'])
|
|
212
|
+
// The hero is the building in the world's own light, wherever the person's eye is.
|
|
213
|
+
const restoreAmbiance = temporarilyWithoutAmbiance()
|
|
224
214
|
const priorLights = new Map<string, boolean>()
|
|
225
215
|
const restGroups = new Map<string, [number, number, number]>()
|
|
226
216
|
for (const node of Object.values(useScene.getState().nodes)) {
|
|
@@ -333,7 +323,7 @@ export function BakeThumbnail({
|
|
|
333
323
|
}
|
|
334
324
|
|
|
335
325
|
pipeline.applyEnvironment({
|
|
336
|
-
theme: useViewer.getState().
|
|
326
|
+
theme: useViewer.getState().sceneAppearance,
|
|
337
327
|
transparent: false,
|
|
338
328
|
grade: true,
|
|
339
329
|
edges: useViewer.getState().edges,
|
|
@@ -399,6 +389,7 @@ export function BakeThumbnail({
|
|
|
399
389
|
.getState()
|
|
400
390
|
.setProceduralLights(id as import('@pascal-app/core').AnyNodeId, on)
|
|
401
391
|
restoreNodeVisibility()
|
|
392
|
+
restoreAmbiance()
|
|
402
393
|
}
|
|
403
394
|
}
|
|
404
395
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { expect, spyOn, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
BaseNode,
|
|
6
|
+
LevelNode,
|
|
7
|
+
nodeRegistry,
|
|
8
|
+
objectId,
|
|
9
|
+
registerNode,
|
|
10
|
+
runAsSceneCommitAuthor,
|
|
11
|
+
sceneRegistry,
|
|
12
|
+
useScene,
|
|
13
|
+
} from '@pascal-app/core'
|
|
14
|
+
import {
|
|
15
|
+
type RevealGroupEvent,
|
|
16
|
+
startConstructionReveal,
|
|
17
|
+
subscribeRevealEvents,
|
|
18
|
+
useViewer,
|
|
19
|
+
} from '@pascal-app/viewer'
|
|
20
|
+
import { act, create } from '@react-three/test-renderer'
|
|
21
|
+
import { BoxGeometry, Mesh, MeshBasicMaterial } from 'three'
|
|
22
|
+
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
23
|
+
import { ConstructionRevealAudio } from './construction-reveal-audio'
|
|
24
|
+
|
|
25
|
+
test('real reveal landings respect audio controls and the shared cue clock', async () => {
|
|
26
|
+
const restore = nodeRegistry._snapshot()
|
|
27
|
+
const savedScene = useScene.getState()
|
|
28
|
+
const savedHistory = useScene.temporal.getState()
|
|
29
|
+
const savedViewer = useViewer.getState()
|
|
30
|
+
const savedObjects = new Map(sceneRegistry.nodes)
|
|
31
|
+
const actDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'IS_REACT_ACT_ENVIRONMENT')
|
|
32
|
+
const rafDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'requestAnimationFrame')
|
|
33
|
+
const cancelDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'cancelAnimationFrame')
|
|
34
|
+
Object.defineProperties(globalThis, {
|
|
35
|
+
IS_REACT_ACT_ENVIRONMENT: { configurable: true, value: true },
|
|
36
|
+
requestAnimationFrame: { configurable: true, value: () => 0 },
|
|
37
|
+
cancelAnimationFrame: { configurable: true, value: () => {} },
|
|
38
|
+
})
|
|
39
|
+
let audioClock = 0
|
|
40
|
+
const clock = spyOn(performance, 'now').mockImplementation(() => audioClock)
|
|
41
|
+
const cues: string[] = []
|
|
42
|
+
const observe = (name: string) => cues.push(name)
|
|
43
|
+
sfxEmitter.on('*', observe)
|
|
44
|
+
const events: RevealGroupEvent[] = []
|
|
45
|
+
const stopObserve = subscribeRevealEvents((event) => {
|
|
46
|
+
if (event.type === 'land') events.push(event)
|
|
47
|
+
})
|
|
48
|
+
let instant = false
|
|
49
|
+
let time = 0
|
|
50
|
+
let index = 0
|
|
51
|
+
const meshes: Mesh[] = []
|
|
52
|
+
const schema = BaseNode.extend({ id: objectId('audio-smoke') })
|
|
53
|
+
for (const [kind, phase] of [
|
|
54
|
+
['audio:structure', 'structure'],
|
|
55
|
+
['audio:furnishing', 'furnishing'],
|
|
56
|
+
] as const) {
|
|
57
|
+
registerNode({
|
|
58
|
+
kind,
|
|
59
|
+
schemaVersion: 1,
|
|
60
|
+
schema,
|
|
61
|
+
category: 'utility',
|
|
62
|
+
defaults: () => ({}),
|
|
63
|
+
capabilities: { reveal: { phase, style: 'scale' } },
|
|
64
|
+
} as unknown as AnyNodeDefinition)
|
|
65
|
+
}
|
|
66
|
+
const level = LevelNode.parse({ id: 'level_audio_smoke' })
|
|
67
|
+
useScene.setState({ nodes: { [level.id]: level }, rootNodeIds: [level.id], readOnly: false })
|
|
68
|
+
useViewer.setState({ renderContext: 'editor', isExporting: false })
|
|
69
|
+
useViewer
|
|
70
|
+
.getState()
|
|
71
|
+
.setSelection({ levelId: level.id, buildingId: null, selectedIds: [], zoneId: null })
|
|
72
|
+
const driver = startConstructionReveal({
|
|
73
|
+
reveals: (commit) => commit.author === 'agent',
|
|
74
|
+
level: () => 'full',
|
|
75
|
+
prefersReducedMotion: () => instant,
|
|
76
|
+
})
|
|
77
|
+
const renderer = await create(<ConstructionRevealAudio enabled={false} level="full" />)
|
|
78
|
+
async function land(kind = 'audio:structure') {
|
|
79
|
+
const id = `audio-smoke_${++index}`
|
|
80
|
+
const node = { ...schema.parse({ id }), type: kind, parentId: level.id } as unknown as AnyNode
|
|
81
|
+
const mesh = new Mesh(new BoxGeometry(1, 1, 1), new MeshBasicMaterial())
|
|
82
|
+
meshes.push(mesh)
|
|
83
|
+
sceneRegistry.nodes.set(node.id, mesh)
|
|
84
|
+
const before = events.length
|
|
85
|
+
await act(async () =>
|
|
86
|
+
runAsSceneCommitAuthor('agent', () => useScene.getState().createNode(node, level.id)),
|
|
87
|
+
)
|
|
88
|
+
// A real CPU mesh supplies the renderer's ready handshake, not drawn pixels.
|
|
89
|
+
useScene.getState().clearDirty(node.id)
|
|
90
|
+
for (let frame = 0; frame < 150; frame++) {
|
|
91
|
+
time += 20
|
|
92
|
+
await act(async () => driver.tick(time, 20))
|
|
93
|
+
}
|
|
94
|
+
expect(events.length).toBeGreaterThan(before)
|
|
95
|
+
expect(Boolean(events.at(-1)?.instant)).toBe(instant)
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
await land()
|
|
99
|
+
expect(cues).toEqual([])
|
|
100
|
+
await renderer.update(<ConstructionRevealAudio enabled={true} level="simple" />)
|
|
101
|
+
await land()
|
|
102
|
+
expect(cues).toEqual([])
|
|
103
|
+
await renderer.update(<ConstructionRevealAudio enabled={true} level="off" />)
|
|
104
|
+
await land()
|
|
105
|
+
expect(cues).toEqual([])
|
|
106
|
+
await renderer.update(<ConstructionRevealAudio enabled={true} level="full" />)
|
|
107
|
+
await land()
|
|
108
|
+
expect(cues).toEqual(['sfx:structure-build'])
|
|
109
|
+
await renderer.update(<ConstructionRevealAudio enabled={false} level="full" />)
|
|
110
|
+
await renderer.update(<ConstructionRevealAudio enabled={true} level="framing" />)
|
|
111
|
+
audioClock = 249
|
|
112
|
+
await land()
|
|
113
|
+
expect(cues).toEqual(['sfx:structure-build'])
|
|
114
|
+
audioClock = 250
|
|
115
|
+
await land('audio:furnishing')
|
|
116
|
+
expect(cues).toEqual(['sfx:structure-build', 'sfx:item-place'])
|
|
117
|
+
audioClock = 1000
|
|
118
|
+
instant = true
|
|
119
|
+
await land()
|
|
120
|
+
expect(cues).toHaveLength(2)
|
|
121
|
+
await renderer.unmount()
|
|
122
|
+
instant = false
|
|
123
|
+
await land()
|
|
124
|
+
expect(cues).toHaveLength(2)
|
|
125
|
+
} finally {
|
|
126
|
+
driver.stop()
|
|
127
|
+
await renderer.unmount()
|
|
128
|
+
stopObserve()
|
|
129
|
+
sfxEmitter.off('*', observe)
|
|
130
|
+
clock.mockRestore()
|
|
131
|
+
restore()
|
|
132
|
+
useScene.setState(savedScene, true)
|
|
133
|
+
useScene.temporal.setState(savedHistory, true)
|
|
134
|
+
useViewer.setState(savedViewer, true)
|
|
135
|
+
sceneRegistry.nodes.clear()
|
|
136
|
+
for (const [id, mesh] of savedObjects) sceneRegistry.nodes.set(id, mesh)
|
|
137
|
+
for (const mesh of meshes) {
|
|
138
|
+
mesh.geometry.dispose()
|
|
139
|
+
;(mesh.material as MeshBasicMaterial).dispose()
|
|
140
|
+
}
|
|
141
|
+
for (const [name, descriptor] of [
|
|
142
|
+
['IS_REACT_ACT_ENVIRONMENT', actDescriptor],
|
|
143
|
+
['requestAnimationFrame', rafDescriptor],
|
|
144
|
+
['cancelAnimationFrame', cancelDescriptor],
|
|
145
|
+
] as const) {
|
|
146
|
+
if (descriptor) Object.defineProperty(globalThis, name, descriptor)
|
|
147
|
+
else Reflect.deleteProperty(globalThis, name)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { RevealPhase } from '@pascal-app/core'
|
|
4
|
+
import { type ConstructionRevealLevel, subscribeRevealEvents } from '@pascal-app/viewer'
|
|
5
|
+
import { useEffect, useRef } from 'react'
|
|
6
|
+
import { triggerSFX } from '../../lib/sfx-bus'
|
|
7
|
+
|
|
8
|
+
const REVEAL_PHASE_CUES = {
|
|
9
|
+
foundation: 'sfx:structure-build',
|
|
10
|
+
structure: 'sfx:structure-build',
|
|
11
|
+
circulation: 'sfx:structure-build',
|
|
12
|
+
openings: 'sfx:structure-build',
|
|
13
|
+
roof: 'sfx:structure-build',
|
|
14
|
+
furnishing: 'sfx:item-place',
|
|
15
|
+
} as const satisfies Record<RevealPhase, 'sfx:structure-build' | 'sfx:item-place'>
|
|
16
|
+
|
|
17
|
+
/** Mount once alongside ConstructionReveal; its driver owns the commit policy. */
|
|
18
|
+
export function ConstructionRevealAudio({
|
|
19
|
+
enabled,
|
|
20
|
+
level,
|
|
21
|
+
}: {
|
|
22
|
+
enabled: boolean
|
|
23
|
+
level: ConstructionRevealLevel
|
|
24
|
+
}) {
|
|
25
|
+
// Prop changes and Strict Effects must not reset the shared phase/level gate.
|
|
26
|
+
const lastPlayedMs = useRef(Number.NEGATIVE_INFINITY)
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!enabled || (level !== 'full' && level !== 'framing')) return
|
|
30
|
+
return subscribeRevealEvents((event) => {
|
|
31
|
+
if (event.type !== 'land' || event.instant) return
|
|
32
|
+
const nowMs = performance.now()
|
|
33
|
+
if (nowMs - lastPlayedMs.current < 250) return
|
|
34
|
+
lastPlayedMs.current = nowMs
|
|
35
|
+
triggerSFX(REVEAL_PHASE_CUES[event.phase])
|
|
36
|
+
})
|
|
37
|
+
}, [enabled, level])
|
|
38
|
+
|
|
39
|
+
return null
|
|
40
|
+
}
|