@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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
BuildingNode,
|
|
4
|
+
clearSceneHistory,
|
|
5
|
+
emitter,
|
|
6
|
+
endScenePreview,
|
|
7
|
+
ItemNode,
|
|
8
|
+
isScenePreviewing,
|
|
9
|
+
LevelNode,
|
|
10
|
+
registerPointAskHandler,
|
|
11
|
+
useScene,
|
|
12
|
+
ZoneNode,
|
|
13
|
+
} from '@pascal-app/core'
|
|
14
|
+
import { type BubbleTarget, usePointAsk } from '../../store/use-point-ask'
|
|
15
|
+
import { endCompare, sendBubble, startCompare, startPointAskSession } from './session'
|
|
16
|
+
|
|
17
|
+
// Hold to see before, for a turn that BUILDS: "Furnish this room" on an empty room creates nodes
|
|
18
|
+
// the "before" has never heard of. The ask-flow suite holds turns that only change a wall; the
|
|
19
|
+
// owner held an ask that furnished an empty room (13 October) and the furniture stayed. Real
|
|
20
|
+
// stores; only the chat's words (statuses) and the DOM are stood in for.
|
|
21
|
+
|
|
22
|
+
const building = BuildingNode.parse({ id: 'building_created' })
|
|
23
|
+
const level = LevelNode.parse({ id: 'level_created', parentId: building.id })
|
|
24
|
+
const room = ZoneNode.parse({
|
|
25
|
+
id: 'zone_created',
|
|
26
|
+
parentId: level.id,
|
|
27
|
+
name: 'Formal Dining Room',
|
|
28
|
+
polygon: [
|
|
29
|
+
[0, 0],
|
|
30
|
+
[5, 0],
|
|
31
|
+
[5, 4],
|
|
32
|
+
[0, 4],
|
|
33
|
+
],
|
|
34
|
+
})
|
|
35
|
+
building.children = [level.id]
|
|
36
|
+
level.children = [room.id]
|
|
37
|
+
|
|
38
|
+
const original = useScene.getState()
|
|
39
|
+
const originalWindow = globalThis.window
|
|
40
|
+
const originalDocument = globalThis.document
|
|
41
|
+
const originalRaf = globalThis.requestAnimationFrame
|
|
42
|
+
const originalCancelRaf = globalThis.cancelAnimationFrame
|
|
43
|
+
let stopSession = () => {}
|
|
44
|
+
let unregisterHandler = () => {}
|
|
45
|
+
|
|
46
|
+
const target = {
|
|
47
|
+
id: room.id,
|
|
48
|
+
type: 'zone',
|
|
49
|
+
name: 'Formal Dining Room',
|
|
50
|
+
levelId: level.id,
|
|
51
|
+
size: { length: 5, height: 0, thickness: 4 },
|
|
52
|
+
box: { min: [0, 0, 0], max: [5, 2.6, 4] },
|
|
53
|
+
} as unknown as BubbleTarget
|
|
54
|
+
|
|
55
|
+
const furniture = (id: string, position: [number, number, number]) =>
|
|
56
|
+
ItemNode.parse({
|
|
57
|
+
id,
|
|
58
|
+
parentId: level.id,
|
|
59
|
+
position,
|
|
60
|
+
asset: {
|
|
61
|
+
id: `asset_${id}`,
|
|
62
|
+
name: 'Chair',
|
|
63
|
+
category: 'chair',
|
|
64
|
+
src: '/fixture-chair.glb',
|
|
65
|
+
thumbnail: '',
|
|
66
|
+
dimensions: [0.5, 0.8, 0.5],
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
function resetScene() {
|
|
71
|
+
useScene.setState({
|
|
72
|
+
nodes: { [building.id]: building, [level.id]: level, [room.id]: room },
|
|
73
|
+
rootNodeIds: [building.id],
|
|
74
|
+
dirtyNodes: new Set(),
|
|
75
|
+
readOnly: false,
|
|
76
|
+
} as never)
|
|
77
|
+
clearSceneHistory()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
const stub = { addEventListener() {}, removeEventListener() {} }
|
|
82
|
+
globalThis.window = { ...stub, innerWidth: 1280, innerHeight: 800 } as never
|
|
83
|
+
globalThis.document = { ...stub, hidden: false } as never
|
|
84
|
+
globalThis.requestAnimationFrame = (callback) => {
|
|
85
|
+
callback(0)
|
|
86
|
+
return 0
|
|
87
|
+
}
|
|
88
|
+
globalThis.cancelAnimationFrame = () => {}
|
|
89
|
+
resetScene()
|
|
90
|
+
usePointAsk.setState({ pins: {}, pinOrder: [], bubble: null, comparing: null, busy: false })
|
|
91
|
+
unregisterHandler = registerPointAskHandler(async () => ({ ok: true }))
|
|
92
|
+
stopSession = startPointAskSession()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
endCompare()
|
|
97
|
+
endScenePreview()
|
|
98
|
+
stopSession()
|
|
99
|
+
unregisterHandler()
|
|
100
|
+
useScene.setState(original, true)
|
|
101
|
+
clearSceneHistory()
|
|
102
|
+
globalThis.window = originalWindow
|
|
103
|
+
globalThis.document = originalDocument
|
|
104
|
+
if (originalRaf) globalThis.requestAnimationFrame = originalRaf
|
|
105
|
+
else Reflect.deleteProperty(globalThis, 'requestAnimationFrame')
|
|
106
|
+
if (originalCancelRaf) globalThis.cancelAnimationFrame = originalCancelRaf
|
|
107
|
+
else Reflect.deleteProperty(globalThis, 'cancelAnimationFrame')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
async function sendAsk(text = 'furnish this room') {
|
|
111
|
+
usePointAsk.getState().openBubble({
|
|
112
|
+
targets: [target],
|
|
113
|
+
anchor: [2.5, 0, 2],
|
|
114
|
+
hit: { point: [2.5, 0, 2], normal: [0, 1, 0] },
|
|
115
|
+
gesture: 'click',
|
|
116
|
+
capturedAt: new Date().toISOString(),
|
|
117
|
+
camera: null,
|
|
118
|
+
crop: { state: 'failed' },
|
|
119
|
+
draft: '',
|
|
120
|
+
error: null,
|
|
121
|
+
})
|
|
122
|
+
await sendBubble(text, { x: 0, y: 0, width: 10, height: 10 })
|
|
123
|
+
const askId = usePointAsk.getState().pinOrder.at(-1)
|
|
124
|
+
if (!askId) throw new Error('the ask did not plant a pin')
|
|
125
|
+
return askId
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const status = (askId: string, rest: Record<string, unknown>) =>
|
|
129
|
+
emitter.emit('point-ask:status', { askId, ...rest } as never)
|
|
130
|
+
|
|
131
|
+
/** A scene action of the agent: history tracks it, and is left as it was found. */
|
|
132
|
+
function agentWrite(write: () => void) {
|
|
133
|
+
const wasTracking = useScene.temporal.getState().isTracking
|
|
134
|
+
useScene.temporal.getState().resume()
|
|
135
|
+
try {
|
|
136
|
+
write()
|
|
137
|
+
} finally {
|
|
138
|
+
if (!wasTracking) useScene.temporal.getState().pause()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const has = (id: string) => Boolean(useScene.getState().nodes[id as never])
|
|
143
|
+
|
|
144
|
+
describe('hold to see before, for a turn that builds', () => {
|
|
145
|
+
test('the furniture the turn placed is not in the before', async () => {
|
|
146
|
+
const askId = await sendAsk()
|
|
147
|
+
status(askId, { status: 'working', sentence: 'Furnishing the room' })
|
|
148
|
+
agentWrite(() =>
|
|
149
|
+
useScene.getState().createNodes([
|
|
150
|
+
{ node: furniture('item_table', [2.5, 0, 2]), parentId: level.id },
|
|
151
|
+
{ node: furniture('item_chair_a', [1.5, 0, 2]), parentId: level.id },
|
|
152
|
+
{ node: furniture('item_chair_b', [3.5, 0, 2]), parentId: level.id },
|
|
153
|
+
] as never),
|
|
154
|
+
)
|
|
155
|
+
status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
|
|
156
|
+
expect(usePointAsk.getState().pins[askId]?.model.state).toBe('done')
|
|
157
|
+
expect(has('item_table')).toBe(true)
|
|
158
|
+
|
|
159
|
+
expect(startCompare(askId)).toBe(true)
|
|
160
|
+
expect(isScenePreviewing()).toBe(true)
|
|
161
|
+
expect(has('item_table')).toBe(false)
|
|
162
|
+
expect(has('item_chair_a')).toBe(false)
|
|
163
|
+
expect(has('item_chair_b')).toBe(false)
|
|
164
|
+
expect(
|
|
165
|
+
(useScene.getState().nodes[level.id] as { children: string[] }).children.includes(
|
|
166
|
+
'item_table',
|
|
167
|
+
),
|
|
168
|
+
).toBe(false)
|
|
169
|
+
|
|
170
|
+
endCompare()
|
|
171
|
+
expect(has('item_table')).toBe(true)
|
|
172
|
+
expect(has('item_chair_b')).toBe(true)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('furniture placed in several steps, one after another, is all out of the before', async () => {
|
|
176
|
+
const askId = await sendAsk()
|
|
177
|
+
status(askId, { status: 'working', sentence: 'Furnishing the room' })
|
|
178
|
+
agentWrite(() =>
|
|
179
|
+
useScene
|
|
180
|
+
.getState()
|
|
181
|
+
.createNodes([{ node: furniture('item_table', [2.5, 0, 2]), parentId: level.id }] as never),
|
|
182
|
+
)
|
|
183
|
+
agentWrite(() =>
|
|
184
|
+
useScene
|
|
185
|
+
.getState()
|
|
186
|
+
.createNodes([
|
|
187
|
+
{ node: furniture('item_chair_a', [1.5, 0, 2]), parentId: level.id },
|
|
188
|
+
] as never),
|
|
189
|
+
)
|
|
190
|
+
status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
|
|
191
|
+
expect(startCompare(askId)).toBe(true)
|
|
192
|
+
expect(has('item_table')).toBe(false)
|
|
193
|
+
expect(has('item_chair_a')).toBe(false)
|
|
194
|
+
})
|
|
195
|
+
})
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
BuildingNode,
|
|
4
|
+
clearSceneHistory,
|
|
5
|
+
emitter,
|
|
6
|
+
endScenePreview,
|
|
7
|
+
isScenePreviewing,
|
|
8
|
+
LevelNode,
|
|
9
|
+
registerPointAskHandler,
|
|
10
|
+
useScene,
|
|
11
|
+
WallNode,
|
|
12
|
+
} from '@pascal-app/core'
|
|
13
|
+
import { asksWait, type BubbleTarget, usePointAsk } from '../../store/use-point-ask'
|
|
14
|
+
import { setPointAskBehindQueue, setPointAskBusy } from './host-api'
|
|
15
|
+
import { endCompare, sendBubble, startCompare, startPointAskSession } from './session'
|
|
16
|
+
|
|
17
|
+
// The whole ask path with the real stores (hold to see before; an ask while Pascal is working): the session listening to the chat's
|
|
18
|
+
// statuses, a turn writing the scene through the store's own actions (each a history step), the pin
|
|
19
|
+
// reaching "done", and the hold. Only the chat's words (statuses) and the DOM are stood in for.
|
|
20
|
+
|
|
21
|
+
const building = BuildingNode.parse({ id: 'building_flow' })
|
|
22
|
+
const level = LevelNode.parse({ id: 'level_flow', parentId: building.id })
|
|
23
|
+
const wall = WallNode.parse({
|
|
24
|
+
id: 'wall_flow',
|
|
25
|
+
parentId: level.id,
|
|
26
|
+
start: [0, 0],
|
|
27
|
+
end: [4, 0],
|
|
28
|
+
height: 2.6,
|
|
29
|
+
})
|
|
30
|
+
building.children = [level.id]
|
|
31
|
+
level.children = [wall.id]
|
|
32
|
+
|
|
33
|
+
const STOCK_HEIGHT = 2.6
|
|
34
|
+
const original = useScene.getState()
|
|
35
|
+
const originalWindow = globalThis.window
|
|
36
|
+
const originalDocument = globalThis.document
|
|
37
|
+
const originalRaf = globalThis.requestAnimationFrame
|
|
38
|
+
const originalCancelRaf = globalThis.cancelAnimationFrame
|
|
39
|
+
let stopSession = () => {}
|
|
40
|
+
let unregisterHandler = () => {}
|
|
41
|
+
|
|
42
|
+
const target = {
|
|
43
|
+
id: wall.id,
|
|
44
|
+
type: 'wall',
|
|
45
|
+
name: 'Wall',
|
|
46
|
+
levelId: level.id,
|
|
47
|
+
size: { length: 4, height: STOCK_HEIGHT, thickness: 0.2 },
|
|
48
|
+
box: { min: [0, 0, -0.1], max: [4, STOCK_HEIGHT, 0.1] },
|
|
49
|
+
} as unknown as BubbleTarget
|
|
50
|
+
|
|
51
|
+
function resetScene() {
|
|
52
|
+
useScene.setState({
|
|
53
|
+
nodes: { [building.id]: building, [level.id]: level, [wall.id]: wall },
|
|
54
|
+
rootNodeIds: [building.id],
|
|
55
|
+
dirtyNodes: new Set(),
|
|
56
|
+
readOnly: false,
|
|
57
|
+
} as never)
|
|
58
|
+
clearSceneHistory()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
const stub = { addEventListener() {}, removeEventListener() {} }
|
|
63
|
+
globalThis.window = { ...stub, innerWidth: 1280, innerHeight: 800 } as never
|
|
64
|
+
globalThis.document = { ...stub, hidden: false } as never
|
|
65
|
+
globalThis.requestAnimationFrame = (callback) => {
|
|
66
|
+
callback(0)
|
|
67
|
+
return 0
|
|
68
|
+
}
|
|
69
|
+
globalThis.cancelAnimationFrame = () => {}
|
|
70
|
+
resetScene()
|
|
71
|
+
usePointAsk.setState({ pins: {}, pinOrder: [], bubble: null, comparing: null, busy: false })
|
|
72
|
+
unregisterHandler = registerPointAskHandler(async () => ({ ok: true }))
|
|
73
|
+
stopSession = startPointAskSession()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
endCompare()
|
|
78
|
+
endScenePreview()
|
|
79
|
+
stopSession()
|
|
80
|
+
unregisterHandler()
|
|
81
|
+
useScene.setState(original, true)
|
|
82
|
+
clearSceneHistory()
|
|
83
|
+
globalThis.window = originalWindow
|
|
84
|
+
globalThis.document = originalDocument
|
|
85
|
+
if (originalRaf) globalThis.requestAnimationFrame = originalRaf
|
|
86
|
+
else Reflect.deleteProperty(globalThis, 'requestAnimationFrame')
|
|
87
|
+
if (originalCancelRaf) globalThis.cancelAnimationFrame = originalCancelRaf
|
|
88
|
+
else Reflect.deleteProperty(globalThis, 'cancelAnimationFrame')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
/** The person points at the wall and sends; returns the ask's id. */
|
|
92
|
+
async function sendAsk() {
|
|
93
|
+
usePointAsk.getState().openBubble({
|
|
94
|
+
targets: [target],
|
|
95
|
+
anchor: [2, 1, 0],
|
|
96
|
+
hit: { point: [2, 1, 0], normal: [0, 0, 1] },
|
|
97
|
+
gesture: 'click',
|
|
98
|
+
capturedAt: new Date().toISOString(),
|
|
99
|
+
camera: null,
|
|
100
|
+
crop: { state: 'failed' },
|
|
101
|
+
draft: '',
|
|
102
|
+
error: null,
|
|
103
|
+
})
|
|
104
|
+
await sendBubble('make this wall taller', { x: 0, y: 0, width: 10, height: 10 })
|
|
105
|
+
const askId = usePointAsk.getState().pinOrder.at(-1)
|
|
106
|
+
if (!askId) throw new Error('the ask did not plant a pin')
|
|
107
|
+
return askId
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const status = (askId: string, rest: Record<string, unknown>) =>
|
|
111
|
+
emitter.emit('point-ask:status', { askId, ...rest } as never)
|
|
112
|
+
|
|
113
|
+
/** A scene action of the agent: history tracks it, and is left as it was found (the hosted executor's contract). */
|
|
114
|
+
function agentWrite(write: () => void) {
|
|
115
|
+
const wasTracking = useScene.temporal.getState().isTracking
|
|
116
|
+
useScene.temporal.getState().resume()
|
|
117
|
+
try {
|
|
118
|
+
write()
|
|
119
|
+
} finally {
|
|
120
|
+
if (!wasTracking) useScene.temporal.getState().pause()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** What the hosted executor did before it was fixed: always paused after, whatever it found. */
|
|
125
|
+
function leftoverPause(write: () => void) {
|
|
126
|
+
useScene.temporal.getState().resume()
|
|
127
|
+
try {
|
|
128
|
+
write()
|
|
129
|
+
} finally {
|
|
130
|
+
useScene.temporal.getState().pause()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const wallHeight = () => (useScene.getState().nodes[wall.id] as { height: number }).height
|
|
135
|
+
|
|
136
|
+
describe('a history left untracked after the turn', () => {
|
|
137
|
+
test('refuses the hold, and the card is told why (it was silent: the owner, 12 October)', async () => {
|
|
138
|
+
const askId = await sendAsk()
|
|
139
|
+
status(askId, { status: 'working', sentence: 'Raising the wall' })
|
|
140
|
+
leftoverPause(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
|
|
141
|
+
status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
|
|
142
|
+
expect(startCompare(askId)).toBe(false)
|
|
143
|
+
expect(isScenePreviewing()).toBe(false)
|
|
144
|
+
expect(usePointAsk.getState().compareRefusal).toEqual({ askId, reason: 'history-held' })
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('hold to see before, end to end', () => {
|
|
149
|
+
test('a turn that wrote the scene can be held to show the scene as it was', async () => {
|
|
150
|
+
const askId = await sendAsk()
|
|
151
|
+
status(askId, { status: 'working', sentence: 'Raising the wall' })
|
|
152
|
+
agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
|
|
153
|
+
status(askId, {
|
|
154
|
+
status: 'ended',
|
|
155
|
+
outcome: 'completed',
|
|
156
|
+
asked: false,
|
|
157
|
+
wroteScene: true,
|
|
158
|
+
})
|
|
159
|
+
expect(usePointAsk.getState().pins[askId]?.model.state).toBe('done')
|
|
160
|
+
expect(wallHeight()).toBe(3.4)
|
|
161
|
+
|
|
162
|
+
expect(startCompare(askId)).toBe(true)
|
|
163
|
+
expect(isScenePreviewing()).toBe(true)
|
|
164
|
+
expect(wallHeight()).toBe(STOCK_HEIGHT)
|
|
165
|
+
expect(usePointAsk.getState().comparing).toBe(askId)
|
|
166
|
+
|
|
167
|
+
endCompare()
|
|
168
|
+
expect(isScenePreviewing()).toBe(false)
|
|
169
|
+
expect(wallHeight()).toBe(3.4)
|
|
170
|
+
expect(usePointAsk.getState().comparing).toBeNull()
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('a turn of several writes shows the state from before the first of them', async () => {
|
|
174
|
+
const askId = await sendAsk()
|
|
175
|
+
status(askId, { status: 'working', sentence: 'Building' })
|
|
176
|
+
agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3 }))
|
|
177
|
+
agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.2 }))
|
|
178
|
+
agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
|
|
179
|
+
status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
|
|
180
|
+
expect(startCompare(askId)).toBe(true)
|
|
181
|
+
expect(wallHeight()).toBe(STOCK_HEIGHT)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('a write that lands before the chat reports "working" is not part of the before', async () => {
|
|
185
|
+
const askId = await sendAsk()
|
|
186
|
+
// The chat accepted the ask; its first write comes before its first status.
|
|
187
|
+
agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
|
|
188
|
+
status(askId, { status: 'working', sentence: 'Reading your request' })
|
|
189
|
+
agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.8 }))
|
|
190
|
+
status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
|
|
191
|
+
expect(startCompare(askId)).toBe(true)
|
|
192
|
+
expect(wallHeight()).toBe(STOCK_HEIGHT)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('a turn that is not writing does not stop the hold, even when a pin never heard its end', async () => {
|
|
196
|
+
const first = await sendAsk()
|
|
197
|
+
status(first, { status: 'working', sentence: 'Raising the wall' })
|
|
198
|
+
agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
|
|
199
|
+
status(first, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
|
|
200
|
+
// Another ask whose end the pin never heard: it says "working", but the chat is idle.
|
|
201
|
+
const stuck = usePointAsk.getState().pins[first]
|
|
202
|
+
if (!stuck) throw new Error('the ask has no pin')
|
|
203
|
+
usePointAsk.getState().addPin({
|
|
204
|
+
...stuck,
|
|
205
|
+
model: { ...stuck.model, askId: 'ask_stuck', state: 'working' },
|
|
206
|
+
})
|
|
207
|
+
usePointAsk.setState({ busy: false })
|
|
208
|
+
expect(startCompare(first)).toBe(true)
|
|
209
|
+
endCompare()
|
|
210
|
+
usePointAsk.setState({ busy: true })
|
|
211
|
+
expect(startCompare(first)).toBe(false)
|
|
212
|
+
expect(usePointAsk.getState().compareRefusal).toEqual({
|
|
213
|
+
askId: first,
|
|
214
|
+
reason: 'turn-working',
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
function openBubbleOnWall(draft = '') {
|
|
220
|
+
usePointAsk.getState().openBubble({
|
|
221
|
+
targets: [target],
|
|
222
|
+
anchor: [2, 1, 0],
|
|
223
|
+
hit: { point: [2, 1, 0], normal: [0, 0, 1] },
|
|
224
|
+
gesture: 'click',
|
|
225
|
+
capturedAt: new Date().toISOString(),
|
|
226
|
+
camera: null,
|
|
227
|
+
crop: { state: 'failed' },
|
|
228
|
+
draft,
|
|
229
|
+
error: null,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
describe('an ask while Pascal is working', () => {
|
|
234
|
+
test('is handed to the chat, which queues it as its own turn: the pin says queued', async () => {
|
|
235
|
+
const handed: string[] = []
|
|
236
|
+
unregisterHandler()
|
|
237
|
+
unregisterHandler = registerPointAskHandler(async (submit) => {
|
|
238
|
+
handed.push(submit.askId)
|
|
239
|
+
emitter.emit('point-ask:status', { askId: submit.askId, status: 'queued' } as never)
|
|
240
|
+
return { ok: true }
|
|
241
|
+
})
|
|
242
|
+
usePointAsk.setState({ busy: true })
|
|
243
|
+
openBubbleOnWall()
|
|
244
|
+
await sendBubble('make this wall taller', { x: 0, y: 0, width: 10, height: 10 })
|
|
245
|
+
const state = usePointAsk.getState()
|
|
246
|
+
expect(handed).toHaveLength(1)
|
|
247
|
+
expect(state.bubble).toBeNull()
|
|
248
|
+
expect(state.pinOrder).toHaveLength(1)
|
|
249
|
+
expect(state.pins[handed[0]!]?.model.state).toBe('queued')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test("takes none of the running turn's changes, and its before is the scene when its own turn starts", async () => {
|
|
253
|
+
unregisterHandler()
|
|
254
|
+
unregisterHandler = registerPointAskHandler(async (submit) => {
|
|
255
|
+
emitter.emit('point-ask:status', { askId: submit.askId, status: 'queued' } as never)
|
|
256
|
+
return { ok: true }
|
|
257
|
+
})
|
|
258
|
+
// An earlier ask is running and writes.
|
|
259
|
+
const running = await sendAsk()
|
|
260
|
+
status(running, { status: 'working', sentence: 'Raising the wall' })
|
|
261
|
+
usePointAsk.setState({ busy: true })
|
|
262
|
+
openBubbleOnWall()
|
|
263
|
+
await sendBubble('paint it', { x: 0, y: 0, width: 10, height: 10 })
|
|
264
|
+
const queued = usePointAsk.getState().pinOrder.at(-1)!
|
|
265
|
+
agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3 }))
|
|
266
|
+
status(running, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
|
|
267
|
+
expect(usePointAsk.getState().pins[queued]?.model.committed).toBe(false)
|
|
268
|
+
// Its own turn starts and writes.
|
|
269
|
+
status(queued, { status: 'working', sentence: 'Painting' })
|
|
270
|
+
agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
|
|
271
|
+
status(queued, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
|
|
272
|
+
const pin = usePointAsk.getState().pins[queued]?.model
|
|
273
|
+
expect(pin?.state).toBe('done')
|
|
274
|
+
expect(pin?.changedTargetIds).toEqual([wall.id])
|
|
275
|
+
expect(startCompare(queued)).toBe(true)
|
|
276
|
+
// The state it started from: the running turn's change is in it.
|
|
277
|
+
expect(wallHeight()).toBe(3)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test('a host that does not queue answers busy, and the bubble comes back with its reason', async () => {
|
|
281
|
+
unregisterHandler()
|
|
282
|
+
unregisterHandler = registerPointAskHandler(async () => ({
|
|
283
|
+
ok: false,
|
|
284
|
+
code: 'busy',
|
|
285
|
+
message: 'Pascal is working. Ask again when it is done.',
|
|
286
|
+
}))
|
|
287
|
+
openBubbleOnWall()
|
|
288
|
+
await sendBubble('make this wall taller', { x: 0, y: 0, width: 10, height: 10 })
|
|
289
|
+
const state = usePointAsk.getState()
|
|
290
|
+
expect(state.pinOrder).toHaveLength(0)
|
|
291
|
+
expect(state.bubble?.error).toBe('Pascal is working. Ask again when it is done.')
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
describe("which pin takes the scene's changes", () => {
|
|
296
|
+
test("a stale pin never absorbs the running ask's changes: they go to its pin alone", async () => {
|
|
297
|
+
// Two earlier asks said "working" and the pin never heard their end (a dropped turn, a reload of
|
|
298
|
+
// the chat), and they point at the same wall.
|
|
299
|
+
const staleWorking = await sendAsk()
|
|
300
|
+
status(staleWorking, { status: 'working', sentence: 'Raising the wall' })
|
|
301
|
+
const staleAsking = await sendAsk()
|
|
302
|
+
status(staleAsking, { status: 'working', sentence: 'Painting' })
|
|
303
|
+
status(staleAsking, { status: 'asking', question: 'Which colour?' })
|
|
304
|
+
// The ask that runs now.
|
|
305
|
+
const running = await sendAsk()
|
|
306
|
+
status(running, { status: 'working', sentence: 'Reading your request' })
|
|
307
|
+
agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
|
|
308
|
+
status(running, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
|
|
309
|
+
|
|
310
|
+
const { pins } = usePointAsk.getState()
|
|
311
|
+
expect(pins[running]?.model.state).toBe('done')
|
|
312
|
+
expect(pins[running]?.model.changedTargetIds).toEqual([wall.id])
|
|
313
|
+
for (const stale of [staleWorking, staleAsking]) {
|
|
314
|
+
expect(pins[stale]?.model.committed).toBe(false)
|
|
315
|
+
expect(pins[stale]?.model.changedTargetIds).toEqual([])
|
|
316
|
+
expect(pins[stale]?.model.alsoChangedIds).toEqual([])
|
|
317
|
+
expect(pins[stale]?.model.state).not.toBe('done')
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test('changes made before any ask is working go to no pin', async () => {
|
|
322
|
+
const waiting = await sendAsk()
|
|
323
|
+
agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
|
|
324
|
+
status(waiting, { status: 'ended', outcome: 'completed', asked: false, wroteScene: false })
|
|
325
|
+
expect(usePointAsk.getState().pins[waiting]?.model.committed).toBe(false)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test("a stale pin is settled when another ask starts working: it is not left 'working'", async () => {
|
|
329
|
+
const stale = await sendAsk()
|
|
330
|
+
status(stale, { status: 'working', sentence: 'Raising the wall' })
|
|
331
|
+
const running = await sendAsk()
|
|
332
|
+
status(running, { status: 'working', sentence: 'Reading your request' })
|
|
333
|
+
expect(usePointAsk.getState().pins[stale]?.model.state).not.toBe('working')
|
|
334
|
+
expect(usePointAsk.getState().pins[running]?.model.state).toBe('working')
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
test("a plain message's changes after an ask whose end was never heard are no ask's", async () => {
|
|
338
|
+
const stale = await sendAsk()
|
|
339
|
+
setPointAskBusy(true)
|
|
340
|
+
status(stale, { status: 'working', sentence: 'Raising the wall' })
|
|
341
|
+
// The chat goes idle without the pin having heard the end, then a plain message runs.
|
|
342
|
+
setPointAskBusy(false)
|
|
343
|
+
setPointAskBusy(true)
|
|
344
|
+
agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
|
|
345
|
+
setPointAskBusy(false)
|
|
346
|
+
const pin = usePointAsk.getState().pins[stale]?.model
|
|
347
|
+
expect(pin?.committed).toBe(false)
|
|
348
|
+
expect(pin?.state).not.toBe('working')
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
describe('messages already queued or held', () => {
|
|
353
|
+
// The chat queues an ask behind messages that wait (a queue held after an error needs Resume),
|
|
354
|
+
// with no turn running: the bubble must say Queue, but nothing is working.
|
|
355
|
+
test('a new ask waits behind them: the bubble says Queue, and no turn is said to run', () => {
|
|
356
|
+
expect(asksWait(usePointAsk.getState())).toBe(false)
|
|
357
|
+
setPointAskBehindQueue(true)
|
|
358
|
+
const state = usePointAsk.getState()
|
|
359
|
+
expect(asksWait(state)).toBe(true)
|
|
360
|
+
expect(state.busy).toBe(false)
|
|
361
|
+
setPointAskBehindQueue(false)
|
|
362
|
+
expect(asksWait(usePointAsk.getState())).toBe(false)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('a running turn makes asks wait too, whether or not anything is queued', () => {
|
|
366
|
+
setPointAskBusy(true)
|
|
367
|
+
expect(asksWait(usePointAsk.getState())).toBe(true)
|
|
368
|
+
setPointAskBusy(false)
|
|
369
|
+
expect(asksWait(usePointAsk.getState())).toBe(false)
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
test('they do not refuse a hold, nor settle a pin the way an idle chat does', async () => {
|
|
373
|
+
const askId = await sendAsk()
|
|
374
|
+
status(askId, { status: 'working', sentence: 'Raising the wall' })
|
|
375
|
+
agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
|
|
376
|
+
status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
|
|
377
|
+
setPointAskBehindQueue(true)
|
|
378
|
+
expect(startCompare(askId)).toBe(true)
|
|
379
|
+
})
|
|
380
|
+
})
|