@things-factory/figure-ui 10.1.50 → 10.1.52
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/figure-v3-links.ts +94 -11
- package/client/modeller/v3-editor.ts +24 -3
- package/client/modeller/v3-links.ts +70 -11
- package/client/pages/figure-v3-modeller-page.ts +11 -1
- package/dist-client/modeller/figure-v3-capability.d.ts +1 -1
- package/dist-client/modeller/figure-v3-clips.d.ts +1 -1
- package/dist-client/modeller/figure-v3-controls.d.ts +1 -1
- package/dist-client/modeller/figure-v3-links.d.ts +8 -0
- package/dist-client/modeller/figure-v3-links.js +95 -12
- package/dist-client/modeller/figure-v3-links.js.map +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +1 -1
- package/dist-client/modeller/figure-v3-properties.d.ts +1 -1
- package/dist-client/modeller/v3-clips.d.ts +1 -1
- package/dist-client/modeller/v3-editor.js +24 -2
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-links.d.ts +41 -5
- package/dist-client/modeller/v3-links.js +61 -11
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/pages/figure-v3-modeller-page.js +11 -1
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/browser/modeller.check.mjs +49 -0
- package/test/v3-first-save.test.ts +9 -0
- package/test/v3-links.test.ts +78 -7
- package/translations/en.json +10 -1
- package/translations/ja.json +10 -1
- package/translations/ko.json +10 -1
- package/translations/ms.json +10 -1
- package/translations/zh.json +10 -1
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
alignOf,
|
|
14
14
|
AXES,
|
|
15
15
|
canFasten,
|
|
16
|
+
facesOf,
|
|
16
17
|
canFollow,
|
|
17
18
|
dimensionNow,
|
|
18
19
|
followableAxes,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
placeFacesOf,
|
|
29
30
|
placeIdFor,
|
|
30
31
|
placesOn,
|
|
32
|
+
rowAxesOf,
|
|
31
33
|
positionsWorkedOut,
|
|
32
34
|
type Alignment,
|
|
33
35
|
type Axis,
|
|
@@ -39,6 +41,21 @@ import { HelpToggles } from './v3-help.js'
|
|
|
39
41
|
|
|
40
42
|
const FACES = Object.keys(V3_FACES) as V3Face[]
|
|
41
43
|
|
|
44
|
+
const freshFasten = () => ({
|
|
45
|
+
face: 'bottom' as V3Face,
|
|
46
|
+
toPlane: false,
|
|
47
|
+
target: '',
|
|
48
|
+
targetFace: 'top' as V3Face,
|
|
49
|
+
facing: 'meet' as 'meet' | 'flush',
|
|
50
|
+
gap: 0,
|
|
51
|
+
twist: 0,
|
|
52
|
+
align: {} as Partial<Record<Axis, Alignment>>,
|
|
53
|
+
alignMm: {} as Partial<Record<Axis, number>>
|
|
54
|
+
})
|
|
55
|
+
const freshMotion = (): MotionForm => ({ kind: 'slide', axis: 'x', id: '', min: 0, max: 500, follow: '', multiplier: 1, offset: 0 })
|
|
56
|
+
const freshRow = () => ({ axis: '' as Axis | '', pitch: 200, fills: true, count: 5 })
|
|
57
|
+
const freshPlace = () => ({ role: 'grip' as 'seat' | 'grip', face: 'top' as V3Face, label: '', accepts: '', capacity: 1 })
|
|
58
|
+
|
|
42
59
|
/**
|
|
43
60
|
* What a part is tied to — its size response, what it is fastened to, how it moves (parity rows ④ ⑤ ⑥).
|
|
44
61
|
*
|
|
@@ -84,9 +101,23 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
84
101
|
/** Which ties this shows: the part's shape (size and placement, in 속성), its places (자리·능력) or its motion (움직임). */
|
|
85
102
|
@property() section: 'shape' | 'places' | 'motion' = 'shape'
|
|
86
103
|
|
|
87
|
-
@state() private fasten =
|
|
88
|
-
@state() private motion: MotionForm =
|
|
89
|
-
@state() private place =
|
|
104
|
+
@state() private fasten = freshFasten()
|
|
105
|
+
@state() private motion: MotionForm = freshMotion()
|
|
106
|
+
@state() private place = freshPlace()
|
|
107
|
+
@state() private row = freshRow()
|
|
108
|
+
|
|
109
|
+
/*
|
|
110
|
+
The forms describe the next command for the selected part, so another part starts from the defaults: carrying the
|
|
111
|
+
last part's choices over fastened a shutter curtain level with a right edge nobody chose for it (2026-09-25).
|
|
112
|
+
*/
|
|
113
|
+
willUpdate(changed: Map<string, unknown>) {
|
|
114
|
+
if (changed.has('partId') && changed.get('partId') !== undefined) {
|
|
115
|
+
this.fasten = freshFasten()
|
|
116
|
+
this.motion = freshMotion()
|
|
117
|
+
this.place = freshPlace()
|
|
118
|
+
this.row = freshRow()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
90
121
|
|
|
91
122
|
render() {
|
|
92
123
|
const asset = this.asset
|
|
@@ -107,6 +138,7 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
107
138
|
${this.renderPositions(asset)}
|
|
108
139
|
${canFasten(asset, this.partId) ? this.renderFastening(asset) : html`<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
|
|
109
140
|
</section>
|
|
141
|
+
${this.renderRow(asset)}
|
|
110
142
|
`
|
|
111
143
|
}
|
|
112
144
|
|
|
@@ -259,6 +291,13 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
259
291
|
const to = fastenedTo(asset, this.partId)
|
|
260
292
|
const f = this.fasten
|
|
261
293
|
const targets = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFasten(asset, one))
|
|
294
|
+
/* The faces are the ones each shape declares: a part is fastened by one of its outer faces, to any face of the target. */
|
|
295
|
+
const mineFaces = facesOf(asset, this.partId).filter(face => FACES.includes(face as V3Face))
|
|
296
|
+
const myFace = mineFaces.includes(f.face) ? f.face : (mineFaces[0] as V3Face)
|
|
297
|
+
const theirFaces = f.target ? facesOf(asset, f.target) : FACES
|
|
298
|
+
const targetFace = theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]!
|
|
299
|
+
const aim = f.toPlane ? ('plane' as const) : { part: f.target, face: targetFace }
|
|
300
|
+
const align = f.target || f.toPlane ? alignOf(asset, this.partId, aim, f.align, f.alignMm, !!f.twist) : undefined
|
|
262
301
|
return html`
|
|
263
302
|
${to
|
|
264
303
|
? html`<div pair>
|
|
@@ -271,7 +310,7 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
271
310
|
<div form>
|
|
272
311
|
<span>${i18next.t('figure.label.my-face')}</span>
|
|
273
312
|
<select @change=${(e: Event) => (this.fasten = { ...f, face: (e.target as HTMLSelectElement).value as V3Face })}>
|
|
274
|
-
${
|
|
313
|
+
${mineFaces.map(face => html`<option value=${face} ?selected=${face === myFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
|
|
275
314
|
</select>
|
|
276
315
|
</div>
|
|
277
316
|
<label toggle>
|
|
@@ -288,7 +327,7 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
288
327
|
</select>
|
|
289
328
|
<span>${i18next.t('figure.label.target-face')}</span>
|
|
290
329
|
<select @change=${(e: Event) => (this.fasten = { ...f, targetFace: (e.target as HTMLSelectElement).value as V3Face })}>
|
|
291
|
-
${
|
|
330
|
+
${theirFaces.map(face => html`<option value=${face} ?selected=${face === targetFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
|
|
292
331
|
</select>
|
|
293
332
|
<span>${i18next.t('figure.label.facing-meet')}</span>
|
|
294
333
|
<select @change=${(e: Event) => (this.fasten = { ...f, facing: (e.target as HTMLSelectElement).value as 'meet' | 'flush' })}>
|
|
@@ -296,10 +335,12 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
296
335
|
<option value="flush" ?selected=${f.facing === 'flush'}>${i18next.t('figure.label.facing-flush')}</option>
|
|
297
336
|
</select>
|
|
298
337
|
</div>`}
|
|
299
|
-
${this.renderAlignment()}
|
|
338
|
+
${this.renderAlignment(f.toPlane || f.target ? alignAxesOf(asset, this.partId, aim, !!f.twist) : [])}
|
|
300
339
|
<div form>
|
|
301
340
|
<span>${i18next.t('figure.label.gap')}</span>
|
|
302
341
|
<input type="number" step="any" .value=${String(f.gap)} @change=${(e: Event) => (this.fasten = { ...f, gap: Number((e.target as HTMLInputElement).value) || 0 })} />
|
|
342
|
+
<span>${i18next.t('figure.label.twist')}</span>
|
|
343
|
+
<input type="number" step="any" .value=${String(f.twist)} @change=${(e: Event) => (this.fasten = { ...f, twist: Number((e.target as HTMLInputElement).value) || 0 })} />
|
|
303
344
|
</div>
|
|
304
345
|
<button
|
|
305
346
|
act
|
|
@@ -308,11 +349,12 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
308
349
|
this.command({
|
|
309
350
|
kind: 'attach',
|
|
310
351
|
part: this.partId,
|
|
311
|
-
face:
|
|
312
|
-
to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face:
|
|
352
|
+
face: myFace,
|
|
353
|
+
to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: targetFace },
|
|
313
354
|
...(f.toPlane ? {} : { facing: f.facing }),
|
|
314
355
|
...(f.gap ? { gap: f.gap } : {}),
|
|
315
|
-
|
|
356
|
+
twist: f.twist,
|
|
357
|
+
...(align ? { align } : {}),
|
|
316
358
|
replace: !!to
|
|
317
359
|
} as V3AuthoringAction)}
|
|
318
360
|
>
|
|
@@ -325,12 +367,12 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
325
367
|
* Where on the target face the part sits, on the two axes across it: centred, level with the target's edge on
|
|
326
368
|
* either side, or a distance off centre. A door hung level with the frame's front edge turns about that edge.
|
|
327
369
|
*/
|
|
328
|
-
private renderAlignment() {
|
|
370
|
+
private renderAlignment(axes: { axis: Axis; edges: boolean }[]) {
|
|
329
371
|
const f = this.fasten
|
|
330
372
|
const set = (axis: Axis, how: Alignment) => (this.fasten = { ...f, align: { ...f.align, [axis]: how } })
|
|
331
373
|
const setMm = (axis: Axis, mm: number) => (this.fasten = { ...f, alignMm: { ...f.alignMm, [axis]: mm } })
|
|
332
374
|
return html`<div form>
|
|
333
|
-
${
|
|
375
|
+
${axes.map(({ axis, edges }) => {
|
|
334
376
|
const how = f.align[axis] === 'min' || f.align[axis] === 'max' ? (edges ? f.align[axis]! : 'centre') : (f.align[axis] ?? 'centre')
|
|
335
377
|
return html`<span>${i18next.t('figure.label.align-' + axis)}</span>
|
|
336
378
|
<select @change=${(e: Event) => set(axis, (e.target as HTMLSelectElement).value as Alignment)}>
|
|
@@ -348,6 +390,47 @@ export class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
348
390
|
</div>`
|
|
349
391
|
}
|
|
350
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Making the part a row across what it is fastened to (figure-model `make-repeat`): along one of the axes across its
|
|
395
|
+
* face, copies a pitch apart filling the parent's length, or a count spread over it. Once made, the row's own section
|
|
396
|
+
* (the editor's) shows it and takes it away.
|
|
397
|
+
*/
|
|
398
|
+
private renderRow(asset: V3Asset) {
|
|
399
|
+
const axes = rowAxesOf(asset, this.partId)
|
|
400
|
+
if (!axes.length) return nothing
|
|
401
|
+
const r = this.row
|
|
402
|
+
const axis = axes.includes(r.axis as Axis) ? (r.axis as Axis) : axes[0]!
|
|
403
|
+
const ok = r.pitch > 0 && (r.fills || (Number.isSafeInteger(r.count) && r.count >= 1))
|
|
404
|
+
return html`<section>
|
|
405
|
+
<h3>${i18next.t('figure.label.repeat')}</h3>
|
|
406
|
+
<div form>
|
|
407
|
+
<span>${i18next.t('figure.label.repeat-axis')}</span>
|
|
408
|
+
<select @change=${(e: Event) => (this.row = { ...r, axis: (e.target as HTMLSelectElement).value as Axis })}>
|
|
409
|
+
${axes.map(a => html`<option value=${a} ?selected=${a === axis}>${a.toUpperCase()}</option>`)}
|
|
410
|
+
</select>
|
|
411
|
+
<span>${i18next.t('figure.label.repeat-pitch')} (mm)</span>
|
|
412
|
+
<input type="number" step="any" min="0" .value=${String(r.pitch)} @change=${(e: Event) => (this.row = { ...r, pitch: Number((e.target as HTMLInputElement).value) })} />
|
|
413
|
+
</div>
|
|
414
|
+
<label toggle>
|
|
415
|
+
<input type="checkbox" .checked=${r.fills} @change=${(e: Event) => (this.row = { ...r, fills: (e.target as HTMLInputElement).checked })} />
|
|
416
|
+
${i18next.t('figure.label.repeat-fills')}
|
|
417
|
+
</label>
|
|
418
|
+
${r.fills
|
|
419
|
+
? nothing
|
|
420
|
+
: html`<div form>
|
|
421
|
+
<span>${i18next.t('figure.label.repeat-count')}</span>
|
|
422
|
+
<input type="number" step="1" min="1" .value=${String(r.count)} @change=${(e: Event) => (this.row = { ...r, count: Number((e.target as HTMLInputElement).value) })} />
|
|
423
|
+
</div>`}
|
|
424
|
+
<button
|
|
425
|
+
act
|
|
426
|
+
?disabled=${this.locked || !ok}
|
|
427
|
+
@click=${() => this.command({ kind: 'make-repeat', part: this.partId, axis, pitch: r.pitch, ...(r.fills ? {} : { count: r.count }) } as V3AuthoringAction)}
|
|
428
|
+
>
|
|
429
|
+
<md-icon>view_week</md-icon>${i18next.t('figure.button.make-repeat')}
|
|
430
|
+
</button>
|
|
431
|
+
</section>`
|
|
432
|
+
}
|
|
433
|
+
|
|
351
434
|
private renderMotion(asset: V3Asset) {
|
|
352
435
|
const now = motionOf(asset, this.partId)
|
|
353
436
|
const joint = asset.joints?.find(j => j.body1.part === this.partId)
|
|
@@ -31,7 +31,7 @@ import { nudgeOf } from './v3-stage-model.js'
|
|
|
31
31
|
import { sizeRangeRefusalOf } from './v3-refusals.js'
|
|
32
32
|
import type { V3RenderPart } from './v3-renderer.js'
|
|
33
33
|
import type { V3BudgetLine } from './v3-maturity.js'
|
|
34
|
-
import { instanceRangeOf, rangeDiffers, turnsAboutAJoint } from './v3-links.js'
|
|
34
|
+
import { instanceRangeOf, instanceRangeToDeclare, rangeDiffers, turnsAboutAJoint } from './v3-links.js'
|
|
35
35
|
import { jointFanOf } from './v3-joint-fan.js'
|
|
36
36
|
import { jointDragOf } from './v3-joint-drag.js'
|
|
37
37
|
import type { FirstSave } from './v3-first-save.js'
|
|
@@ -721,7 +721,7 @@ export class FigureV3Editor extends LitElement {
|
|
|
721
721
|
label.addEventListener('pointercancel', up)
|
|
722
722
|
}
|
|
723
723
|
/** The measured range, kept for the revision it was measured at: measuring evaluates the figure at its edges. */
|
|
724
|
-
private range?: { at: string; of: ReturnType<typeof instanceRangeOf> }
|
|
724
|
+
private range?: { at: string; of: ReturnType<typeof instanceRangeOf>; toDeclare?: ReturnType<typeof instanceRangeToDeclare> }
|
|
725
725
|
/** The sizes a board may give the figure, and the way to measure them again after its parts changed. */
|
|
726
726
|
private renderInstanceRange(asset: V3Asset | undefined, unavailable: boolean) {
|
|
727
727
|
if (!asset) return nothing
|
|
@@ -729,14 +729,30 @@ export class FigureV3Editor extends LitElement {
|
|
|
729
729
|
const at = `${this.session?.read().context ?? ''}:${this.revision}`
|
|
730
730
|
if (this.range?.at !== at) {
|
|
731
731
|
let of: ReturnType<typeof instanceRangeOf>
|
|
732
|
+
let toDeclare: ReturnType<typeof instanceRangeToDeclare>
|
|
732
733
|
try {
|
|
733
734
|
of = instanceRangeOf(asset)
|
|
734
735
|
} catch {
|
|
735
736
|
of = undefined
|
|
736
737
|
}
|
|
737
|
-
|
|
738
|
+
try {
|
|
739
|
+
toDeclare = of ? undefined : instanceRangeToDeclare(asset)
|
|
740
|
+
} catch {
|
|
741
|
+
toDeclare = undefined
|
|
742
|
+
}
|
|
743
|
+
this.range = { at, of, toDeclare }
|
|
738
744
|
}
|
|
739
745
|
const range = this.range.of
|
|
746
|
+
/* No size yet: the figure is declared its size from its parts, so a board can resize it and parts can follow it. */
|
|
747
|
+
const toDeclare = this.range.toDeclare
|
|
748
|
+
if (!range && toDeclare)
|
|
749
|
+
return html`<section>
|
|
750
|
+
<h3>${i18next.t('figure.label.instance-range')}</h3>
|
|
751
|
+
<p hint>${i18next.t('figure.text.instance-size-undeclared')}</p>
|
|
752
|
+
<button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size', range: toDeclare.range, at: toDeclare.at })}>
|
|
753
|
+
<md-icon>straighten</md-icon>${i18next.t('figure.button.declare-instance-size')}
|
|
754
|
+
</button>
|
|
755
|
+
</section>`
|
|
740
756
|
if (!range) return nothing
|
|
741
757
|
const mm = (v: number) => Math.round(v).toLocaleString()
|
|
742
758
|
const differs = rangeDiffers(range.declared, range.measured)
|
|
@@ -855,6 +871,11 @@ export class FigureV3Editor extends LitElement {
|
|
|
855
871
|
})
|
|
856
872
|
: i18next.t(fits ? 'figure.text.repeat-fits-length' : 'figure.text.repeat-fixed-count')}
|
|
857
873
|
</p>
|
|
874
|
+
${asset.joints?.some(j => j.body1.part === this.selected && j.repeat)
|
|
875
|
+
? html`<button act ?disabled=${unavailable} @click=${() => this.run(() => this.session!.command({ kind: 'remove-repeat', part: this.selected }))}>
|
|
876
|
+
<md-icon>close</md-icon>${i18next.t('figure.button.remove-repeat')}
|
|
877
|
+
</button>`
|
|
878
|
+
: nothing}
|
|
858
879
|
</section>`
|
|
859
880
|
}
|
|
860
881
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { V3_FACES, compileV3Graph, defaultInstanceSizeRange, foldInstanceSizeRange, v3AttachmentsOf, v3BindingsOf, v3InstanceSizeLimitsOf } from '@hatiolab/figure-model'
|
|
1
|
+
import { V3_FACES, v3ShapeFaces, proposeV3Occupancy, compileV3Graph, defaultInstanceSizeRange, foldInstanceSizeRange, v3AttachmentsOf, v3BindingsOf, v3InstanceSizeLimitsOf } from '@hatiolab/figure-model'
|
|
2
2
|
import type { V3SizeRange } from '@hatiolab/figure-model'
|
|
3
3
|
import type { V3Asset, V3BindingPlace, V3Face, V3Joint } from '@hatiolab/figure-model'
|
|
4
4
|
|
|
@@ -115,29 +115,73 @@ export function dimensionNow(asset: V3Asset, part: string, axis: Axis): number |
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
/**
|
|
119
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The flat faces a part's shape declares, by name -- figure-model's shape definitions are the one place they are
|
|
120
|
+
* written (a portal's opening sides as well as its outer faces). A sphere has none.
|
|
121
|
+
*/
|
|
122
|
+
export function facesOf(asset: V3Asset, part: string): string[] {
|
|
123
|
+
const shape = modelOf(asset).nodes.find(n => n.id === `${part}.shape`)
|
|
124
|
+
return Object.keys((shape && v3ShapeFaces(shape.op)) ?? {})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The axis a face of a part faces along, from the same definitions. */
|
|
128
|
+
export function faceAxisOf(asset: V3Asset, part: string, face: string): Axis | undefined {
|
|
120
129
|
const shape = modelOf(asset).nodes.find(n => n.id === `${part}.shape`)
|
|
121
|
-
return
|
|
130
|
+
return (shape && v3ShapeFaces(shape.op)?.[face]?.axis) as Axis | undefined
|
|
122
131
|
}
|
|
123
132
|
|
|
133
|
+
/** Whether `attach` can take this part: any part whose shape has a flat face. */
|
|
134
|
+
export function canFasten(asset: V3Asset, part: string): boolean {
|
|
135
|
+
return facesOf(asset, part).length > 0
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const OUTER: Record<Axis, [string, string]> = { x: ['left', 'right'], y: ['bottom', 'top'], z: ['back', 'front'] }
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The axes a part can be made a row along (r3 §8, figure-model `make-repeat`): across the face it is fastened by, when
|
|
142
|
+
* it is fastened fixed to another part and holds nothing. None for a part that is loose, moves, stands on the floor or
|
|
143
|
+
* already repeats -- the command would refuse those, so they are not offered.
|
|
144
|
+
*/
|
|
145
|
+
export function rowAxesOf(asset: V3Asset, part: string): Axis[] {
|
|
146
|
+
const joints = asset.joints ?? []
|
|
147
|
+
const j = joints.find(o => o.body1.part === part)
|
|
148
|
+
if (!j || j.type !== 'fixed' || j.repeat || !('part' in j.body0)) return []
|
|
149
|
+
if (joints.some(o => 'part' in o.body0 && o.body0.part === part)) return []
|
|
150
|
+
return AXES.filter(axis => axis !== V3_FACES[j.body1.face])
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The row a part is made into, as its fastening's record keeps it. */
|
|
154
|
+
export const rowOf = (asset: V3Asset, part: string) => asset.joints?.find(o => o.body1.part === part)?.repeat
|
|
155
|
+
|
|
124
156
|
/** Where a part sits across the face it is fastened to, on one axis: centred, level with an edge, or a distance off centre. */
|
|
125
157
|
export type Alignment = 'centre' | 'min' | 'max' | 'mm'
|
|
126
158
|
|
|
127
159
|
/**
|
|
128
|
-
* The axes across the face a part is fastened to — the two a fastening does not set by itself.
|
|
129
|
-
*
|
|
160
|
+
* The axes across the face a part is fastened to — the two a fastening does not set by itself. Edges to line up with
|
|
161
|
+
* are offered where both parts have their two outer faces on that axis; the plane the figure is mounted on has none,
|
|
162
|
+
* so on it (X and Z) only centred or a distance is offered.
|
|
130
163
|
*/
|
|
131
|
-
export function alignAxesOf(target:
|
|
164
|
+
export function alignAxesOf(asset: V3Asset, part: string, target: { part: string; face: string } | 'plane', twisted = false): { axis: Axis; edges: boolean }[] {
|
|
132
165
|
if (target === 'plane') return (['x', 'z'] as Axis[]).map(axis => ({ axis, edges: false }))
|
|
133
|
-
const along =
|
|
134
|
-
|
|
166
|
+
const along = faceAxisOf(asset, target.part, target.face)
|
|
167
|
+
if (!along) return []
|
|
168
|
+
const theirs = new Set(facesOf(asset, target.part)),
|
|
169
|
+
mine = new Set(facesOf(asset, part))
|
|
170
|
+
/* A part twisted about the face's normal has no side level with the target's edges. */
|
|
171
|
+
return AXES.filter(axis => axis !== along).map(axis => ({ axis, edges: !twisted && OUTER[axis].every(f => theirs.has(f) && mine.has(f)) }))
|
|
135
172
|
}
|
|
136
173
|
|
|
137
174
|
/** The `align` of an attach command from the form's choices: only the axes across that face, and centred ones left out. */
|
|
138
|
-
export function alignOf(
|
|
175
|
+
export function alignOf(
|
|
176
|
+
asset: V3Asset,
|
|
177
|
+
part: string,
|
|
178
|
+
target: { part: string; face: string } | 'plane',
|
|
179
|
+
how: Partial<Record<Axis, Alignment>>,
|
|
180
|
+
mm: Partial<Record<Axis, number>>,
|
|
181
|
+
twisted = false
|
|
182
|
+
) {
|
|
139
183
|
const align: Partial<Record<Axis, 'min' | 'max' | { mm: number }>> = {}
|
|
140
|
-
for (const { axis, edges } of alignAxesOf(target)) {
|
|
184
|
+
for (const { axis, edges } of alignAxesOf(asset, part, target, twisted)) {
|
|
141
185
|
const one = how[axis] ?? 'centre'
|
|
142
186
|
if (one === 'mm' && mm[axis]) align[axis] = { mm: mm[axis]! }
|
|
143
187
|
else if ((one === 'min' || one === 'max') && edges) align[axis] = one
|
|
@@ -314,6 +358,21 @@ export function instanceRangeOf(asset: V3Asset): { declared: V3SizeRange; measur
|
|
|
314
358
|
return { declared, measured }
|
|
315
359
|
}
|
|
316
360
|
|
|
361
|
+
/**
|
|
362
|
+
* The size range to declare for a figure that has none yet: its parts' extent now as the size it is authored at, and
|
|
363
|
+
* as wide as its parts allow around it (the same measure 「다시 재기」 uses). Undefined when the figure already has a
|
|
364
|
+
* size or has nothing to measure. Without a declared size a board cannot resize the figure, and no part is offered
|
|
365
|
+
* to follow its size -- every figure made in the modeller was left there (user, 2026-09-25).
|
|
366
|
+
*/
|
|
367
|
+
export function instanceRangeToDeclare(asset: V3Asset): { range: V3SizeRange; at: Record<Axis, number> } | undefined {
|
|
368
|
+
if (instanceRangeOf(asset) || !asset.appearance.length) return undefined
|
|
369
|
+
const proposal = proposeV3Occupancy(asset, { over: 'rest' })
|
|
370
|
+
const at = Object.fromEntries(AXES.map(axis => [axis, Math.round((proposal.at[axis].max - proposal.at[axis].min) * 1e6) / 1e6])) as Record<Axis, number>
|
|
371
|
+
if (AXES.some(axis => !(at[axis] > 0))) return undefined
|
|
372
|
+
const start = defaultInstanceSizeRange(at)
|
|
373
|
+
return { range: foldInstanceSizeRange(start, v3InstanceSizeLimitsOf(asset, start)).range, at }
|
|
374
|
+
}
|
|
375
|
+
|
|
317
376
|
/** Whether measuring again would change the declared range (to a micrometre). */
|
|
318
377
|
export function rangeDiffers(a: V3SizeRange, b: V3SizeRange): boolean {
|
|
319
378
|
return AXES.some(axis => Math.abs(a[axis].min - b[axis].min) > 1e-3 || Math.abs(a[axis].max - b[axis].max) > 1e-3)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { html, css } from 'lit'
|
|
2
2
|
import { customElement, state } from 'lit/decorators.js'
|
|
3
|
-
import { PageView } from '@operato/shell'
|
|
3
|
+
import { navigate, PageView } from '@operato/shell'
|
|
4
|
+
import { modellerPathOf } from '../modeller/modeller-route.js'
|
|
4
5
|
import { createV3ProductSession } from '../modeller/v3-product-session.js'
|
|
5
6
|
import { v3ProductStore } from '../graphql/v3-store.js'
|
|
6
7
|
import { i18next } from '@operato/i18n'
|
|
@@ -144,6 +145,15 @@ export class FigureV3ModellerPage extends PageView {
|
|
|
144
145
|
this.updateContext()
|
|
145
146
|
}
|
|
146
147
|
if (!view.saving) this.announce()
|
|
148
|
+
/*
|
|
149
|
+
A new figure's first save gives it an id: the address becomes the figure's own, in place of the history entry.
|
|
150
|
+
Left at the bare modeller address, opening the modeller again from the menu was taken for the same page and kept
|
|
151
|
+
the saved figure -- its type showing where a new figure's was expected (2026-09-25).
|
|
152
|
+
*/
|
|
153
|
+
if (view.id && !this.routeId && !view.saving) {
|
|
154
|
+
this.routeId = view.id
|
|
155
|
+
navigate(modellerPathOf({ id: view.id }), true)
|
|
156
|
+
}
|
|
147
157
|
/* 저장이 끝난 순간 행을 다시 읽는다 — 발행된 도형을 고쳐 저장하면 서버가 초안으로 내린다. */
|
|
148
158
|
if (this.wasDirty && !view.dirty && !view.saving && view.id) this.readRow(view.id)
|
|
149
159
|
this.wasDirty = !!view.dirty
|
|
@@ -15,7 +15,7 @@ export declare class FigureV3Capability extends FigureV3Capability_base {
|
|
|
15
15
|
/** The selected part, whose places are marked. */
|
|
16
16
|
selected: string;
|
|
17
17
|
locked: boolean;
|
|
18
|
-
render(): import("lit-html").TemplateResult<1
|
|
18
|
+
render(): typeof nothing | import("lit-html").TemplateResult<1>;
|
|
19
19
|
private setRung;
|
|
20
20
|
private pick;
|
|
21
21
|
}
|
|
@@ -18,7 +18,7 @@ export declare class FigureV3Clips extends FigureV3Clips_base {
|
|
|
18
18
|
/** Where each clip is scrubbed to, in seconds. */
|
|
19
19
|
private at;
|
|
20
20
|
private adding;
|
|
21
|
-
render(): import("lit-html").TemplateResult<1
|
|
21
|
+
render(): typeof nothing | import("lit-html").TemplateResult<1>;
|
|
22
22
|
private renderClip;
|
|
23
23
|
private renderTrack;
|
|
24
24
|
private renderAdd;
|
|
@@ -22,7 +22,7 @@ export declare class FigureV3Controls extends FigureV3Controls_base {
|
|
|
22
22
|
private open;
|
|
23
23
|
/** Values in hand while a slider is dragged: the dial and the number follow them before the release commits. */
|
|
24
24
|
private live;
|
|
25
|
-
render(): import("lit-html").TemplateResult<1
|
|
25
|
+
render(): typeof nothing | import("lit-html").TemplateResult<1>;
|
|
26
26
|
private renderControl;
|
|
27
27
|
private renderDial;
|
|
28
28
|
private renderSettings;
|
|
@@ -30,6 +30,8 @@ export declare class FigureV3Links extends FigureV3Links_base {
|
|
|
30
30
|
private fasten;
|
|
31
31
|
private motion;
|
|
32
32
|
private place;
|
|
33
|
+
private row;
|
|
34
|
+
willUpdate(changed: Map<string, unknown>): void;
|
|
33
35
|
render(): import("lit-html").TemplateResult<1> | typeof nothing;
|
|
34
36
|
/** Where things are held on this part: a seat things rest on, or a grip things are held in (parity row ⑤). */
|
|
35
37
|
private renderPlaces;
|
|
@@ -48,6 +50,12 @@ export declare class FigureV3Links extends FigureV3Links_base {
|
|
|
48
50
|
* either side, or a distance off centre. A door hung level with the frame's front edge turns about that edge.
|
|
49
51
|
*/
|
|
50
52
|
private renderAlignment;
|
|
53
|
+
/**
|
|
54
|
+
* Making the part a row across what it is fastened to (figure-model `make-repeat`): along one of the axes across its
|
|
55
|
+
* face, copies a pitch apart filling the parent's length, or a count spread over it. Once made, the row's own section
|
|
56
|
+
* (the editor's) shows it and takes it away.
|
|
57
|
+
*/
|
|
58
|
+
private renderRow;
|
|
51
59
|
private renderMotion;
|
|
52
60
|
private setKind;
|
|
53
61
|
private command;
|
|
@@ -4,10 +4,24 @@ import { css, html, LitElement, nothing } from 'lit';
|
|
|
4
4
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
5
5
|
import { i18next, localize } from '@operato/i18n';
|
|
6
6
|
import { V3_FACES } from '@hatiolab/figure-model';
|
|
7
|
-
import { acceptsOf, alignAxesOf, alignOf, AXES, canFasten, canFollow, dimensionNow, followableAxes, positionIsShare, choiceOf, fastenedTo, followOf, hasInstanceSize, holdsThings, leadersFor, motionOf, motionJoint, placeFacesOf, placeIdFor, placesOn, positionsWorkedOut } from './v3-links.js';
|
|
7
|
+
import { acceptsOf, alignAxesOf, alignOf, AXES, canFasten, facesOf, canFollow, dimensionNow, followableAxes, positionIsShare, choiceOf, fastenedTo, followOf, hasInstanceSize, holdsThings, leadersFor, motionOf, motionJoint, placeFacesOf, placeIdFor, placesOn, rowAxesOf, positionsWorkedOut } from './v3-links.js';
|
|
8
8
|
import { SidePanelStyles } from './side-panel-styles.js';
|
|
9
9
|
import { HelpToggles } from './v3-help.js';
|
|
10
10
|
const FACES = Object.keys(V3_FACES);
|
|
11
|
+
const freshFasten = () => ({
|
|
12
|
+
face: 'bottom',
|
|
13
|
+
toPlane: false,
|
|
14
|
+
target: '',
|
|
15
|
+
targetFace: 'top',
|
|
16
|
+
facing: 'meet',
|
|
17
|
+
gap: 0,
|
|
18
|
+
twist: 0,
|
|
19
|
+
align: {},
|
|
20
|
+
alignMm: {}
|
|
21
|
+
});
|
|
22
|
+
const freshMotion = () => ({ kind: 'slide', axis: 'x', id: '', min: 0, max: 500, follow: '', multiplier: 1, offset: 0 });
|
|
23
|
+
const freshRow = () => ({ axis: '', pitch: 200, fills: true, count: 5 });
|
|
24
|
+
const freshPlace = () => ({ role: 'grip', face: 'top', label: '', accepts: '', capacity: 1 });
|
|
11
25
|
/**
|
|
12
26
|
* What a part is tied to — its size response, what it is fastened to, how it moves (parity rows ④ ⑤ ⑥).
|
|
13
27
|
*
|
|
@@ -30,9 +44,10 @@ let FigureV3Links = class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
30
44
|
this.locked = false;
|
|
31
45
|
/** Which ties this shows: the part's shape (size and placement, in 속성), its places (자리·능력) or its motion (움직임). */
|
|
32
46
|
this.section = 'shape';
|
|
33
|
-
this.fasten =
|
|
34
|
-
this.motion =
|
|
35
|
-
this.place =
|
|
47
|
+
this.fasten = freshFasten();
|
|
48
|
+
this.motion = freshMotion();
|
|
49
|
+
this.place = freshPlace();
|
|
50
|
+
this.row = freshRow();
|
|
36
51
|
}
|
|
37
52
|
static { this.styles = [SidePanelStyles, css `
|
|
38
53
|
:host {
|
|
@@ -52,6 +67,18 @@ let FigureV3Links = class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
52
67
|
font-weight: 600;
|
|
53
68
|
}
|
|
54
69
|
`]; }
|
|
70
|
+
/*
|
|
71
|
+
The forms describe the next command for the selected part, so another part starts from the defaults: carrying the
|
|
72
|
+
last part's choices over fastened a shutter curtain level with a right edge nobody chose for it (2026-09-25).
|
|
73
|
+
*/
|
|
74
|
+
willUpdate(changed) {
|
|
75
|
+
if (changed.has('partId') && changed.get('partId') !== undefined) {
|
|
76
|
+
this.fasten = freshFasten();
|
|
77
|
+
this.motion = freshMotion();
|
|
78
|
+
this.place = freshPlace();
|
|
79
|
+
this.row = freshRow();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
55
82
|
render() {
|
|
56
83
|
const asset = this.asset;
|
|
57
84
|
if (!asset || !this.partId)
|
|
@@ -72,6 +99,7 @@ let FigureV3Links = class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
72
99
|
${this.renderPositions(asset)}
|
|
73
100
|
${canFasten(asset, this.partId) ? this.renderFastening(asset) : html `<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
|
|
74
101
|
</section>
|
|
102
|
+
${this.renderRow(asset)}
|
|
75
103
|
`;
|
|
76
104
|
}
|
|
77
105
|
/** Where things are held on this part: a seat things rest on, or a grip things are held in (parity row ⑤). */
|
|
@@ -209,6 +237,13 @@ let FigureV3Links = class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
209
237
|
const to = fastenedTo(asset, this.partId);
|
|
210
238
|
const f = this.fasten;
|
|
211
239
|
const targets = asset.appearance.map(look => look.target).filter(one => one !== this.partId && canFasten(asset, one));
|
|
240
|
+
/* The faces are the ones each shape declares: a part is fastened by one of its outer faces, to any face of the target. */
|
|
241
|
+
const mineFaces = facesOf(asset, this.partId).filter(face => FACES.includes(face));
|
|
242
|
+
const myFace = mineFaces.includes(f.face) ? f.face : mineFaces[0];
|
|
243
|
+
const theirFaces = f.target ? facesOf(asset, f.target) : FACES;
|
|
244
|
+
const targetFace = theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0];
|
|
245
|
+
const aim = f.toPlane ? 'plane' : { part: f.target, face: targetFace };
|
|
246
|
+
const align = f.target || f.toPlane ? alignOf(asset, this.partId, aim, f.align, f.alignMm, !!f.twist) : undefined;
|
|
212
247
|
return html `
|
|
213
248
|
${to
|
|
214
249
|
? html `<div pair>
|
|
@@ -221,7 +256,7 @@ let FigureV3Links = class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
221
256
|
<div form>
|
|
222
257
|
<span>${i18next.t('figure.label.my-face')}</span>
|
|
223
258
|
<select @change=${(e) => (this.fasten = { ...f, face: e.target.value })}>
|
|
224
|
-
${
|
|
259
|
+
${mineFaces.map(face => html `<option value=${face} ?selected=${face === myFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
|
|
225
260
|
</select>
|
|
226
261
|
</div>
|
|
227
262
|
<label toggle>
|
|
@@ -238,7 +273,7 @@ let FigureV3Links = class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
238
273
|
</select>
|
|
239
274
|
<span>${i18next.t('figure.label.target-face')}</span>
|
|
240
275
|
<select @change=${(e) => (this.fasten = { ...f, targetFace: e.target.value })}>
|
|
241
|
-
${
|
|
276
|
+
${theirFaces.map(face => html `<option value=${face} ?selected=${face === targetFace}>${i18next.t('figure.label.face-' + face)}</option>`)}
|
|
242
277
|
</select>
|
|
243
278
|
<span>${i18next.t('figure.label.facing-meet')}</span>
|
|
244
279
|
<select @change=${(e) => (this.fasten = { ...f, facing: e.target.value })}>
|
|
@@ -246,10 +281,12 @@ let FigureV3Links = class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
246
281
|
<option value="flush" ?selected=${f.facing === 'flush'}>${i18next.t('figure.label.facing-flush')}</option>
|
|
247
282
|
</select>
|
|
248
283
|
</div>`}
|
|
249
|
-
${this.renderAlignment()}
|
|
284
|
+
${this.renderAlignment(f.toPlane || f.target ? alignAxesOf(asset, this.partId, aim, !!f.twist) : [])}
|
|
250
285
|
<div form>
|
|
251
286
|
<span>${i18next.t('figure.label.gap')}</span>
|
|
252
287
|
<input type="number" step="any" .value=${String(f.gap)} @change=${(e) => (this.fasten = { ...f, gap: Number(e.target.value) || 0 })} />
|
|
288
|
+
<span>${i18next.t('figure.label.twist')}</span>
|
|
289
|
+
<input type="number" step="any" .value=${String(f.twist)} @change=${(e) => (this.fasten = { ...f, twist: Number(e.target.value) || 0 })} />
|
|
253
290
|
</div>
|
|
254
291
|
<button
|
|
255
292
|
act
|
|
@@ -257,11 +294,12 @@ let FigureV3Links = class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
257
294
|
@click=${() => this.command({
|
|
258
295
|
kind: 'attach',
|
|
259
296
|
part: this.partId,
|
|
260
|
-
face:
|
|
261
|
-
to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face:
|
|
297
|
+
face: myFace,
|
|
298
|
+
to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: targetFace },
|
|
262
299
|
...(f.toPlane ? {} : { facing: f.facing }),
|
|
263
300
|
...(f.gap ? { gap: f.gap } : {}),
|
|
264
|
-
|
|
301
|
+
twist: f.twist,
|
|
302
|
+
...(align ? { align } : {}),
|
|
265
303
|
replace: !!to
|
|
266
304
|
})}
|
|
267
305
|
>
|
|
@@ -273,12 +311,12 @@ let FigureV3Links = class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
273
311
|
* Where on the target face the part sits, on the two axes across it: centred, level with the target's edge on
|
|
274
312
|
* either side, or a distance off centre. A door hung level with the frame's front edge turns about that edge.
|
|
275
313
|
*/
|
|
276
|
-
renderAlignment() {
|
|
314
|
+
renderAlignment(axes) {
|
|
277
315
|
const f = this.fasten;
|
|
278
316
|
const set = (axis, how) => (this.fasten = { ...f, align: { ...f.align, [axis]: how } });
|
|
279
317
|
const setMm = (axis, mm) => (this.fasten = { ...f, alignMm: { ...f.alignMm, [axis]: mm } });
|
|
280
318
|
return html `<div form>
|
|
281
|
-
${
|
|
319
|
+
${axes.map(({ axis, edges }) => {
|
|
282
320
|
const how = f.align[axis] === 'min' || f.align[axis] === 'max' ? (edges ? f.align[axis] : 'centre') : (f.align[axis] ?? 'centre');
|
|
283
321
|
return html `<span>${i18next.t('figure.label.align-' + axis)}</span>
|
|
284
322
|
<select @change=${(e) => set(axis, e.target.value)}>
|
|
@@ -295,6 +333,47 @@ let FigureV3Links = class FigureV3Links extends localize(i18next)(LitElement) {
|
|
|
295
333
|
})}
|
|
296
334
|
</div>`;
|
|
297
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Making the part a row across what it is fastened to (figure-model `make-repeat`): along one of the axes across its
|
|
338
|
+
* face, copies a pitch apart filling the parent's length, or a count spread over it. Once made, the row's own section
|
|
339
|
+
* (the editor's) shows it and takes it away.
|
|
340
|
+
*/
|
|
341
|
+
renderRow(asset) {
|
|
342
|
+
const axes = rowAxesOf(asset, this.partId);
|
|
343
|
+
if (!axes.length)
|
|
344
|
+
return nothing;
|
|
345
|
+
const r = this.row;
|
|
346
|
+
const axis = axes.includes(r.axis) ? r.axis : axes[0];
|
|
347
|
+
const ok = r.pitch > 0 && (r.fills || (Number.isSafeInteger(r.count) && r.count >= 1));
|
|
348
|
+
return html `<section>
|
|
349
|
+
<h3>${i18next.t('figure.label.repeat')}</h3>
|
|
350
|
+
<div form>
|
|
351
|
+
<span>${i18next.t('figure.label.repeat-axis')}</span>
|
|
352
|
+
<select @change=${(e) => (this.row = { ...r, axis: e.target.value })}>
|
|
353
|
+
${axes.map(a => html `<option value=${a} ?selected=${a === axis}>${a.toUpperCase()}</option>`)}
|
|
354
|
+
</select>
|
|
355
|
+
<span>${i18next.t('figure.label.repeat-pitch')} (mm)</span>
|
|
356
|
+
<input type="number" step="any" min="0" .value=${String(r.pitch)} @change=${(e) => (this.row = { ...r, pitch: Number(e.target.value) })} />
|
|
357
|
+
</div>
|
|
358
|
+
<label toggle>
|
|
359
|
+
<input type="checkbox" .checked=${r.fills} @change=${(e) => (this.row = { ...r, fills: e.target.checked })} />
|
|
360
|
+
${i18next.t('figure.label.repeat-fills')}
|
|
361
|
+
</label>
|
|
362
|
+
${r.fills
|
|
363
|
+
? nothing
|
|
364
|
+
: html `<div form>
|
|
365
|
+
<span>${i18next.t('figure.label.repeat-count')}</span>
|
|
366
|
+
<input type="number" step="1" min="1" .value=${String(r.count)} @change=${(e) => (this.row = { ...r, count: Number(e.target.value) })} />
|
|
367
|
+
</div>`}
|
|
368
|
+
<button
|
|
369
|
+
act
|
|
370
|
+
?disabled=${this.locked || !ok}
|
|
371
|
+
@click=${() => this.command({ kind: 'make-repeat', part: this.partId, axis, pitch: r.pitch, ...(r.fills ? {} : { count: r.count }) })}
|
|
372
|
+
>
|
|
373
|
+
<md-icon>view_week</md-icon>${i18next.t('figure.button.make-repeat')}
|
|
374
|
+
</button>
|
|
375
|
+
</section>`;
|
|
376
|
+
}
|
|
298
377
|
renderMotion(asset) {
|
|
299
378
|
const now = motionOf(asset, this.partId);
|
|
300
379
|
const joint = asset.joints?.find(j => j.body1.part === this.partId);
|
|
@@ -412,6 +491,10 @@ __decorate([
|
|
|
412
491
|
state(),
|
|
413
492
|
__metadata("design:type", Object)
|
|
414
493
|
], FigureV3Links.prototype, "place", void 0);
|
|
494
|
+
__decorate([
|
|
495
|
+
state(),
|
|
496
|
+
__metadata("design:type", Object)
|
|
497
|
+
], FigureV3Links.prototype, "row", void 0);
|
|
415
498
|
FigureV3Links = __decorate([
|
|
416
499
|
customElement('figure-v3-links')
|
|
417
500
|
], FigureV3Links);
|