@pascal-app/editor 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -0
- package/package.json +39 -37
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/bake-thumbnail.tsx +6 -15
- package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
- package/src/components/editor/construction-reveal-audio.tsx +40 -0
- package/src/components/editor/custom-camera-controls.tsx +81 -8
- package/src/components/editor/editor-layout-mobile.tsx +1 -1
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/export-manager.tsx +14 -19
- package/src/components/editor/first-person/build-collider-world.test.ts +269 -0
- package/src/components/editor/first-person/build-collider-world.ts +22 -5
- package/src/components/editor/first-person/ground-holes.test.ts +128 -0
- package/src/components/editor/first-person/ground-holes.ts +130 -0
- package/src/components/editor/first-person/mechanism-targets.test.ts +89 -2
- package/src/components/editor/first-person/mechanism-targets.ts +52 -0
- package/src/components/editor/first-person-controls.tsx +19 -11
- package/src/components/editor/floor-aware-camera-controls.test.ts +24 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +72 -23
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/group-actions.ts +2 -0
- package/src/components/editor/group-move-3d.ts +2 -0
- package/src/components/editor/group-rotate-handle.tsx +2 -0
- package/src/components/editor/group-transform-shared.test.ts +460 -0
- package/src/components/editor/index.tsx +166 -79
- package/src/components/editor/riser-diagram-panel.tsx +14 -7
- package/src/components/editor/room-highlight.tsx +13 -5
- package/src/components/editor/room-labels.test.ts +621 -128
- package/src/components/editor/room-labels.tsx +1090 -190
- package/src/components/editor/selection-manager.tsx +38 -117
- package/src/components/editor/slab-hole-highlights.tsx +2 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +3 -2
- package/src/components/editor/snapshot-capture.test.ts +69 -58
- package/src/components/editor/snapshot-capture.ts +7 -1
- package/src/components/editor/thumbnail-generator.test.ts +350 -4
- package/src/components/editor/thumbnail-generator.tsx +128 -54
- package/src/components/editor/use-floorplan-background-placement.ts +3 -1
- package/src/components/editor/views/plugins-view.tsx +1 -8
- package/src/components/editor/views/view-bar.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +140 -17
- package/src/components/editor/wall-snap-beacon-layer.tsx +5 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +3 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +1 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +29 -7
- package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
- package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
- package/src/components/point-ask/bubble.tsx +538 -0
- package/src/components/point-ask/hover-chip.tsx +91 -0
- package/src/components/point-ask/mode-pill.tsx +92 -0
- package/src/components/point-ask/optional-motion.ts +22 -0
- package/src/components/point-ask/outline-corners.tsx +93 -0
- package/src/components/point-ask/pins.tsx +711 -0
- package/src/components/point-ask/point-ask-layer.tsx +83 -0
- package/src/components/point-ask/point-ask-styles.ts +105 -0
- package/src/components/point-ask/region-marquee-styles.ts +15 -0
- package/src/components/point-ask/region-marquee.tsx +64 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- package/src/components/systems/roof/roof-edit-system.tsx +1 -0
- package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
- package/src/components/tools/distribution/connection-compatibility.ts +43 -0
- package/src/components/tools/distribution/distribution-public.ts +9 -0
- package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
- package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
- package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
- package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
- package/src/components/tools/distribution/draw-alignment.ts +35 -0
- package/src/components/tools/distribution/ports.test.ts +56 -0
- package/src/components/tools/distribution/ports.ts +378 -0
- package/src/components/tools/distribution/run-cursor.test.ts +135 -0
- package/src/components/tools/distribution/run-cursor.ts +36 -0
- package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
- package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
- package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
- package/src/components/tools/distribution/run-port-snap.ts +25 -0
- package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
- package/src/components/tools/distribution/run-screen-direction.ts +30 -0
- package/src/components/tools/elevator/elevator-tool.tsx +3 -6
- package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
- package/src/components/tools/item/placement-history.test.tsx +38 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +3 -6
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- package/src/components/tools/item/use-draft-node.test.tsx +94 -94
- package/src/components/tools/item/use-placement-coordinator.tsx +55 -49
- package/src/components/tools/registry/item-surface-move.test.ts +62 -63
- package/src/components/tools/registry/item-surface-move.ts +2 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
- package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +63 -29
- package/src/components/tools/tool-manager.tsx +14 -5
- package/src/components/tools/wall/wall-drafting.test.ts +170 -57
- package/src/components/ui/action-menu/action-button.tsx +6 -1
- package/src/components/ui/action-menu/camera-actions.tsx +7 -3
- package/src/components/ui/action-menu/control-modes.tsx +13 -7
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
- package/src/components/ui/action-menu/index.tsx +56 -33
- package/src/components/ui/action-menu/measurement-control.tsx +11 -9
- package/src/components/ui/action-menu/placement.ts +20 -0
- package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
- package/src/components/ui/action-menu/view-toggles.tsx +47 -42
- package/src/components/ui/build-panel/build-panel.tsx +10 -3
- package/src/components/ui/command-palette/editor-commands.tsx +3 -12
- package/src/components/ui/command-palette/index.tsx +45 -15
- package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
- package/src/components/ui/controls/action-button.tsx +5 -2
- package/src/components/ui/controls/metric-control.tsx +1 -1
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/slider-control.tsx +1 -1
- package/src/components/ui/controls/toggle-control.tsx +3 -3
- package/src/components/ui/controls/tool-options-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +150 -45
- package/src/components/ui/helpers/contextual-helper-panel.tsx +32 -7
- package/src/components/ui/helpers/helper-manager.test.tsx +54 -2
- package/src/components/ui/helpers/helper-manager.tsx +7 -1
- package/src/components/ui/helpers/item-helper.tsx +9 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +8 -8
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +6 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +120 -3
- package/src/components/ui/panels/panel-wrapper.tsx +53 -27
- package/src/components/ui/panels/parametric-field-control.tsx +2 -2
- package/src/components/ui/panels/parametric-inspector.tsx +3 -3
- package/src/components/ui/panels/room-panel.tsx +65 -0
- package/src/components/ui/primitives/badge.tsx +8 -0
- package/src/components/ui/primitives/sidebar.tsx +1 -1
- package/src/components/ui/right-stack.tsx +21 -2
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +3 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
- package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +107 -23
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +5 -12
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -10
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/tab-bar.tsx +4 -4
- package/src/components/viewer/viewer-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-rooms.tsx +16 -0
- package/src/hooks/use-auto-save.integration.test.tsx +96 -38
- package/src/hooks/use-auto-save.ts +26 -1
- package/src/hooks/use-ceiling-events.test.ts +123 -118
- package/src/hooks/use-grid-events.test.ts +105 -105
- package/src/hooks/use-keyboard.test.ts +65 -16
- package/src/hooks/use-keyboard.ts +29 -12
- package/src/hooks/use-placement-notice.test.ts +24 -1
- package/src/hooks/use-placement-notice.ts +37 -6
- package/src/index.tsx +71 -24
- package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
- package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
- package/src/lib/add-storey.test.ts +177 -0
- package/src/lib/add-storey.ts +59 -0
- package/src/lib/below-grade.test.tsx +171 -0
- package/src/lib/below-grade.ts +133 -0
- package/src/lib/camera-facing.test.ts +24 -0
- package/src/lib/camera-facing.ts +24 -0
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.ts +9 -2
- package/src/lib/editor-views.ts +35 -1
- package/src/lib/export-texture-utils.ts +2 -4
- package/src/lib/floor-foundation-support.test.ts +1 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +31 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +39 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +4 -0
- package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
- package/src/lib/floorplan/sheet-export.test.ts +24 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +34 -45
- package/src/lib/floorplan/site-plan/coverage.ts +2 -4
- package/src/lib/floorplan/site-plan/front-edge.ts +2 -1
- package/src/lib/floorplan/site-plan/geometry.test.ts +9 -15
- package/src/lib/floorplan/site-plan/geometry.ts +1 -416
- package/src/lib/floorplan/site-plan/index.ts +0 -16
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +117 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -2
- package/src/lib/floorplan/site-plan/site-parts.ts +14 -106
- package/src/lib/floorplan/site-plan/yards.test.ts +2 -1
- package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
- package/src/lib/follow-pascal-exterior.test.ts +171 -0
- package/src/lib/follow-pascal-exterior.ts +39 -0
- package/src/lib/follow-pascal-presentation.test.ts +203 -0
- package/src/lib/follow-pascal-presentation.ts +129 -0
- package/src/lib/follow-pascal-sight.ts +84 -0
- package/src/lib/follow-pascal-targets.test.ts +406 -0
- package/src/lib/follow-pascal-targets.ts +198 -0
- package/src/lib/follow-pascal.test.ts +350 -0
- package/src/lib/follow-pascal.ts +284 -0
- package/src/lib/fresh-planar-placement.test.ts +112 -0
- package/src/lib/fresh-planar-placement.ts +57 -3
- package/src/lib/geometry-script/author.ts +6 -1
- package/src/lib/geometry-script/client.ts +8 -1
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +1 -0
- package/src/lib/glb-export.test.ts +187 -162
- package/src/lib/glb-export.ts +4 -1
- package/src/lib/history.test.ts +12 -7
- package/src/lib/interaction/scope.ts +4 -0
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- package/src/lib/lot/clean-ring.test.ts +2 -1
- package/src/lib/lot/drop-in.ts +9 -24
- package/src/lib/lot/lot-patch.test.ts +1 -1
- package/src/lib/lot/lot-patch.ts +2 -2
- package/src/lib/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -0
- package/src/lib/orbit-floor-clearance.test.ts +36 -0
- package/src/lib/orbit-floor-clearance.ts +48 -0
- package/src/lib/paint-pass-through.test.ts +11 -0
- package/src/lib/paint-pass-through.ts +4 -2
- package/src/lib/paint-scope.ts +1 -0
- package/src/lib/parametric-node-update.ts +16 -7
- package/src/lib/plugin-chat.ts +15 -0
- package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
- package/src/lib/point-ask/__fixtures__/house.ts +81 -0
- package/src/lib/point-ask/ask-created.test.ts +195 -0
- package/src/lib/point-ask/ask-flow.test.ts +380 -0
- package/src/lib/point-ask/build-context.test.ts +173 -0
- package/src/lib/point-ask/build-context.ts +94 -0
- package/src/lib/point-ask/capture-crop.ts +129 -0
- package/src/lib/point-ask/choreography.ts +69 -0
- package/src/lib/point-ask/compare.test.ts +145 -0
- package/src/lib/point-ask/compare.ts +73 -0
- package/src/lib/point-ask/describe-target.test.ts +165 -0
- package/src/lib/point-ask/describe-target.ts +234 -0
- package/src/lib/point-ask/dictation.ts +79 -0
- package/src/lib/point-ask/edge-marker.test.ts +61 -0
- package/src/lib/point-ask/edge-marker.ts +27 -0
- package/src/lib/point-ask/frame-loop.ts +25 -0
- package/src/lib/point-ask/history-marks.test.ts +94 -0
- package/src/lib/point-ask/history-marks.ts +39 -0
- package/src/lib/point-ask/host-api.ts +32 -0
- package/src/lib/point-ask/outline-box.test.ts +30 -0
- package/src/lib/point-ask/outline-box.ts +11 -0
- package/src/lib/point-ask/overlay-metrics.ts +6 -0
- package/src/lib/point-ask/pick.test.ts +220 -0
- package/src/lib/point-ask/pick.ts +121 -0
- package/src/lib/point-ask/pin-machine.test.ts +494 -0
- package/src/lib/point-ask/pin-machine.ts +217 -0
- package/src/lib/point-ask/placement.test.ts +203 -0
- package/src/lib/point-ask/placement.ts +141 -0
- package/src/lib/point-ask/pointer-tracker.ts +34 -0
- package/src/lib/point-ask/projector.ts +83 -0
- package/src/lib/point-ask/region.test.ts +157 -0
- package/src/lib/point-ask/region.ts +136 -0
- package/src/lib/point-ask/retry.test.ts +217 -0
- package/src/lib/point-ask/retry.ts +37 -0
- package/src/lib/point-ask/scene-commit.test.ts +312 -0
- package/src/lib/point-ask/scene-commit.ts +32 -0
- package/src/lib/point-ask/session.ts +1391 -0
- package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
- package/src/lib/point-ask/spring-keyframes.ts +38 -0
- package/src/lib/point-ask/spring.test.ts +63 -0
- package/src/lib/point-ask/spring.ts +107 -0
- package/src/lib/point-ask/suggestions.test.ts +80 -0
- package/src/lib/point-ask/suggestions.ts +125 -0
- package/src/lib/point-ask/target-scope.test.ts +622 -0
- package/src/lib/point-ask/target-scope.ts +338 -0
- package/src/lib/point-ask/types.ts +48 -0
- package/src/lib/portable-export.test.ts +34 -1
- package/src/lib/portable-export.ts +119 -53
- package/src/lib/print-feature-thickness.ts +10 -1
- package/src/lib/print-roof-solids.test.ts +39 -1
- package/src/lib/print-roof-solids.ts +43 -10
- package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
- package/src/lib/room-assembly-overlay.ts +2 -3
- package/src/lib/room-selection.test.ts +45 -1
- package/src/lib/scene-clipboard.test.ts +246 -1
- package/src/lib/scene-clipboard.ts +44 -14
- package/src/lib/selection-routing.ts +1 -1
- package/src/lib/sfx-bus.ts +13 -0
- package/src/lib/sfx-player.test.ts +70 -9
- package/src/lib/sfx-player.ts +6 -0
- package/src/lib/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.ts +2 -1
- package/src/lib/viewer-selection.ts +22 -11
- package/src/lib/wall-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -0
- package/src/lib/window-changes.ts +35 -0
- package/src/store/use-audio.test.ts +146 -0
- package/src/store/use-audio.tsx +127 -3
- package/src/store/use-construction-reveal.test.ts +173 -0
- package/src/store/use-construction-reveal.ts +145 -0
- package/src/store/use-corner-snap-hint.test.ts +24 -0
- package/src/store/use-corner-snap-hint.ts +47 -0
- package/src/store/use-follow-pascal.test.ts +62 -0
- package/src/store/use-follow-pascal.ts +53 -0
- package/src/store/use-point-ask.ts +247 -0
- package/src/store/use-stair-build-preview.ts +25 -5
- package/src/store/view-layout.test.ts +27 -1
- package/src/store/view-layout.ts +4 -1
- package/src/test-utils/render-current-markup.tsx +16 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
- package/src/components/tools/stair/stair-click-guard.ts +0 -72
- package/src/components/tools/stair/stair-defaults.ts +0 -15
- package/src/components/tools/stair/stair-tool.tsx +0 -682
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getProjectFileSink, type ProjectFileSink } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
/** Persist before reporting a successful download; a failed save is shown by the export UI. */
|
|
4
|
+
export async function downloadProjectFile(
|
|
5
|
+
blob: Blob,
|
|
6
|
+
name: string,
|
|
7
|
+
sink: ProjectFileSink | null = getProjectFileSink(),
|
|
8
|
+
) {
|
|
9
|
+
await sink?.({ blob, name })
|
|
10
|
+
if (typeof document === 'undefined') return
|
|
11
|
+
const url = URL.createObjectURL(blob)
|
|
12
|
+
const anchor = document.createElement('a')
|
|
13
|
+
anchor.href = url
|
|
14
|
+
anchor.download = name
|
|
15
|
+
anchor.click()
|
|
16
|
+
setTimeout(() => URL.revokeObjectURL(url), 0)
|
|
17
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useViewer } from '@pascal-app/viewer'
|
|
2
2
|
import { useEffect } from 'react'
|
|
3
|
-
import
|
|
3
|
+
import useEditor, { type CaptureMode } from '../store/use-editor'
|
|
4
|
+
import { useBelowGrade, useGroundWindowBox } from './below-grade'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Whether the editor shows every level. Preview and the walkthrough show the
|
|
@@ -27,9 +28,15 @@ export function showsWholeBuilding(state: {
|
|
|
27
28
|
*
|
|
28
29
|
* The walkthrough shows the whole building, so the upper levels come back
|
|
29
30
|
* while it is on; Preview hands the display to the viewer's own floor rule
|
|
30
|
-
* (`useViewerFloorDisplay`).
|
|
31
|
+
* (`useViewerFloorDisplay`). A level below grade is marked as shown, and its
|
|
32
|
+
* ground veiled, except on the site, where the ground is what is being worked on.
|
|
31
33
|
*/
|
|
32
34
|
export function useEditorLevelDisplay(showWholeBuilding: boolean) {
|
|
35
|
+
const sitePhase = useEditor((state) => state.phase === 'site')
|
|
36
|
+
useBelowGrade(!(showWholeBuilding || sitePhase))
|
|
37
|
+
// The ground window (a hillside in the eye's way) follows the shown level, and the whole
|
|
38
|
+
// building while it stands whole — except on the site, where the ground is the work.
|
|
39
|
+
useGroundWindowBox(!sitePhase)
|
|
33
40
|
useEffect(() => {
|
|
34
41
|
if (showWholeBuilding) return
|
|
35
42
|
const keepStacked = (mode: string) => {
|
package/src/lib/editor-views.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IconRef, LazyComponent } from '@pascal-app/core'
|
|
2
2
|
import useEditor from '../store/use-editor'
|
|
3
|
-
import { VIEW_2D, VIEW_3D } from '../store/view-layout'
|
|
3
|
+
import { VIEW_2D, VIEW_3D, withoutView } from '../store/view-layout'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* A view fills a pane of the stage, beside or instead of the 3D scene and the
|
|
@@ -19,6 +19,10 @@ export type EditorHostView = {
|
|
|
19
19
|
description?: string
|
|
20
20
|
/** Shown in the view bar until the user unpins it. Default false: the "More views" menu only. */
|
|
21
21
|
defaultPinned?: boolean
|
|
22
|
+
/** Always in the view bar: the user cannot unpin it. */
|
|
23
|
+
alwaysPinned?: boolean
|
|
24
|
+
/** Session views such as open documents can be dismissed from the tab bar. */
|
|
25
|
+
onClose?: () => void
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
/** A view as the stage needs it. The scene views have no component: the stage owns their canvases. */
|
|
@@ -93,6 +97,31 @@ class EditorHostViewRegistryImpl {
|
|
|
93
97
|
this.emit()
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
updateView(id: string, patch: Pick<EditorHostView, 'label'>): void {
|
|
101
|
+
const view = this.views.get(id)
|
|
102
|
+
if (!view) return
|
|
103
|
+
this.views.set(id, { ...view, ...patch })
|
|
104
|
+
this.emit()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
unregisterView(id: string): void {
|
|
108
|
+
if (!this.views.delete(id)) return
|
|
109
|
+
useEditor.getState().closeView(id)
|
|
110
|
+
useEditor.setState((state) => {
|
|
111
|
+
const { [id]: _, ...pinnedViews } = state.pinnedViews
|
|
112
|
+
return {
|
|
113
|
+
pinnedViews,
|
|
114
|
+
viewLayouts: Object.fromEntries(
|
|
115
|
+
Object.entries(state.viewLayouts).map(([workspace, layout]) => [
|
|
116
|
+
workspace,
|
|
117
|
+
withoutView(layout, id),
|
|
118
|
+
]),
|
|
119
|
+
),
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
this.emit()
|
|
123
|
+
}
|
|
124
|
+
|
|
96
125
|
private emit(): void {
|
|
97
126
|
this.cached = Array.from(this.views.values())
|
|
98
127
|
for (const listener of this.listeners) listener()
|
|
@@ -105,6 +134,10 @@ export function registerEditorHostView(view: EditorHostView): void {
|
|
|
105
134
|
editorHostViewRegistry.registerView(view)
|
|
106
135
|
}
|
|
107
136
|
|
|
137
|
+
export function unregisterEditorHostView(id: string): void {
|
|
138
|
+
editorHostViewRegistry.unregisterView(id)
|
|
139
|
+
}
|
|
140
|
+
|
|
108
141
|
/**
|
|
109
142
|
* Show a view: focus it if it is on screen, else put it in the focused pane —
|
|
110
143
|
* or, with `beside`, split and open it next to what is there.
|
|
@@ -140,5 +173,6 @@ export function isViewPinned(
|
|
|
140
173
|
view: EditorViewDescriptor,
|
|
141
174
|
pinnedViews: Readonly<Record<string, boolean>>,
|
|
142
175
|
): boolean {
|
|
176
|
+
if (view.alwaysPinned) return true
|
|
143
177
|
return pinnedViews[view.id] ?? view.defaultPinned ?? false
|
|
144
178
|
}
|
|
@@ -56,7 +56,6 @@ export function createExportTextureUtils(
|
|
|
56
56
|
): ExportTextureUtils {
|
|
57
57
|
let pending: Promise<WebGPURenderer> | null = null
|
|
58
58
|
let disposed = false
|
|
59
|
-
const quad = new QuadMesh()
|
|
60
59
|
|
|
61
60
|
return {
|
|
62
61
|
async decompress(texture, maxTextureSize = Number.POSITIVE_INFINITY) {
|
|
@@ -67,7 +66,7 @@ export function createExportTextureUtils(
|
|
|
67
66
|
const renderer = await pending
|
|
68
67
|
|
|
69
68
|
// Everything below the await is synchronous, which is what lets
|
|
70
|
-
// overlapping calls share
|
|
69
|
+
// overlapping calls share the renderer's size and colour space.
|
|
71
70
|
const image = texture.image as { width: number; height: number }
|
|
72
71
|
const width = Math.min(image.width, maxTextureSize)
|
|
73
72
|
const height = Math.min(image.height, maxTextureSize)
|
|
@@ -77,9 +76,8 @@ export function createExportTextureUtils(
|
|
|
77
76
|
const previousColorSpace = renderer.outputColorSpace
|
|
78
77
|
renderer.setSize(width, height)
|
|
79
78
|
renderer.outputColorSpace = texture.colorSpace
|
|
80
|
-
quad.material = material
|
|
81
79
|
try {
|
|
82
|
-
|
|
80
|
+
new QuadMesh(material).render(renderer)
|
|
83
81
|
} finally {
|
|
84
82
|
renderer.outputColorSpace = previousColorSpace
|
|
85
83
|
material.dispose()
|
|
@@ -70,6 +70,7 @@ beforeEach(() => {
|
|
|
70
70
|
readOnly: false,
|
|
71
71
|
})
|
|
72
72
|
useScene.temporal.getState().resume()
|
|
73
|
+
useScene.temporal.getState().clear()
|
|
73
74
|
stop = initSpaceDetectionSync(useScene, { getState: () => ({ spaces: {}, setSpaces: () => {} }) })
|
|
74
75
|
room(GROUND, rect(0, 0, 8, 5), 'Living')
|
|
75
76
|
room(UPPER, rect(0, 0, 8, 5), 'Bedroom')
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
/// <reference path="./pdfkit-standard-fonts.d.ts" />
|
|
2
|
+
import { getProjectFileSink } from '@pascal-app/core'
|
|
1
3
|
import type PdfKitDocument from 'pdfkit'
|
|
4
|
+
import { downloadProjectFile } from '../download-project-file'
|
|
2
5
|
|
|
3
6
|
type PdfKitDocumentInstance = InstanceType<typeof PdfKitDocument>
|
|
4
7
|
|
|
8
|
+
/** pdfkit 0.20's browser-build export, which @types/pdfkit does not describe. */
|
|
9
|
+
type PdfKitStandardFontRegistry = { registerStdFonts?: (...fonts: unknown[]) => void }
|
|
10
|
+
|
|
5
11
|
type PdfTextOptions = {
|
|
6
12
|
align?: 'left' | 'center' | 'right'
|
|
7
13
|
maxWidth?: number
|
|
@@ -461,16 +467,30 @@ export async function createFloorplanPdfDocument(
|
|
|
461
467
|
defaultPageSize: readonly [number, number],
|
|
462
468
|
options: { title?: string } = {},
|
|
463
469
|
) {
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
470
|
+
const fileSink = getProjectFileSink()
|
|
471
|
+
const [pdfkit, helvetica, helveticaBold, courier, courierBold, { default: blobStream }, fonts] =
|
|
472
|
+
await Promise.all([
|
|
473
|
+
import('pdfkit'),
|
|
474
|
+
import('pdfkit/standard-fonts/Helvetica'),
|
|
475
|
+
import('pdfkit/standard-fonts/HelveticaBold'),
|
|
476
|
+
import('pdfkit/standard-fonts/Courier'),
|
|
477
|
+
import('pdfkit/standard-fonts/CourierBold'),
|
|
478
|
+
import('blob-stream'),
|
|
479
|
+
// an export in the standard fonts beats no export when the font files are unreachable
|
|
480
|
+
loadFloorplanPdfFonts().catch((error: unknown) => {
|
|
481
|
+
console.warn('[floorplan-export] Falling back to the standard PDF fonts', error)
|
|
482
|
+
return null
|
|
483
|
+
}),
|
|
484
|
+
])
|
|
485
|
+
// The browser build has no standard-font metrics until they are registered, and
|
|
486
|
+
// the constructor already sets Helvetica; the Node build loads its own and has no registry.
|
|
487
|
+
;(pdfkit as PdfKitStandardFontRegistry).registerStdFonts?.(
|
|
488
|
+
helvetica.default,
|
|
489
|
+
helveticaBold.default,
|
|
490
|
+
courier.default,
|
|
491
|
+
courierBold.default,
|
|
492
|
+
)
|
|
493
|
+
const raw = new pdfkit.default({
|
|
474
494
|
autoFirstPage: false,
|
|
475
495
|
compress: true,
|
|
476
496
|
margin: 0,
|
|
@@ -487,12 +507,7 @@ export async function createFloorplanPdfDocument(
|
|
|
487
507
|
raw.on('error', reject)
|
|
488
508
|
raw.end()
|
|
489
509
|
})
|
|
490
|
-
|
|
491
|
-
const anchor = document.createElement('a')
|
|
492
|
-
anchor.href = url
|
|
493
|
-
anchor.download = filename
|
|
494
|
-
anchor.click()
|
|
495
|
-
setTimeout(() => URL.revokeObjectURL(url), 0)
|
|
510
|
+
await downloadProjectFile(blob, filename, fileSink)
|
|
496
511
|
},
|
|
497
512
|
}
|
|
498
513
|
}
|
|
@@ -98,6 +98,45 @@ describe('renderFloorplanGeometryToPdfKit', () => {
|
|
|
98
98
|
expect(pdf).toMatch(/f\*/)
|
|
99
99
|
})
|
|
100
100
|
|
|
101
|
+
test('ends an unpainted shape so the next fill does not paint it', async () => {
|
|
102
|
+
// Zones emit fill/stroke 'none' outlines; walls fill right after them.
|
|
103
|
+
const geometry = {
|
|
104
|
+
kind: 'group',
|
|
105
|
+
children: [
|
|
106
|
+
{
|
|
107
|
+
kind: 'polygon',
|
|
108
|
+
points: [
|
|
109
|
+
[0, 0],
|
|
110
|
+
[4, 0],
|
|
111
|
+
[4, 4],
|
|
112
|
+
[0, 4],
|
|
113
|
+
],
|
|
114
|
+
fill: 'none',
|
|
115
|
+
stroke: 'none',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
kind: 'polygon',
|
|
119
|
+
points: [
|
|
120
|
+
[0, 0],
|
|
121
|
+
[1, 0],
|
|
122
|
+
[1, 0.2],
|
|
123
|
+
[0, 0.2],
|
|
124
|
+
],
|
|
125
|
+
fill: '#374151',
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
} as FloorplanGeometry
|
|
129
|
+
|
|
130
|
+
const pdf = await renderTestPdf(geometry)
|
|
131
|
+
const ops = pdf.split('\n')
|
|
132
|
+
const firstClose = ops.indexOf('h')
|
|
133
|
+
|
|
134
|
+
// the unpainted outline is closed and discarded ('n') before the filled one starts
|
|
135
|
+
expect(firstClose).toBeGreaterThan(-1)
|
|
136
|
+
expect(ops[firstClose + 1]).toBe('n')
|
|
137
|
+
expect(pdf).toMatch(/\bf\n/)
|
|
138
|
+
})
|
|
139
|
+
|
|
101
140
|
test('writes a data-url PNG image without resolving it as an asset', async () => {
|
|
102
141
|
const geometry = {
|
|
103
142
|
kind: 'image',
|
|
@@ -217,6 +217,10 @@ function paintStyledGeometry(
|
|
|
217
217
|
if (fill && stroke) raw.fillAndStroke(fill, stroke, fillRule)
|
|
218
218
|
else if (fill) raw.fill(fill, fillRule)
|
|
219
219
|
else if (stroke) raw.stroke(stroke)
|
|
220
|
+
// Unpainted shape (fill and stroke both none, e.g. zone hit areas): end the
|
|
221
|
+
// path without painting ('n'). Otherwise the open path is carried into the
|
|
222
|
+
// next fill and printed in that shape's colour.
|
|
223
|
+
else raw.addContent('n')
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
function isPaint(value: unknown): boolean {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { setProjectFileSink } from '@pascal-app/core'
|
|
2
3
|
import { exportSheetsToPdf, type SheetPdfPage } from './sheet-export'
|
|
3
4
|
|
|
4
5
|
const sheet = (number: string, title: string): SheetPdfPage => ({
|
|
@@ -74,6 +75,29 @@ function titles(pdf: string): string[] {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
describe('exportSheetsToPdf', () => {
|
|
78
|
+
test('retains the PDF in project files before downloading and reports a failed save', async () => {
|
|
79
|
+
let kept: { name: string; blob: Blob } | null = null
|
|
80
|
+
const clear = setProjectFileSink(async (file) => {
|
|
81
|
+
kept = file
|
|
82
|
+
})
|
|
83
|
+
try {
|
|
84
|
+
const { filename } = await exportToBytes([sheet('A0.0', 'Cover')], 'Project files')
|
|
85
|
+
expect(kept!.name).toBe(filename)
|
|
86
|
+
expect(await kept!.blob.text()).toStartWith('%PDF-')
|
|
87
|
+
} finally {
|
|
88
|
+
clear()
|
|
89
|
+
}
|
|
90
|
+
const clearFailed = setProjectFileSink(async () => {
|
|
91
|
+
throw new Error('File could not be saved')
|
|
92
|
+
})
|
|
93
|
+
try {
|
|
94
|
+
await expect(exportToBytes([sheet('A0.0', 'Cover')], 'Project files')).rejects.toThrow(
|
|
95
|
+
'File could not be saved',
|
|
96
|
+
)
|
|
97
|
+
} finally {
|
|
98
|
+
clearFailed()
|
|
99
|
+
}
|
|
100
|
+
})
|
|
77
101
|
test('bookmarks every sheet by its number and title and titles the document', async () => {
|
|
78
102
|
const { pdf, filename } = await exportToBytes(
|
|
79
103
|
[
|
|
@@ -2,14 +2,28 @@ import {
|
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
4
|
type BuildingNode,
|
|
5
|
+
castYardDimensionsOriented,
|
|
6
|
+
classifyEdges,
|
|
7
|
+
edgeHeadingDeg,
|
|
8
|
+
edgeLength,
|
|
5
9
|
envelopeFrontEdge,
|
|
6
10
|
type FloorplanGeometry,
|
|
11
|
+
footprintOutline,
|
|
12
|
+
isRoofLevel,
|
|
7
13
|
type LevelNode,
|
|
14
|
+
levelFootprintLoops,
|
|
15
|
+
outwardNormal,
|
|
8
16
|
type SceneSnapshot,
|
|
9
17
|
type SiteNode,
|
|
18
|
+
setbackForRole,
|
|
10
19
|
sightTriangle,
|
|
20
|
+
siteFrame,
|
|
21
|
+
siteFrontEdge,
|
|
22
|
+
siteSetbackEnvelope,
|
|
11
23
|
streetCorners,
|
|
12
24
|
terrainFieldOf,
|
|
25
|
+
type YardDimension,
|
|
26
|
+
yawOf,
|
|
13
27
|
} from '@pascal-app/core'
|
|
14
28
|
import { sitePlanContributions } from './contributors'
|
|
15
29
|
import {
|
|
@@ -20,21 +34,11 @@ import {
|
|
|
20
34
|
} from './coverage'
|
|
21
35
|
import {
|
|
22
36
|
type Bounds,
|
|
23
|
-
boundsInsidePolygon,
|
|
24
|
-
castYardDimensionsOriented,
|
|
25
|
-
classifyEdges,
|
|
26
37
|
compassLabel,
|
|
27
|
-
edgeHeadingDeg,
|
|
28
|
-
edgeLength,
|
|
29
38
|
formatFeetInches,
|
|
30
39
|
METRES_PER_FOOT,
|
|
31
|
-
outwardNormal,
|
|
32
40
|
type Pt,
|
|
33
41
|
polygonBounds,
|
|
34
|
-
resolveFrontEdge,
|
|
35
|
-
setbackEnvelope,
|
|
36
|
-
setbackForRole,
|
|
37
|
-
type YardDimension,
|
|
38
42
|
} from './geometry'
|
|
39
43
|
import {
|
|
40
44
|
contourPrimitives,
|
|
@@ -47,17 +51,14 @@ import {
|
|
|
47
51
|
streetEdgeNames,
|
|
48
52
|
} from './site-annotations'
|
|
49
53
|
import {
|
|
54
|
+
aboveGradeLevels,
|
|
50
55
|
findBuilding,
|
|
51
56
|
findLowestLevel,
|
|
52
57
|
findSite,
|
|
53
|
-
footprintOutline,
|
|
54
|
-
levelFootprintLoops,
|
|
55
58
|
roofOutlineRings,
|
|
56
|
-
siteFrame,
|
|
57
|
-
yawOf,
|
|
58
59
|
} from './site-parts'
|
|
59
60
|
|
|
60
|
-
export {
|
|
61
|
+
export { roofOutlineRings }
|
|
61
62
|
|
|
62
63
|
/** Contract shared by every drawing producer (see docs/construction-documents.md). */
|
|
63
64
|
export interface SitePlanDrawing {
|
|
@@ -71,8 +72,11 @@ export interface SitePlanDrawing {
|
|
|
71
72
|
frontEdge: number
|
|
72
73
|
lot: Pt[]
|
|
73
74
|
envelope: Pt[]
|
|
74
|
-
/**
|
|
75
|
-
|
|
75
|
+
/**
|
|
76
|
+
* The envelope edge behind the lot's front line (the envelope has its own vertex count now);
|
|
77
|
+
* null with no envelope, or when no envelope edge faces the street.
|
|
78
|
+
*/
|
|
79
|
+
envelopeFrontEdge: number | null
|
|
76
80
|
/** The corner sight triangles (corner, leg end A, leg end B), site metres. */
|
|
77
81
|
sightTriangles: Pt[][]
|
|
78
82
|
/** Per-wall footprint bands of the lowest level, in SITE metres. */
|
|
@@ -316,10 +320,10 @@ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
|
|
|
316
320
|
const lot = (site?.polygon?.points ?? []) as Pt[]
|
|
317
321
|
const building = findBuilding(scene, site)
|
|
318
322
|
const level = findLowestLevel(scene, building)
|
|
319
|
-
const footprintLoops = levelFootprintLoops(scene, level, building)
|
|
323
|
+
const footprintLoops = levelFootprintLoops(scene.nodes, level, building)
|
|
320
324
|
const footprintBounds = unionBounds(footprintLoops)
|
|
321
325
|
const northRotation = site?.northRotation ?? 0
|
|
322
|
-
const frontEdge =
|
|
326
|
+
const frontEdge = site ? siteFrontEdge(site) : 0
|
|
323
327
|
|
|
324
328
|
const empty: SitePlanDrawing = {
|
|
325
329
|
primitives: [],
|
|
@@ -330,7 +334,7 @@ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
|
|
|
330
334
|
frontEdge,
|
|
331
335
|
lot,
|
|
332
336
|
envelope: [],
|
|
333
|
-
envelopeFrontEdge:
|
|
337
|
+
envelopeFrontEdge: null,
|
|
334
338
|
sightTriangles: [],
|
|
335
339
|
footprintLoops,
|
|
336
340
|
footprintBounds,
|
|
@@ -393,9 +397,7 @@ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
|
|
|
393
397
|
typeof site?.sightTriangleFt === 'number' && site.sightTriangleFt > 0
|
|
394
398
|
? site.sightTriangleFt * METRES_PER_FOOT
|
|
395
399
|
: 0
|
|
396
|
-
const envelope = site
|
|
397
|
-
? setbackEnvelope(lot, site.setbacks, frontEdge, { streetEdges, sightTriangleM })
|
|
398
|
-
: []
|
|
400
|
+
const envelope = site ? siteSetbackEnvelope(site) : []
|
|
399
401
|
// the corner sight triangles (clear-vision at a street intersection):
|
|
400
402
|
// dashed, labelled with the leg — the ordinance's figure, verify locally
|
|
401
403
|
const sightTriangles: Pt[][] = []
|
|
@@ -566,7 +568,11 @@ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
|
|
|
566
568
|
}
|
|
567
569
|
|
|
568
570
|
// ── The roof line, dashed over the footprint ─────────────────────────
|
|
569
|
-
|
|
571
|
+
// Every storey's roofs, and the roof level's above them all.
|
|
572
|
+
const roofRings = aboveGradeLevels(scene, building).flatMap((roofed) =>
|
|
573
|
+
roofOutlineRings(scene, roofed, building),
|
|
574
|
+
)
|
|
575
|
+
for (const ring of roofRings) {
|
|
570
576
|
primitives.push({
|
|
571
577
|
kind: 'polygon',
|
|
572
578
|
points: ring,
|
|
@@ -650,7 +656,9 @@ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
|
|
|
650
656
|
let storeys = 0
|
|
651
657
|
for (const childId of building?.children ?? []) {
|
|
652
658
|
const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
|
|
653
|
-
|
|
659
|
+
// The roof level is not a storey: a bungalow under it is one storey.
|
|
660
|
+
if (child?.type === 'level' && (child as LevelNode).level >= 0 && !isRoofLevel(child))
|
|
661
|
+
storeys++
|
|
654
662
|
}
|
|
655
663
|
const cx = (footprintBounds.minX + footprintBounds.maxX) / 2
|
|
656
664
|
const cy = (footprintBounds.minY + footprintBounds.maxY) / 2
|
|
@@ -748,7 +756,7 @@ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
|
|
|
748
756
|
frontEdge,
|
|
749
757
|
lot,
|
|
750
758
|
envelope,
|
|
751
|
-
envelopeFrontEdge: envelope.length >= 3 ? envelopeFrontEdge(lot, frontEdge, envelope) :
|
|
759
|
+
envelopeFrontEdge: envelope.length >= 3 ? envelopeFrontEdge(lot, frontEdge, envelope) : null,
|
|
752
760
|
sightTriangles,
|
|
753
761
|
footprintLoops,
|
|
754
762
|
footprintBounds,
|
|
@@ -758,22 +766,3 @@ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
|
|
|
758
766
|
},
|
|
759
767
|
}
|
|
760
768
|
}
|
|
761
|
-
|
|
762
|
-
/**
|
|
763
|
-
* Translation (site metres, `[dx, dz]`) that centres the building's footprint
|
|
764
|
-
* on the lot centroid. `null` when there is nothing to move or the footprint
|
|
765
|
-
* already sits inside the ring.
|
|
766
|
-
*/
|
|
767
|
-
export function buildingRecentreOffset(
|
|
768
|
-
lot: readonly Pt[],
|
|
769
|
-
footprintBounds: Bounds | null,
|
|
770
|
-
): [number, number] | null {
|
|
771
|
-
if (lot.length < 3 || !footprintBounds) return null
|
|
772
|
-
if (boundsInsidePolygon(lot, footprintBounds)) return null
|
|
773
|
-
const lotB = polygonBounds(lot)
|
|
774
|
-
const lotCx = (lotB.minX + lotB.maxX) / 2
|
|
775
|
-
const lotCy = (lotB.minY + lotB.maxY) / 2
|
|
776
|
-
const fpCx = (footprintBounds.minX + footprintBounds.maxX) / 2
|
|
777
|
-
const fpCy = (footprintBounds.minY + footprintBounds.maxY) / 2
|
|
778
|
-
return [lotCx - fpCx, lotCy - fpCy]
|
|
779
|
-
}
|
|
@@ -26,15 +26,13 @@
|
|
|
26
26
|
* drop-in resolved one (`site.parcel.lotAreaSqFt`), else the drawn lot
|
|
27
27
|
* polygon. Pure: reads a scene snapshot, touches no store.
|
|
28
28
|
*/
|
|
29
|
-
import type
|
|
29
|
+
import { footprintOutline, levelFootprintLoops, type SceneSnapshot } from '@pascal-app/core'
|
|
30
30
|
import { METRES_PER_FOOT, type Pt, pointInPolygon, polygonArea, polygonBounds } from './geometry'
|
|
31
31
|
import {
|
|
32
32
|
aboveGradeLevels,
|
|
33
33
|
findBuilding,
|
|
34
34
|
findLowestLevel,
|
|
35
35
|
findSite,
|
|
36
|
-
footprintOutline,
|
|
37
|
-
levelFootprintLoops,
|
|
38
36
|
type OutdoorPart,
|
|
39
37
|
outdoorParts,
|
|
40
38
|
roofOutlineRings,
|
|
@@ -133,7 +131,7 @@ export function computeSiteCoverage(scene: Pick<SceneSnapshot, 'nodes'>): SiteCo
|
|
|
133
131
|
const levels = storeys.length > 0 ? storeys : ground ? [ground] : []
|
|
134
132
|
|
|
135
133
|
// the footprint: every storey's wall bands, unioned, outer rings only
|
|
136
|
-
const loops = levels.flatMap((level) => levelFootprintLoops(snapshot, level, building))
|
|
134
|
+
const loops = levels.flatMap((level) => levelFootprintLoops(snapshot.nodes, level, building))
|
|
137
135
|
const outline = loops.length > 0 ? footprintOutline(loops) : []
|
|
138
136
|
const buildingM2 = outline.reduce((s, ring) => s + polygonArea(ring), 0)
|
|
139
137
|
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* Coordinates are the site plan frame (metres, x east, y south), the frame
|
|
23
23
|
* the lot polygon and the OSM roads share.
|
|
24
24
|
*/
|
|
25
|
-
import { edgeLength, outwardNormal
|
|
25
|
+
import { edgeLength, outwardNormal } from '@pascal-app/core'
|
|
26
|
+
import type { Pt } from './geometry'
|
|
26
27
|
import { parseStreetName, sameStreet } from './street-name'
|
|
27
28
|
|
|
28
29
|
export interface RoadCenterline {
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test'
|
|
2
|
-
import { sightTriangle, streetCorners } from '@pascal-app/core'
|
|
3
2
|
import {
|
|
4
|
-
boundsInsidePolygon,
|
|
5
|
-
castYardDimensions,
|
|
6
3
|
classifyEdges,
|
|
7
4
|
edgeHeadingDeg,
|
|
8
|
-
formatFeetInches,
|
|
9
|
-
METRES_PER_FOOT,
|
|
10
5
|
mostNorthFacingEdge,
|
|
11
6
|
outwardNormal,
|
|
12
|
-
type Pt,
|
|
13
|
-
polygonArea,
|
|
14
7
|
rayToPolygon,
|
|
15
8
|
resolveFrontEdge,
|
|
16
9
|
setbackEnvelope,
|
|
17
10
|
setbackForRole,
|
|
11
|
+
sightTriangle,
|
|
12
|
+
streetCorners,
|
|
13
|
+
} from '@pascal-app/core'
|
|
14
|
+
import {
|
|
15
|
+
castYardDimensions,
|
|
16
|
+
formatFeetInches,
|
|
17
|
+
METRES_PER_FOOT,
|
|
18
|
+
type Pt,
|
|
19
|
+
polygonArea,
|
|
18
20
|
} from './geometry'
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -258,14 +260,6 @@ describe('rayToPolygon + castYardDimensions', () => {
|
|
|
258
260
|
})
|
|
259
261
|
})
|
|
260
262
|
|
|
261
|
-
describe('boundsInsidePolygon', () => {
|
|
262
|
-
it('is true only when every bbox corner is inside the lot', () => {
|
|
263
|
-
expect(boundsInsidePolygon(LOT, { minX: 5, minY: 5, maxX: 25, maxY: 15 })).toBe(true)
|
|
264
|
-
expect(boundsInsidePolygon(LOT, { minX: -5, minY: 5, maxX: 25, maxY: 15 })).toBe(false)
|
|
265
|
-
expect(boundsInsidePolygon(LOT, { minX: 100, minY: 100, maxX: 110, maxY: 110 })).toBe(false)
|
|
266
|
-
})
|
|
267
|
-
})
|
|
268
|
-
|
|
269
263
|
describe('formatFeetInches', () => {
|
|
270
264
|
it('renders metres as feet-and-inches', () => {
|
|
271
265
|
expect(formatFeetInches(7.62)).toBe(`25'-0"`)
|