@things-factory/figure-ui 10.1.204 → 10.1.206
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/client/modeller/canvas.ts +91 -9
- package/client/modeller/editor-styles.ts +23 -22
- package/client/modeller/editor.ts +217 -184
- package/client/modeller/fasten-form.ts +130 -12
- package/client/modeller/figure-fasten-dialog.ts +45 -11
- package/client/modeller/figure-fasten-drawing.ts +233 -12
- package/client/modeller/figure-links.ts +2 -2
- package/client/modeller/figure-preview.ts +6 -6
- package/client/modeller/figure-section-editor.ts +6 -6
- package/client/modeller/figure-slot-layout-panel.ts +1 -1
- package/client/modeller/gizmo.ts +22 -1
- package/client/modeller/keys.ts +10 -1
- package/client/modeller/links.ts +79 -46
- package/client/modeller/palette.ts +1 -1
- package/client/modeller/product-session.ts +16 -4
- package/client/modeller/refusal-teller.ts +3 -25
- package/client/modeller/refusals.ts +19 -26
- package/client/modeller/section-drawing.ts +7 -7
- package/client/modeller/size-drag.ts +112 -0
- package/client/modeller/slot-layout-host.ts +3 -3
- package/client/modeller/stage-model.ts +6 -9
- package/client/pages/figure-modeller-page.ts +1 -4
- package/dist-client/modeller/canvas.d.ts +24 -3
- package/dist-client/modeller/canvas.js +96 -11
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +23 -22
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +35 -35
- package/dist-client/modeller/editor.js +221 -193
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +46 -4
- package/dist-client/modeller/fasten-form.js +120 -11
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +18 -4
- package/dist-client/modeller/figure-fasten-dialog.js +49 -12
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.d.ts +33 -1
- package/dist-client/modeller/figure-fasten-drawing.js +235 -11
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +1 -1
- package/dist-client/modeller/figure-links.js +2 -2
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +4 -4
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +3 -3
- package/dist-client/modeller/figure-section-editor.js +4 -4
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.js +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -1
- package/dist-client/modeller/gizmo.d.ts +18 -1
- package/dist-client/modeller/gizmo.js +19 -0
- package/dist-client/modeller/gizmo.js.map +1 -1
- package/dist-client/modeller/keys.d.ts +1 -1
- package/dist-client/modeller/keys.js +6 -0
- package/dist-client/modeller/keys.js.map +1 -1
- package/dist-client/modeller/links.d.ts +30 -24
- package/dist-client/modeller/links.js +67 -42
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/palette.js +1 -1
- package/dist-client/modeller/palette.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +15 -13
- package/dist-client/modeller/product-session.js +15 -5
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusal-teller.d.ts +1 -12
- package/dist-client/modeller/refusal-teller.js +3 -22
- package/dist-client/modeller/refusal-teller.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +6 -14
- package/dist-client/modeller/refusals.js +16 -27
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +3 -4
- package/dist-client/modeller/section-drawing.js +10 -9
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/size-drag.d.ts +38 -0
- package/dist-client/modeller/size-drag.js +85 -0
- package/dist-client/modeller/size-drag.js.map +1 -0
- package/dist-client/modeller/slot-layout-host.d.ts +1 -3
- package/dist-client/modeller/slot-layout-host.js +3 -3
- package/dist-client/modeller/slot-layout-host.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +5 -7
- package/dist-client/modeller/stage-model.js +6 -6
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +0 -2
- package/dist-client/pages/figure-modeller-page.js +1 -6
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/align.test.ts +7 -11
- package/test/browser/acceptance-page.js +11 -1
- package/test/browser/editing.check.mjs +17 -13
- package/test/browser/links-page.js +2 -2
- package/test/browser/links.check.mjs +124 -69
- package/test/browser/modeller-page.js +30 -18
- package/test/browser/modeller-revision.check.mjs +39 -0
- package/test/browser/modeller.check.mjs +77 -26
- package/test/browser/parts.check.mjs +49 -0
- package/test/browser/screen-page.js +0 -2
- package/test/browser/screen.check.mjs +11 -13
- package/test/browser/section.check.mjs +29 -2
- package/test/fasten-form.test.ts +102 -1
- package/test/gizmo.test.ts +25 -16
- package/test/keys.test.ts +11 -0
- package/test/links.test.ts +46 -36
- package/test/refusals.test.ts +17 -21
- package/test/resize-with-placement.test.ts +14 -13
- package/test/revision.test.ts +3 -2
- package/test/section-editor.test.ts +9 -10
- package/test/size-drag.test.ts +114 -0
- package/test/slot-layout-host.test.ts +13 -8
- package/translations/en.json +32 -34
- package/translations/es.json +32 -34
- package/translations/ja.json +32 -34
- package/translations/ko.json +32 -34
- package/translations/ms.json +32 -34
- package/translations/zh.json +32 -34
|
@@ -2,16 +2,23 @@ import { css, html, LitElement, nothing, svg } from 'lit'
|
|
|
2
2
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
3
3
|
|
|
4
4
|
import { i18next, localize } from '@operato/i18n'
|
|
5
|
-
import
|
|
5
|
+
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
6
|
+
import { figureAroundNormalOf, figureAroundOf, figureCaughtAngleOf, figureSurfaceAnglesOf, figureSurfacePlaceOf } from '@hatiolab/figure-model'
|
|
7
|
+
import type { FigureFace, FigureSurfacePlace } from '@hatiolab/figure-model'
|
|
6
8
|
|
|
7
9
|
import { alignmentAt, fastenLabelsOf, offsetOf, overhangOf, screenOf, type FaceView, type FastenMode } from './fasten-geometry.js'
|
|
8
10
|
import type { Alignment } from './links.js'
|
|
9
11
|
|
|
10
12
|
type Axis = 'x' | 'y' | 'z'
|
|
13
|
+
const AXES: Axis[] = ['x', 'y', 'z']
|
|
14
|
+
type SidePlace = Extract<FigureSurfacePlace, { around: number }>
|
|
15
|
+
type RoundPlace = Extract<FigureSurfacePlace, { toward: unknown }>
|
|
11
16
|
|
|
12
17
|
/** A change the drawing asks of the fastening form. */
|
|
13
18
|
export type FastenChange =
|
|
14
19
|
| { targetFace: string }
|
|
20
|
+
| { onSurface: true }
|
|
21
|
+
| { surface: FigureSurfacePlace }
|
|
15
22
|
| { mode: FastenMode }
|
|
16
23
|
| { axis: Axis; how: Alignment; mm?: number }
|
|
17
24
|
| { gap: number }
|
|
@@ -49,7 +56,8 @@ const WHERE_H = 200
|
|
|
49
56
|
*/
|
|
50
57
|
@customElement('figure-fasten-drawing')
|
|
51
58
|
export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
52
|
-
|
|
59
|
+
/* The panel's own field rows (where along a cylinder's side), as the window's other rows are. */
|
|
60
|
+
static styles = [SidePanelStyles, css`
|
|
53
61
|
:host {
|
|
54
62
|
display: grid;
|
|
55
63
|
gap: 8px;
|
|
@@ -90,6 +98,29 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
90
98
|
div[modes] button:hover:not([on]):not(:disabled) {
|
|
91
99
|
background: var(--md-sys-color-surface-container);
|
|
92
100
|
}
|
|
101
|
+
/* Faces beyond the box's six and a curved surface: tags as the hidden faces are drawn beside the box -- one look for every face. */
|
|
102
|
+
div[face-tags] {
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-wrap: wrap;
|
|
105
|
+
justify-content: center;
|
|
106
|
+
gap: 6px;
|
|
107
|
+
}
|
|
108
|
+
button[face-tag] {
|
|
109
|
+
height: 20px;
|
|
110
|
+
padding: 0 10px;
|
|
111
|
+
font: inherit;
|
|
112
|
+
font-size: var(--ui-t-small, 11.5px);
|
|
113
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
114
|
+
background: transparent;
|
|
115
|
+
border: 1px dashed var(--md-sys-color-outline);
|
|
116
|
+
border-radius: 10px;
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
}
|
|
119
|
+
button[face-tag][on] {
|
|
120
|
+
color: var(--md-sys-color-on-primary-container);
|
|
121
|
+
background: var(--md-sys-color-primary-container);
|
|
122
|
+
border: 1px solid var(--md-sys-color-primary);
|
|
123
|
+
}
|
|
93
124
|
div[modes] button[on] {
|
|
94
125
|
background: var(--md-sys-color-primary-container);
|
|
95
126
|
color: var(--md-sys-color-on-primary-container);
|
|
@@ -105,8 +136,12 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
105
136
|
display: grid;
|
|
106
137
|
gap: 8px;
|
|
107
138
|
}
|
|
139
|
+
/* The place is drawn at its own size, never blown up past it: its words stay the window's size. */
|
|
108
140
|
div[where] {
|
|
109
141
|
position: relative;
|
|
142
|
+
width: 100%;
|
|
143
|
+
max-width: 360px;
|
|
144
|
+
justify-self: center;
|
|
110
145
|
}
|
|
111
146
|
div[where] input {
|
|
112
147
|
position: absolute;
|
|
@@ -119,7 +154,7 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
119
154
|
small[off] {
|
|
120
155
|
color: var(--md-sys-color-error);
|
|
121
156
|
}
|
|
122
|
-
`
|
|
157
|
+
`]
|
|
123
158
|
|
|
124
159
|
/** The target's name, or empty for the plane the figure stands by. */
|
|
125
160
|
@property() target = ''
|
|
@@ -140,6 +175,12 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
140
175
|
@property({ type: Boolean }) said = false
|
|
141
176
|
/** Which of its questions it draws: the face, or the way. */
|
|
142
177
|
@property() show: 'face' | 'how' = 'face'
|
|
178
|
+
/** The curved surface the target offers to be fastened to at a point (ADR-0140 3.9), if any. */
|
|
179
|
+
@property() surfaceForm: 'around' | 'toward' | '' = ''
|
|
180
|
+
/** Fastened to a point of that surface: which, about which axis, and where; undefined while on a face. */
|
|
181
|
+
@property({ attribute: false }) surface?: { form: 'around' | 'toward'; axis: Axis; place: FigureSurfacePlace }
|
|
182
|
+
/** The angles while one is dragged round the surface, told to the form when let go. */
|
|
183
|
+
@state() private turning?: { around?: number; azimuth?: number; elevation?: number }
|
|
143
184
|
|
|
144
185
|
/** Where the footprint is while dragging, and what it is caught by; undefined when nothing is dragged. */
|
|
145
186
|
@state() private dragged?: { at: Record<Axis, number>; caught: Partial<Record<Axis, string>> }
|
|
@@ -156,6 +197,8 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
156
197
|
|
|
157
198
|
render() {
|
|
158
199
|
if (this.show === 'how') return this.renderModes()
|
|
200
|
+
/* A part with no face (a sphere) has only its surface: its tag above, the place across the whole width. */
|
|
201
|
+
if (this.target && !this.faces.length) return html`${this.renderTarget()}<div>${this.renderWhere()}</div>`
|
|
159
202
|
return html`<div face-row>${this.target ? html`<div>${this.renderTarget()}</div>` : nothing}<div>${this.renderWhere()}</div></div>`
|
|
160
203
|
}
|
|
161
204
|
|
|
@@ -166,18 +209,18 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
166
209
|
const has = (f: string) => this.faces.includes(f)
|
|
167
210
|
/* A press is a whole change, as a drag let go is: the form applies a fastening the part already has. */
|
|
168
211
|
const pick = (f: string) => () => {
|
|
169
|
-
if (this.locked || !has(f) || f === this.face) return
|
|
212
|
+
if (this.locked || !has(f) || (!this.surface && f === this.face)) return
|
|
170
213
|
this.change({ targetFace: f })
|
|
171
214
|
this.released()
|
|
172
215
|
}
|
|
173
216
|
const seen = SEEN.map(([f, d]) => {
|
|
174
|
-
const on = this.face === f
|
|
217
|
+
const on = !this.surface && this.face === f
|
|
175
218
|
return svg`<path face=${f} d=${d} @click=${pick(f)}
|
|
176
219
|
style=${`cursor:${has(f) ? 'pointer' : 'default'};fill:${on ? 'var(--md-sys-color-primary-container)' : 'var(--md-sys-color-surface-container)'};fill-opacity:${has(f) ? 1 : 0.4};stroke:${on ? 'var(--md-sys-color-primary)' : 'var(--md-sys-color-outline)'};stroke-width:${on ? 2 : 1}`}>
|
|
177
220
|
<title>${i18next.t('figure.label.face-' + f)}</title></path>`
|
|
178
221
|
})
|
|
179
222
|
const hidden = HIDDEN.map(([f, x, y]) => {
|
|
180
|
-
const on = this.face === f
|
|
223
|
+
const on = !this.surface && this.face === f
|
|
181
224
|
return svg`<g face=${f} @click=${pick(f)} style=${`cursor:${has(f) ? 'pointer' : 'default'};opacity:${has(f) ? 1 : 0.35}`}>
|
|
182
225
|
<rect x=${x - 26} y=${y - 10} width="52" height="20" rx="10"
|
|
183
226
|
style=${`fill:${on ? 'var(--md-sys-color-primary-container)' : 'transparent'};stroke:${on ? 'var(--md-sys-color-primary)' : 'var(--md-sys-color-outline)'};stroke-dasharray:${on ? '' : '3 2'}`} />
|
|
@@ -186,14 +229,25 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
186
229
|
})
|
|
187
230
|
/* A shape's faces beyond its six outer ones -- a door frame's opening -- are tags of their own under the box. */
|
|
188
231
|
const others = this.faces.filter(f => !SEEN.some(([s]) => s === f) && !HIDDEN.some(([h]) => h === f))
|
|
189
|
-
|
|
232
|
+
/* A sphere has no face: no box of faces it has not, only its surface. */
|
|
233
|
+
return html`${this.faces.length
|
|
234
|
+
? svg`<svg viewBox="-20 0 ${PANEL_W} 126" role="img" aria-label=${this.target}>
|
|
190
235
|
${seen}
|
|
191
236
|
<text x="110" y="70" text-anchor="middle" font-size="12" pointer-events="none" style="fill:var(--md-sys-color-on-surface)">${this.target}</text>
|
|
192
237
|
${hidden}
|
|
193
|
-
</svg>`
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
238
|
+
</svg>`
|
|
239
|
+
: nothing}
|
|
240
|
+
${others.length || this.surfaceForm
|
|
241
|
+
? html`<div face-tags>
|
|
242
|
+
${others.map(f => html`<button face-tag ?on=${!this.surface && f === this.face} ?disabled=${this.locked} @click=${pick(f)}>${i18next.t('figure.label.face-' + f, { defaultValue: f })}</button>`)}
|
|
243
|
+
${this.surfaceForm
|
|
244
|
+
? html`<button face-tag surface ?on=${!!this.surface} ?disabled=${this.locked}
|
|
245
|
+
@click=${() => {
|
|
246
|
+
if (this.locked || this.surface) return
|
|
247
|
+
this.change({ onSurface: true })
|
|
248
|
+
this.released()
|
|
249
|
+
}}>${i18next.t('figure.label.surface-' + this.surfaceForm)}</button>`
|
|
250
|
+
: nothing}
|
|
197
251
|
</div>`
|
|
198
252
|
: nothing}`
|
|
199
253
|
}
|
|
@@ -266,6 +320,7 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
266
320
|
}
|
|
267
321
|
|
|
268
322
|
private renderWhere() {
|
|
323
|
+
if (this.surface) return this.renderSurfaceWhere(this.surface)
|
|
269
324
|
const view = this.view
|
|
270
325
|
if (!view) return nothing
|
|
271
326
|
const { h, v, sign } = screenOf(view.axes)
|
|
@@ -286,6 +341,8 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
286
341
|
const px = cx + sign[h] * at[h] * k - pw / 2,
|
|
287
342
|
py = cy + sign[v] * at[v] * k - ph / 2
|
|
288
343
|
const faceSized = view.face[h] !== undefined && view.face[v] !== undefined
|
|
344
|
+
/* An end disc is drawn as the disc it is: its overhang is measured on it (ADR-0140 3.9). */
|
|
345
|
+
const disc = !!view.measure.disc
|
|
289
346
|
const fx = cx - fw / 2,
|
|
290
347
|
fy = cy - fh / 2
|
|
291
348
|
const ix = Math.max(fx, px),
|
|
@@ -326,7 +383,7 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
326
383
|
@pointermove=${(ev: PointerEvent) => this.drag(ev, view, cx, cy)}
|
|
327
384
|
@pointerup=${() => this.drop()} @pointercancel=${() => this.drop()}>
|
|
328
385
|
${faceSized
|
|
329
|
-
? svg`<rect x=${fx} y=${fy} width=${fw} height=${fh} rx
|
|
386
|
+
? svg`<rect x=${fx} y=${fy} width=${fw} height=${fh} rx=${disc ? fw / 2 : 4} ry=${disc ? fh / 2 : 4} style="fill:var(--md-sys-color-primary-container);stroke:var(--md-sys-color-primary);stroke-width:2" />`
|
|
330
387
|
: svg`<rect x="1" y="1" width=${W - 2} height=${H - 2} rx="8" style="fill:var(--md-sys-color-primary-container);stroke:var(--md-sys-color-primary);stroke-width:2" />`}
|
|
331
388
|
<text face-name x=${labels.face.x} y=${labels.face.y} font-size="11" pointer-events="none" style="fill:var(--md-sys-color-primary)">${faceName}${sizeWords}</text>
|
|
332
389
|
${guide(h)}${guide(v)}
|
|
@@ -429,4 +486,168 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
429
486
|
})
|
|
430
487
|
this.released()
|
|
431
488
|
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Where on a curved surface (ADR-0140 3.9). A cylinder's side is seen from its end: its section a circle, the point
|
|
492
|
+
* on its rim at the angle `around`, dragged round it -- caught at every 45°, where the faces it had are named -- and
|
|
493
|
+
* along its length chosen below it. A sphere is seen from above and from the side: its point dragged round the
|
|
494
|
+
* first for the direction across its axis, along the second for how high. Nothing is told the form until let go.
|
|
495
|
+
*/
|
|
496
|
+
private renderSurfaceWhere(surface: { form: 'around' | 'toward'; axis: Axis; place: FigureSurfacePlace }) {
|
|
497
|
+
const R = 56
|
|
498
|
+
const axis = surface.axis
|
|
499
|
+
const across = (['x', 'y', 'z'] as Axis[]).filter(a => a !== axis) as [Axis, Axis]
|
|
500
|
+
const { h, v, sign } = screenOf(across)
|
|
501
|
+
const deg = (r: number) => (r * 180) / Math.PI
|
|
502
|
+
const rad = (d: number) => (d * Math.PI) / 180
|
|
503
|
+
/* A direction across the axis on the circle drawn at (cx, cy): its point there, and the way back from a pointer. */
|
|
504
|
+
const onCircle = (n: number[], cx: number, cy: number, r = R) => ({ x: cx + sign[h] * n[AXES.indexOf(h)]! * r, y: cy + sign[v] * n[AXES.indexOf(v)]! * r })
|
|
505
|
+
const angleAt = (p: { x: number; y: number }, cx: number, cy: number) => {
|
|
506
|
+
const d = [0, 0, 0]
|
|
507
|
+
d[AXES.indexOf(h)] = sign[h] * (p.x - cx)
|
|
508
|
+
d[AXES.indexOf(v)] = sign[v] * (p.y - cy)
|
|
509
|
+
return figureAroundOf(axis, d)
|
|
510
|
+
}
|
|
511
|
+
const caught = (a: number) => figureCaughtAngleOf(a) % 360
|
|
512
|
+
/* The faces the four quarters were named by, round the circle: where a part fastened to one of them now stands. */
|
|
513
|
+
const FACE_AT: Record<Axis, [string, string]> = { x: ['left', 'right'], y: ['bottom', 'top'], z: ['back', 'front'] }
|
|
514
|
+
const ticks = (cx: number, cy: number) =>
|
|
515
|
+
[0, 90, 180, 270].map(t => {
|
|
516
|
+
const n = figureAroundNormalOf(axis, t)
|
|
517
|
+
const k = n.findIndex(c => Math.abs(c) > 0.5)
|
|
518
|
+
const word = i18next.t('figure.label.face-' + FACE_AT[AXES[k]!][n[k]! > 0 ? 1 : 0])
|
|
519
|
+
const p = onCircle(n, cx, cy, R + 16)
|
|
520
|
+
return svg`<text x=${p.x} y=${p.y + 4} text-anchor="middle" font-size="10" pointer-events="none" style="fill:var(--md-sys-color-on-surface-variant)">${word}</text>`
|
|
521
|
+
})
|
|
522
|
+
const marker = (p: { x: number; y: number }, cx: number, cy: number, label: string) => svg`
|
|
523
|
+
<line x1=${cx} y1=${cy} x2=${p.x} y2=${p.y} style="stroke:var(--md-sys-color-primary);stroke-dasharray:3 2" />
|
|
524
|
+
<circle marker cx=${p.x} cy=${p.y} r="8" style=${`fill:var(--md-sys-color-tertiary-container);stroke:var(--md-sys-color-on-surface);cursor:${this.locked ? 'default' : 'grab'}`} />
|
|
525
|
+
<text x=${cx} y=${Math.hypot(p.x - cx, p.y - cy) < 16 ? cy - 14 : cy + 4} text-anchor="middle" font-size="11" pointer-events="none" style="fill:var(--md-sys-color-primary)">${label}</text>`
|
|
526
|
+
const drag = (on: (p: { x: number; y: number }) => void) => ({
|
|
527
|
+
down: (ev: PointerEvent) => {
|
|
528
|
+
if (this.locked) return
|
|
529
|
+
;(ev.currentTarget as Element).setPointerCapture(ev.pointerId)
|
|
530
|
+
on(this.pointIn(ev))
|
|
531
|
+
},
|
|
532
|
+
move: (ev: PointerEvent) => {
|
|
533
|
+
if (!this.turning || !(ev.currentTarget as Element).hasPointerCapture?.(ev.pointerId)) return
|
|
534
|
+
on(this.pointIn(ev))
|
|
535
|
+
},
|
|
536
|
+
up: () => this.letGo(surface)
|
|
537
|
+
})
|
|
538
|
+
if (surface.form === 'around') {
|
|
539
|
+
const place = surface.place as SidePlace
|
|
540
|
+
const around = this.turning?.around ?? place.around
|
|
541
|
+
/* The section in the middle under its name, the ticks clear of both. */
|
|
542
|
+
const cx = WHERE_W / 2,
|
|
543
|
+
cy = 110
|
|
544
|
+
const d = drag(p => (this.turning = { around: caught(angleAt(p, cx, cy)) }))
|
|
545
|
+
return html`<div where surface tabindex="0" @keydown=${(ev: KeyboardEvent) => this.turnKeys(ev, surface)}>
|
|
546
|
+
${svg`<svg viewBox="0 0 ${WHERE_W} ${WHERE_H}" role="img" aria-label=${i18next.t('figure.label.surface-seen-end', { part: this.target })}
|
|
547
|
+
@pointerdown=${d.down} @pointermove=${d.move} @pointerup=${d.up} @pointercancel=${d.up}>
|
|
548
|
+
<text x=${WHERE_W / 2} y="14" text-anchor="middle" font-size="11" pointer-events="none" style="fill:var(--md-sys-color-primary)">${i18next.t('figure.label.surface-seen-end', { part: this.target })}</text>
|
|
549
|
+
<circle cx=${cx} cy=${cy} r=${R} style="fill:var(--md-sys-color-primary-container);stroke:var(--md-sys-color-primary);stroke-width:2" />
|
|
550
|
+
${ticks(cx, cy)}
|
|
551
|
+
${marker(onCircle(figureAroundNormalOf(axis, around), cx, cy), cx, cy, `${Math.round(around)}°`)}
|
|
552
|
+
</svg>`}
|
|
553
|
+
</div>
|
|
554
|
+
${this.renderAlong(surface.axis, place.along)}`
|
|
555
|
+
}
|
|
556
|
+
/* A sphere: its direction across its axis (the azimuth, by the same rule as a cylinder's around) and how high. */
|
|
557
|
+
const now = figureSurfaceAnglesOf(surface, surface.place) as { azimuth: number; elevation: number }
|
|
558
|
+
const azimuth = this.turning?.azimuth ?? now.azimuth
|
|
559
|
+
const elevation = this.turning?.elevation ?? now.elevation
|
|
560
|
+
const ax = 110,
|
|
561
|
+
ay = 110,
|
|
562
|
+
sx = 250,
|
|
563
|
+
sy = 110
|
|
564
|
+
const aboveAt = onCircle(figureAroundNormalOf(axis, azimuth), ax, ay, R * Math.cos(rad(elevation)) || 0)
|
|
565
|
+
const sideAt = { x: sx + R * Math.cos(rad(elevation)), y: sy - R * Math.sin(rad(elevation)) }
|
|
566
|
+
const d = drag(p => {
|
|
567
|
+
const above = Math.hypot(p.x - ax, p.y - ay) <= R + 16 && p.x < (ax + sx) / 2
|
|
568
|
+
this.turning = above
|
|
569
|
+
? { azimuth: caught(angleAt(p, ax, ay)), elevation }
|
|
570
|
+
: { azimuth, elevation: Math.max(-90, Math.min(90, figureCaughtAngleOf(deg(Math.atan2(sy - p.y, Math.max(0, p.x - sx)))))) }
|
|
571
|
+
})
|
|
572
|
+
return html`<div where surface tabindex="0" @keydown=${(ev: KeyboardEvent) => this.turnKeys(ev, surface)}>
|
|
573
|
+
${svg`<svg viewBox="0 0 ${WHERE_W} ${WHERE_H}" role="img" aria-label=${i18next.t('figure.label.surface-toward')}
|
|
574
|
+
@pointerdown=${d.down} @pointermove=${d.move} @pointerup=${d.up} @pointercancel=${d.up}>
|
|
575
|
+
<text x=${ax} y="14" text-anchor="middle" font-size="11" pointer-events="none" style="fill:var(--md-sys-color-primary)">${i18next.t('figure.label.surface-seen-above')}</text>
|
|
576
|
+
<circle cx=${ax} cy=${ay} r=${R} style="fill:var(--md-sys-color-primary-container);stroke:var(--md-sys-color-primary);stroke-width:2" />
|
|
577
|
+
${ticks(ax, ay)}
|
|
578
|
+
${marker(aboveAt, ax, ay, `${Math.round(azimuth)}°`)}
|
|
579
|
+
<text x=${sx + R / 2} y="14" text-anchor="middle" font-size="11" pointer-events="none" style="fill:var(--md-sys-color-primary)">${i18next.t('figure.label.surface-seen-side')}</text>
|
|
580
|
+
<path d=${`M ${sx} ${sy - R} A ${R} ${R} 0 0 1 ${sx} ${sy + R}`} style="fill:var(--md-sys-color-primary-container);stroke:var(--md-sys-color-primary);stroke-width:2" />
|
|
581
|
+
<line x1=${sx} y1=${sy - R} x2=${sx} y2=${sy + R} style="stroke:var(--md-sys-color-primary)" />
|
|
582
|
+
${marker(sideAt, sx, sy, `${Math.round(elevation)}°`)}
|
|
583
|
+
</svg>`}
|
|
584
|
+
</div>`
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Where along a cylinder's side: centred, level with an end, or a distance from the middle -- its alignment words. */
|
|
588
|
+
private renderAlong(axis: Axis, along: unknown) {
|
|
589
|
+
const how = along === 'min' || along === 'max' ? along : along && typeof along === 'object' ? 'mm' : 'centre'
|
|
590
|
+
const mm = how === 'mm' ? Number((along as { mm: number }).mm) || 0 : 0
|
|
591
|
+
const place = this.surface!.place as SidePlace
|
|
592
|
+
const tell = (next: unknown) => {
|
|
593
|
+
this.change({ surface: { around: place.around, along: next as never } })
|
|
594
|
+
this.released()
|
|
595
|
+
}
|
|
596
|
+
return html`<div form along>
|
|
597
|
+
<span>${i18next.t('figure.label.align-' + axis)}</span>
|
|
598
|
+
<select ?disabled=${this.locked} @change=${(e: Event) => {
|
|
599
|
+
const v = (e.target as HTMLSelectElement).value
|
|
600
|
+
tell(v === 'mm' ? { mm } : v)
|
|
601
|
+
}}>
|
|
602
|
+
<option value="centre" ?selected=${how === 'centre'}>${i18next.t('figure.label.align-centre')}</option>
|
|
603
|
+
<option value="min" ?selected=${how === 'min'}>${i18next.t(`figure.label.align-${axis}-min`)}</option>
|
|
604
|
+
<option value="max" ?selected=${how === 'max'}>${i18next.t(`figure.label.align-${axis}-max`)}</option>
|
|
605
|
+
<option value="mm" ?selected=${how === 'mm'}>${i18next.t('figure.label.align-mm')}</option>
|
|
606
|
+
</select>
|
|
607
|
+
${how === 'mm'
|
|
608
|
+
? html`<span></span><input type="number" step="any" aria-label=${i18next.t('figure.label.align-mm')} .value=${String(mm)}
|
|
609
|
+
?disabled=${this.locked} @change=${(e: Event) => tell({ mm: Number((e.target as HTMLInputElement).value) || 0 })} />`
|
|
610
|
+
: nothing}
|
|
611
|
+
</div>`
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** A pointer's place in the drawing's own units. */
|
|
615
|
+
private pointIn(ev: PointerEvent) {
|
|
616
|
+
const svgEl = (ev.currentTarget as Element).closest('svg') as SVGSVGElement
|
|
617
|
+
const pt = svgEl.createSVGPoint()
|
|
618
|
+
pt.x = ev.clientX
|
|
619
|
+
pt.y = ev.clientY
|
|
620
|
+
return pt.matrixTransform(svgEl.getScreenCTM()!.inverse())
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Let go: the angles dragged become the place, told to the form once. */
|
|
624
|
+
private letGo(surface: { form: 'around' | 'toward'; axis: Axis; place: FigureSurfacePlace }) {
|
|
625
|
+
const t = this.turning
|
|
626
|
+
if (!t) return
|
|
627
|
+
this.turning = undefined
|
|
628
|
+
this.change({ surface: this.placeOf(surface, t) })
|
|
629
|
+
this.released()
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** The place the angles come to: the kernel's (`figureSurfacePlaceOf`), along a side kept. */
|
|
633
|
+
private placeOf(surface: { form: 'around' | 'toward'; axis: Axis; place: FigureSurfacePlace }, t: { around?: number; azimuth?: number; elevation?: number }): FigureSurfacePlace {
|
|
634
|
+
const now = figureSurfaceAnglesOf(surface, surface.place) as { around?: number; azimuth?: number; elevation?: number }
|
|
635
|
+
const angles = surface.form === 'around' ? { around: t.around ?? now.around! } : { azimuth: t.azimuth ?? now.azimuth!, elevation: t.elevation ?? now.elevation! }
|
|
636
|
+
return figureSurfacePlaceOf(surface, surface.place, angles)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Arrows turn it a degree, fifteen with Shift: round the side, or round and up and down a sphere. */
|
|
640
|
+
private turnKeys(ev: KeyboardEvent, surface: { form: 'around' | 'toward'; axis: Axis; place: FigureSurfacePlace }) {
|
|
641
|
+
const step = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, 1], ArrowDown: [0, -1] }[ev.key]
|
|
642
|
+
if (!step || this.locked) return
|
|
643
|
+
ev.preventDefault()
|
|
644
|
+
ev.stopPropagation()
|
|
645
|
+
const by = ev.shiftKey ? 15 : 1
|
|
646
|
+
const now = figureSurfaceAnglesOf(surface, surface.place) as { around?: number; azimuth?: number; elevation?: number }
|
|
647
|
+
if (surface.form === 'around') {
|
|
648
|
+
if (!step[0]) return
|
|
649
|
+
this.change({ surface: this.placeOf(surface, { around: now.around! + step[0] * by }) })
|
|
650
|
+
} else this.change({ surface: this.placeOf(surface, { azimuth: now.azimuth! + step[0] * by, elevation: Math.max(-90, Math.min(90, now.elevation! + step[1] * by)) }) })
|
|
651
|
+
this.released()
|
|
652
|
+
}
|
|
432
653
|
}
|
|
@@ -278,7 +278,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
278
278
|
<section size-response>
|
|
279
279
|
<h3>${i18next.t('figure.label.group-size')}</h3>
|
|
280
280
|
<p group-question>${i18next.t('figure.label.resize-intent')}</p>
|
|
281
|
-
${canFollow(asset, this.partId) ? this.renderDimensions(asset) : html`<p hint>${i18next.t('figure.text.
|
|
281
|
+
${canFollow(asset, this.partId) ? this.renderDimensions(asset) : html`<p hint>${i18next.t('figure.text.part-cannot-follow')}</p>`}
|
|
282
282
|
</section>
|
|
283
283
|
${positions.length
|
|
284
284
|
? html`<section position-response>
|
|
@@ -302,7 +302,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
302
302
|
${groupMenu([{ label: i18next.t('figure.button.detach'), icon: 'link_off', disabled: this.locked || !held, run: () => this.command({ kind: 'detach', part: this.partId }) }])}
|
|
303
303
|
</h3>${this.help.hint('figure.text.fastening-explained')}
|
|
304
304
|
<slot name="place"></slot>
|
|
305
|
-
${canFasten(asset, this.partId) ? this.renderFastening(asset) : html`<p hint>${i18next.t('figure.text.
|
|
305
|
+
${canFasten(asset, this.partId) ? this.renderFastening(asset) : html`<p hint>${i18next.t('figure.text.part-cannot-fasten')}</p>`}
|
|
306
306
|
</section>
|
|
307
307
|
`
|
|
308
308
|
this.candidate = this.fastenCandidate
|
|
@@ -3,14 +3,14 @@ import { customElement, property, query, state } from 'lit/decorators.js'
|
|
|
3
3
|
|
|
4
4
|
import { i18next, localize } from '@operato/i18n'
|
|
5
5
|
import { compileFigureAsset } from '@hatiolab/figure-model'
|
|
6
|
-
import type { FigureAsset, FigureDataRow, FigureDataValue } from '@hatiolab/figure-model'
|
|
6
|
+
import type { FigureAsset, FigureDataRow, FigureDataValue, FigureGraphStateInput } from '@hatiolab/figure-model'
|
|
7
7
|
import { create, registerFigure, unregisterFigure } from '@hatiolab/things-scene'
|
|
8
8
|
|
|
9
9
|
import { DEFAULT_VIEW, previewBoardDepth, sceneLook, WORKBENCH, type ViewSettings } from '@things-factory/figure-viewer/dist-client/figure-view.js'
|
|
10
10
|
import { dialShape } from './angle-dial.js'
|
|
11
11
|
import { domainOf, motionWrittenByData, typedValueOf, valueRowsOf } from './data-interface.js'
|
|
12
12
|
import { derivedOf, type FigureRevision } from './revision.js'
|
|
13
|
-
import { authoredSizeOf, occupancyAtSize, PREVIEW_FACTOR, PREVIEW_WAYS, previewClampsOf, previewInstances, previewOneInstance, previewTypeOf, setupPartOf,
|
|
13
|
+
import { authoredSizeOf, occupancyAtSize, PREVIEW_FACTOR, PREVIEW_WAYS, previewClampsOf, previewInstances, previewOneInstance, previewTypeOf, setupPartOf, sizeSpanOf, sizeWithinRange } from '@things-factory/figure-viewer/dist-client/preview-model.js'
|
|
14
14
|
import { previewSizeWordsOf } from './preview-size.js'
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -335,7 +335,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
335
335
|
if (!asset.occupancy) return html`<div sizes><p said>${i18next.t('figure.text.preview-needs-space')}</p></div>`
|
|
336
336
|
return html`<div sizes>
|
|
337
337
|
${(['x', 'y', 'z'] as const).map(axis => {
|
|
338
|
-
const range =
|
|
338
|
+
const range = sizeSpanOf(asset, axis)
|
|
339
339
|
if (!range) return nothing
|
|
340
340
|
return html`<label>
|
|
341
341
|
<span>${i18next.t(`figure.label.measure-${axis}`)}</span>
|
|
@@ -377,7 +377,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
377
377
|
this.scene?.root?.fitCameraToModel?.()
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
-
private renderDrivers(asset: FigureAsset, controls:
|
|
380
|
+
private renderDrivers(asset: FigureAsset, controls: FigureGraphStateInput[], clips: string[]) {
|
|
381
381
|
const changed = controls.filter(c => this.giving[c.id] !== undefined && this.giving[c.id] !== asset.stateDefaults[c.id])
|
|
382
382
|
return html`<div clips>
|
|
383
383
|
${changed.length
|
|
@@ -609,8 +609,8 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
609
609
|
type: 'model-layer',
|
|
610
610
|
width: board.width,
|
|
611
611
|
height: board.height,
|
|
612
|
-
/* As tall as the figure may grow: twice for the eight, the
|
|
613
|
-
depth: previewBoardDepth({ placement: asset.placement, base: { y: size.y } }, view.ceilingHeight, this.way === 'one' ? Math.max(1, (
|
|
612
|
+
/* As tall as the figure may grow: twice for the eight, the top of the height slider for the one. */
|
|
613
|
+
depth: previewBoardDepth({ placement: asset.placement, base: { y: size.y } }, view.ceilingHeight, this.way === 'one' ? Math.max(1, (sizeSpanOf(asset, 'y')?.max ?? size.y) / size.y) : PREVIEW_FACTOR),
|
|
614
614
|
threed: true,
|
|
615
615
|
sky: 'color',
|
|
616
616
|
...sceneLook(view),
|
|
@@ -267,8 +267,8 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
267
267
|
@property({ attribute: false }) body?: { id: string; shape: FigureEvaluatedShape }
|
|
268
268
|
/** Which of its measures can be set (figure-model's `figureSectionMeasuresOf`). */
|
|
269
269
|
@property({ attribute: false }) measures?: { measures: Record<Axis, FigureSectionMeasure>; round: boolean }
|
|
270
|
-
/**
|
|
271
|
-
@property({ attribute: false }) kept?:
|
|
270
|
+
/** What a length typed keeps: the part's middle, or its fastening (ADR-0140 4.1); undefined when it keeps no point. */
|
|
271
|
+
@property({ attribute: false }) kept?: 'centre' | 'relation' | 'none'
|
|
272
272
|
/** Why a push's end cannot be drawn to a point now, in the kernel's words (what is fastened to it); empty when it can. */
|
|
273
273
|
@property({ attribute: false }) pointBlocked = ''
|
|
274
274
|
/** The figure's revision now; the editor remembers the one it opened at. */
|
|
@@ -490,10 +490,10 @@ export class FigureSectionEditor extends localize(i18next)(LitElement) {
|
|
|
490
490
|
return { h: Math.round(p.x), v: Math.round(p.y) }
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
-
/** What a measure's field says under it: following another value, or for the length
|
|
494
|
-
private noteWords(axis: Axis, kept?:
|
|
495
|
-
const note = measureNoteOf(this.measures?.measures[axis], kept
|
|
496
|
-
return note ? i18next.t(note.key
|
|
493
|
+
/** What a measure's field says under it: following another value, or for the length what it keeps. */
|
|
494
|
+
private noteWords(axis: Axis, kept?: 'centre' | 'relation' | 'none'): string {
|
|
495
|
+
const note = measureNoteOf(this.measures?.measures[axis], kept)
|
|
496
|
+
return note ? i18next.t(note.key) : ''
|
|
497
497
|
}
|
|
498
498
|
|
|
499
499
|
render() {
|
|
@@ -169,7 +169,7 @@ export class FigureSlotLayoutPanel extends localize(i18next)(LitElement) {
|
|
|
169
169
|
const anchorFaces = anchorFacesOf(form.anchors, geometry)
|
|
170
170
|
const value = {
|
|
171
171
|
canUseDefault: false,
|
|
172
|
-
presets: slotPresetsOf(form.anchors
|
|
172
|
+
presets: slotPresetsOf(form.anchors),
|
|
173
173
|
/* The first anchor's face is the size a preset is laid out over; the slot height is the author's. */
|
|
174
174
|
presetSize: anchorFaces[form.anchors[0]?.id ?? ''] ?? {},
|
|
175
175
|
anchors: form.anchors.map(a => ({ id: a.id, label: `${a.id} · ${a.part} ${i18next.t('figure.label.face-' + a.face)}` })),
|
package/client/modeller/gizmo.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { figureAttachmentsOf, figureShapeProviderOf } from '@hatiolab/figure-model'
|
|
2
|
-
import type { FigureAsset, FigureInputChange } from '@hatiolab/figure-model'
|
|
2
|
+
import type { FigureAsset, FigureInputChange, FigureSizeKeep } from '@hatiolab/figure-model'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* What the gizmo may write for a part: move · turn · size, on a asset.
|
|
@@ -111,10 +111,31 @@ export function gizmoReachOf(asset: AssetLike, part: string): GizmoReach {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/** Where a drag left the part, in the asset frame: millimetres, degrees and per-axis scale. */
|
|
114
|
+
/**
|
|
115
|
+
* The size handles that change one measure (ADR-0140 4.1): a round part's radius across is one value, so its X and Z
|
|
116
|
+
* handles are the same diameter, and a sphere's three are one. Said on the canvas while the size tool is on, to see how
|
|
117
|
+
* it handles before deciding to show fewer. Nothing for a part whose handles each change their own measure.
|
|
118
|
+
*/
|
|
119
|
+
export function sameMeasureAxesOf(reach: GizmoReach): Array<'x' | 'y' | 'z'> | undefined {
|
|
120
|
+
if (!reach.scale || !reach.round) return undefined
|
|
121
|
+
return reach.round.along.length ? ['x', 'z'] : ['x', 'y', 'z']
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
export interface GizmoResult {
|
|
115
125
|
position?: [number, number, number]
|
|
116
126
|
rotation?: [number, number, number]
|
|
117
127
|
scale?: [number, number, number]
|
|
128
|
+
/** The handle a size drag held, per own axis -1 · 0 · 1 (the scene's `scaleGrab`): what figure-model keeps from. */
|
|
129
|
+
grab?: { x: number; y: number; z: number }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* What a size drag keeps (ADR-0140 4.1): the side opposite a handle on one axis; the middle for the uniform handle, or
|
|
134
|
+
* when no handle is known -- figure-model's own words for it, so the drag and its commit ask the same.
|
|
135
|
+
*/
|
|
136
|
+
export function sizeKeepOf(grab: GizmoResult['grab']): FigureSizeKeep {
|
|
137
|
+
const held = (['x', 'y', 'z'] as const).filter(axis => grab?.[axis])
|
|
138
|
+
return held.length === 1 ? { keep: 'opposite', axis: held[0]!, side: grab![held[0]!]! > 0 ? 1 : -1 } : { keep: 'centre' }
|
|
118
139
|
}
|
|
119
140
|
|
|
120
141
|
/**
|
package/client/modeller/keys.ts
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
* Typing is never taken: a key pressed in a field belongs to the field -- Backspace in a name box deletes a letter,
|
|
6
6
|
* not the part.
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
/*
|
|
9
|
+
* `copy` · `paste` are the modeller's own: a part copied and pasted is the kernel's duplicate, with a name of its own.
|
|
10
|
+
* Left to the scene, Ctrl+C / Ctrl+V copied the canvas object with the part's name -- two objects of one name, the copy
|
|
11
|
+
* moving the original (user 2026-10-21). `hold` is a key kept from the scene that the modeller has no use for: the
|
|
12
|
+
* scene's cut and bulk paste act on canvas objects alone and leave the figure out of step with them.
|
|
13
|
+
*/
|
|
14
|
+
export type KeyAction = 'undo' | 'redo' | 'duplicate' | 'remove' | 'deselect' | 'copy' | 'paste' | 'hold' | GizmoKey
|
|
9
15
|
|
|
10
16
|
/** The gizmo's modes on W, E and R, as the scene has them. */
|
|
11
17
|
export type GizmoKey = 'translate' | 'rotate' | 'scale'
|
|
@@ -36,6 +42,9 @@ export function keyActionOf(e: KeyLike, selected: string): KeyAction | null {
|
|
|
36
42
|
const key = e.key.toLowerCase()
|
|
37
43
|
if (mod && key === 'z') return e.shiftKey ? 'redo' : 'undo'
|
|
38
44
|
if (mod && key === 'y') return 'redo'
|
|
45
|
+
if (mod && key === 'c') return selected ? 'copy' : 'hold'
|
|
46
|
+
if (mod && key === 'v') return e.shiftKey ? 'hold' : 'paste'
|
|
47
|
+
if (mod && key === 'x') return 'hold'
|
|
39
48
|
if (!selected) return null
|
|
40
49
|
if (mod && key === 'd') return 'duplicate'
|
|
41
50
|
if (!mod && (key === 'delete' || key === 'backspace')) return 'remove'
|