@things-factory/figure-ui 10.1.204 → 10.1.205
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 +7 -22
- package/client/modeller/editor.ts +176 -145
- package/client/modeller/fasten-form.ts +85 -6
- package/client/modeller/figure-fasten-dialog.ts +42 -9
- package/client/modeller/figure-fasten-drawing.ts +233 -12
- package/client/modeller/figure-section-editor.ts +6 -6
- package/client/modeller/figure-slot-layout-panel.ts +1 -1
- package/client/modeller/gizmo.ts +12 -1
- package/client/modeller/links.ts +48 -6
- 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 +7 -22
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +28 -32
- package/dist-client/modeller/editor.js +176 -153
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +36 -3
- package/dist-client/modeller/fasten-form.js +80 -5
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +17 -3
- package/dist-client/modeller/figure-fasten-dialog.js +47 -10
- 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-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 +12 -1
- package/dist-client/modeller/gizmo.js +8 -0
- package/dist-client/modeller/gizmo.js.map +1 -1
- package/dist-client/modeller/links.d.ts +14 -2
- package/dist-client/modeller/links.js +42 -4
- package/dist-client/modeller/links.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/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 +10 -0
- package/test/browser/editing.check.mjs +17 -13
- package/test/browser/links-page.js +2 -2
- package/test/browser/links.check.mjs +94 -56
- package/test/browser/modeller-page.js +30 -18
- package/test/browser/modeller-revision.check.mjs +39 -0
- package/test/browser/modeller.check.mjs +32 -10
- package/test/browser/screen-page.js +0 -2
- package/test/browser/screen.check.mjs +11 -13
- package/test/browser/section.check.mjs +2 -2
- package/test/fasten-form.test.ts +62 -1
- package/test/gizmo.test.ts +13 -15
- package/test/links.test.ts +8 -3
- 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 +25 -22
- package/translations/es.json +25 -22
- package/translations/ja.json +25 -22
- package/translations/ko.json +25 -22
- package/translations/ms.json +25 -22
- package/translations/zh.json +25 -22
|
@@ -2,10 +2,10 @@ import { LitElement, html, css, unsafeCSS, type PropertyValues } from 'lit'
|
|
|
2
2
|
import { customElement, property, query } from 'lit/decorators.js'
|
|
3
3
|
import { create, Model, niceGridStep } from '@hatiolab/things-scene'
|
|
4
4
|
|
|
5
|
-
import type { GizmoReach } from './gizmo.js'
|
|
5
|
+
import type { GizmoReach, GizmoResult } from './gizmo.js'
|
|
6
6
|
import type { FigureRenderPart } from './renderer.js'
|
|
7
7
|
import { DEFAULT_VIEW, sceneLook, WORKBENCH, type ViewSettings } from '@things-factory/figure-viewer/dist-client/figure-view.js'
|
|
8
|
-
import { boardFor, changedKeys, componentOf, gizmoResultOf, needsRefit, type Board
|
|
8
|
+
import { boardFor, changedKeys, componentOf, gizmoResultOf, needsRefit, type Board } from './stage-model.js'
|
|
9
9
|
import { JOINT_FAN } from './joint-fan.js'
|
|
10
10
|
import { SLOT_MARK } from './slot-layout-host.js'
|
|
11
11
|
import { PREVIEW, TARGET_FACE } from './command-preview.js'
|
|
@@ -77,11 +77,15 @@ export class FigureCanvas extends LitElement {
|
|
|
77
77
|
`
|
|
78
78
|
@property({ attribute: false }) parts: FigureRenderPart[] = []
|
|
79
79
|
@property({ attribute: false }) palette: Record<string, string> = {}
|
|
80
|
-
/** The face a size drag keeps, per part (figure-model's `figureScaleAnchorOf`), so the drag shows what is written. */
|
|
81
|
-
@property({ attribute: false }) anchors: Record<string, ScaleAnchor> = {}
|
|
82
80
|
/** Which figure is open. Another one stands the board up again. */
|
|
83
81
|
@property() context = ''
|
|
84
82
|
@property() selected = ''
|
|
83
|
+
/**
|
|
84
|
+
* The fastening window is open (ADR-0140 stage 4): a part pressed is aimed at, not chosen -- told out as `part-aimed`
|
|
85
|
+
* with where the press landed (the scene's `pickedAt`: the point and the drawn triangle's outward normal, in the
|
|
86
|
+
* figure's frame), and the part being fastened stays chosen.
|
|
87
|
+
*/
|
|
88
|
+
@property({ type: Boolean }) aiming = false
|
|
85
89
|
/** What the gizmo may write for the selected part; a change beyond it is not applied and the part springs back. */
|
|
86
90
|
@property({ attribute: false }) reach: GizmoReach = { translate: false, rotate: false, scale: false }
|
|
87
91
|
@property({ attribute: false }) view: ViewSettings = DEFAULT_VIEW
|
|
@@ -133,7 +137,7 @@ export class FigureCanvas extends LitElement {
|
|
|
133
137
|
|
|
134
138
|
protected updated(changed: PropertyValues) {
|
|
135
139
|
if (!this.hasUpdated) return
|
|
136
|
-
if (['parts', 'palette', 'context', 'fan', '
|
|
140
|
+
if (['parts', 'palette', 'context', 'fan', 'ghost', 'highlight', 'marks'].some(key => changed.has(key))) this.standUp()
|
|
137
141
|
if (changed.has('view')) this.applyView()
|
|
138
142
|
if (changed.has('selected')) this.showSelected()
|
|
139
143
|
}
|
|
@@ -235,8 +239,20 @@ export class FigureCanvas extends LitElement {
|
|
|
235
239
|
}
|
|
236
240
|
}
|
|
237
241
|
|
|
242
|
+
/**
|
|
243
|
+
* A part's component state, kept for the part object it was made from while the palette and the board are the same: a
|
|
244
|
+
* size drag redraws the board every frame, and only the parts the frame moved are new objects (ADR-0140 4.4).
|
|
245
|
+
*/
|
|
246
|
+
private made = new WeakMap<FigureRenderPart, Record<string, unknown> | null>()
|
|
247
|
+
private madeFor?: [Record<string, string>, Board]
|
|
248
|
+
private componentFor(part: FigureRenderPart): Record<string, unknown> | null {
|
|
249
|
+
if (this.madeFor?.[0] !== this.palette || this.madeFor?.[1] !== this.board) (this.made = new WeakMap()), (this.madeFor = [this.palette, this.board!])
|
|
250
|
+
if (!this.made.has(part)) this.made.set(part, componentOf(part, this.palette, this.board!, true))
|
|
251
|
+
return this.made.get(part)!
|
|
252
|
+
}
|
|
253
|
+
|
|
238
254
|
private components(): Record<string, unknown>[] {
|
|
239
|
-
const parts = this.parts.map(part =>
|
|
255
|
+
const parts = this.parts.map(part => this.componentFor(part)).filter((c): c is Record<string, unknown> => !!c)
|
|
240
256
|
/* The fan in the accent colour, translucent; `capturable: false` keeps clicks, the marquee and the gizmo off it. */
|
|
241
257
|
const fan = this.fan ? componentOf(this.fan, { ...this.palette, [JOINT_FAN]: this.palette['palette.accent'] ?? FAN_COLOUR }, this.board!) : null
|
|
242
258
|
const accent = this.palette['palette.accent'] ?? FAN_COLOUR
|
|
@@ -285,7 +301,13 @@ export class FigureCanvas extends LitElement {
|
|
|
285
301
|
const found = standing.get(name)
|
|
286
302
|
if (found) {
|
|
287
303
|
const diff = changedKeys(found.state, state)
|
|
288
|
-
|
|
304
|
+
/*
|
|
305
|
+
A part whose size is being dragged keeps the size the gizmo gives it: what is drawn does not go back into what the
|
|
306
|
+
gizmo measures (ADR-0140 4.4). Written back, a size the frame could not change (one that follows the figure) put
|
|
307
|
+
the part back to its old size, and the release wrote nothing.
|
|
308
|
+
*/
|
|
309
|
+
if (diff && this.pointerDown && this.grabs.has(name)) for (const key of ['width', 'depth', 'height']) delete diff[key]
|
|
310
|
+
if (diff && Object.keys(diff).length) found.set(diff)
|
|
289
311
|
continue
|
|
290
312
|
}
|
|
291
313
|
const made = Model.compile({ ...state }, layer.app as never) as SceneComponent | null
|
|
@@ -309,10 +331,32 @@ export class FigureCanvas extends LitElement {
|
|
|
309
331
|
element hears it even after it left the page. A canvas not on the page has nothing to say about the choice.
|
|
310
332
|
*/
|
|
311
333
|
if (!this.isConnected) return
|
|
334
|
+
/*
|
|
335
|
+
While aiming, a press is told by the click (below); the part being fastened is chosen again once the scene has
|
|
336
|
+
done with the pick -- put back while the scene was still telling it, the selection read right but the outline
|
|
337
|
+
stayed on the part pressed (:3300, 2026-10-20).
|
|
338
|
+
*/
|
|
339
|
+
if (this.aiming) {
|
|
340
|
+
setTimeout(() => this.showSelected())
|
|
341
|
+
return
|
|
342
|
+
}
|
|
312
343
|
const name = (after as SceneComponent[] | undefined)?.[0]?.state?.name
|
|
313
344
|
const id = typeof name === 'string' ? name : ''
|
|
314
345
|
if (ownerOf(id) !== this.selected) this.dispatchEvent(new CustomEvent('part-selected', { detail: id, bubbles: true, composed: true }))
|
|
315
346
|
})
|
|
347
|
+
/*
|
|
348
|
+
A press while aiming: the part pressed -- the one being fastened too, which changes no selection and so is told by
|
|
349
|
+
no `selected` -- with where it landed (`pickedAt`, filled before the click is told). It comes up to the board
|
|
350
|
+
with the component in the last argument's `origin`, as a change does.
|
|
351
|
+
*/
|
|
352
|
+
layer.on?.('click', (...args: unknown[]) => {
|
|
353
|
+
if (!this.aiming || !this.isConnected) return
|
|
354
|
+
const origin = (args[args.length - 1] as { origin?: SceneComponent & { realObject?: { pickedAt?: { point: number[]; normal: number[] } } } } | undefined)?.origin
|
|
355
|
+
const id = origin?.state?.name
|
|
356
|
+
if (typeof id !== 'string' || !id) return
|
|
357
|
+
const at = origin?.realObject?.pickedAt
|
|
358
|
+
this.dispatchEvent(new CustomEvent('part-aimed', { detail: { id, ...(at ? { point: at.point, normal: at.normal } : {}) }, bubbles: true, composed: true }))
|
|
359
|
+
})
|
|
316
360
|
/*
|
|
317
361
|
A change comes up to the board with the component in the last argument's `origin` (the scene appends
|
|
318
362
|
{ origin, name, deliverer } as it bubbles). Reading the first argument as the component does nothing.
|
|
@@ -330,6 +374,12 @@ export class FigureCanvas extends LitElement {
|
|
|
330
374
|
const name = origin?.state?.name
|
|
331
375
|
if (typeof name !== 'string' || !this.parts.some(p => p.id === name)) return
|
|
332
376
|
this.pending.set(name, origin!)
|
|
377
|
+
/* A size drag: the scene tells the handle held; figure-model draws each frame from it (ADR-0140 4.4). */
|
|
378
|
+
const grab = origin!.state?.scaleGrab as GizmoResult['grab'] | undefined
|
|
379
|
+
if (this.pointerDown && grab) {
|
|
380
|
+
this.grabs.set(name, { ...grab })
|
|
381
|
+
this.sizingSoon(name)
|
|
382
|
+
}
|
|
333
383
|
if (!this.pointerDown) this.commitSoon(80)
|
|
334
384
|
})
|
|
335
385
|
this.stage.addEventListener('pointerdown', this.onPointerDown, true)
|
|
@@ -339,10 +389,34 @@ export class FigureCanvas extends LitElement {
|
|
|
339
389
|
|
|
340
390
|
/** Components the gizmo changed since the last commit, by part name. */
|
|
341
391
|
private pending = new Map<string, SceneComponent>()
|
|
392
|
+
/** The handle each size drag held, by part name, from the scene's `scaleGrab` -- told with the release. */
|
|
393
|
+
private grabs = new Map<string, NonNullable<GizmoResult['grab']>>()
|
|
394
|
+
private sizingFrame?: number
|
|
395
|
+
/** Once an animation frame: where a size drag stands, told so the editor draws figure-model's result for it. */
|
|
396
|
+
private sizingSoon(name: string) {
|
|
397
|
+
if (this.sizingFrame !== undefined) return
|
|
398
|
+
this.sizingFrame = requestAnimationFrame(() => {
|
|
399
|
+
this.sizingFrame = undefined
|
|
400
|
+
const part = this.pressedPart(name)
|
|
401
|
+
const component = this.pending.get(name)
|
|
402
|
+
if (!part || !component || !this.board || !this.pointerDown) return
|
|
403
|
+
const result = { ...gizmoResultOf(part, component.state, this.board), grab: this.grabs.get(name) }
|
|
404
|
+
if (result.scale) this.dispatchEvent(new CustomEvent('part-sizing', { detail: { id: part.id, result }, bubbles: true, composed: true }))
|
|
405
|
+
})
|
|
406
|
+
}
|
|
342
407
|
private pointerDown = false
|
|
343
408
|
private commitTimer?: ReturnType<typeof setTimeout>
|
|
409
|
+
/**
|
|
410
|
+
* The parts as they stood when the pointer went down. A drag is measured against them: while a size drag is drawn, the
|
|
411
|
+
* parts given are its frames (ADR-0140 4.4), and measured against those it grew on itself and its release wrote nothing.
|
|
412
|
+
*/
|
|
413
|
+
private atPress?: Map<string, FigureRenderPart>
|
|
344
414
|
private onPointerDown = () => {
|
|
345
415
|
this.pointerDown = true
|
|
416
|
+
this.atPress = new Map(this.parts.map(p => [p.id, p]))
|
|
417
|
+
}
|
|
418
|
+
private pressedPart(name: string): FigureRenderPart | undefined {
|
|
419
|
+
return this.atPress?.get(name) ?? this.parts.find(p => p.id === name)
|
|
346
420
|
}
|
|
347
421
|
private onPointerUp = () => {
|
|
348
422
|
if (!this.pointerDown) return
|
|
@@ -364,13 +438,18 @@ export class FigureCanvas extends LitElement {
|
|
|
364
438
|
if (this.pointerDown || !this.board || !this.pending.size) return
|
|
365
439
|
const changed = [...this.pending]
|
|
366
440
|
this.pending.clear()
|
|
441
|
+
if (this.sizingFrame !== undefined) cancelAnimationFrame(this.sizingFrame)
|
|
442
|
+
this.sizingFrame = undefined
|
|
367
443
|
for (const [name, component] of changed) {
|
|
368
|
-
const part = this.
|
|
444
|
+
const part = this.pressedPart(name)
|
|
369
445
|
if (!part) continue
|
|
370
|
-
const
|
|
446
|
+
const grab = this.grabs.get(name)
|
|
447
|
+
const result = { ...gizmoResultOf(part, component.state, this.board), ...(grab ? { grab } : {}) }
|
|
371
448
|
if (!result.position && !result.rotation && !result.scale) continue
|
|
372
449
|
this.dispatchEvent(new CustomEvent('part-transformed', { detail: { id: part.id, result }, bubbles: true, composed: true }))
|
|
373
450
|
}
|
|
451
|
+
this.grabs.clear()
|
|
452
|
+
this.atPress = undefined
|
|
374
453
|
setTimeout(() => this.sync())
|
|
375
454
|
}
|
|
376
455
|
|
|
@@ -397,6 +476,9 @@ export class FigureCanvas extends LitElement {
|
|
|
397
476
|
private teardown() {
|
|
398
477
|
clearTimeout(this.commitTimer)
|
|
399
478
|
this.pending.clear()
|
|
479
|
+
this.grabs.clear()
|
|
480
|
+
if (this.sizingFrame !== undefined) cancelAnimationFrame(this.sizingFrame)
|
|
481
|
+
this.sizingFrame = undefined
|
|
400
482
|
this.stage?.removeEventListener('pointerdown', this.onPointerDown, true)
|
|
401
483
|
window.removeEventListener('pointerup', this.onPointerUp, true)
|
|
402
484
|
window.removeEventListener('pointercancel', this.onPointerUp, true)
|
|
@@ -87,10 +87,6 @@ export const EditorStyles = css`
|
|
|
87
87
|
padding: 0 8px;
|
|
88
88
|
color: var(--md-sys-color-on-surface-variant);
|
|
89
89
|
}
|
|
90
|
-
ox-canvas-bar .status[dirty] {
|
|
91
|
-
color: var(--md-sys-color-primary);
|
|
92
|
-
font-weight: 600;
|
|
93
|
-
}
|
|
94
90
|
/*
|
|
95
91
|
The bar's two commitments, one shape (user 2026-10-18: 「발행버튼도 저장버튼도 라운드 버튼으로 … 버튼 안에 적절한 아이콘」):
|
|
96
92
|
round, an icon inside, so neither is taken for the other. Save, done many times a day, is the filled one; release,
|
|
@@ -249,18 +245,13 @@ export const EditorStyles = css`
|
|
|
249
245
|
white-space: nowrap;
|
|
250
246
|
text-overflow: ellipsis;
|
|
251
247
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
line-height: 1.5;
|
|
260
|
-
color: var(--md-sys-color-on-primary-container);
|
|
261
|
-
background: var(--md-sys-color-primary-container);
|
|
262
|
-
border-radius: 999em;
|
|
263
|
-
pointer-events: none;
|
|
248
|
+
/* A range's two ends read as one: smallest – largest. */
|
|
249
|
+
div[pair][range] {
|
|
250
|
+
align-items: center;
|
|
251
|
+
}
|
|
252
|
+
div[pair][range] > span[dash] {
|
|
253
|
+
flex: none;
|
|
254
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
264
255
|
}
|
|
265
256
|
/* The sections inside carry the margins (side-panel-styles); the panel only scrolls. */
|
|
266
257
|
div[panel] {
|
|
@@ -332,12 +323,6 @@ export const EditorStyles = css`
|
|
|
332
323
|
min-height: 0;
|
|
333
324
|
overflow: auto;
|
|
334
325
|
}
|
|
335
|
-
/* The fastening window keeps to the panel's width: a popup's own sizing made it wider than the panel. */
|
|
336
|
-
div[fasten-panel] > figure-fasten-dialog {
|
|
337
|
-
box-sizing: border-box;
|
|
338
|
-
width: 100%;
|
|
339
|
-
min-width: 0;
|
|
340
|
-
}
|
|
341
326
|
/* Which part the panel speaks of. */
|
|
342
327
|
div[subject] {
|
|
343
328
|
position: sticky;
|