@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
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
getRoofGlazing,
|
|
4
|
+
getWallThickness,
|
|
5
|
+
isGlazedRoofSegment,
|
|
6
|
+
} from '@pascal-app/core'
|
|
2
7
|
import {
|
|
3
8
|
mergePrintExportDiagnostics,
|
|
4
9
|
type PrintExportDiagnostic,
|
|
@@ -30,6 +35,10 @@ function semanticThicknessMeters(node: AnyNode): number | null {
|
|
|
30
35
|
case 'slab':
|
|
31
36
|
return node.thickness
|
|
32
37
|
case 'roof-segment': {
|
|
38
|
+
if (isGlazedRoofSegment(node)) {
|
|
39
|
+
const glazing = getRoofGlazing(node)
|
|
40
|
+
return Math.min(glazing.frameWidth, glazing.frameDepth, glazing.glassThickness)
|
|
41
|
+
}
|
|
33
42
|
const structuralThicknesses = [node.wallThickness, node.deckThickness + node.shingleThickness]
|
|
34
43
|
if (node.roofType === 'dutch') structuralThicknesses.push(node.dutchTopRakeThickness)
|
|
35
44
|
return Math.min(...structuralThicknesses)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import { DoorNode, RoofSegmentNode, type RoofType } from '@pascal-app/core'
|
|
3
|
+
import { buildPrintableRoofSegmentSolids } from '@pascal-app/editor'
|
|
3
4
|
import * as THREE from 'three'
|
|
4
|
-
import { buildPrintableRoofSegmentSolids } from './print-roof-solids'
|
|
5
5
|
|
|
6
6
|
const ROOF_TYPES: RoofType[] = [
|
|
7
7
|
'gable',
|
|
@@ -68,6 +68,21 @@ describe('buildPrintableRoofSegmentSolids', () => {
|
|
|
68
68
|
}
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
+
test("a gable's print shell reaches its gable overhang at the ends, the eaves along the sides", () => {
|
|
72
|
+
const extent = (node: RoofSegmentNode) => {
|
|
73
|
+
const mesh = onlyMesh(buildPrintableRoofSegmentSolids(node).object!)
|
|
74
|
+
mesh.geometry.computeBoundingBox()
|
|
75
|
+
const box = mesh.geometry.boundingBox!.clone()
|
|
76
|
+
mesh.geometry.dispose()
|
|
77
|
+
return box
|
|
78
|
+
}
|
|
79
|
+
const plain = extent(fixture('gable'))
|
|
80
|
+
const raked = extent(fixture('gable', { gableOverhang: 0.6 }))
|
|
81
|
+
expect(raked.max.x).toBeCloseTo(4 / 2 + 0.15 / 2 + 0.6, 6)
|
|
82
|
+
expect(raked.min.x).toBeCloseTo(-(4 / 2 + 0.15 / 2 + 0.6), 6)
|
|
83
|
+
expect(raked.max.z).toBeCloseTo(plain.max.z, 6)
|
|
84
|
+
})
|
|
85
|
+
|
|
71
86
|
test('preserves segment provenance and local transform', () => {
|
|
72
87
|
const node = fixture('gable', { position: [1, 2, 3], rotation: Math.PI / 3 })
|
|
73
88
|
const result = buildPrintableRoofSegmentSolids(node)
|
|
@@ -131,3 +146,26 @@ describe('buildPrintableRoofSegmentSolids', () => {
|
|
|
131
146
|
)
|
|
132
147
|
})
|
|
133
148
|
})
|
|
149
|
+
|
|
150
|
+
for (const roofType of ['gable', 'shed', 'barrel'] as const)
|
|
151
|
+
test(`glazed ${roofType} prints pane and frame solids without an opaque roof shell`, () => {
|
|
152
|
+
const result = buildPrintableRoofSegmentSolids(
|
|
153
|
+
fixture(roofType, { roofSystem: 'glazed', wallThickness: 0 }),
|
|
154
|
+
)
|
|
155
|
+
expect(result.status).toBe('ready')
|
|
156
|
+
if (result.status !== 'ready') return
|
|
157
|
+
try {
|
|
158
|
+
expect(result.object.children.some((child) => child.name === 'print-glazing-frame')).toBe(
|
|
159
|
+
true,
|
|
160
|
+
)
|
|
161
|
+
expect(result.object.children.some((child) => child.name === 'print-glazing-pane')).toBe(true)
|
|
162
|
+
expect(result.object.children.some((child) => child.name === 'print-roof-shell')).toBe(false)
|
|
163
|
+
for (const child of result.object.children) {
|
|
164
|
+
const geometry = (child as THREE.Mesh).geometry
|
|
165
|
+
expect(geometry.getIndex()!.count).toBeGreaterThan(0)
|
|
166
|
+
expect([...geometry.getAttribute('position').array].every(Number.isFinite)).toBe(true)
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
for (const child of result.object.children) (child as THREE.Mesh).geometry.dispose()
|
|
170
|
+
}
|
|
171
|
+
})
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
|
+
getBarrelRoofSolidPieces,
|
|
3
4
|
getConicalRoofCoverage,
|
|
5
|
+
getGlazedRoofPieceFaces,
|
|
6
|
+
getGlazedRoofPieces,
|
|
4
7
|
getRoofModuleFaces,
|
|
8
|
+
getRoofSegmentDeckReach,
|
|
5
9
|
getRoofShapeInsets,
|
|
6
10
|
getRoofShapeRatios,
|
|
7
11
|
getSegmentSlopeFrame,
|
|
12
|
+
isGlazedRoofSegment,
|
|
8
13
|
nodeRegistry,
|
|
9
14
|
normalizeRoofSegmentTrim,
|
|
10
15
|
type RoofSegmentNode,
|
|
@@ -411,11 +416,17 @@ function buildRoofModuleGeometry(
|
|
|
411
416
|
|
|
412
417
|
function getVolumeFaces(
|
|
413
418
|
node: RoofSegmentNode,
|
|
414
|
-
options: {
|
|
419
|
+
options: {
|
|
420
|
+
widthExtension: number
|
|
421
|
+
/** How far it reaches along x, where a gable's rakes are; `widthExtension` when absent. */
|
|
422
|
+
rakeExtension?: number
|
|
423
|
+
verticalOffset: number
|
|
424
|
+
isVoid: boolean
|
|
425
|
+
},
|
|
415
426
|
): RoofFace[] {
|
|
416
427
|
const conicalCoverage = getConicalRoofCoverage(node)
|
|
417
428
|
const { activeRh, tanTheta } = getSegmentSlopeFrame(node)
|
|
418
|
-
const width = Math.max(0.01, node.width + options.widthExtension * 2)
|
|
429
|
+
const width = Math.max(0.01, node.width + (options.rakeExtension ?? options.widthExtension) * 2)
|
|
419
430
|
const depth = Math.max(0.01, node.depth + options.widthExtension * 2)
|
|
420
431
|
const autoDrop = options.widthExtension * tanTheta
|
|
421
432
|
const wallHeight = Math.max(0.01, node.wallHeight - autoDrop + options.verticalOffset)
|
|
@@ -448,13 +459,13 @@ function getShingleOuterFaces(node: RoofSegmentNode): RoofFace[] {
|
|
|
448
459
|
const conicalCoverage = getConicalRoofCoverage(node)
|
|
449
460
|
const { activeRh, tanTheta, cosTheta, sinTheta } = getSegmentSlopeFrame(node)
|
|
450
461
|
const shapeRatios = getRoofShapeRatios(node)
|
|
451
|
-
const
|
|
452
|
-
const deckExtension =
|
|
462
|
+
const reach = getRoofSegmentDeckReach(node)
|
|
463
|
+
const deckExtension = reach.z
|
|
453
464
|
const deckVerticalThickness = activeRh > 0 ? node.deckThickness / cosTheta : node.deckThickness
|
|
454
465
|
const deckDrop = deckExtension * tanTheta
|
|
455
466
|
const shingleHorizontalThickness = node.shingleThickness * sinTheta
|
|
456
467
|
const shingleVerticalThickness = node.shingleThickness * cosTheta
|
|
457
|
-
const baseWidth = Math.max(0.01, node.width +
|
|
468
|
+
const baseWidth = Math.max(0.01, node.width + reach.x * 2)
|
|
458
469
|
const baseDepth = Math.max(0.01, node.depth + deckExtension * 2)
|
|
459
470
|
const baseWallHeight = node.wallHeight - deckDrop + deckVerticalThickness
|
|
460
471
|
const baseRoofHeight =
|
|
@@ -529,8 +540,9 @@ function blockingDiagnostics(
|
|
|
529
540
|
dimensions.some((value) => !Number.isFinite(value) || value < 0) ||
|
|
530
541
|
node.width <= FACE_EPSILON ||
|
|
531
542
|
node.depth <= FACE_EPSILON ||
|
|
532
|
-
node
|
|
533
|
-
|
|
543
|
+
(!isGlazedRoofSegment(node) &&
|
|
544
|
+
(node.wallThickness <= FACE_EPSILON ||
|
|
545
|
+
node.deckThickness + node.shingleThickness <= FACE_EPSILON))
|
|
534
546
|
) {
|
|
535
547
|
diagnostics.push({
|
|
536
548
|
severity: 'error',
|
|
@@ -581,8 +593,28 @@ export function buildPrintableRoofSegmentSolids(
|
|
|
581
593
|
): PrintRoofSolidResult {
|
|
582
594
|
const diagnostics = blockingDiagnostics(node, nodes)
|
|
583
595
|
if (diagnostics.length > 0) return { status: 'blocked', object: null, diagnostics }
|
|
596
|
+
if (isGlazedRoofSegment(node) || node.roofType === 'barrel') {
|
|
597
|
+
const root = new THREE.Group()
|
|
598
|
+
root.name = isGlazedRoofSegment(node) ? 'print-glazed-roof-solids' : 'print-barrel-roof-solids'
|
|
599
|
+
root.userData = { pascalId: node.id }
|
|
600
|
+
root.position.set(...node.position)
|
|
601
|
+
root.rotation.y = node.rotation
|
|
602
|
+
for (const piece of isGlazedRoofSegment(node)
|
|
603
|
+
? getGlazedRoofPieces(node)
|
|
604
|
+
: getBarrelRoofSolidPieces(node)) {
|
|
605
|
+
const geometry = geometryFromFaces(getGlazedRoofPieceFaces(piece))
|
|
606
|
+
if (!geometry) continue
|
|
607
|
+
const mesh = new THREE.Mesh(geometry)
|
|
608
|
+
mesh.name = isGlazedRoofSegment(node)
|
|
609
|
+
? piece.slot === 0
|
|
610
|
+
? 'print-glazing-frame'
|
|
611
|
+
: 'print-glazing-pane'
|
|
612
|
+
: 'print-roof-shell'
|
|
613
|
+
root.add(mesh)
|
|
614
|
+
}
|
|
615
|
+
return { status: 'ready', object: root, diagnostics: [] }
|
|
616
|
+
}
|
|
584
617
|
|
|
585
|
-
const { cosTheta } = getSegmentSlopeFrame(node)
|
|
586
618
|
const wallOuter = getVolumeFaces(node, {
|
|
587
619
|
widthExtension: node.wallThickness / 2,
|
|
588
620
|
verticalOffset: 0,
|
|
@@ -593,9 +625,10 @@ export function buildPrintableRoofSegmentSolids(
|
|
|
593
625
|
verticalOffset: 0,
|
|
594
626
|
isVoid: false,
|
|
595
627
|
})
|
|
596
|
-
const
|
|
628
|
+
const reach = getRoofSegmentDeckReach(node)
|
|
597
629
|
const roofInner = getVolumeFaces(node, {
|
|
598
|
-
widthExtension:
|
|
630
|
+
widthExtension: reach.z,
|
|
631
|
+
rakeExtension: reach.x,
|
|
599
632
|
verticalOffset: 0,
|
|
600
633
|
isVoid: true,
|
|
601
634
|
})
|
|
@@ -27,7 +27,7 @@ type ManifoldFactory = (config?: {
|
|
|
27
27
|
// emscripten then locates manifold.wasm relative to the glue's own URL. Hosts
|
|
28
28
|
// that can't reach the CDN (offline, CSP) pass their own URLs through
|
|
29
29
|
// configureManifoldRuntime.
|
|
30
|
-
const MANIFOLD_VERSION = '3.5.
|
|
30
|
+
const MANIFOLD_VERSION = '3.5.4'
|
|
31
31
|
const FALLBACK_MODULE_URL = `https://cdn.jsdelivr.net/npm/manifold-3d@${MANIFOLD_VERSION}/manifold.js`
|
|
32
32
|
|
|
33
33
|
function importUntraced(specifier: string): Promise<{ default: ManifoldFactory }> {
|
|
@@ -6,9 +6,8 @@ import {
|
|
|
6
6
|
getWallBaseElevationForNodes,
|
|
7
7
|
getWallCurveFrameAt,
|
|
8
8
|
getWallFaceOffsets,
|
|
9
|
-
|
|
9
|
+
getWallTop,
|
|
10
10
|
resolveCeilingHeight,
|
|
11
|
-
resolveWallTop,
|
|
12
11
|
roomDrawnFloor,
|
|
13
12
|
type WallNode,
|
|
14
13
|
} from '@pascal-app/core'
|
|
@@ -78,7 +77,7 @@ export function resolveRoomAssemblyHeights(
|
|
|
78
77
|
const wall = nodes[id]
|
|
79
78
|
if (wall?.type !== 'wall') continue
|
|
80
79
|
const base = getWallBaseElevationForNodes(wall, sceneNodes)
|
|
81
|
-
wallTops.set(id,
|
|
80
|
+
wallTops.set(id, getWallTop(wall, levelId, sceneNodes, base))
|
|
82
81
|
}
|
|
83
82
|
const ceilingNode = room.ceilingId ? nodes[room.ceilingId] : undefined
|
|
84
83
|
const ceiling =
|
|
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, jest, spyOn, test } from 'bun:test'
|
|
|
2
2
|
import { readFileSync } from 'node:fs'
|
|
3
3
|
import {
|
|
4
4
|
type AnyNode,
|
|
5
|
+
type AnyNodeDefinition,
|
|
5
6
|
BuildingNode,
|
|
6
7
|
CeilingNode,
|
|
7
8
|
createRoomTopologyIndex,
|
|
@@ -10,7 +11,9 @@ import {
|
|
|
10
11
|
ItemNode,
|
|
11
12
|
LevelNode,
|
|
12
13
|
type NodeEvent,
|
|
14
|
+
nodeRegistry,
|
|
13
15
|
reconcileLevelStructure,
|
|
16
|
+
registerNode,
|
|
14
17
|
SlabNode,
|
|
15
18
|
SpawnNode,
|
|
16
19
|
StairNode,
|
|
@@ -24,11 +27,12 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
24
27
|
import { _roots, act } from '@react-three/fiber'
|
|
25
28
|
import { createElement } from 'react'
|
|
26
29
|
import { Group, type LineSegments } from 'three'
|
|
30
|
+
import { z } from 'zod'
|
|
27
31
|
import { SelectionManager } from '../components/editor/selection-manager'
|
|
28
32
|
import { cancelActiveTool, runHistoryShortcut } from '../hooks/use-keyboard'
|
|
29
33
|
import { resolvePlanRoomHit, roomPickingEnabled, useSelectedRoom } from '../hooks/use-selected-room'
|
|
30
34
|
import useEditor from '../store/use-editor'
|
|
31
|
-
import useInteractionScope from '../store/use-interaction-scope'
|
|
35
|
+
import useInteractionScope, { getMovingNode } from '../store/use-interaction-scope'
|
|
32
36
|
import useSessionGroups from '../store/use-session-groups'
|
|
33
37
|
import { withSelectionHarness } from '../test-utils/selection-harness'
|
|
34
38
|
import { startRoomDivide } from './room-divide-session'
|
|
@@ -646,6 +650,46 @@ describe('room drill-down state through the mounted selection manager', () => {
|
|
|
646
650
|
expect(useEditor.getState().room).toBeNull()
|
|
647
651
|
})
|
|
648
652
|
})
|
|
653
|
+
test('a second click on the selected crate picks it up, and that click cannot drop it', async () => {
|
|
654
|
+
jest.useFakeTimers()
|
|
655
|
+
if (!nodeRegistry.has('test-crate')) {
|
|
656
|
+
registerNode({
|
|
657
|
+
kind: 'test-crate',
|
|
658
|
+
schemaVersion: 1,
|
|
659
|
+
schema: z.object({ type: z.literal('test-crate') }) as never,
|
|
660
|
+
category: 'furnish',
|
|
661
|
+
defaults: () => ({}),
|
|
662
|
+
capabilities: { selectable: {}, movable: { axes: ['x', 'z'] } },
|
|
663
|
+
renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
|
|
664
|
+
} as AnyNodeDefinition)
|
|
665
|
+
}
|
|
666
|
+
const crate = {
|
|
667
|
+
id: 'test-crate_a',
|
|
668
|
+
type: 'test-crate',
|
|
669
|
+
parentId: levelId,
|
|
670
|
+
position: [2, 0, 2],
|
|
671
|
+
rotation: [0, 0, 0],
|
|
672
|
+
} as unknown as AnyNode
|
|
673
|
+
await withRooms(async ({ click }) => {
|
|
674
|
+
await act(async () => {
|
|
675
|
+
useScene.setState({ nodes: { ...useScene.getState().nodes, [crate.id]: crate } })
|
|
676
|
+
})
|
|
677
|
+
await click(crate.id, [2, 2])
|
|
678
|
+
jest.advanceTimersByTime(50)
|
|
679
|
+
await click(crate.id, [2, 2])
|
|
680
|
+
expect(getMovingNode()?.id).toBe(crate.id)
|
|
681
|
+
|
|
682
|
+
// The browser's click that ends this press would otherwise commit the move in place.
|
|
683
|
+
const endingClick = new Event('click', { cancelable: true })
|
|
684
|
+
window.dispatchEvent(endingClick)
|
|
685
|
+
expect(endingClick.defaultPrevented).toBe(true)
|
|
686
|
+
const dropClick = new Event('click', { cancelable: true })
|
|
687
|
+
window.dispatchEvent(dropClick)
|
|
688
|
+
expect(dropClick.defaultPrevented).toBe(false)
|
|
689
|
+
await act(async () => useEditor.getState().setMovingNode(null))
|
|
690
|
+
})
|
|
691
|
+
})
|
|
692
|
+
|
|
649
693
|
test('plan and box furniture selections end the selected room', async () => {
|
|
650
694
|
await withRooms(async ({ click, left }) => {
|
|
651
695
|
const chair = ItemNode.parse({
|
|
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, mock, test } from 'bun:test'
|
|
|
2
2
|
import {
|
|
3
3
|
type AnyNode,
|
|
4
4
|
type AnyNodeId,
|
|
5
|
+
BuildingNode,
|
|
5
6
|
CabinetModuleNode,
|
|
6
7
|
type CabinetModuleNode as CabinetModuleNodeType,
|
|
7
8
|
CabinetNode,
|
|
@@ -11,14 +12,17 @@ import {
|
|
|
11
12
|
configureArtifactStore,
|
|
12
13
|
getArtifactStore,
|
|
13
14
|
ItemNode,
|
|
14
|
-
|
|
15
|
+
LevelNode,
|
|
15
16
|
MeasurementNode,
|
|
17
|
+
RoofNode,
|
|
18
|
+
RoofSegmentNode,
|
|
16
19
|
SceneMaterial,
|
|
17
20
|
type SceneMaterialId,
|
|
18
21
|
SlabNode,
|
|
19
22
|
useScene,
|
|
20
23
|
WallNode,
|
|
21
24
|
WindowNode,
|
|
25
|
+
ZoneNode,
|
|
22
26
|
} from '@pascal-app/core'
|
|
23
27
|
import { useViewer } from '@pascal-app/viewer'
|
|
24
28
|
import {
|
|
@@ -366,6 +370,194 @@ describe('scene clipboard', () => {
|
|
|
366
370
|
expect(scriptedY).toBeCloseTo(3 / 2)
|
|
367
371
|
})
|
|
368
372
|
|
|
373
|
+
// A roof lives on the level above the walls that carry it: a paste puts it there, and walls
|
|
374
|
+
// pasted on the roof level's row go to the storey it covers.
|
|
375
|
+
function roofedRoomOn(levelId: LevelNode['id']) {
|
|
376
|
+
const corners: [number, number][] = [
|
|
377
|
+
[0, 0],
|
|
378
|
+
[8, 0],
|
|
379
|
+
[8, 6],
|
|
380
|
+
[0, 6],
|
|
381
|
+
]
|
|
382
|
+
const walls = corners.map((start, i) =>
|
|
383
|
+
WallNode.parse({ parentId: levelId, start, end: corners[(i + 1) % 4], height: 2.5 }),
|
|
384
|
+
)
|
|
385
|
+
const segment = RoofSegmentNode.parse({ width: 8, depth: 6, wallHeight: 0 })
|
|
386
|
+
const roof = RoofNode.parse({
|
|
387
|
+
parentId: levelId,
|
|
388
|
+
position: [4, 2.5, 3],
|
|
389
|
+
children: [segment.id],
|
|
390
|
+
})
|
|
391
|
+
return { walls, roof, segment: { ...segment, parentId: roof.id } }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function seedHouse(...levels: LevelNode[]) {
|
|
395
|
+
const building = BuildingNode.parse({ children: levels.map((level) => level.id) })
|
|
396
|
+
const { walls, roof, segment } = roofedRoomOn(sourceLevelId)
|
|
397
|
+
useScene.setState((state) => ({
|
|
398
|
+
nodes: {
|
|
399
|
+
...state.nodes,
|
|
400
|
+
[sourceLevelId]: makeLevel(sourceLevelId, [...walls.map((wall) => wall.id), roof.id]),
|
|
401
|
+
...Object.fromEntries(walls.map((wall) => [wall.id, wall])),
|
|
402
|
+
[roof.id]: roof,
|
|
403
|
+
[segment.id]: segment,
|
|
404
|
+
[building.id]: building,
|
|
405
|
+
...Object.fromEntries(
|
|
406
|
+
levels.map((level) => [level.id, { ...level, parentId: building.id }]),
|
|
407
|
+
),
|
|
408
|
+
},
|
|
409
|
+
}))
|
|
410
|
+
return { walls, roof }
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
test('walls pasted with the roof they carry: the roof lands on a new Roof level, in one undo step', () => {
|
|
414
|
+
const storey = LevelNode.parse({ level: 0 })
|
|
415
|
+
const { walls, roof } = seedHouse(storey)
|
|
416
|
+
const before = Object.keys(useScene.getState().nodes).length
|
|
417
|
+
expect(copySelectedNodesToEditorClipboard([...walls.map((wall) => wall.id), roof.id])).toBe(
|
|
418
|
+
true,
|
|
419
|
+
)
|
|
420
|
+
const { pastedIds } = pasteEditorClipboardToLevel(storey.id)!
|
|
421
|
+
const nodes = useScene.getState().nodes
|
|
422
|
+
const pastedRoof = pastedIds.map((id) => nodes[id]!).find((node) => node.type === 'roof')!
|
|
423
|
+
const roofLevel = nodes[pastedRoof.parentId as AnyNodeId] as LevelNode
|
|
424
|
+
expect(roofLevel).toMatchObject({ type: 'level', level: 1, metadata: { role: 'roof' } })
|
|
425
|
+
expect(roofLevel.children).toEqual([pastedRoof.id])
|
|
426
|
+
expect((nodes[storey.id] as LevelNode).children).toHaveLength(4)
|
|
427
|
+
useScene.temporal.getState().undo()
|
|
428
|
+
expect(Object.keys(useScene.getState().nodes)).toHaveLength(before)
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
test('walls pasted on the roof level go to the storey it covers, the selection with them', () => {
|
|
432
|
+
const storey = LevelNode.parse({ level: 0 })
|
|
433
|
+
const roofLevel = LevelNode.parse({ level: 1, metadata: { role: 'roof' } })
|
|
434
|
+
const { walls } = seedHouse(storey, roofLevel)
|
|
435
|
+
expect(copySelectedNodesToEditorClipboard([walls[0]!.id])).toBe(true)
|
|
436
|
+
const [pastedId] = pasteEditorClipboardToLevel(roofLevel.id)!.pastedIds
|
|
437
|
+
expect(useScene.getState().nodes[pastedId!]?.parentId).toBe(storey.id)
|
|
438
|
+
expect(useViewer.getState().selection.levelId).toBe(storey.id)
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
test('a retained roof pasted onto a roof level keeps its roof ownership and can be undone', () => {
|
|
442
|
+
const storey = LevelNode.parse({ level: 0 })
|
|
443
|
+
const roofLevel = LevelNode.parse({ level: 1, metadata: { role: 'roof' } })
|
|
444
|
+
seedHouse(storey, roofLevel)
|
|
445
|
+
const retained = ItemNode.parse({
|
|
446
|
+
parentId: sourceLevelId,
|
|
447
|
+
position: [1, 0.25, 2],
|
|
448
|
+
metadata: { role: 'roof' },
|
|
449
|
+
asset: {
|
|
450
|
+
id: 'retained',
|
|
451
|
+
name: 'Authored roof',
|
|
452
|
+
category: 'custom',
|
|
453
|
+
thumbnail: '',
|
|
454
|
+
src: '/roof.glb',
|
|
455
|
+
},
|
|
456
|
+
provenance: { refs: [{ ns: 'source', id: 'building/roof' }] },
|
|
457
|
+
})
|
|
458
|
+
useScene.getState().createNode(retained, sourceLevelId)
|
|
459
|
+
expect(copySelectedNodesToEditorClipboard([retained.id])).toBe(true)
|
|
460
|
+
const [pastedId] = pasteEditorClipboardToLevel(roofLevel.id)!.pastedIds
|
|
461
|
+
const pasted = useScene.getState().nodes[pastedId!] as typeof retained
|
|
462
|
+
expect(pasted.parentId).toBe(roofLevel.id)
|
|
463
|
+
expect(pasted.metadata.role).toBe('roof')
|
|
464
|
+
expect(pasted.asset).toEqual(retained.asset)
|
|
465
|
+
expect(pasted.provenance).toEqual(retained.provenance)
|
|
466
|
+
expect(useViewer.getState().selection.levelId).toBe(roofLevel.id)
|
|
467
|
+
useScene.temporal.getState().undo()
|
|
468
|
+
expect(useScene.getState().nodes[pastedId!]).toBeUndefined()
|
|
469
|
+
expect(useScene.getState().nodes[retained.id]).toBeDefined()
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
// Measurements, cabinets and the like stayed on the roof level at its floor while the walls
|
|
473
|
+
// copied with them went down: a paste lands on one level.
|
|
474
|
+
test('a paste carrying walls lands whole on the storey a roof level covers', () => {
|
|
475
|
+
const storey = LevelNode.parse({ level: 0 })
|
|
476
|
+
const roofLevel = LevelNode.parse({ level: 1, metadata: { role: 'roof' } })
|
|
477
|
+
const { walls } = seedHouse(storey, roofLevel)
|
|
478
|
+
const measurement = MeasurementNode.parse({
|
|
479
|
+
parentId: sourceLevelId,
|
|
480
|
+
measurement: {
|
|
481
|
+
kind: 'distance',
|
|
482
|
+
points: [
|
|
483
|
+
[0, 0, 0],
|
|
484
|
+
[8, 0, 0],
|
|
485
|
+
],
|
|
486
|
+
},
|
|
487
|
+
})
|
|
488
|
+
useScene.setState((state) => {
|
|
489
|
+
const source = state.nodes[sourceLevelId] as LevelNode
|
|
490
|
+
return {
|
|
491
|
+
nodes: {
|
|
492
|
+
...state.nodes,
|
|
493
|
+
[measurement.id]: measurement,
|
|
494
|
+
[sourceLevelId]: { ...source, children: [...source.children, measurement.id] },
|
|
495
|
+
},
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
expect(copySelectedNodesToEditorClipboard([walls[0]!.id, measurement.id])).toBe(true)
|
|
499
|
+
const { pastedIds } = pasteEditorClipboardToLevel(roofLevel.id)!
|
|
500
|
+
const nodes = useScene.getState().nodes
|
|
501
|
+
expect(pastedIds.map((id) => nodes[id]?.parentId)).toEqual([storey.id, storey.id])
|
|
502
|
+
expect((nodes[roofLevel.id] as LevelNode).children).toEqual([])
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
// Pasted corner windows kept naming the originals, so the copies drew as plain windows that
|
|
506
|
+
// could not be joined while the originals stood.
|
|
507
|
+
test('a corner window pasted with both its walls joins its own copies; one side alone is plain', () => {
|
|
508
|
+
const south = WallNode.parse({
|
|
509
|
+
id: 'wall_clipboard-south',
|
|
510
|
+
parentId: sourceLevelId,
|
|
511
|
+
start: [0, 0],
|
|
512
|
+
end: [4, 0],
|
|
513
|
+
})
|
|
514
|
+
const east = WallNode.parse({
|
|
515
|
+
id: 'wall_clipboard-east',
|
|
516
|
+
parentId: sourceLevelId,
|
|
517
|
+
start: [4, 0],
|
|
518
|
+
end: [4, 3],
|
|
519
|
+
})
|
|
520
|
+
const onSouth = WindowNode.parse({
|
|
521
|
+
id: 'window_clipboard-south',
|
|
522
|
+
parentId: south.id,
|
|
523
|
+
wallId: south.id,
|
|
524
|
+
position: [3.4, 1.2, 0],
|
|
525
|
+
corner: { end: 'end', partnerId: 'window_clipboard-east', post: 'none' },
|
|
526
|
+
})
|
|
527
|
+
const onEast = WindowNode.parse({
|
|
528
|
+
id: 'window_clipboard-east',
|
|
529
|
+
parentId: east.id,
|
|
530
|
+
wallId: east.id,
|
|
531
|
+
position: [0.6, 1.2, 0],
|
|
532
|
+
corner: { end: 'start', partnerId: onSouth.id, post: 'none' },
|
|
533
|
+
})
|
|
534
|
+
useScene.setState((state) => ({
|
|
535
|
+
nodes: {
|
|
536
|
+
...state.nodes,
|
|
537
|
+
[sourceLevelId]: makeLevel(sourceLevelId, [south.id, east.id]),
|
|
538
|
+
[south.id]: { ...south, children: [onSouth.id] },
|
|
539
|
+
[east.id]: { ...east, children: [onEast.id] },
|
|
540
|
+
[onSouth.id]: onSouth,
|
|
541
|
+
[onEast.id]: onEast,
|
|
542
|
+
},
|
|
543
|
+
}))
|
|
544
|
+
const windowsOf = (wallIds: readonly AnyNodeId[]) =>
|
|
545
|
+
wallIds.flatMap((id) =>
|
|
546
|
+
(useScene.getState().nodes[id] as WallNode).children.map(
|
|
547
|
+
(child) => useScene.getState().nodes[child as AnyNodeId] as WindowNode,
|
|
548
|
+
),
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
expect(copySelectedNodesToEditorClipboard([south.id, east.id])).toBe(true)
|
|
552
|
+
const [a, b] = windowsOf(pasteEditorClipboardToLevel(targetLevelId)!.pastedIds)
|
|
553
|
+
expect(a?.corner?.partnerId).toBe(b?.id)
|
|
554
|
+
expect(b?.corner?.partnerId).toBe(a?.id)
|
|
555
|
+
|
|
556
|
+
expect(copySelectedNodesToEditorClipboard([south.id])).toBe(true)
|
|
557
|
+
const [alone] = windowsOf(pasteEditorClipboardToLevel(targetLevelId)!.pastedIds)
|
|
558
|
+
expect(alone?.corner).toBeUndefined()
|
|
559
|
+
})
|
|
560
|
+
|
|
369
561
|
test('a paste keeps collection membership on both sides in one undo step, or drops collections the scene lacks', () => {
|
|
370
562
|
const collectionId = 'collection_clipboard-lighting' as CollectionId
|
|
371
563
|
const lamp = ItemNode.parse({
|
|
@@ -552,6 +744,59 @@ describe('scene clipboard', () => {
|
|
|
552
744
|
}
|
|
553
745
|
})
|
|
554
746
|
|
|
747
|
+
test("a room copied to another project brings its floor script's artifacts", async () => {
|
|
748
|
+
let systemClipboardText = ''
|
|
749
|
+
Object.defineProperty(navigator, 'clipboard', {
|
|
750
|
+
configurable: true,
|
|
751
|
+
value: {
|
|
752
|
+
readText: async () => systemClipboardText,
|
|
753
|
+
writeText: async (text: string) => {
|
|
754
|
+
systemClipboardText = text
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
})
|
|
758
|
+
const room = ZoneNode.parse({
|
|
759
|
+
id: 'zone_clipboard-floor-script',
|
|
760
|
+
parentId: sourceLevelId,
|
|
761
|
+
name: 'Hall',
|
|
762
|
+
polygon: [
|
|
763
|
+
[0, 0],
|
|
764
|
+
[3, 0],
|
|
765
|
+
[3, 3],
|
|
766
|
+
[0, 3],
|
|
767
|
+
],
|
|
768
|
+
floor: {
|
|
769
|
+
script: {
|
|
770
|
+
kind: 'script',
|
|
771
|
+
script: 'e'.repeat(64),
|
|
772
|
+
artifact: 'f'.repeat(64),
|
|
773
|
+
inputs: 'key',
|
|
774
|
+
manifest: { bounds: { min: [0, 0, 0], max: [3, 0.01, 3] }, triangles: 2 },
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
})
|
|
778
|
+
useScene.setState((state) => ({
|
|
779
|
+
nodes: {
|
|
780
|
+
...state.nodes,
|
|
781
|
+
[sourceLevelId]: makeLevel(sourceLevelId, [room.id]),
|
|
782
|
+
[room.id]: room,
|
|
783
|
+
},
|
|
784
|
+
}))
|
|
785
|
+
useViewer.getState().setProjectId('project_clipboard-source')
|
|
786
|
+
expect(copySelectedNodesToEditorClipboard([room.id])).toBe(true)
|
|
787
|
+
useViewer.getState().setProjectId('project_clipboard-target')
|
|
788
|
+
const copyFrom = mock(async (_projectId: string, _sha256s: string[]) => [] as string[])
|
|
789
|
+
const previousStore = getArtifactStore()
|
|
790
|
+
configureArtifactStore({ ...previousStore, copyFrom })
|
|
791
|
+
try {
|
|
792
|
+
await pasteSystemEditorClipboardToLevel(targetLevelId)
|
|
793
|
+
expect(new Set(copyFrom.mock.calls[0]![1])).toEqual(new Set(['e'.repeat(64), 'f'.repeat(64)]))
|
|
794
|
+
} finally {
|
|
795
|
+
configureArtifactStore(previousStore)
|
|
796
|
+
useViewer.getState().setProjectId(null)
|
|
797
|
+
}
|
|
798
|
+
})
|
|
799
|
+
|
|
555
800
|
test('navigation during artifact copy does not paste into the next project', async () => {
|
|
556
801
|
copyScriptedWindow()
|
|
557
802
|
const previousStore = getArtifactStore()
|