@pascal-app/editor 1.0.3 → 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 +42 -38
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +307 -5
- package/src/components/editor/capture-level-picker.tsx +72 -0
- 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 +275 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +37 -26
- package/src/components/editor/editor-layout-v2.tsx +132 -59
- package/src/components/editor/export-manager.tsx +47 -26
- package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
- package/src/components/editor/first-person/build-collider-world.ts +28 -7
- 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 +162 -0
- package/src/components/editor/first-person/mechanism-targets.ts +88 -0
- package/src/components/editor/first-person-controls.tsx +220 -9
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +414 -363
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +56 -2
- 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/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +409 -197
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/riser-diagram-panel.tsx +14 -7
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +233 -0
- package/src/components/editor/room-labels.test.ts +850 -0
- package/src/components/editor/room-labels.tsx +1890 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +474 -419
- package/src/components/editor/slab-hole-highlights.tsx +5 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
- package/src/components/editor/snapshot-capture.test.ts +119 -60
- package/src/components/editor/snapshot-capture.ts +36 -5
- package/src/components/editor/thumbnail-generator.test.ts +654 -0
- package/src/components/editor/thumbnail-generator.tsx +1103 -223
- package/src/components/editor/use-floorplan-background-placement.ts +34 -12
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +663 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +343 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
- 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/editor-2d/svg-paths.ts +12 -15
- 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/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/roof/roof-edit-system.tsx +1 -0
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- 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 +512 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- 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 +66 -30
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/tool-manager.tsx +59 -24
- package/src/components/tools/wall/wall-drafting.test.ts +175 -47
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +9 -1
- package/src/components/ui/action-menu/camera-actions.tsx +9 -4
- package/src/components/ui/action-menu/control-modes.tsx +15 -42
- 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 +14 -19
- 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 +242 -0
- package/src/components/ui/command-palette/editor-commands.tsx +7 -42
- package/src/components/ui/command-palette/index.tsx +50 -39
- 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/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/metric-control.tsx +1 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +65 -0
- 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 +159 -59
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
- package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
- package/src/components/ui/helpers/helper-manager.tsx +331 -14
- package/src/components/ui/helpers/item-helper.tsx +17 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +130 -62
- package/src/components/ui/panels/parametric-field-control.tsx +3 -3
- package/src/components/ui/panels/parametric-inspector.tsx +7 -7
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +790 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/badge.tsx +8 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +3 -1
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +164 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +8 -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 +138 -27
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- 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/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
- package/src/components/ui/sidebar/tab-bar.tsx +14 -4
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +191 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +215 -43
- package/src/hooks/use-auto-save.ts +60 -9
- 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 +243 -19
- package/src/hooks/use-keyboard.ts +118 -83
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +71 -0
- package/src/hooks/use-placement-notice.ts +73 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +312 -58
- 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/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +55 -0
- package/src/lib/editor-views.ts +178 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/export-texture-utils.ts +2 -4
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +284 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +124 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +230 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
- package/src/lib/floorplan/site-plan/geometry.ts +142 -0
- package/src/lib/floorplan/site-plan/index.ts +55 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
- package/src/lib/floorplan/types.ts +1 -27
- 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 +148 -0
- package/src/lib/fresh-planar-placement.ts +114 -37
- package/src/lib/geometry-script/author.ts +157 -0
- package/src/lib/geometry-script/client.ts +159 -0
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +13 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +636 -126
- package/src/lib/glb-export.ts +241 -120
- package/src/lib/history.test.ts +18 -7
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +22 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +187 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +392 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +201 -0
- package/src/lib/orbit-floor-clearance.ts +154 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +122 -0
- package/src/lib/paint-pass-through.ts +78 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +267 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +16 -7
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +65 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -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/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +265 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +1017 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +610 -1
- package/src/lib/scene-clipboard.ts +269 -58
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +54 -75
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- 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/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +214 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +580 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-changes.ts +35 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- 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-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- 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-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +33 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +201 -0
- package/src/store/view-layout.ts +199 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/render-current-markup.tsx +16 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- 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 -18
- package/src/components/tools/stair/stair-tool.tsx +0 -634
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test'
|
|
|
2
2
|
import type { FloorplanGeometry } from '@pascal-app/core'
|
|
3
3
|
import PDFDocument from 'pdfkit'
|
|
4
4
|
import { floorplanGeometryMetadata } from './floorplan-extension'
|
|
5
|
-
import { FloorplanPdfDocument } from './floorplan-pdfkit-document'
|
|
5
|
+
import { FloorplanPdfDocument, loadFloorplanPdfFonts } from './floorplan-pdfkit-document'
|
|
6
6
|
import { renderFloorplanGeometryToPdfKit } from './floorplan-pdfkit-renderer'
|
|
7
7
|
|
|
8
8
|
describe('renderFloorplanGeometryToPdfKit', () => {
|
|
@@ -20,7 +20,7 @@ describe('renderFloorplanGeometryToPdfKit', () => {
|
|
|
20
20
|
const pdf = await renderTestPdf(geometry)
|
|
21
21
|
|
|
22
22
|
expect(pdf).toContain('BT')
|
|
23
|
-
expect(pdf).
|
|
23
|
+
expect(extractPdfText(pdf)).toBe('13m')
|
|
24
24
|
expect(pdf).toMatch(/0\.1 w/)
|
|
25
25
|
expect(pdf).toMatch(/0\.15 w/)
|
|
26
26
|
expect(pdf).not.toMatch(/ c\n/)
|
|
@@ -40,7 +40,7 @@ describe('renderFloorplanGeometryToPdfKit', () => {
|
|
|
40
40
|
|
|
41
41
|
const pdf = await renderTestPdf(geometry, 45)
|
|
42
42
|
|
|
43
|
-
expect(pdf).
|
|
43
|
+
expect(extractPdfText(pdf)).toBe('ROOM 101')
|
|
44
44
|
expect(pdf).toContain('BT')
|
|
45
45
|
})
|
|
46
46
|
|
|
@@ -76,13 +76,13 @@ describe('renderFloorplanGeometryToPdfKit', () => {
|
|
|
76
76
|
|
|
77
77
|
const pdfs = await Promise.all(geometries.map((geometry) => renderTestPdf(geometry)))
|
|
78
78
|
const baseFonts = pdfs.flatMap((pdf) =>
|
|
79
|
-
[...pdf.matchAll(/\/BaseFont \/([^\n]+)/g)].map((match) => match[1]),
|
|
79
|
+
[...pdf.matchAll(/\/BaseFont \/(?:[A-Z]{6}\+)?([^\n]+)/g)].map((match) => match[1]),
|
|
80
80
|
)
|
|
81
81
|
const fontSizes = pdfs.flatMap((pdf) =>
|
|
82
82
|
[...pdf.matchAll(/\/F\d+ ([\d.]+) Tf/g)].map((match) => match[1]),
|
|
83
83
|
)
|
|
84
84
|
|
|
85
|
-
expect([...new Set(baseFonts)]).toEqual(['
|
|
85
|
+
expect([...new Set(baseFonts)]).toEqual(['GeistMono-Regular'])
|
|
86
86
|
expect([...new Set(fontSizes)]).toEqual(['1.6'])
|
|
87
87
|
})
|
|
88
88
|
test('uses even-odd fill for compound plugin paths', async () => {
|
|
@@ -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',
|
|
@@ -113,17 +152,101 @@ describe('renderFloorplanGeometryToPdfKit', () => {
|
|
|
113
152
|
})
|
|
114
153
|
})
|
|
115
154
|
|
|
116
|
-
|
|
155
|
+
describe('embedded PDF fonts', () => {
|
|
156
|
+
// what the permit set prints: code limits, dimension strings, schedule
|
|
157
|
+
// callouts, revision deltas — none of them in pdfkit's WinAnsi standard fonts
|
|
158
|
+
const PROBLEM_TEXT =
|
|
159
|
+
'Vult ≤ 195 mph, runs ≥ 24" · VERIFY ≈ 3\'-2" × 6\'-10 ½" — 29.69°N ¼ ¾ ⅛ 5′ 6″ △ ΔT → √0.6'
|
|
160
|
+
|
|
161
|
+
const textAt = (text: string, fontFamily?: string, fontWeight?: number) =>
|
|
162
|
+
({
|
|
163
|
+
kind: 'text',
|
|
164
|
+
x: 1,
|
|
165
|
+
y: 1,
|
|
166
|
+
text,
|
|
167
|
+
fontSize: 0.5,
|
|
168
|
+
fontFamily,
|
|
169
|
+
fontWeight,
|
|
170
|
+
dominantBaseline: 'alphabetic',
|
|
171
|
+
}) satisfies FloorplanGeometry
|
|
172
|
+
|
|
173
|
+
test('the problem glyphs round-trip through the text layer in the sans and mono families', async () => {
|
|
174
|
+
for (const [family, weight] of [
|
|
175
|
+
['"IBM Plex Sans", Helvetica, Arial, sans-serif', 400],
|
|
176
|
+
['"IBM Plex Sans", Helvetica, Arial, sans-serif', 700],
|
|
177
|
+
['"IBM Plex Mono", Menlo, monospace', 400],
|
|
178
|
+
['"IBM Plex Mono", Menlo, monospace', 700],
|
|
179
|
+
] as const) {
|
|
180
|
+
const pdf = await renderTestPdf(textAt(PROBLEM_TEXT, family, weight), 0, false)
|
|
181
|
+
expect(extractPdfText(pdf)).toBe(PROBLEM_TEXT)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('embeds subset TrueType faces, never the unembedded standard 14', async () => {
|
|
186
|
+
const pdf = await renderTestPdf(textAt(PROBLEM_TEXT), 0, false)
|
|
187
|
+
const baseFonts = [...pdf.matchAll(/\/BaseFont \/([^\n]+)/g)].map((match) => match[1])
|
|
188
|
+
expect(baseFonts.length).toBeGreaterThan(0)
|
|
189
|
+
for (const name of baseFonts) expect(name).toMatch(/^[A-Z]{6}\+(LiberationSans|GeistMono)/)
|
|
190
|
+
expect(pdf).toContain('/FontFile2')
|
|
191
|
+
expect(pdf).not.toMatch(/\/BaseFont \/(Helvetica|Courier)/)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('draws the symbols neither face carries — warning, pointer, angle — as linework', async () => {
|
|
195
|
+
const pdf = await renderTestPdf(textAt('∠ 45° ⚠ open porch ▸ note'), 0, false)
|
|
196
|
+
// the warning keeps a real "!" in the text layer; nothing falls to the .notdef glyph
|
|
197
|
+
expect(extractPdfText(pdf)).toBe(' 45° ! open porch note')
|
|
198
|
+
expect(extractPdfText(pdf)).not.toContain('\u0000')
|
|
199
|
+
// the triangle's ring is an even-odd fill; the pointer and the angle's strokes are plain fills
|
|
200
|
+
expect(pdf).toMatch(/f\*/)
|
|
201
|
+
expect(pdf.match(/\nf\n/g)?.length).toBeGreaterThanOrEqual(2)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('collapses runs of white space the way SVG text does', async () => {
|
|
205
|
+
const pdf = await renderTestPdf(
|
|
206
|
+
{ ...textAt(' APN 06075 \t 030\n062 '), textAnchor: 'end' },
|
|
207
|
+
0,
|
|
208
|
+
false,
|
|
209
|
+
)
|
|
210
|
+
expect(extractPdfText(pdf)).toBe('APN 06075 030 062')
|
|
211
|
+
|
|
212
|
+
const raw = new PDFDocument({ autoFirstPage: false, compress: false })
|
|
213
|
+
const doc = new FloorplanPdfDocument(raw, [200, 200], await loadFloorplanPdfFonts())
|
|
214
|
+
const style = { fontFamily: 'sans-serif', fontSize: 10 }
|
|
215
|
+
expect(doc.measureText(`RIDGE 20'-1"`, style)).toBe(doc.measureText(`RIDGE 20'-1"`, style))
|
|
216
|
+
// a no-break space is content, not white space
|
|
217
|
+
expect(doc.measureText('A\u00a0\u00a0B', style)).toBeGreaterThan(doc.measureText('A B', style))
|
|
218
|
+
raw.end()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('measures a mixed-face string as the sum of the runs it draws', async () => {
|
|
222
|
+
const raw = new PDFDocument({ autoFirstPage: false, compress: false })
|
|
223
|
+
const doc = new FloorplanPdfDocument(raw, [200, 200], await loadFloorplanPdfFonts())
|
|
224
|
+
const style = { fontFamily: 'sans-serif', fontSize: 10 }
|
|
225
|
+
const whole = doc.measureText('≤3△', style)
|
|
226
|
+
const parts =
|
|
227
|
+
doc.measureText('≤3', style) + doc.measureText('△', { ...style, fontFamily: 'monospace' })
|
|
228
|
+
expect(whole).toBeCloseTo(parts, 6)
|
|
229
|
+
// Liberation Sans keeps Helvetica's advance widths (the sheets were laid out against them)
|
|
230
|
+
expect(doc.measureText('0', style)).toBeCloseTo(5.56, 2)
|
|
231
|
+
raw.end()
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
async function renderTestPdf(
|
|
236
|
+
geometry: FloorplanGeometry,
|
|
237
|
+
rotationDeg = 0,
|
|
238
|
+
annotationLayer = true,
|
|
239
|
+
): Promise<string> {
|
|
117
240
|
const raw = new PDFDocument({ autoFirstPage: false, compress: false })
|
|
118
241
|
const chunks: Buffer[] = []
|
|
119
242
|
raw.on('data', (chunk: Buffer) => chunks.push(chunk))
|
|
120
243
|
const completed = new Promise<string>((resolve) => {
|
|
121
244
|
raw.on('end', () => resolve(Buffer.concat(chunks).toString('latin1')))
|
|
122
245
|
})
|
|
123
|
-
const doc = new FloorplanPdfDocument(raw, [200, 200])
|
|
246
|
+
const doc = new FloorplanPdfDocument(raw, [200, 200], await loadFloorplanPdfFonts())
|
|
124
247
|
doc.addPage()
|
|
125
248
|
await renderFloorplanGeometryToPdfKit(doc, geometry, {
|
|
126
|
-
annotationLayer
|
|
249
|
+
annotationLayer,
|
|
127
250
|
placement: { x: 20, y: 20, width: 100, height: 100 },
|
|
128
251
|
rotationDeg,
|
|
129
252
|
viewport: { x: 0, y: -2, width: 20, height: 20 },
|
|
@@ -131,3 +254,49 @@ async function renderTestPdf(geometry: FloorplanGeometry, rotationDeg = 0): Prom
|
|
|
131
254
|
raw.end()
|
|
132
255
|
return completed
|
|
133
256
|
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The text an uncompressed pdfkit file carries, in drawing order: every TJ's
|
|
260
|
+
* glyph ids decoded through its font's ToUnicode CMap — what a PDF reader's
|
|
261
|
+
* text layer (search, copy, pdftotext) reads back.
|
|
262
|
+
*/
|
|
263
|
+
function extractPdfText(pdf: string): string {
|
|
264
|
+
const objects = new Map<string, string>()
|
|
265
|
+
for (const match of pdf.matchAll(/(\d+) 0 obj\n([\s\S]*?)\nendobj/g)) {
|
|
266
|
+
objects.set(match[1]!, match[2]!)
|
|
267
|
+
}
|
|
268
|
+
const fontObjects = new Map<string, string>()
|
|
269
|
+
for (const match of pdf.matchAll(/\/(F\d+) (\d+) 0 R/g)) fontObjects.set(match[1]!, match[2]!)
|
|
270
|
+
const cmaps = new Map<string, Map<number, string>>()
|
|
271
|
+
const cmapFor = (resource: string) => {
|
|
272
|
+
let cmap = cmaps.get(resource)
|
|
273
|
+
if (cmap) return cmap
|
|
274
|
+
cmap = new Map()
|
|
275
|
+
const font = objects.get(fontObjects.get(resource) ?? '') ?? ''
|
|
276
|
+
const stream = objects.get(font.match(/\/ToUnicode (\d+) 0 R/)?.[1] ?? '') ?? ''
|
|
277
|
+
for (const range of stream.matchAll(/<([0-9a-f]{4})> <[0-9a-f]{4}> \[([^\]]*)\]/g)) {
|
|
278
|
+
let gid = Number.parseInt(range[1]!, 16)
|
|
279
|
+
for (const entry of range[2]!.matchAll(/<([0-9a-f ]+)>/g)) {
|
|
280
|
+
const units = entry[1]!.split(' ').map((hex) => Number.parseInt(hex, 16))
|
|
281
|
+
cmap.set(gid++, String.fromCharCode(...units))
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
cmaps.set(resource, cmap)
|
|
285
|
+
return cmap
|
|
286
|
+
}
|
|
287
|
+
let text = ''
|
|
288
|
+
let font = ''
|
|
289
|
+
for (const op of pdf.matchAll(/\/(F\d+) [\d.]+ Tf|\[([^\]]*)\] TJ/g)) {
|
|
290
|
+
if (op[1]) {
|
|
291
|
+
font = op[1]
|
|
292
|
+
continue
|
|
293
|
+
}
|
|
294
|
+
const cmap = cmapFor(font)
|
|
295
|
+
for (const hex of op[2]!.matchAll(/<([0-9a-f]+)>/g)) {
|
|
296
|
+
for (let i = 0; i < hex[1]!.length; i += 4) {
|
|
297
|
+
text += cmap.get(Number.parseInt(hex[1]!.slice(i, i + 4), 16)) ?? '\uFFFD'
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return text
|
|
302
|
+
}
|
|
@@ -11,11 +11,11 @@ import {
|
|
|
11
11
|
import { resolveFloorplanLabelAngle } from '../../components/editor-2d/renderers/floorplan-label-angle'
|
|
12
12
|
import type { FloorplanExportBounds } from './floorplan-export'
|
|
13
13
|
import { readFloorplanGeometryMetadata } from './floorplan-extension'
|
|
14
|
-
import type { FloorplanPdfDocument } from './floorplan-pdfkit-document'
|
|
14
|
+
import type { FloorplanPdfBaseline, FloorplanPdfDocument } from './floorplan-pdfkit-document'
|
|
15
15
|
|
|
16
16
|
const DIMENSION_LINE_WIDTH_PT = 0.5
|
|
17
17
|
const DIMENSION_TICK_WIDTH_PT = 0.75
|
|
18
|
-
const DIMENSION_TEXT_FONT_FAMILY = '
|
|
18
|
+
const DIMENSION_TEXT_FONT_FAMILY = 'monospace'
|
|
19
19
|
const DIMENSION_TEXT_FONT_SIZE_PT = 8
|
|
20
20
|
const DIMENSION_TEXT_FONT_WEIGHT = 400
|
|
21
21
|
const DIMENSION_BASELINE_OFFSET_PT = 5
|
|
@@ -35,6 +35,7 @@ type RenderContext = {
|
|
|
35
35
|
annotationLabelShiftIndex: number
|
|
36
36
|
annotationLabelShifts: readonly FloorplanPoint[]
|
|
37
37
|
annotationLayer: boolean
|
|
38
|
+
dimensionTextSizePt: number
|
|
38
39
|
sceneRotationDeg: number
|
|
39
40
|
unitsPerPoint: number
|
|
40
41
|
}
|
|
@@ -52,6 +53,8 @@ export async function renderFloorplanGeometryToPdfKit(
|
|
|
52
53
|
options: {
|
|
53
54
|
annotationLabelShifts?: readonly FloorplanPoint[]
|
|
54
55
|
annotationLayer: boolean
|
|
56
|
+
/** Paper size of dimension strings; a drafted sheet prints them larger. */
|
|
57
|
+
dimensionTextSizePt?: number
|
|
55
58
|
placement: FloorplanPdfKitPlacement
|
|
56
59
|
rotationDeg: number
|
|
57
60
|
viewport: FloorplanExportBounds
|
|
@@ -70,6 +73,7 @@ export async function renderFloorplanGeometryToPdfKit(
|
|
|
70
73
|
annotationLabelShiftIndex: 0,
|
|
71
74
|
annotationLabelShifts: options.annotationLabelShifts ?? [],
|
|
72
75
|
annotationLayer: options.annotationLayer,
|
|
76
|
+
dimensionTextSizePt: options.dimensionTextSizePt ?? DIMENSION_TEXT_FONT_SIZE_PT,
|
|
73
77
|
sceneRotationDeg: options.rotationDeg,
|
|
74
78
|
unitsPerPoint: 1 / pointsPerUnit,
|
|
75
79
|
})
|
|
@@ -186,8 +190,11 @@ function paintStyledGeometry(
|
|
|
186
190
|
geometry: StyledGeometry,
|
|
187
191
|
context: RenderContext,
|
|
188
192
|
): void {
|
|
189
|
-
|
|
190
|
-
|
|
193
|
+
// SVG's `transparent` / `none` mean "no paint" — pdfkit would read either
|
|
194
|
+
// as a colour name it does not know and paint black (the furniture sprites'
|
|
195
|
+
// hit-target polygons printed as black boxes).
|
|
196
|
+
const fill = isPaint(geometry.fill) ? (geometry.fill as string) : null
|
|
197
|
+
const stroke = isPaint(geometry.stroke) ? (geometry.stroke as string) : null
|
|
191
198
|
const opacity = geometry.opacity ?? 1
|
|
192
199
|
const fillOpacity = (geometry.fillOpacity ?? 1) * opacity
|
|
193
200
|
const strokeOpacity = (geometry.strokeOpacity ?? 1) * opacity
|
|
@@ -210,6 +217,22 @@ function paintStyledGeometry(
|
|
|
210
217
|
if (fill && stroke) raw.fillAndStroke(fill, stroke, fillRule)
|
|
211
218
|
else if (fill) raw.fill(fill, fillRule)
|
|
212
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')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isPaint(value: unknown): boolean {
|
|
227
|
+
if (typeof value !== 'string') return false
|
|
228
|
+
const v = value.trim().toLowerCase()
|
|
229
|
+
return (
|
|
230
|
+
v !== '' &&
|
|
231
|
+
v !== 'none' &&
|
|
232
|
+
v !== 'transparent' &&
|
|
233
|
+
!v.startsWith('url(') &&
|
|
234
|
+
!v.startsWith('var(')
|
|
235
|
+
)
|
|
213
236
|
}
|
|
214
237
|
|
|
215
238
|
function resolveStrokeWidth(geometry: StyledGeometry, context: RenderContext): number {
|
|
@@ -247,7 +270,7 @@ function drawGeometryText(
|
|
|
247
270
|
const dimensionValue =
|
|
248
271
|
readFloorplanGeometryMetadata(geometry).annotationRole === 'automatic-dimension'
|
|
249
272
|
const fontSize = context.annotationLayer
|
|
250
|
-
? (dimensionValue ?
|
|
273
|
+
? (dimensionValue ? context.dimensionTextSizePt : annotationTextSizePt(geometry)) *
|
|
251
274
|
context.unitsPerPoint
|
|
252
275
|
: geometry.fontSize
|
|
253
276
|
const outlinedForScreen = geometry.paintOrder === 'stroke' && !!geometry.stroke
|
|
@@ -258,6 +281,7 @@ function drawGeometryText(
|
|
|
258
281
|
drawNativeText(doc, {
|
|
259
282
|
angleDeg: geometry.upright ? -context.sceneRotationDeg : 0,
|
|
260
283
|
anchor: geometry.textAnchor ?? 'start',
|
|
284
|
+
baseline: geometry.dominantBaseline ?? 'middle',
|
|
261
285
|
fill,
|
|
262
286
|
fontFamily: dimensionValue ? DIMENSION_TEXT_FONT_FAMILY : geometry.fontFamily,
|
|
263
287
|
fontSize,
|
|
@@ -270,7 +294,10 @@ function drawGeometryText(
|
|
|
270
294
|
}
|
|
271
295
|
|
|
272
296
|
function annotationTextSizePt(geometry: Extract<FloorplanGeometry, { kind: 'text' }>): number {
|
|
273
|
-
|
|
297
|
+
const metadata = readFloorplanGeometryMetadata(geometry)
|
|
298
|
+
// an explicit paper size wins over the role's (a sheet's room tag sets its own)
|
|
299
|
+
if (typeof metadata.textSizePt === 'number' && metadata.textSizePt > 0) return metadata.textSizePt
|
|
300
|
+
switch (metadata.annotationRole) {
|
|
274
301
|
case 'room-label':
|
|
275
302
|
if (geometry.fontSize >= 0.18) return DEFAULT_ANNOTATION_FONT_SIZE_PT
|
|
276
303
|
if (geometry.fontSize >= 0.145) return ROOM_NUMBER_FONT_SIZE_PT
|
|
@@ -480,7 +507,7 @@ function drawDimensionText(
|
|
|
480
507
|
stroke: string,
|
|
481
508
|
context: RenderContext,
|
|
482
509
|
): void {
|
|
483
|
-
const fontSize =
|
|
510
|
+
const fontSize = context.dimensionTextSizePt * context.unitsPerPoint
|
|
484
511
|
const y =
|
|
485
512
|
geometry.textPosition === 'centered'
|
|
486
513
|
? fontSize * 0.35
|
|
@@ -495,6 +522,7 @@ function drawDimensionText(
|
|
|
495
522
|
.translate(shift[0], shift[1])
|
|
496
523
|
drawNativeText(doc, {
|
|
497
524
|
anchor: 'middle',
|
|
525
|
+
baseline: 'alphabetic',
|
|
498
526
|
fill: stroke,
|
|
499
527
|
fontFamily: DIMENSION_TEXT_FONT_FAMILY,
|
|
500
528
|
fontSize,
|
|
@@ -512,7 +540,7 @@ function drawDimensionLabel(
|
|
|
512
540
|
context: RenderContext,
|
|
513
541
|
): void {
|
|
514
542
|
const unitsPerPoint = context.unitsPerPoint
|
|
515
|
-
const fontSize =
|
|
543
|
+
const fontSize = context.dimensionTextSizePt * unitsPerPoint
|
|
516
544
|
const padX = 6 * unitsPerPoint
|
|
517
545
|
const padY = 3 * unitsPerPoint
|
|
518
546
|
const textWidth = geometry.text.length * 6.2 * unitsPerPoint
|
|
@@ -538,6 +566,7 @@ function drawDimensionLabel(
|
|
|
538
566
|
.fill()
|
|
539
567
|
drawNativeText(doc, {
|
|
540
568
|
anchor: 'middle',
|
|
569
|
+
baseline: 'middle',
|
|
541
570
|
fill: '#111827',
|
|
542
571
|
fontFamily: DIMENSION_TEXT_FONT_FAMILY,
|
|
543
572
|
fontSize,
|
|
@@ -569,8 +598,9 @@ function drawEqualSpacingBadge(
|
|
|
569
598
|
.fill()
|
|
570
599
|
drawNativeText(doc, {
|
|
571
600
|
anchor: 'middle',
|
|
601
|
+
baseline: 'middle',
|
|
572
602
|
fill: '#334155',
|
|
573
|
-
fontFamily:
|
|
603
|
+
fontFamily: DIMENSION_TEXT_FONT_FAMILY,
|
|
574
604
|
fontSize,
|
|
575
605
|
fontWeight: 600,
|
|
576
606
|
text: geometry.text,
|
|
@@ -585,6 +615,7 @@ function drawNativeText(
|
|
|
585
615
|
options: {
|
|
586
616
|
angleDeg?: number
|
|
587
617
|
anchor: 'start' | 'middle' | 'end'
|
|
618
|
+
baseline: FloorplanPdfBaseline
|
|
588
619
|
fill: string
|
|
589
620
|
fontFamily?: string
|
|
590
621
|
fontSize: number
|
|
@@ -596,24 +627,17 @@ function drawNativeText(
|
|
|
596
627
|
},
|
|
597
628
|
): void {
|
|
598
629
|
const raw = doc.raw
|
|
599
|
-
const normalizedFamily = options.fontFamily?.toLocaleLowerCase() ?? ''
|
|
600
|
-
const family =
|
|
601
|
-
normalizedFamily.includes('mono') || normalizedFamily.includes('courier')
|
|
602
|
-
? 'Courier'
|
|
603
|
-
: 'Helvetica'
|
|
604
|
-
const numericWeight = Number.parseInt(String(options.fontWeight ?? 400), 10)
|
|
605
|
-
const bold =
|
|
606
|
-
options.fontWeight === 'bold' || (Number.isFinite(numericWeight) && numericWeight >= 500)
|
|
607
630
|
raw.save().translate(options.x, options.y)
|
|
608
631
|
if (options.angleDeg) raw.rotate(options.angleDeg)
|
|
609
|
-
raw
|
|
610
|
-
.font(bold ? `${family}-Bold` : family)
|
|
611
|
-
.fontSize(options.fontSize)
|
|
612
|
-
.fillColor(options.fill)
|
|
632
|
+
raw.fillColor(options.fill)
|
|
613
633
|
raw.fillOpacity(options.opacity ?? 1)
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
634
|
+
doc.drawText(
|
|
635
|
+
options.text,
|
|
636
|
+
0,
|
|
637
|
+
0,
|
|
638
|
+
{ fontFamily: options.fontFamily, fontSize: options.fontSize, fontWeight: options.fontWeight },
|
|
639
|
+
{ anchor: options.anchor, baseline: options.baseline },
|
|
640
|
+
)
|
|
617
641
|
raw.restore()
|
|
618
642
|
}
|
|
619
643
|
|
|
@@ -653,7 +677,18 @@ async function drawImage(
|
|
|
653
677
|
raw.restore()
|
|
654
678
|
}
|
|
655
679
|
|
|
656
|
-
function blobToDataUrl(blob: Blob): Promise<string> {
|
|
680
|
+
async function blobToDataUrl(blob: Blob): Promise<string> {
|
|
681
|
+
// FileReader exists only in browsers; the headless export (scripts/demo/
|
|
682
|
+
// print-set.ts) runs the same renderer under Bun, where the bytes are
|
|
683
|
+
// base64-encoded directly instead.
|
|
684
|
+
if (typeof FileReader === 'undefined') {
|
|
685
|
+
const bytes = new Uint8Array(await blob.arrayBuffer())
|
|
686
|
+
let binary = ''
|
|
687
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
688
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000))
|
|
689
|
+
}
|
|
690
|
+
return `data:${blob.type || 'image/png'};base64,${btoa(binary)}`
|
|
691
|
+
}
|
|
657
692
|
return new Promise((resolve, reject) => {
|
|
658
693
|
const reader = new FileReader()
|
|
659
694
|
reader.onerror = () => reject(reader.error)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
LevelNode,
|
|
6
|
+
loadPlugin,
|
|
7
|
+
nodeRegistry,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
import { FloorplanPreview } from '../../components/viewer/floorplan-preview'
|
|
12
|
+
import { DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY } from './annotation-visibility'
|
|
13
|
+
import { collectFloorplanGeometry, floorplanExportInstalledPlugins } from './floorplan-export'
|
|
14
|
+
import { FLOORPLAN_NODE_EXTENSION_KEY } from './floorplan-extension'
|
|
15
|
+
|
|
16
|
+
// Plugin API v1, no-install case: a plugin kind's floor-plan output appears in
|
|
17
|
+
// the read-only preview and the PDF export only while the project has the
|
|
18
|
+
// plugin installed.
|
|
19
|
+
|
|
20
|
+
const PLUGIN_ID = 'fixture:floorplan'
|
|
21
|
+
const KIND = 'fixture:bed'
|
|
22
|
+
const LINKED_KIND = 'fixture:shaft'
|
|
23
|
+
const MARKER_FILL = '#a09a09'
|
|
24
|
+
|
|
25
|
+
const level = LevelNode.parse({ id: 'level_plugin_plan', children: ['fxbed_plan'] })
|
|
26
|
+
const bed = {
|
|
27
|
+
object: 'node',
|
|
28
|
+
id: 'fxbed_plan',
|
|
29
|
+
type: KIND,
|
|
30
|
+
parentId: level.id,
|
|
31
|
+
visible: true,
|
|
32
|
+
metadata: {},
|
|
33
|
+
} as unknown as AnyNode
|
|
34
|
+
// Lives on the upper level but draws on this one through `linkedLevelIds`.
|
|
35
|
+
const upper = LevelNode.parse({ id: 'level_plugin_upper', level: 1, children: ['fxshaft_plan'] })
|
|
36
|
+
const shaft = {
|
|
37
|
+
object: 'node',
|
|
38
|
+
id: 'fxshaft_plan',
|
|
39
|
+
type: LINKED_KIND,
|
|
40
|
+
parentId: upper.id,
|
|
41
|
+
visible: true,
|
|
42
|
+
metadata: {},
|
|
43
|
+
} as unknown as AnyNode
|
|
44
|
+
const nodes = { [level.id]: level, [bed.id]: bed, [upper.id]: upper, [shaft.id]: shaft } as Record<
|
|
45
|
+
string,
|
|
46
|
+
AnyNode
|
|
47
|
+
>
|
|
48
|
+
|
|
49
|
+
let restoreRegistry: () => void
|
|
50
|
+
|
|
51
|
+
beforeEach(async () => {
|
|
52
|
+
restoreRegistry = nodeRegistry._snapshot()
|
|
53
|
+
await loadPlugin({
|
|
54
|
+
id: PLUGIN_ID,
|
|
55
|
+
apiVersion: 1,
|
|
56
|
+
nodes: [
|
|
57
|
+
{
|
|
58
|
+
kind: KIND,
|
|
59
|
+
schemaVersion: 1,
|
|
60
|
+
schema: z.looseObject({ id: z.string(), type: z.literal(KIND) }),
|
|
61
|
+
category: 'structure',
|
|
62
|
+
defaults: () => ({}),
|
|
63
|
+
capabilities: {},
|
|
64
|
+
floorplan: () => ({ kind: 'rect', x: 0, y: 0, width: 2, height: 1, fill: MARKER_FILL }),
|
|
65
|
+
} as unknown as AnyNodeDefinition,
|
|
66
|
+
{
|
|
67
|
+
kind: LINKED_KIND,
|
|
68
|
+
schemaVersion: 1,
|
|
69
|
+
schema: z.looseObject({ id: z.string(), type: z.literal(LINKED_KIND) }),
|
|
70
|
+
category: 'structure',
|
|
71
|
+
defaults: () => ({}),
|
|
72
|
+
capabilities: {},
|
|
73
|
+
extensions: { [FLOORPLAN_NODE_EXTENSION_KEY]: { linkedLevelIds: () => [level.id] } },
|
|
74
|
+
floorplan: () => ({ kind: 'rect', x: 3, y: 0, width: 1, height: 1 }),
|
|
75
|
+
} as unknown as AnyNodeDefinition,
|
|
76
|
+
],
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
restoreRegistry()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
function exportedIds(installedPlugins: readonly string[] | undefined) {
|
|
85
|
+
return collectFloorplanGeometry(
|
|
86
|
+
nodes,
|
|
87
|
+
level.id,
|
|
88
|
+
'full',
|
|
89
|
+
'metric',
|
|
90
|
+
'meters',
|
|
91
|
+
DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
|
|
92
|
+
'floor-plan',
|
|
93
|
+
'finished-faces',
|
|
94
|
+
installedPlugins,
|
|
95
|
+
).map(({ id }) => id)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function previewMarkup(installedPlugins: readonly string[]) {
|
|
99
|
+
return renderToStaticMarkup(
|
|
100
|
+
<FloorplanPreview
|
|
101
|
+
levelId={level.id}
|
|
102
|
+
scene={{ nodes, installedPlugins }}
|
|
103
|
+
showCompass={false}
|
|
104
|
+
showLevelSelector={false}
|
|
105
|
+
/>,
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
describe('floor-plan output of a plugin kind', () => {
|
|
110
|
+
test('the read-only preview draws it only while the plugin is installed', () => {
|
|
111
|
+
expect(previewMarkup([PLUGIN_ID])).toContain(MARKER_FILL)
|
|
112
|
+
expect(previewMarkup([])).not.toContain(MARKER_FILL)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('the PDF export draws level and linked-level kinds while the plugin is installed', () => {
|
|
116
|
+
expect(exportedIds([PLUGIN_ID]).sort()).toEqual([bed.id, shaft.id].sort())
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('the PDF export omits level and linked-level kinds while the plugin is not installed', () => {
|
|
120
|
+
expect(exportedIds([])).toEqual([])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('a legacy scene without explicit install state draws every loaded plugin kind', () => {
|
|
124
|
+
const legacy = floorplanExportInstalledPlugins({
|
|
125
|
+
installedPlugins: ['host:default-panel'],
|
|
126
|
+
hasExplicitPluginInstallState: false,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
expect(legacy).toBeUndefined()
|
|
130
|
+
expect(exportedIds(legacy).sort()).toEqual([bed.id, shaft.id].sort())
|
|
131
|
+
expect(
|
|
132
|
+
floorplanExportInstalledPlugins({
|
|
133
|
+
installedPlugins: [],
|
|
134
|
+
hasExplicitPluginInstallState: true,
|
|
135
|
+
}),
|
|
136
|
+
).toEqual([])
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -10,6 +10,10 @@ export type FloorplanViewState = {
|
|
|
10
10
|
unit: 'metric' | 'imperial'
|
|
11
11
|
metricNotation?: 'meters' | 'millimeters'
|
|
12
12
|
purpose?: 'edit' | 'document'
|
|
13
|
+
/** Sheet drafting conventions (see `FloorplanContextExtension.drafting`). */
|
|
14
|
+
drafting?: boolean
|
|
15
|
+
/** Room labels drawn as screen pills (see `FloorplanContextExtension.roomLabelOverlay`). */
|
|
16
|
+
roomLabelOverlay?: boolean
|
|
13
17
|
wallDimensionReference?: FloorplanWallDimensionReference
|
|
14
18
|
highlighted: boolean
|
|
15
19
|
hovered: boolean
|
|
@@ -59,6 +63,8 @@ export function buildFloorplanContext(
|
|
|
59
63
|
automaticDimensions: viewState.automaticDimensions,
|
|
60
64
|
metricNotation: viewState.metricNotation ?? 'meters',
|
|
61
65
|
purpose: viewState.purpose ?? 'edit',
|
|
66
|
+
drafting: viewState.drafting === true,
|
|
67
|
+
roomLabelOverlay: viewState.roomLabelOverlay === true,
|
|
62
68
|
wallDimensionReference: viewState.wallDimensionReference,
|
|
63
69
|
}),
|
|
64
70
|
viewState: viewState.palette
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Point2D } from '@pascal-app/core'
|
|
2
|
+
import { getWallFaceOffsets, type WallJustification } from '@pascal-app/core'
|
|
2
3
|
import type { FloorplanLineSegment, FloorplanSelectionBounds } from './types'
|
|
3
4
|
|
|
4
5
|
// Baseline rotation (deg) that orients the plan-local scene "north up" on
|
|
@@ -107,7 +108,11 @@ export function movePlanPointTowards(start: Point2D, end: Point2D, distance: num
|
|
|
107
108
|
return interpolatePlanPoint(start, end, Math.min(1, distance / totalDistance))
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
export function getThickPlanLinePolygon(
|
|
111
|
+
export function getThickPlanLinePolygon(
|
|
112
|
+
line: FloorplanLineSegment,
|
|
113
|
+
thickness: number,
|
|
114
|
+
justification?: WallJustification,
|
|
115
|
+
): Point2D[] {
|
|
111
116
|
const dx = line.end.x - line.start.x
|
|
112
117
|
const dy = line.end.y - line.start.y
|
|
113
118
|
const length = Math.hypot(dx, dy)
|
|
@@ -116,15 +121,17 @@ export function getThickPlanLinePolygon(line: FloorplanLineSegment, thickness: n
|
|
|
116
121
|
return [line.start, line.end, line.end, line.start]
|
|
117
122
|
}
|
|
118
123
|
|
|
119
|
-
const
|
|
120
|
-
const normalX = (-dy / length) *
|
|
121
|
-
const normalY = (dx / length) *
|
|
124
|
+
const { a, b } = getWallFaceOffsets({ thickness, justification })
|
|
125
|
+
const normalX = (-dy / length) * a
|
|
126
|
+
const normalY = (dx / length) * a
|
|
122
127
|
|
|
128
|
+
const backX = (-dy / length) * -b
|
|
129
|
+
const backY = (dx / length) * -b
|
|
123
130
|
return [
|
|
124
131
|
{ x: line.start.x + normalX, y: line.start.y + normalY },
|
|
125
132
|
{ x: line.end.x + normalX, y: line.end.y + normalY },
|
|
126
|
-
{ x: line.end.x -
|
|
127
|
-
{ x: line.start.x -
|
|
133
|
+
{ x: line.end.x - backX, y: line.end.y - backY },
|
|
134
|
+
{ x: line.start.x - backX, y: line.start.y - backY },
|
|
128
135
|
]
|
|
129
136
|
}
|
|
130
137
|
|
|
@@ -29,20 +29,11 @@ export {
|
|
|
29
29
|
collectLevelDescendants,
|
|
30
30
|
getItemFloorplanTransform,
|
|
31
31
|
} from './items'
|
|
32
|
-
export {
|
|
33
|
-
buildFloorplanStairEntry,
|
|
34
|
-
computeFloorplanStairSegmentTransforms,
|
|
35
|
-
getFloorplanStairSegmentPolygon,
|
|
36
|
-
} from './stairs'
|
|
37
32
|
export type {
|
|
38
33
|
FloorplanItemEntry,
|
|
39
34
|
FloorplanLineSegment,
|
|
40
35
|
FloorplanNodeTransform,
|
|
41
36
|
FloorplanSelectionBounds,
|
|
42
|
-
FloorplanStairArrowEntry,
|
|
43
|
-
FloorplanStairEntry,
|
|
44
|
-
FloorplanStairSegmentEntry,
|
|
45
37
|
LevelDescendantMap,
|
|
46
|
-
StairSegmentTransform,
|
|
47
38
|
} from './types'
|
|
48
39
|
export { getFloorplanWall, getFloorplanWallThickness } from './walls'
|