@pascal-app/editor 1.0.7 → 1.1.0
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 +7 -7
- package/src/components/editor/custom-camera-controls.tsx +90 -34
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/first-person-controls.tsx +5 -7
- package/src/components/editor/fit-scene-framing.test.ts +318 -0
- package/src/components/editor/fit-scene-framing.ts +142 -0
- package/src/components/editor/floating-action-menu.tsx +4 -2
- package/src/components/editor/floor-openings-overlay.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +173 -8
- package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
- package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/group-transform-shared.ts +3 -1
- package/src/components/editor/index.tsx +20 -0
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +1 -1
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -2
- package/src/components/editor/open-wall-ends-hint.tsx +1 -1
- package/src/components/editor/quick-measurement-card.tsx +1 -1
- package/src/components/editor/room-controls.tsx +3 -2
- package/src/components/editor/room-handles-2d.tsx +2 -1
- package/src/components/editor/room-handles.tsx +3 -3
- package/src/components/editor/room-labels.tsx +4 -2
- package/src/components/editor/site-edge-labels.tsx +6 -2
- package/src/components/editor/wall-measurement-label.tsx +30 -14
- package/src/components/editor/wall-move-side-handles.tsx +10 -6
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +53 -32
- package/src/components/editor-2d/floorplan-group-move.tsx +1 -1
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +12 -12
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +22 -4
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +22 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +14 -2
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +30 -19
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +150 -32
- package/src/components/tools/item/move-tool.tsx +1 -6
- package/src/components/tools/item/use-placement-coordinator.tsx +1 -1
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
- package/src/components/tools/shared/placement-box.tsx +1 -5
- package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
- package/src/components/tools/shared/segment-angle.ts +0 -4
- package/src/components/tools/tool-manager.tsx +1 -24
- package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/ui/controls/slider-control.tsx +18 -6
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/helpers/helper-manager.test.tsx +53 -0
- package/src/components/ui/helpers/helper-manager.tsx +54 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +1 -1
- package/src/components/ui/panels/floor-opening-panel.tsx +1 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +28 -24
- package/src/components/ui/panels/multi-height-mode.tsx +3 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +1 -1
- package/src/components/ui/panels/panel-manager.tsx +4 -2
- package/src/components/ui/panels/panel-wrapper.tsx +41 -18
- package/src/components/ui/panels/room-construction-rows.tsx +1 -1
- package/src/components/ui/panels/room-openings-field.tsx +1 -1
- package/src/components/ui/panels/room-panel.tsx +1 -1
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +21 -10
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +1 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
- package/src/components/viewer/viewer-rooms.tsx +1 -1
- package/src/i18n/locales/en/messages.po +856 -124
- package/src/i18n/locales/en/messages.ts +1 -1
- package/src/i18n/locales/en-XA/messages.ts +1 -1
- package/src/i18n/locales/es/messages.po +2087 -257
- package/src/i18n/locales/fr/messages.po +2087 -257
- package/src/i18n/locales/ja/messages.po +2087 -257
- package/src/i18n/locales/zh-CN/messages.po +2087 -257
- package/src/index.tsx +74 -47
- package/src/lib/contextual-help.ts +6 -0
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +1 -1
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +8 -3
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/layer-thickness.ts +1 -1
- package/src/lib/level-selection.test.ts +18 -2
- package/src/lib/level-selection.ts +31 -8
- package/src/lib/linear-display.ts +1 -1
- package/src/lib/live-gesture-round-trip.test.ts +1 -1
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurements.test.ts +8 -7
- package/src/lib/measurements.ts +1 -218
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/placement-metadata.ts +4 -0
- package/src/lib/point-ask/describe-target.ts +6 -8
- package/src/lib/point-ask/region.ts +1 -1
- package/src/lib/print-shell-compiler.ts +12 -1
- package/src/lib/print-wall-solids.test.ts +11 -0
- package/src/lib/print-wall-solids.ts +9 -0
- package/src/lib/room-assembly-overlay.ts +1 -1
- package/src/lib/room-built-on.test.ts +2 -5
- package/src/lib/room-built-on.ts +1 -15
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-controls-phase6.test.ts +1 -1
- package/src/lib/room-handle-drag.ts +6 -25
- package/src/lib/room-push-dimensions.test.ts +1 -1
- package/src/lib/scene-bounds.test.ts +144 -0
- package/src/lib/scene-bounds.ts +79 -15
- package/src/lib/scene-clipboard.ts +2 -1
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/viewer-selection.ts +1 -1
- package/src/lib/wall-region-list.ts +3 -1
- package/src/lib/window-interaction.test.ts +4 -2
- package/src/store/use-editor.tsx +5 -0
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/components/tools/elevator/elevator-defaults.ts +0 -8
- package/src/components/tools/elevator/elevator-tool.tsx +0 -372
- package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/window-interaction.ts +0 -142
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
isPlacementTypingKey,
|
|
4
|
+
PLACEMENT_TYPING_DISTANCE_OFFSET,
|
|
5
|
+
PLACEMENT_TYPING_LENGTH_ANGLE,
|
|
6
|
+
routePlacementTypingKey,
|
|
7
|
+
usePlacementTyping,
|
|
8
|
+
} from './use-placement-typing'
|
|
9
|
+
|
|
10
|
+
const key = (
|
|
11
|
+
k: string,
|
|
12
|
+
mods: Partial<Pick<KeyboardEvent, 'metaKey' | 'ctrlKey' | 'altKey'>> = {},
|
|
13
|
+
) => ({
|
|
14
|
+
key: k,
|
|
15
|
+
metaKey: false,
|
|
16
|
+
ctrlKey: false,
|
|
17
|
+
altKey: false,
|
|
18
|
+
...mods,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const beginTwoFields = (activeField: 0 | 1) => {
|
|
22
|
+
usePlacementTyping.getState().begin({ labels: PLACEMENT_TYPING_DISTANCE_OFFSET, activeField })
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('usePlacementTyping', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
usePlacementTyping.getState().clear()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('keeps two independent field buffers and toggles the active field', () => {
|
|
32
|
+
const typing = usePlacementTyping.getState()
|
|
33
|
+
typing.begin({ labels: PLACEMENT_TYPING_DISTANCE_OFFSET, fieldDefaults: ['1.2', '0'] })
|
|
34
|
+
typing.append('2')
|
|
35
|
+
typing.toggleField()
|
|
36
|
+
typing.append('-')
|
|
37
|
+
typing.append('4')
|
|
38
|
+
|
|
39
|
+
expect(usePlacementTyping.getState().fields).toEqual(['2', '-4'])
|
|
40
|
+
expect(usePlacementTyping.getState().fieldDefaults).toEqual(['1.2', '0'])
|
|
41
|
+
expect(usePlacementTyping.getState().activeField).toBe(1)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('backspace only changes the active field', () => {
|
|
45
|
+
const typing = usePlacementTyping.getState()
|
|
46
|
+
typing.begin({ labels: PLACEMENT_TYPING_DISTANCE_OFFSET })
|
|
47
|
+
typing.append('25')
|
|
48
|
+
typing.toggleField()
|
|
49
|
+
typing.append('3')
|
|
50
|
+
typing.backspace()
|
|
51
|
+
|
|
52
|
+
expect(usePlacementTyping.getState().fields).toEqual(['25', ''])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('publishes projection and an enter request without clearing input', () => {
|
|
56
|
+
const typing = usePlacementTyping.getState()
|
|
57
|
+
typing.begin({ labels: PLACEMENT_TYPING_DISTANCE_OFFSET })
|
|
58
|
+
typing.append('2')
|
|
59
|
+
typing.setProjectedPosition([2, 0, 1])
|
|
60
|
+
typing.requestCommit()
|
|
61
|
+
|
|
62
|
+
expect(usePlacementTyping.getState().projectedPosition).toEqual([2, 0, 1])
|
|
63
|
+
expect(usePlacementTyping.getState().submitRevision).toBe(1)
|
|
64
|
+
expect(usePlacementTyping.getState().isActive).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('clear ends the session and forgets its labels', () => {
|
|
68
|
+
usePlacementTyping.getState().begin({ labels: PLACEMENT_TYPING_LENGTH_ANGLE })
|
|
69
|
+
usePlacementTyping.getState().append('3')
|
|
70
|
+
usePlacementTyping.getState().clear()
|
|
71
|
+
const state = usePlacementTyping.getState()
|
|
72
|
+
expect(state.isActive).toBe(false)
|
|
73
|
+
expect(state.fields).toEqual(['', ''])
|
|
74
|
+
expect(state.labels).toEqual([])
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('routePlacementTypingKey', () => {
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
usePlacementTyping.getState().clear()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('a digit or a leading dot opens a session on the first field', () => {
|
|
84
|
+
expect(routePlacementTypingKey(key('5'), { begin: beginTwoFields })).toBe('edit')
|
|
85
|
+
expect(usePlacementTyping.getState().fields).toEqual(['5', ''])
|
|
86
|
+
usePlacementTyping.getState().clear()
|
|
87
|
+
expect(routePlacementTypingKey(key('.'), { begin: beginTwoFields })).toBe('edit')
|
|
88
|
+
expect(usePlacementTyping.getState().fields).toEqual(['.', ''])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('letters, signs and separators leave tool shortcuts alone until a session opens', () => {
|
|
92
|
+
for (const k of ['c', 'm', 't', 'f', '-', ' ', ',']) {
|
|
93
|
+
expect(routePlacementTypingKey(key(k), { begin: beginTwoFields })).toBeNull()
|
|
94
|
+
}
|
|
95
|
+
expect(usePlacementTyping.getState().isActive).toBe(false)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('no session opens when the tool refuses or offers no begin', () => {
|
|
99
|
+
expect(routePlacementTypingKey(key('5'), { begin: () => false })).toBeNull()
|
|
100
|
+
expect(routePlacementTypingKey(key('5'))).toBeNull()
|
|
101
|
+
expect(usePlacementTyping.getState().isActive).toBe(false)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('Tab opens a session on the second field only when the tool asks for it', () => {
|
|
105
|
+
expect(routePlacementTypingKey(key('Tab'), { begin: beginTwoFields })).toBeNull()
|
|
106
|
+
expect(routePlacementTypingKey(key('Tab'), { begin: beginTwoFields, tabBegins: true })).toBe(
|
|
107
|
+
'edit',
|
|
108
|
+
)
|
|
109
|
+
expect(usePlacementTyping.getState().activeField).toBe(1)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('inside a session: unit letters extend, Tab and comma switch, Enter commits', () => {
|
|
113
|
+
routePlacementTypingKey(key('1'), { begin: beginTwoFields })
|
|
114
|
+
for (const k of ['8', '0', 'c', 'm']) routePlacementTypingKey(key(k))
|
|
115
|
+
expect(routePlacementTypingKey(key('Tab'))).toBe('edit')
|
|
116
|
+
routePlacementTypingKey(key('-'))
|
|
117
|
+
routePlacementTypingKey(key('2'))
|
|
118
|
+
expect(routePlacementTypingKey(key(','))).toBe('edit')
|
|
119
|
+
expect(usePlacementTyping.getState().fields).toEqual(['180cm', '-2'])
|
|
120
|
+
expect(usePlacementTyping.getState().activeField).toBe(0)
|
|
121
|
+
expect(routePlacementTypingKey(key('Enter'))).toBe('commit')
|
|
122
|
+
expect(usePlacementTyping.getState().isActive).toBe(true)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('Backspace edits, Delete empties the active field', () => {
|
|
126
|
+
routePlacementTypingKey(key('4'), { begin: beginTwoFields })
|
|
127
|
+
routePlacementTypingKey(key('2'))
|
|
128
|
+
routePlacementTypingKey(key('Backspace'))
|
|
129
|
+
expect(usePlacementTyping.getState().fields[0]).toBe('4')
|
|
130
|
+
routePlacementTypingKey(key('Delete'))
|
|
131
|
+
expect(usePlacementTyping.getState().fields[0]).toBe('')
|
|
132
|
+
expect(usePlacementTyping.getState().isActive).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('the first Escape only ends the session', () => {
|
|
136
|
+
routePlacementTypingKey(key('3'), { begin: beginTwoFields })
|
|
137
|
+
expect(routePlacementTypingKey(key('Escape'))).toBe('clear')
|
|
138
|
+
expect(usePlacementTyping.getState().isActive).toBe(false)
|
|
139
|
+
expect(routePlacementTypingKey(key('Escape'))).toBeNull()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('chords are never consumed', () => {
|
|
143
|
+
expect(
|
|
144
|
+
routePlacementTypingKey(key('5', { metaKey: true }), { begin: beginTwoFields }),
|
|
145
|
+
).toBeNull()
|
|
146
|
+
routePlacementTypingKey(key('5'), { begin: beginTwoFields })
|
|
147
|
+
expect(routePlacementTypingKey(key('z', { ctrlKey: true }))).toBeNull()
|
|
148
|
+
expect(routePlacementTypingKey(key('Enter', { altKey: true }))).toBeNull()
|
|
149
|
+
expect(usePlacementTyping.getState().fields[0]).toBe('5')
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('isPlacementTypingKey', () => {
|
|
154
|
+
test('accepts the measurement grammar', () => {
|
|
155
|
+
for (const k of ['0', '9', 'm', 'c', '.', "'", '"', '-', '+', ' ', '/']) {
|
|
156
|
+
expect(isPlacementTypingKey(k)).toBe(true)
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('rejects control keys and the field separator', () => {
|
|
161
|
+
for (const k of ['Enter', 'Escape', 'Tab', 'Shift', '', 'F1', ',']) {
|
|
162
|
+
expect(isPlacementTypingKey(k)).toBe(false)
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
})
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { MessageDescriptor } from '@pascal-app/core'
|
|
2
|
+
import { create } from 'zustand'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Typed coordinate entry shared by the placement tools: a drafted wall's
|
|
6
|
+
* length/angle and a cabinet's distance/offset along its wall. One session at
|
|
7
|
+
* a time; the active tool owns what the two fields mean and reads them on
|
|
8
|
+
* every change, the HUD (`PlacementCoordinateInput`) only displays them.
|
|
9
|
+
*/
|
|
10
|
+
export type PlacementTypingField = 0 | 1
|
|
11
|
+
export type PlacementTypingValues = [string, string]
|
|
12
|
+
export type PlacementTypingProjection = [number, number, number]
|
|
13
|
+
|
|
14
|
+
export const PLACEMENT_TYPING_DISTANCE_OFFSET: readonly MessageDescriptor[] = [
|
|
15
|
+
/* i18n */ { id: 'editor.placementTyping.distance', message: 'Distance' },
|
|
16
|
+
/* i18n */ { id: 'editor.placementTyping.offset', message: 'Offset' },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
export const PLACEMENT_TYPING_LENGTH_ANGLE: readonly MessageDescriptor[] = [
|
|
20
|
+
/* i18n */ { id: 'editor.placementTyping.length', message: 'Length' },
|
|
21
|
+
/* i18n */ { id: 'editor.placementTyping.angle', message: 'Angle' },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
export type PlacementTypingSession = {
|
|
25
|
+
/** What each field means, in field order; the HUD shows one box per label. */
|
|
26
|
+
labels: readonly MessageDescriptor[]
|
|
27
|
+
fieldDefaults?: PlacementTypingValues
|
|
28
|
+
activeField?: PlacementTypingField
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type PlacementTypingState = {
|
|
32
|
+
isActive: boolean
|
|
33
|
+
labels: readonly MessageDescriptor[]
|
|
34
|
+
fields: PlacementTypingValues
|
|
35
|
+
/** What an empty field stands for, shown as its placeholder. */
|
|
36
|
+
fieldDefaults: PlacementTypingValues
|
|
37
|
+
activeField: PlacementTypingField
|
|
38
|
+
projectedPosition: PlacementTypingProjection | null
|
|
39
|
+
submitRevision: number
|
|
40
|
+
begin: (session: PlacementTypingSession) => void
|
|
41
|
+
append: (value: string) => void
|
|
42
|
+
backspace: () => void
|
|
43
|
+
setField: (field: PlacementTypingField, value: string) => void
|
|
44
|
+
setActiveField: (field: PlacementTypingField) => void
|
|
45
|
+
toggleField: () => void
|
|
46
|
+
setProjectedPosition: (position: PlacementTypingProjection | null) => void
|
|
47
|
+
requestCommit: () => void
|
|
48
|
+
clear: () => void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const EMPTY_VALUES: PlacementTypingValues = ['', '']
|
|
52
|
+
|
|
53
|
+
export const usePlacementTyping = create<PlacementTypingState>((set) => ({
|
|
54
|
+
isActive: false,
|
|
55
|
+
labels: [],
|
|
56
|
+
fields: [...EMPTY_VALUES],
|
|
57
|
+
fieldDefaults: [...EMPTY_VALUES],
|
|
58
|
+
activeField: 0,
|
|
59
|
+
projectedPosition: null,
|
|
60
|
+
submitRevision: 0,
|
|
61
|
+
begin: ({ labels, fieldDefaults = EMPTY_VALUES, activeField = 0 }) =>
|
|
62
|
+
set({
|
|
63
|
+
isActive: true,
|
|
64
|
+
labels,
|
|
65
|
+
fields: [...EMPTY_VALUES],
|
|
66
|
+
fieldDefaults: [...fieldDefaults],
|
|
67
|
+
activeField,
|
|
68
|
+
projectedPosition: null,
|
|
69
|
+
}),
|
|
70
|
+
append: (value) =>
|
|
71
|
+
set((state) => {
|
|
72
|
+
const fields: PlacementTypingValues = [...state.fields]
|
|
73
|
+
fields[state.activeField] += value
|
|
74
|
+
return { isActive: true, fields }
|
|
75
|
+
}),
|
|
76
|
+
backspace: () =>
|
|
77
|
+
set((state) => {
|
|
78
|
+
const fields: PlacementTypingValues = [...state.fields]
|
|
79
|
+
fields[state.activeField] = fields[state.activeField].slice(0, -1)
|
|
80
|
+
return { fields }
|
|
81
|
+
}),
|
|
82
|
+
setField: (field, value) =>
|
|
83
|
+
set((state) => {
|
|
84
|
+
const fields: PlacementTypingValues = [...state.fields]
|
|
85
|
+
fields[field] = value
|
|
86
|
+
return { isActive: true, fields }
|
|
87
|
+
}),
|
|
88
|
+
setActiveField: (activeField) => set({ activeField }),
|
|
89
|
+
toggleField: () => set((state) => ({ activeField: state.activeField === 0 ? 1 : 0 })),
|
|
90
|
+
setProjectedPosition: (projectedPosition) => set({ projectedPosition }),
|
|
91
|
+
requestCommit: () =>
|
|
92
|
+
set((state: PlacementTypingState) => ({ submitRevision: state.submitRevision + 1 })),
|
|
93
|
+
clear: () =>
|
|
94
|
+
set({
|
|
95
|
+
isActive: false,
|
|
96
|
+
labels: [],
|
|
97
|
+
fields: [...EMPTY_VALUES],
|
|
98
|
+
fieldDefaults: [...EMPTY_VALUES],
|
|
99
|
+
activeField: 0,
|
|
100
|
+
projectedPosition: null,
|
|
101
|
+
}),
|
|
102
|
+
}))
|
|
103
|
+
|
|
104
|
+
/** Keys that extend a field: digits, unit letters, separators, signs, fractions. */
|
|
105
|
+
export function isPlacementTypingKey(key: string): boolean {
|
|
106
|
+
return key.length === 1 && /^[0-9a-zA-Z.'"+\-/ ]$/.test(key)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Keys that may open a session. Letters only ever continue a field, so a
|
|
111
|
+
* tool's single-letter shortcuts keep working until a number is typed.
|
|
112
|
+
*/
|
|
113
|
+
export function isPlacementTypingStartKey(key: string): boolean {
|
|
114
|
+
return key.length === 1 && /^[0-9.]$/.test(key)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type PlacementTypingKeyOutcome = 'edit' | 'commit' | 'clear'
|
|
118
|
+
|
|
119
|
+
export type PlacementTypingKeyOptions = {
|
|
120
|
+
/**
|
|
121
|
+
* Opens a session for a start key (or Tab, with `tabBegins`) when none is
|
|
122
|
+
* active. Returns false when the tool cannot take typed entry right now.
|
|
123
|
+
*/
|
|
124
|
+
begin?: (activeField: PlacementTypingField) => boolean
|
|
125
|
+
/** Tab with no session opens one on the second field. */
|
|
126
|
+
tabBegins?: boolean
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The one keyboard convention for typed entry. Tab or comma switches field,
|
|
131
|
+
* Enter asks the tool to commit, Escape clears the session (the tool's next
|
|
132
|
+
* Escape cancels its draft), Backspace edits and Delete empties the active
|
|
133
|
+
* field. Chords with Cmd/Ctrl/Alt are never consumed. Returns null when the
|
|
134
|
+
* key is not part of typed entry; the caller prevents default and stops
|
|
135
|
+
* propagation for every other outcome.
|
|
136
|
+
*/
|
|
137
|
+
export function routePlacementTypingKey(
|
|
138
|
+
event: Pick<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey' | 'altKey'>,
|
|
139
|
+
options: PlacementTypingKeyOptions = {},
|
|
140
|
+
): PlacementTypingKeyOutcome | null {
|
|
141
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return null
|
|
142
|
+
const typing = usePlacementTyping.getState()
|
|
143
|
+
const { key } = event
|
|
144
|
+
if (!typing.isActive) {
|
|
145
|
+
if (isPlacementTypingStartKey(key)) {
|
|
146
|
+
if (!options.begin?.(0)) return null
|
|
147
|
+
usePlacementTyping.getState().append(key)
|
|
148
|
+
return 'edit'
|
|
149
|
+
}
|
|
150
|
+
if (key === 'Tab' && options.tabBegins && options.begin?.(1)) return 'edit'
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
if (key === 'Tab' || key === ',') {
|
|
154
|
+
typing.toggleField()
|
|
155
|
+
return 'edit'
|
|
156
|
+
}
|
|
157
|
+
if (key === 'Enter') return 'commit'
|
|
158
|
+
if (key === 'Escape') {
|
|
159
|
+
typing.clear()
|
|
160
|
+
return 'clear'
|
|
161
|
+
}
|
|
162
|
+
if (key === 'Backspace') {
|
|
163
|
+
typing.backspace()
|
|
164
|
+
return 'edit'
|
|
165
|
+
}
|
|
166
|
+
if (key === 'Delete') {
|
|
167
|
+
typing.setField(typing.activeField, '')
|
|
168
|
+
return 'edit'
|
|
169
|
+
}
|
|
170
|
+
if (isPlacementTypingKey(key)) {
|
|
171
|
+
typing.append(key)
|
|
172
|
+
return 'edit'
|
|
173
|
+
}
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Parse options for a typed length: a bare number is read in the display unit. */
|
|
178
|
+
export function placementTypingLengthOptions(
|
|
179
|
+
unit: 'metric' | 'imperial',
|
|
180
|
+
metricNotation: 'meters' | 'millimeters',
|
|
181
|
+
) {
|
|
182
|
+
return {
|
|
183
|
+
bareUnit: unit === 'imperial' ? 'in' : metricNotation === 'millimeters' ? 'mm' : 'm',
|
|
184
|
+
system: unit === 'imperial' ? ('imperial' as const) : ('metric' as const),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_ELEVATOR_WIDTH = 1.84
|
|
2
|
-
export const DEFAULT_ELEVATOR_DEPTH = 1.84
|
|
3
|
-
export const DEFAULT_ELEVATOR_CAB_HEIGHT = 2.35
|
|
4
|
-
export const DEFAULT_ELEVATOR_DOOR_WIDTH = 0.95
|
|
5
|
-
export const DEFAULT_ELEVATOR_DOOR_HEIGHT = 2.1
|
|
6
|
-
export const DEFAULT_ELEVATOR_SPEED = 2.2
|
|
7
|
-
export const DEFAULT_ELEVATOR_DOOR_DURATION_MS = 900
|
|
8
|
-
export const DEFAULT_ELEVATOR_DWELL_MS = 1400
|