@pascal-app/editor 1.1.0 → 1.1.2
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/package.json +9 -15
- package/src/components/editor/action-menu-button.tsx +1 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
- package/src/components/editor/custom-camera-controls.tsx +11 -11
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +20 -1981
- package/src/components/editor/floating-action-menu.tsx +1 -1
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +6 -2
- package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
- package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
- package/src/components/editor/floorplan-panel.tsx +26 -22
- package/src/components/editor/floorplan-wall-typing-keys.ts +1 -1
- package/src/components/editor/follow-pascal.test.tsx +1 -2
- package/src/components/editor/follow-pascal.tsx +1 -1
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/group-actions.ts +1 -2
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +1 -2
- package/src/components/editor/group-rotate-handle.tsx +1 -2
- package/src/components/editor/handles/use-handle-drag.ts +1 -2
- package/src/components/editor/index.tsx +24 -14
- package/src/components/editor/inspector-extension-actions.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +1 -1
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/room-controls.tsx +7 -4
- package/src/components/editor/room-handles-2d.tsx +2 -3
- package/src/components/editor/room-handles.tsx +1 -2
- package/src/components/editor/room-highlight.tsx +6 -105
- package/src/components/editor/room-labels.test.ts +8 -822
- package/src/components/editor/room-labels.tsx +40 -1671
- package/src/components/editor/room-webgpu-materials.test.ts +1 -1
- package/src/components/editor/selection-manager.tsx +5 -3
- package/src/components/editor/site-edge-labels.tsx +1 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
- package/src/components/editor/use-floorplan-background-placement.ts +2 -1
- package/src/components/editor/views/plugins-view.tsx +1 -1
- package/src/components/editor/views/view-bar.tsx +9 -9
- package/src/components/editor/wall-measurement-label.tsx +1 -1
- package/src/components/editor/wall-move-side-handles.tsx +1 -1
- package/src/components/editor/wall-pull-grip.tsx +2 -2
- package/src/components/editor-2d/floorplan-group-move.tsx +2 -3
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1 -1
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +1 -2
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +1 -2
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +8 -10
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +25 -33
- package/src/components/systems/roof/roof-edit-system.tsx +4 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +1 -1
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +2 -3
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +6 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +1 -2
- package/src/components/tools/shared/cursor-sphere.tsx +1 -1
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +1 -1
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/ui/action-menu/camera-actions.tsx +2 -2
- package/src/components/ui/action-menu/control-modes.tsx +1 -1
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
- package/src/components/ui/action-menu/index.tsx +3 -3
- package/src/components/ui/action-menu/measurement-control.tsx +3 -3
- package/src/components/ui/action-menu/placement.ts +0 -18
- package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
- package/src/components/ui/action-menu/view-toggles.tsx +5 -5
- package/src/components/ui/controls/finish-swatch.tsx +1 -1
- package/src/components/ui/controls/material-paint-panel.tsx +2 -2
- package/src/components/ui/controls/scene-material-list.tsx +2 -2
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +3 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.test.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.tsx +1 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.test.tsx +46 -0
- package/src/components/ui/node-icon-image.tsx +33 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
- package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
- package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
- package/src/components/ui/panels/node-display.ts +1 -1
- package/src/components/ui/panels/panel-manager.tsx +1 -1
- package/src/components/ui/panels/panel-wrapper.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +1 -1
- package/src/components/ui/panels/room-construction-rows.tsx +6 -6
- package/src/components/ui/panels/room-name-options.ts +1 -5
- package/src/components/ui/panels/room-panel.tsx +6 -8
- package/src/components/ui/primitives/color-dot.tsx +2 -2
- package/src/components/ui/primitives/opacity-control.tsx +2 -2
- package/src/components/ui/primitives/sidebar.tsx +3 -3
- package/src/components/ui/right-stack.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
- package/src/components/viewer-overlay.tsx +1 -2
- package/src/hooks/use-auto-frame.ts +1 -2
- package/src/hooks/use-keyboard.ts +1 -2
- package/src/hooks/use-selected-room.ts +5 -7
- package/src/i18n/floor-footprint-name.ts +1 -1
- package/src/i18n/locales/en/messages.po +180 -182
- package/src/i18n/locales/es/messages.po +180 -182
- package/src/i18n/locales/fr/messages.po +180 -182
- package/src/i18n/locales/ja/messages.po +180 -182
- package/src/i18n/locales/zh-CN/messages.po +180 -182
- package/src/i18n/locales.test.ts +1 -1
- package/src/index.tsx +109 -131
- package/src/lib/below-grade.test.tsx +4 -4
- package/src/lib/camera-facing.test.ts +1 -1
- package/src/lib/camera-facing.ts +2 -3
- package/src/lib/editor-level-display.ts +1 -1
- package/src/lib/floor-footprints.test.ts +2 -2
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.ts +1 -1
- package/src/lib/floorplan/floorplan-export.test.ts +1 -1
- package/src/lib/floorplan/floorplan-export.tsx +8 -11
- package/src/lib/floorplan/floorplan-extension.ts +4 -157
- package/src/lib/floorplan/floorplan-mode.ts +2 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
- package/src/lib/floorplan/geometry.ts +2 -112
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +2 -1
- package/src/lib/floorplan/schedules.ts +6 -3
- package/src/lib/floorplan/sheet-export.ts +7 -3
- package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
- package/src/lib/follow-pascal-presentation.ts +1 -1
- package/src/lib/glb-export.ts +1 -1
- package/src/lib/material-paint.ts +1 -1
- package/src/lib/measurements.test.ts +2 -1
- package/src/lib/mezzanine-selection.test.ts +1 -1
- package/src/lib/paint-region-mode.ts +1 -1
- package/src/lib/point-ask/session.ts +1 -2
- package/src/lib/roof-duplication.test.ts +100 -0
- package/src/lib/roof-duplication.ts +30 -5
- package/src/lib/room-handle-drag.ts +2 -12
- package/src/lib/room-name-catalog.test.ts +2 -1
- package/src/lib/room-name-messages.test.ts +2 -1
- package/src/lib/room-name-messages.ts +1 -118
- package/src/lib/room-name-storage.test.ts +2 -1
- package/src/lib/room-selection-commands.ts +2 -2
- package/src/lib/room-selection.test.ts +6 -6
- package/src/lib/room-transform-session.ts +1 -2
- package/src/lib/room-zone-routing.ts +2 -2
- package/src/lib/selection-routing.test.ts +0 -28
- package/src/lib/selection-routing.ts +1 -40
- package/src/lib/sfx-bus.ts +8 -32
- package/src/lib/sfx-player.test.ts +72 -1
- package/src/lib/sfx-player.ts +49 -337
- package/src/lib/viewer-selection.test.ts +38 -10
- package/src/lib/wall-pull.test.ts +1 -1
- package/src/lib/wall-pull.ts +1 -27
- package/src/store/use-editor.tsx +17 -26
- package/src/store/use-floorplan-annotation-visibility.ts +5 -5
- package/src/store/use-wall-region-handles-selection.ts +1 -2
- package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
- package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
- package/src/components/editor/first-person/build-collider-world.ts +0 -549
- package/src/components/editor/first-person/ground-holes.test.ts +0 -128
- package/src/components/editor/first-person/ground-holes.ts +0 -130
- package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
- package/src/components/editor/first-person/mechanism-targets.ts +0 -88
- package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
- package/src/components/editor/first-person/terrain-collider.ts +0 -212
- package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
- package/src/components/editor/floorplan-camera-sync.ts +0 -213
- package/src/components/editor/localized-html.tsx +0 -22
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
- package/src/components/editor-2d/svg-paths.ts +0 -124
- package/src/components/ui/action-menu/action-button.tsx +0 -89
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
- package/src/components/ui/primitives/button.tsx +0 -69
- package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
- package/src/components/ui/primitives/popover.tsx +0 -42
- package/src/components/ui/primitives/switch.tsx +0 -29
- package/src/components/ui/primitives/tooltip.tsx +0 -57
- package/src/components/ui/scene-loader.tsx +0 -73
- package/src/components/viewer/floorplan-compass-button.tsx +0 -67
- package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
- package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
- package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
- package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
- package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
- package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
- package/src/components/viewer/floorplan-preview.tsx +0 -1065
- package/src/components/viewer/render-settings-menu.tsx +0 -294
- package/src/components/viewer/sky-control.tsx +0 -710
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
- package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
- package/src/components/viewer/viewer-controls-bar.tsx +0 -496
- package/src/components/viewer/viewer-hover-label.tsx +0 -42
- package/src/components/viewer/viewer-rooms.tsx +0 -248
- package/src/components/viewer/viewer-scene-header.tsx +0 -286
- package/src/components/viewer/viewer-selection-manager.tsx +0 -124
- package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
- package/src/components/viewer/viewer-stage-modes.ts +0 -49
- package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
- package/src/components/viewer/viewer-stage.test.tsx +0 -47
- package/src/components/viewer/viewer-stage.tsx +0 -221
- package/src/components/walkthrough-hud.tsx +0 -193
- package/src/hooks/use-mobile.ts +0 -19
- package/src/i18n/level-name.ts +0 -39
- package/src/i18n/scene-theme-name.ts +0 -16
- package/src/lib/below-grade.ts +0 -133
- package/src/lib/camera-pose.test.ts +0 -237
- package/src/lib/camera-pose.ts +0 -172
- package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
- package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
- package/src/lib/floorplan/floorplan-readonly.ts +0 -95
- package/src/lib/floorplan/marks.test.ts +0 -213
- package/src/lib/floorplan/marks.ts +0 -238
- package/src/lib/measurements.ts +0 -8
- package/src/lib/node-display-label.ts +0 -37
- package/src/lib/room-assembly-overlay.test.ts +0 -275
- package/src/lib/room-assembly-overlay.ts +0 -265
- package/src/lib/room-selection.ts +0 -417
- package/src/lib/scene-bounds.test.ts +0 -327
- package/src/lib/scene-bounds.ts +0 -233
- package/src/lib/typing-target.ts +0 -9
- package/src/lib/viewer-selection.ts +0 -595
- package/src/lib/zone-content.ts +0 -133
- package/src/store/camera-pose-store.test.ts +0 -29
- package/src/store/camera-pose-store.ts +0 -27
- package/src/store/navigation-sync-pose-store.test.ts +0 -34
- package/src/store/navigation-sync-pose-store.ts +0 -27
- package/src/store/use-audio.test.ts +0 -146
- package/src/store/use-audio.tsx +0 -169
- package/src/store/use-first-person-hud.ts +0 -40
- package/src/store/use-follow-pascal.test.ts +0 -62
- package/src/store/use-follow-pascal.ts +0 -53
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import type { AnyNode } from '@pascal-app/core/schema'
|
|
3
|
-
import { computeSceneBoundsXZ } from './scene-bounds'
|
|
4
|
-
|
|
5
|
-
function makeWall(start: [number, number], end: [number, number]): AnyNode {
|
|
6
|
-
return {
|
|
7
|
-
object: 'node',
|
|
8
|
-
id: `wall_${start.join('_')}_${end.join('_')}`,
|
|
9
|
-
type: 'wall',
|
|
10
|
-
parentId: null,
|
|
11
|
-
visible: true,
|
|
12
|
-
metadata: {},
|
|
13
|
-
children: [],
|
|
14
|
-
start,
|
|
15
|
-
end,
|
|
16
|
-
frontSide: 'unknown',
|
|
17
|
-
backSide: 'unknown',
|
|
18
|
-
} as unknown as AnyNode
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function makeZone(polygon: [number, number][]): AnyNode {
|
|
22
|
-
return {
|
|
23
|
-
object: 'node',
|
|
24
|
-
id: `zone_${polygon.length}_${polygon[0]?.[0] ?? 0}`,
|
|
25
|
-
type: 'zone',
|
|
26
|
-
parentId: null,
|
|
27
|
-
visible: true,
|
|
28
|
-
metadata: {},
|
|
29
|
-
name: 'Zone',
|
|
30
|
-
polygon,
|
|
31
|
-
color: '#000000',
|
|
32
|
-
} as unknown as AnyNode
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function makeSite(points: [number, number][]): AnyNode {
|
|
36
|
-
return {
|
|
37
|
-
object: 'node',
|
|
38
|
-
id: 'site_test',
|
|
39
|
-
type: 'site',
|
|
40
|
-
parentId: null,
|
|
41
|
-
visible: true,
|
|
42
|
-
metadata: {},
|
|
43
|
-
polygon: { type: 'polygon', points },
|
|
44
|
-
children: [],
|
|
45
|
-
} as unknown as AnyNode
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
describe('computeSceneBoundsXZ', () => {
|
|
49
|
-
test('returns null when given an empty array', () => {
|
|
50
|
-
expect(computeSceneBoundsXZ([])).toBeNull()
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test('returns null when no geometry is found on any node', () => {
|
|
54
|
-
const barren = [
|
|
55
|
-
{
|
|
56
|
-
object: 'node',
|
|
57
|
-
id: 'building_1',
|
|
58
|
-
type: 'building',
|
|
59
|
-
parentId: null,
|
|
60
|
-
visible: true,
|
|
61
|
-
metadata: {},
|
|
62
|
-
children: [],
|
|
63
|
-
} as unknown as AnyNode,
|
|
64
|
-
]
|
|
65
|
-
expect(computeSceneBoundsXZ(barren)).toBeNull()
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test('computes bounds from wall endpoints', () => {
|
|
69
|
-
const nodes: AnyNode[] = [makeWall([0, 0], [4, 0]), makeWall([4, 0], [4, 3])]
|
|
70
|
-
const bounds = computeSceneBoundsXZ(nodes)
|
|
71
|
-
expect(bounds).not.toBeNull()
|
|
72
|
-
expect(bounds!.min).toEqual([0, 0])
|
|
73
|
-
expect(bounds!.max).toEqual([4, 3])
|
|
74
|
-
expect(bounds!.size).toEqual([4, 3])
|
|
75
|
-
expect(bounds!.center).toEqual([2, 1.5])
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
test('includes zone polygons', () => {
|
|
79
|
-
const nodes: AnyNode[] = [
|
|
80
|
-
makeZone([
|
|
81
|
-
[-10, -5],
|
|
82
|
-
[10, -5],
|
|
83
|
-
[10, 5],
|
|
84
|
-
[-10, 5],
|
|
85
|
-
]),
|
|
86
|
-
]
|
|
87
|
-
const bounds = computeSceneBoundsXZ(nodes)
|
|
88
|
-
expect(bounds).not.toBeNull()
|
|
89
|
-
expect(bounds!.min).toEqual([-10, -5])
|
|
90
|
-
expect(bounds!.max).toEqual([10, 5])
|
|
91
|
-
expect(bounds!.size).toEqual([20, 10])
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
test('ignores the default 30×30 site bootstrap polygon', () => {
|
|
95
|
-
const nodes: AnyNode[] = [
|
|
96
|
-
makeSite([
|
|
97
|
-
[-15, -15],
|
|
98
|
-
[15, -15],
|
|
99
|
-
[15, 15],
|
|
100
|
-
[-15, 15],
|
|
101
|
-
]),
|
|
102
|
-
makeWall([1, 1], [2, 2]),
|
|
103
|
-
]
|
|
104
|
-
const bounds = computeSceneBoundsXZ(nodes)
|
|
105
|
-
expect(bounds).not.toBeNull()
|
|
106
|
-
// Only the wall should count — the default site polygon is skipped.
|
|
107
|
-
expect(bounds!.min).toEqual([1, 1])
|
|
108
|
-
expect(bounds!.max).toEqual([2, 2])
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
test('honours a non-default site polygon', () => {
|
|
112
|
-
const nodes: AnyNode[] = [
|
|
113
|
-
makeSite([
|
|
114
|
-
[-25, -20],
|
|
115
|
-
[25, -20],
|
|
116
|
-
[25, 20],
|
|
117
|
-
[-25, 20],
|
|
118
|
-
]),
|
|
119
|
-
]
|
|
120
|
-
const bounds = computeSceneBoundsXZ(nodes)
|
|
121
|
-
expect(bounds).not.toBeNull()
|
|
122
|
-
expect(bounds!.min).toEqual([-25, -20])
|
|
123
|
-
expect(bounds!.max).toEqual([25, 20])
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
test('combines walls, zones and positions across the flat dict', () => {
|
|
127
|
-
const nodes: Record<string, AnyNode> = {
|
|
128
|
-
wallA: makeWall([-8, -3], [4, -3]),
|
|
129
|
-
wallB: makeWall([4, -3], [4, 6]),
|
|
130
|
-
zoneA: makeZone([
|
|
131
|
-
[-8, -3],
|
|
132
|
-
[4, -3],
|
|
133
|
-
[4, 6],
|
|
134
|
-
[-8, 6],
|
|
135
|
-
]),
|
|
136
|
-
item1: {
|
|
137
|
-
object: 'node',
|
|
138
|
-
id: 'item_1',
|
|
139
|
-
type: 'item',
|
|
140
|
-
parentId: null,
|
|
141
|
-
visible: true,
|
|
142
|
-
metadata: {},
|
|
143
|
-
position: [7, 0, 8],
|
|
144
|
-
rotation: [0, 0, 0],
|
|
145
|
-
scale: [1, 1, 1],
|
|
146
|
-
children: [],
|
|
147
|
-
asset: {
|
|
148
|
-
id: 'a',
|
|
149
|
-
category: 'furniture',
|
|
150
|
-
name: 'Chair',
|
|
151
|
-
thumbnail: '',
|
|
152
|
-
src: '',
|
|
153
|
-
dimensions: [1, 1, 1],
|
|
154
|
-
offset: [0, 0, 0],
|
|
155
|
-
rotation: [0, 0, 0],
|
|
156
|
-
scale: [1, 1, 1],
|
|
157
|
-
},
|
|
158
|
-
} as unknown as AnyNode,
|
|
159
|
-
}
|
|
160
|
-
const bounds = computeSceneBoundsXZ(nodes)
|
|
161
|
-
expect(bounds).not.toBeNull()
|
|
162
|
-
expect(bounds!.min).toEqual([-8, -3])
|
|
163
|
-
expect(bounds!.max).toEqual([7, 8])
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
test('handles a single degenerate point with a minimum extent', () => {
|
|
167
|
-
const nodes: AnyNode[] = [makeWall([2, 2], [2, 2])]
|
|
168
|
-
const bounds = computeSceneBoundsXZ(nodes)
|
|
169
|
-
expect(bounds).not.toBeNull()
|
|
170
|
-
expect(bounds!.size[0]).toBeGreaterThan(0)
|
|
171
|
-
expect(bounds!.size[1]).toBeGreaterThan(0)
|
|
172
|
-
expect(bounds!.center).toEqual([2, 2])
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
describe('ancestor transforms', () => {
|
|
176
|
-
function makeBuilding(
|
|
177
|
-
id: string,
|
|
178
|
-
position: [number, number, number],
|
|
179
|
-
rotationY = 0,
|
|
180
|
-
parentId: string | null = null,
|
|
181
|
-
): AnyNode {
|
|
182
|
-
return {
|
|
183
|
-
object: 'node',
|
|
184
|
-
id,
|
|
185
|
-
type: 'building',
|
|
186
|
-
parentId,
|
|
187
|
-
visible: true,
|
|
188
|
-
metadata: {},
|
|
189
|
-
children: [],
|
|
190
|
-
position,
|
|
191
|
-
rotation: [0, rotationY, 0],
|
|
192
|
-
} as unknown as AnyNode
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function makeLevel(id: string, parentId: string): AnyNode {
|
|
196
|
-
return {
|
|
197
|
-
object: 'node',
|
|
198
|
-
id,
|
|
199
|
-
type: 'level',
|
|
200
|
-
parentId,
|
|
201
|
-
visible: true,
|
|
202
|
-
metadata: {},
|
|
203
|
-
children: [],
|
|
204
|
-
level: 0,
|
|
205
|
-
} as unknown as AnyNode
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function child(node: AnyNode, parentId: string): AnyNode {
|
|
209
|
-
return { ...node, parentId } as AnyNode
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function expectClose(actual: [number, number], expected: [number, number]) {
|
|
213
|
-
expect(actual[0]).toBeCloseTo(expected[0], 9)
|
|
214
|
-
expect(actual[1]).toBeCloseTo(expected[1], 9)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
test('carries building-local walls and zones through a translated building', () => {
|
|
218
|
-
const nodes = [
|
|
219
|
-
makeBuilding('building_a', [100, 0, 100]),
|
|
220
|
-
makeLevel('level_a', 'building_a'),
|
|
221
|
-
child(makeWall([-5, -4], [5, -4]), 'level_a'),
|
|
222
|
-
child(
|
|
223
|
-
makeZone([
|
|
224
|
-
[-5, -4],
|
|
225
|
-
[5, -4],
|
|
226
|
-
[5, 4],
|
|
227
|
-
[-5, 4],
|
|
228
|
-
]),
|
|
229
|
-
'level_a',
|
|
230
|
-
),
|
|
231
|
-
]
|
|
232
|
-
const bounds = computeSceneBoundsXZ(nodes)!
|
|
233
|
-
expectClose(bounds.min, [95, 96])
|
|
234
|
-
expectClose(bounds.max, [105, 104])
|
|
235
|
-
expectClose(bounds.center, [100, 100])
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
test('turns building-local geometry by the building yaw', () => {
|
|
239
|
-
// three.js Y rotation by +90°: local (x, z) → (z, −x).
|
|
240
|
-
const nodes = [
|
|
241
|
-
makeBuilding('building_r', [10, 0, 0], Math.PI / 2),
|
|
242
|
-
makeLevel('level_r', 'building_r'),
|
|
243
|
-
child(makeWall([0, 0], [8, 0]), 'level_r'),
|
|
244
|
-
child(makeWall([8, 0], [8, 2]), 'level_r'),
|
|
245
|
-
]
|
|
246
|
-
const bounds = computeSceneBoundsXZ(nodes)!
|
|
247
|
-
expectClose(bounds.min, [10, -8])
|
|
248
|
-
expectClose(bounds.max, [12, 0])
|
|
249
|
-
expectClose(bounds.size, [2, 8])
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
test('composes nested positioned and rotated ancestors', () => {
|
|
253
|
-
const item = {
|
|
254
|
-
object: 'node',
|
|
255
|
-
id: 'item_host',
|
|
256
|
-
type: 'item',
|
|
257
|
-
parentId: 'level_n',
|
|
258
|
-
visible: true,
|
|
259
|
-
metadata: {},
|
|
260
|
-
children: [],
|
|
261
|
-
position: [2, 0, 0],
|
|
262
|
-
rotation: [0, Math.PI / 2, 0],
|
|
263
|
-
} as unknown as AnyNode
|
|
264
|
-
const hosted = {
|
|
265
|
-
object: 'node',
|
|
266
|
-
id: 'item_hosted',
|
|
267
|
-
type: 'item',
|
|
268
|
-
parentId: 'item_host',
|
|
269
|
-
visible: true,
|
|
270
|
-
metadata: {},
|
|
271
|
-
children: [],
|
|
272
|
-
position: [1, 0, 0],
|
|
273
|
-
rotation: [0, 0, 0],
|
|
274
|
-
} as unknown as AnyNode
|
|
275
|
-
// Building at (50, 50) turned 90°: the host lands at (50, 48), and the
|
|
276
|
-
// hosted item (1, 0) under a total 180° yaw lands one metre further −x.
|
|
277
|
-
const nodes = [
|
|
278
|
-
makeBuilding('building_n', [50, 0, 50], Math.PI / 2),
|
|
279
|
-
makeLevel('level_n', 'building_n'),
|
|
280
|
-
item,
|
|
281
|
-
hosted,
|
|
282
|
-
]
|
|
283
|
-
const bounds = computeSceneBoundsXZ(nodes)!
|
|
284
|
-
expectClose(bounds.min, [49, 48])
|
|
285
|
-
expectClose(bounds.max, [50, 50])
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
test('leaves geometry under an untransformed building unchanged', () => {
|
|
289
|
-
const nodes = [
|
|
290
|
-
makeBuilding('building_0', [0, 0, 0]),
|
|
291
|
-
makeLevel('level_0', 'building_0'),
|
|
292
|
-
child(makeWall([0, 0], [4, 0]), 'level_0'),
|
|
293
|
-
child(makeWall([4, 0], [4, 3]), 'level_0'),
|
|
294
|
-
]
|
|
295
|
-
const bounds = computeSceneBoundsXZ(nodes)!
|
|
296
|
-
expect(bounds.min).toEqual([0, 0])
|
|
297
|
-
expect(bounds.max).toEqual([4, 3])
|
|
298
|
-
})
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
test('a door on a wall is bounded by the wall, not its wall-local position', () => {
|
|
302
|
-
const wall = makeWall([50, 50], [60, 50])
|
|
303
|
-
const door = {
|
|
304
|
-
object: 'node',
|
|
305
|
-
id: 'door_1',
|
|
306
|
-
type: 'door',
|
|
307
|
-
parentId: wall.id,
|
|
308
|
-
visible: true,
|
|
309
|
-
metadata: {},
|
|
310
|
-
position: [2, 1, 0],
|
|
311
|
-
} as unknown as AnyNode
|
|
312
|
-
const bounds = computeSceneBoundsXZ([wall, door])
|
|
313
|
-
expect(bounds).not.toBeNull()
|
|
314
|
-
expect(bounds!.min[0]).toBe(50)
|
|
315
|
-
expect(bounds!.max[0]).toBe(60)
|
|
316
|
-
expect(bounds!.center).toEqual([55, 50])
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
test('skips non-finite coordinates', () => {
|
|
320
|
-
const nodes: AnyNode[] = [makeWall([Number.NaN, 0], [4, 2]), makeWall([0, 0], [1, 1])]
|
|
321
|
-
const bounds = computeSceneBoundsXZ(nodes)
|
|
322
|
-
expect(bounds).not.toBeNull()
|
|
323
|
-
// NaN should be ignored; the usable points are (4,2), (0,0), (1,1).
|
|
324
|
-
expect(bounds!.min).toEqual([0, 0])
|
|
325
|
-
expect(bounds!.max).toEqual([4, 2])
|
|
326
|
-
})
|
|
327
|
-
})
|
package/src/lib/scene-bounds.ts
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scene bounds in the X/Z plane.
|
|
3
|
-
*
|
|
4
|
-
* Used by the auto-frame hook to fit the camera onto a freshly loaded scene
|
|
5
|
-
* (see `../hooks/use-auto-frame`). The hook subscribes to the core scene
|
|
6
|
-
* store and, when `nodes` transitions from empty → non-empty, fires a
|
|
7
|
-
* `camera-controls:fit-scene` event on the core event bus carrying the
|
|
8
|
-
* computed bounds.
|
|
9
|
-
*
|
|
10
|
-
* This module contains no rendering code: it only walks the flat-dict node
|
|
11
|
-
* tree and derives an axis-aligned bounding box on the XZ (plan) plane.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { turnPlanPoint, yawOf } from '@pascal-app/core'
|
|
15
|
-
import type { AnyNode } from '@pascal-app/core/schema'
|
|
16
|
-
|
|
17
|
-
export type SceneBoundsXZ = {
|
|
18
|
-
/** Min [x, z] in world units (meters). */
|
|
19
|
-
min: [number, number]
|
|
20
|
-
/** Max [x, z] in world units (meters). */
|
|
21
|
-
max: [number, number]
|
|
22
|
-
/** Center [x, z] = (min + max) / 2. */
|
|
23
|
-
center: [number, number]
|
|
24
|
-
/** Size [w, d] = max - min. */
|
|
25
|
-
size: [number, number]
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// A very small guard against degenerate bounds (e.g. a single wall of zero length).
|
|
29
|
-
const MIN_BOUNDS_EXTENT = 0.0001
|
|
30
|
-
|
|
31
|
-
/** A node's plan frame in world XZ: local (x, z) → origin + turnPlanPoint(x, z, yaw). */
|
|
32
|
-
type PlanFrame = { x: number; z: number; yaw: number }
|
|
33
|
-
|
|
34
|
-
const WORLD_FRAME: PlanFrame = { x: 0, z: 0, yaw: 0 }
|
|
35
|
-
|
|
36
|
-
type BoundsAcc = { minX: number; minZ: number; maxX: number; maxZ: number; hasPoint: boolean }
|
|
37
|
-
|
|
38
|
-
function extendPoint(acc: BoundsAcc, frame: PlanFrame, localX: unknown, localZ: unknown): void {
|
|
39
|
-
if (typeof localX !== 'number' || typeof localZ !== 'number') return
|
|
40
|
-
if (!(Number.isFinite(localX) && Number.isFinite(localZ))) return
|
|
41
|
-
const [tx, tz] = frame.yaw === 0 ? [localX, localZ] : turnPlanPoint(localX, localZ, frame.yaw)
|
|
42
|
-
const x = frame.x + tx
|
|
43
|
-
const z = frame.z + tz
|
|
44
|
-
if (x < acc.minX) acc.minX = x
|
|
45
|
-
if (x > acc.maxX) acc.maxX = x
|
|
46
|
-
if (z < acc.minZ) acc.minZ = z
|
|
47
|
-
if (z > acc.maxZ) acc.maxZ = z
|
|
48
|
-
acc.hasPoint = true
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Compute the axis-aligned XZ bounds of a scene.
|
|
53
|
-
*
|
|
54
|
-
* Walks every node and extracts 2D footprint points from the fields most
|
|
55
|
-
* nodes carry:
|
|
56
|
-
* - `start`/`end` → wall and fence endpoints in level coordinates.
|
|
57
|
-
* - `polygon` → zone, slab, site-boundary polygons.
|
|
58
|
-
* - `position` → building/item/door/window position; uses [x, z] only.
|
|
59
|
-
*
|
|
60
|
-
* Each point is in its parent's frame, so it is carried to world XZ through
|
|
61
|
-
* the ancestors' `position` and `rotation` yaw (a moved or turned building).
|
|
62
|
-
*
|
|
63
|
-
* Site-node polygons are intentionally excluded when they are the default
|
|
64
|
-
* 30×30 bootstrap polygon — otherwise a brand-new empty scene would frame
|
|
65
|
-
* an empty square around the origin. We still include site polygons that
|
|
66
|
-
* look intentional (> 4 points, or any point outside the ±15 m default).
|
|
67
|
-
*
|
|
68
|
-
* Returns `null` if no usable geometry was found.
|
|
69
|
-
*/
|
|
70
|
-
export function computeSceneBoundsXZ(
|
|
71
|
-
nodes: AnyNode[] | Record<string, AnyNode>,
|
|
72
|
-
): SceneBoundsXZ | null {
|
|
73
|
-
const list: AnyNode[] = Array.isArray(nodes) ? nodes : Object.values(nodes)
|
|
74
|
-
if (list.length === 0) return null
|
|
75
|
-
|
|
76
|
-
const acc: BoundsAcc = {
|
|
77
|
-
minX: Number.POSITIVE_INFINITY,
|
|
78
|
-
minZ: Number.POSITIVE_INFINITY,
|
|
79
|
-
maxX: Number.NEGATIVE_INFINITY,
|
|
80
|
-
maxZ: Number.NEGATIVE_INFINITY,
|
|
81
|
-
hasPoint: false,
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const byId = new Map<string, AnyNode>()
|
|
85
|
-
for (const node of list) {
|
|
86
|
-
if (node && typeof node === 'object' && typeof node.id === 'string') byId.set(node.id, node)
|
|
87
|
-
}
|
|
88
|
-
const frameOf = createFrameResolver(byId)
|
|
89
|
-
|
|
90
|
-
for (const node of list) {
|
|
91
|
-
if (!node || typeof node !== 'object') continue
|
|
92
|
-
const anyNode = node as unknown as Record<string, unknown>
|
|
93
|
-
// A node's own fields live in its parent's frame (a building's walls are
|
|
94
|
-
// building-local), so every point goes through the ancestors' transforms.
|
|
95
|
-
const frame = frameOf(node.parentId ?? null)
|
|
96
|
-
|
|
97
|
-
// Wall / fence endpoints in level coordinates.
|
|
98
|
-
const start = anyNode.start as unknown
|
|
99
|
-
const end = anyNode.end as unknown
|
|
100
|
-
if (Array.isArray(start) && start.length >= 2) extendPoint(acc, frame, start[0], start[1])
|
|
101
|
-
if (Array.isArray(end) && end.length >= 2) extendPoint(acc, frame, end[0], end[1])
|
|
102
|
-
|
|
103
|
-
// Zone / slab polygons (and explicit polygon-shaped site boundaries).
|
|
104
|
-
const polygon = anyNode.polygon as unknown
|
|
105
|
-
if (Array.isArray(polygon)) {
|
|
106
|
-
// Zones/slabs expose a plain array of [x,z] tuples. Site nodes nest the
|
|
107
|
-
// points under `polygon.points` (a discriminated PropertyLineData shape).
|
|
108
|
-
for (const point of polygon) {
|
|
109
|
-
if (Array.isArray(point) && point.length >= 2) {
|
|
110
|
-
extendPoint(acc, frame, point[0], point[1])
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
} else if (
|
|
114
|
-
polygon &&
|
|
115
|
-
typeof polygon === 'object' &&
|
|
116
|
-
Array.isArray((polygon as { points?: unknown }).points)
|
|
117
|
-
) {
|
|
118
|
-
// Site nodes only: skip the default bootstrap square so a blank scene
|
|
119
|
-
// isn't auto-framed around an empty ±15 m box. Include any other site
|
|
120
|
-
// polygon (more than 4 points, or any coordinate beyond the default).
|
|
121
|
-
const points = (polygon as { points: unknown[] }).points
|
|
122
|
-
if (node.type === 'site' && isDefaultSitePolygon(points)) {
|
|
123
|
-
// Skip — default bootstrap polygon.
|
|
124
|
-
} else {
|
|
125
|
-
for (const point of points) {
|
|
126
|
-
if (Array.isArray(point) && point.length >= 2) {
|
|
127
|
-
extendPoint(acc, frame, point[0], point[1])
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Position on the XZ plane (3D position = [x, y, z]). A node hosted on a
|
|
134
|
-
// wall (door, window, wall item) has a wall-local position, and the
|
|
135
|
-
// wall's endpoints already bound it.
|
|
136
|
-
const position = anyNode.position as unknown
|
|
137
|
-
if (Array.isArray(position) && position.length >= 3 && !isWallHosted(node, byId)) {
|
|
138
|
-
extendPoint(acc, frame, position[0], position[2])
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (!acc.hasPoint) return null
|
|
143
|
-
|
|
144
|
-
// Ensure a minimum extent so a single-point scene still yields a box.
|
|
145
|
-
let minX = acc.minX
|
|
146
|
-
let minZ = acc.minZ
|
|
147
|
-
let maxX = acc.maxX
|
|
148
|
-
let maxZ = acc.maxZ
|
|
149
|
-
if (maxX - minX < MIN_BOUNDS_EXTENT) {
|
|
150
|
-
const cx = (minX + maxX) / 2
|
|
151
|
-
minX = cx - MIN_BOUNDS_EXTENT / 2
|
|
152
|
-
maxX = cx + MIN_BOUNDS_EXTENT / 2
|
|
153
|
-
}
|
|
154
|
-
if (maxZ - minZ < MIN_BOUNDS_EXTENT) {
|
|
155
|
-
const cz = (minZ + maxZ) / 2
|
|
156
|
-
minZ = cz - MIN_BOUNDS_EXTENT / 2
|
|
157
|
-
maxZ = cz + MIN_BOUNDS_EXTENT / 2
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const centerX = (minX + maxX) / 2
|
|
161
|
-
const centerZ = (minZ + maxZ) / 2
|
|
162
|
-
return {
|
|
163
|
-
min: [minX, minZ],
|
|
164
|
-
max: [maxX, maxZ],
|
|
165
|
-
center: [centerX, centerZ],
|
|
166
|
-
size: [maxX - minX, maxZ - minZ],
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* World plan frame of each node, composed down the parent chain the way the
|
|
172
|
-
* renderers nest groups: a node with a `position` (building, item) translates
|
|
173
|
-
* its children and turns them by its `rotation` yaw; nodes without one (site,
|
|
174
|
-
* level) pass their parent's frame through.
|
|
175
|
-
*/
|
|
176
|
-
function createFrameResolver(byId: Map<string, AnyNode>): (id: string | null) => PlanFrame {
|
|
177
|
-
const cache = new Map<string, PlanFrame>()
|
|
178
|
-
const resolving = new Set<string>()
|
|
179
|
-
|
|
180
|
-
const resolve = (id: string | null): PlanFrame => {
|
|
181
|
-
if (!id) return WORLD_FRAME
|
|
182
|
-
const cached = cache.get(id)
|
|
183
|
-
if (cached) return cached
|
|
184
|
-
const node = byId.get(id)
|
|
185
|
-
if (!node || resolving.has(id)) return WORLD_FRAME
|
|
186
|
-
resolving.add(id)
|
|
187
|
-
const parent = resolve(node.parentId ?? null)
|
|
188
|
-
resolving.delete(id)
|
|
189
|
-
|
|
190
|
-
const anyNode = node as unknown as Record<string, unknown>
|
|
191
|
-
const position = anyNode.position
|
|
192
|
-
let frame = parent
|
|
193
|
-
if (Array.isArray(position) && position.length >= 3) {
|
|
194
|
-
const px = Number.isFinite(position[0]) ? (position[0] as number) : 0
|
|
195
|
-
const pz = Number.isFinite(position[2]) ? (position[2] as number) : 0
|
|
196
|
-
const [tx, tz] = turnPlanPoint(px, pz, parent.yaw)
|
|
197
|
-
frame = { x: parent.x + tx, z: parent.z + tz, yaw: parent.yaw + yawOf(anyNode.rotation) }
|
|
198
|
-
}
|
|
199
|
-
cache.set(id, frame)
|
|
200
|
-
return frame
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return resolve
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function isWallHosted(node: AnyNode, byId: Map<string, AnyNode>): boolean {
|
|
207
|
-
const parent = node.parentId ? byId.get(node.parentId) : undefined
|
|
208
|
-
if (!parent) return false
|
|
209
|
-
const anyParent = parent as unknown as Record<string, unknown>
|
|
210
|
-
return Array.isArray(anyParent.start) && Array.isArray(anyParent.end)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Matches the `SiteNode` bootstrap polygon defined in
|
|
215
|
-
* `packages/core/src/schema/nodes/site.ts` (a 30×30 square at the origin).
|
|
216
|
-
* We ignore it so the default scene doesn't "auto-frame" onto an empty box.
|
|
217
|
-
*/
|
|
218
|
-
function isDefaultSitePolygon(points: unknown[]): boolean {
|
|
219
|
-
if (points.length !== 4) return false
|
|
220
|
-
const expected: [number, number][] = [
|
|
221
|
-
[-15, -15],
|
|
222
|
-
[15, -15],
|
|
223
|
-
[15, 15],
|
|
224
|
-
[-15, 15],
|
|
225
|
-
]
|
|
226
|
-
for (let i = 0; i < 4; i++) {
|
|
227
|
-
const p = points[i]
|
|
228
|
-
const e = expected[i]!
|
|
229
|
-
if (!Array.isArray(p) || p.length < 2) return false
|
|
230
|
-
if (p[0] !== e[0] || p[1] !== e[1]) return false
|
|
231
|
-
}
|
|
232
|
-
return true
|
|
233
|
-
}
|
package/src/lib/typing-target.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// Keys typed into a field (the room-name combobox, any input, an open dialog)
|
|
2
|
-
// belong to that field, never to a canvas gesture.
|
|
3
|
-
const TYPING_TARGET =
|
|
4
|
-
'input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="combobox"], [role="textbox"], [role="dialog"], [role="alertdialog"]'
|
|
5
|
-
|
|
6
|
-
export function isTypingTarget(target: EventTarget | null) {
|
|
7
|
-
const element = target as { closest?: (selector: string) => unknown; isContentEditable?: boolean }
|
|
8
|
-
return !!element && (element.isContentEditable === true || !!element.closest?.(TYPING_TARGET))
|
|
9
|
-
}
|