@things-factory/figure-ui 10.1.47 → 10.1.49
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-parts.ts +86 -11
- package/client/modeller/side-panel-styles.ts +8 -0
- package/client/modeller/v3-canvas.ts +62 -8
- package/client/modeller/v3-editor.ts +196 -24
- package/client/modeller/v3-fields.ts +36 -0
- package/client/modeller/v3-joint-fan.ts +292 -0
- package/client/modeller/v3-links.ts +8 -0
- package/client/modeller/v3-stage-model.ts +24 -0
- package/client/modeller/v3-thumbnail.ts +8 -4
- 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 +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +13 -0
- package/dist-client/modeller/figure-v3-parts.js +81 -11
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-properties.d.ts +1 -1
- package/dist-client/modeller/side-panel-styles.js +8 -0
- package/dist-client/modeller/side-panel-styles.js.map +1 -1
- package/dist-client/modeller/v3-canvas.d.ts +17 -0
- package/dist-client/modeller/v3-canvas.js +72 -8
- package/dist-client/modeller/v3-canvas.js.map +1 -1
- package/dist-client/modeller/v3-clips.d.ts +1 -1
- package/dist-client/modeller/v3-editor.d.ts +34 -0
- package/dist-client/modeller/v3-editor.js +200 -22
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-fields.d.ts +15 -0
- package/dist-client/modeller/v3-fields.js +35 -0
- package/dist-client/modeller/v3-fields.js.map +1 -1
- package/dist-client/modeller/v3-joint-fan.d.ts +9 -0
- package/dist-client/modeller/v3-joint-fan.js +264 -0
- package/dist-client/modeller/v3-joint-fan.js.map +1 -0
- package/dist-client/modeller/v3-links.d.ts +5 -0
- package/dist-client/modeller/v3-links.js +7 -0
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-stage-model.d.ts +8 -0
- package/dist-client/modeller/v3-stage-model.js +26 -0
- package/dist-client/modeller/v3-stage-model.js.map +1 -1
- package/dist-client/modeller/v3-thumbnail.js +6 -3
- package/dist-client/modeller/v3-thumbnail.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/v3-fields.test.ts +46 -1
- package/test/v3-gizmo.test.ts +3 -1
- package/test/v3-joint-fan.test.ts +210 -0
- package/test/v3-links.test.ts +13 -1
- package/test/v3-maturity.test.ts +1 -1
- package/test/v3-palette-icons.test.ts +22 -0
- package/test/v3-thumbnail.test.ts +29 -0
- package/translations/en.json +17 -2
- package/translations/ja.json +17 -2
- package/translations/ko.json +17 -2
- package/translations/ms.json +17 -2
- package/translations/zh.json +17 -2
|
@@ -3,6 +3,7 @@ import { css, html, LitElement, nothing } from 'lit'
|
|
|
3
3
|
import { customElement, property } from 'lit/decorators.js'
|
|
4
4
|
import { i18next, localize } from '@operato/i18n'
|
|
5
5
|
import { ScrollbarStyles } from '@operato/styles'
|
|
6
|
+
import { PART_LIMIT } from '@hatiolab/figure-model'
|
|
6
7
|
import type { V3PrimitiveCreation } from '@hatiolab/figure-model'
|
|
7
8
|
import type { V3BudgetLine } from './v3-maturity.js'
|
|
8
9
|
import { partLabelOf } from './v3-fields.js'
|
|
@@ -28,6 +29,8 @@ export interface V3PartRow {
|
|
|
28
29
|
hidden?: boolean
|
|
29
30
|
/** 자산이 지금 상태에서 보이지 않는다고 한 것. 저장되는 것이고, 여기서는 표시만 한다. */
|
|
30
31
|
invisible?: boolean
|
|
32
|
+
/** 되풀이한 부품이면 지금 몇 개로 그려지나. 되풀이는 한 부품이다 — 한 줄로 보인다. */
|
|
33
|
+
copies?: number
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/** 제공자마다의 아이콘. Material Symbols 이름이다. */
|
|
@@ -59,7 +62,7 @@ const RECIPES: Array<{ kind: V3PaletteKind; icon: string; label: string }> = [
|
|
|
59
62
|
{ kind: 'frustum', icon: 'change_history', label: 'figure.label.primitive-frustum' },
|
|
60
63
|
{ kind: 'polygon', icon: 'hexagon', label: 'figure.label.primitive-polygon' },
|
|
61
64
|
{ kind: 'plate', icon: 'check_box_outline_blank', label: 'figure.label.primitive-plate' },
|
|
62
|
-
{ kind: 'wall', icon: '
|
|
65
|
+
{ kind: 'wall', icon: 'crop_portrait', label: 'figure.label.primitive-wall' },
|
|
63
66
|
{ kind: 'portal', icon: 'door_front', label: 'figure.label.primitive-portal' }
|
|
64
67
|
]
|
|
65
68
|
|
|
@@ -147,25 +150,61 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
147
150
|
font-style: normal;
|
|
148
151
|
color: var(--md-sys-color-error);
|
|
149
152
|
}
|
|
153
|
+
span[room][full] {
|
|
154
|
+
color: var(--md-sys-color-error);
|
|
155
|
+
font-weight: 600;
|
|
156
|
+
}
|
|
157
|
+
p[full] {
|
|
158
|
+
margin: 4px 0 0;
|
|
159
|
+
font-size: 0.68rem;
|
|
160
|
+
color: var(--md-sys-color-error);
|
|
161
|
+
}
|
|
162
|
+
div[level] {
|
|
163
|
+
display: grid;
|
|
164
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
165
|
+
align-items: center;
|
|
166
|
+
gap: 6px;
|
|
167
|
+
margin: 0 0 6px;
|
|
168
|
+
font-size: 0.68rem;
|
|
169
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
170
|
+
}
|
|
171
|
+
div[level] select {
|
|
172
|
+
min-width: 0;
|
|
173
|
+
padding: 2px 4px;
|
|
174
|
+
font: inherit;
|
|
175
|
+
font-size: 0.7rem;
|
|
176
|
+
color: var(--md-sys-color-on-surface);
|
|
177
|
+
background: var(--md-sys-color-surface-container-lowest);
|
|
178
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
179
|
+
border-radius: 6px;
|
|
180
|
+
}
|
|
181
|
+
small[copies] {
|
|
182
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
183
|
+
}
|
|
150
184
|
span[room] {
|
|
151
185
|
font-size: 0.7rem;
|
|
152
186
|
font-weight: 400;
|
|
153
187
|
letter-spacing: 0;
|
|
154
188
|
text-transform: none;
|
|
155
189
|
}
|
|
190
|
+
/* Three to a row, small (user, 2026-09-24): the palette is reached for, not read -- the list below is what grows. */
|
|
156
191
|
div[palette] {
|
|
157
192
|
display: grid;
|
|
158
|
-
grid-template-columns: repeat(
|
|
159
|
-
gap:
|
|
193
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
194
|
+
gap: 3px;
|
|
160
195
|
}
|
|
161
196
|
button[shape] {
|
|
162
197
|
display: flex;
|
|
163
198
|
flex-direction: column;
|
|
164
199
|
align-items: center;
|
|
165
|
-
gap:
|
|
166
|
-
|
|
200
|
+
gap: 1px;
|
|
201
|
+
min-width: 0;
|
|
202
|
+
padding: 5px 1px 4px;
|
|
167
203
|
font: var(--label-font, inherit);
|
|
168
|
-
font-size: 0.
|
|
204
|
+
font-size: 0.6rem;
|
|
205
|
+
line-height: 1.2;
|
|
206
|
+
text-align: center;
|
|
207
|
+
overflow-wrap: anywhere;
|
|
169
208
|
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
170
209
|
background: none;
|
|
171
210
|
border: 1px solid var(--md-sys-color-outline-variant);
|
|
@@ -182,7 +221,7 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
182
221
|
cursor: default;
|
|
183
222
|
}
|
|
184
223
|
button[shape] md-icon {
|
|
185
|
-
--md-icon-size:
|
|
224
|
+
--md-icon-size: 18px;
|
|
186
225
|
}
|
|
187
226
|
ol {
|
|
188
227
|
margin: 0;
|
|
@@ -323,8 +362,15 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
323
362
|
* 기준이 저작을 이끈다(V2 모델러의 정체성). 등급이 D 면 무엇이 넘쳤는지를 적는다. 누르면 성숙도 탭이 열린다.
|
|
324
363
|
*/
|
|
325
364
|
@property({ attribute: false }) budget?: V3BudgetLine
|
|
365
|
+
/**
|
|
366
|
+
* 디테일 등급의 부품 한도 — V2 의 S/M/L(사용자 2026-09-24). 부품을 넣기 **전에** 몇 개 남았는지 보이고, 다 차면 팔레트가
|
|
367
|
+
* 잠긴다. 되풀이는 한 부품으로 센다. 등급을 안 정했으면 한도는 없다.
|
|
368
|
+
*/
|
|
369
|
+
@property({ attribute: false }) partBudget?: { level: 'S' | 'M' | 'L' | null; used: number; limit: number | null }
|
|
326
370
|
|
|
327
371
|
render() {
|
|
372
|
+
const b = this.partBudget
|
|
373
|
+
const full = !!b && b.limit !== null && b.used >= b.limit
|
|
328
374
|
return html`
|
|
329
375
|
<section>
|
|
330
376
|
<h3>${i18next.t('figure.label.shapes')}</h3>
|
|
@@ -333,8 +379,8 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
333
379
|
recipe => html`
|
|
334
380
|
<button
|
|
335
381
|
shape
|
|
336
|
-
?disabled=${this.locked}
|
|
337
|
-
title=${i18next.t(recipe.label)}
|
|
382
|
+
?disabled=${this.locked || full}
|
|
383
|
+
title=${full ? i18next.t('figure.text.part-limit-reached') : i18next.t(recipe.label)}
|
|
338
384
|
@click=${() => this.ask('add-part', { kind: recipe.kind })}
|
|
339
385
|
>
|
|
340
386
|
<md-icon>${recipe.icon}</md-icon>
|
|
@@ -343,14 +389,18 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
343
389
|
`
|
|
344
390
|
)}
|
|
345
391
|
</div>
|
|
392
|
+
${full ? html`<p full>${i18next.t('figure.text.part-limit-reached-short', { level: b!.level, limit: b!.limit })}</p>` : nothing}
|
|
346
393
|
</section>
|
|
347
394
|
|
|
348
395
|
<section list>
|
|
349
396
|
<h3>
|
|
350
397
|
<span>${i18next.t('figure.label.parts')}</span>
|
|
351
|
-
<span room
|
|
398
|
+
<span room ?full=${full} title=${i18next.t('figure.text.limit-comes-from-level')}
|
|
399
|
+
>${b && b.limit !== null ? i18next.t('figure.text.n-of-limit', { used: b.used, limit: b.limit }) : (b?.used ?? this.parts.length)}</span
|
|
400
|
+
>
|
|
352
401
|
${this.renderShowAll()}
|
|
353
402
|
</h3>
|
|
403
|
+
${this.renderLevel()}
|
|
354
404
|
${this.renderBudget()}
|
|
355
405
|
${this.parts.length === 0
|
|
356
406
|
? html`<p quiet>${i18next.t('figure.text.no-parts-yet')}</p>`
|
|
@@ -361,6 +411,31 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
361
411
|
`
|
|
362
412
|
}
|
|
363
413
|
|
|
414
|
+
/** 디테일 등급 — 고르면 한도가 바뀐다. V2 의 자리 그대로, 목록 머리 아래. */
|
|
415
|
+
private renderLevel() {
|
|
416
|
+
const b = this.partBudget
|
|
417
|
+
if (!b) return nothing
|
|
418
|
+
return html`<div level>
|
|
419
|
+
<label for="detail-level">${i18next.t('figure.label.detail-level')}</label>
|
|
420
|
+
<select
|
|
421
|
+
id="detail-level"
|
|
422
|
+
title=${i18next.t('figure.text.detail-level-explained')}
|
|
423
|
+
?disabled=${this.locked}
|
|
424
|
+
@change=${(e: Event) => {
|
|
425
|
+
const value = (e.target as HTMLSelectElement).value
|
|
426
|
+
this.dispatchEvent(new CustomEvent('set-detail-level', { detail: { level: value || null }, bubbles: true, composed: true }))
|
|
427
|
+
}}
|
|
428
|
+
>
|
|
429
|
+
<option value="" ?selected=${!b.level}>${i18next.t('figure.label.detail-none')}</option>
|
|
430
|
+
${(['S', 'M', 'L'] as const).map(
|
|
431
|
+
one => html`<option value=${one} ?selected=${one === b.level}>
|
|
432
|
+
${one} ${i18next.t('figure.label.detail-' + one)} · ${i18next.t('figure.text.n-parts-at-most', { limit: PART_LIMIT[one] })}
|
|
433
|
+
</option>`
|
|
434
|
+
)}
|
|
435
|
+
</select>
|
|
436
|
+
</div>`
|
|
437
|
+
}
|
|
438
|
+
|
|
364
439
|
private renderBudget() {
|
|
365
440
|
const b = this.budget
|
|
366
441
|
if (!b || !this.parts.length) return nothing
|
|
@@ -434,7 +509,7 @@ export class FigureV3Parts extends localize(i18next)(LitElement) {
|
|
|
434
509
|
</button>
|
|
435
510
|
<md-icon>${PROVIDER_ICON[part.provider] ?? 'deployed_code'}</md-icon>
|
|
436
511
|
<span label title=${part.id}
|
|
437
|
-
>${partLabelOf(part.id)}${part.invisible ? html`<small>${i18next.t('figure.label.state-hidden')}</small>` : nothing}</span
|
|
512
|
+
>${partLabelOf(part.id)}${part.copies ? html` <small copies>× ${part.copies}</small>` : nothing}${part.invisible ? html`<small>${i18next.t('figure.label.state-hidden')}</small>` : nothing}</span
|
|
438
513
|
>
|
|
439
514
|
${selected ? this.renderOps(part.id, index) : nothing}
|
|
440
515
|
</li>
|
|
@@ -88,6 +88,14 @@ export const SidePanelStyles = css`
|
|
|
88
88
|
overflow-wrap: anywhere;
|
|
89
89
|
white-space: normal;
|
|
90
90
|
}
|
|
91
|
+
/* A value read, not edited, beside its label: the field's size and colour, not the page's. */
|
|
92
|
+
div[inline-field] > span[value] {
|
|
93
|
+
font-size: 0.76rem;
|
|
94
|
+
color: var(--md-sys-color-on-surface);
|
|
95
|
+
}
|
|
96
|
+
label span[unit] {
|
|
97
|
+
color: var(--md-sys-color-outline);
|
|
98
|
+
}
|
|
91
99
|
div[inline-field] + div[inline-field],
|
|
92
100
|
div[inline-field] + div[vector-row],
|
|
93
101
|
div[vector-row] + div[inline-field] {
|
|
@@ -7,6 +7,8 @@ import type { GizmoReach } from './v3-gizmo.js'
|
|
|
7
7
|
import type { V3RenderPart } from './v3-renderer.js'
|
|
8
8
|
import { DEFAULT_VIEW, sceneLook, WORKBENCH, type ViewSettings } from './figure-view.js'
|
|
9
9
|
import { boardFor, changedKeys, componentOf, gizmoResultOf, type Board } from './v3-stage-model.js'
|
|
10
|
+
import { JOINT_FAN } from './v3-joint-fan.js'
|
|
11
|
+
import { ownerOf } from './v3-fields.js'
|
|
10
12
|
|
|
11
13
|
/*
|
|
12
14
|
* The V3 modeller's canvas -- a things-scene board in editing mode, as the V2 modeller's was.
|
|
@@ -44,6 +46,9 @@ interface SceneHandle {
|
|
|
44
46
|
root?: SceneLayer
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
/* The fan's colour when the figure's palette names no accent. */
|
|
50
|
+
const FAN_COLOUR = '#ff8a00'
|
|
51
|
+
|
|
47
52
|
/* The 2D side of the board, which the author never sees on this page but the scene paints. */
|
|
48
53
|
const PAPER = '#ffffff'
|
|
49
54
|
|
|
@@ -81,6 +86,14 @@ export class FigureV3Canvas extends LitElement {
|
|
|
81
86
|
/** What the gizmo may write for the selected part; a change beyond it is not applied and the part springs back. */
|
|
82
87
|
@property({ attribute: false }) reach: GizmoReach = { translate: false, rotate: false, scale: false }
|
|
83
88
|
@property({ attribute: false }) view: ViewSettings = DEFAULT_VIEW
|
|
89
|
+
/**
|
|
90
|
+
* View only: nothing that happens on the board is told out. The card's picture is taken from a second canvas stood up
|
|
91
|
+
* off screen (`v3-thumbnail.ts`); the scene settles its components as it builds them, and a change told from there
|
|
92
|
+
* would reach the editor as an edit (V2's `viewOnly`, for the same reason).
|
|
93
|
+
*/
|
|
94
|
+
@property({ type: Boolean }) viewOnly = false
|
|
95
|
+
/** Where the selected part turns and how far (`v3-joint-fan`): shown, never picked, never saved. */
|
|
96
|
+
@property({ attribute: false }) fan: V3RenderPart | null = null
|
|
84
97
|
|
|
85
98
|
@query('div[stage]') private stage!: HTMLDivElement
|
|
86
99
|
|
|
@@ -103,7 +116,7 @@ export class FigureV3Canvas extends LitElement {
|
|
|
103
116
|
|
|
104
117
|
protected updated(changed: PropertyValues) {
|
|
105
118
|
if (!this.hasUpdated) return
|
|
106
|
-
if (changed.has('parts') || changed.has('palette') || changed.has('context')) this.standUp()
|
|
119
|
+
if (changed.has('parts') || changed.has('palette') || changed.has('context') || changed.has('fan')) this.standUp()
|
|
107
120
|
if (changed.has('view')) this.applyView()
|
|
108
121
|
if (changed.has('selected')) this.showSelected()
|
|
109
122
|
}
|
|
@@ -115,6 +128,25 @@ export class FigureV3Canvas extends LitElement {
|
|
|
115
128
|
this.teardown()
|
|
116
129
|
}
|
|
117
130
|
|
|
131
|
+
/**
|
|
132
|
+
* The board as a picture, for the card: the 3D view at this size, or nothing when it cannot be taken. The scene's
|
|
133
|
+
* `toDataURL` renders the 2D board (a white sheet, V2 found), and the 3D renderer keeps no drawing buffer, so the
|
|
134
|
+
* scene's own `snapshot3d` draws and reads in one task. A picture that cannot be taken is not an error and is not
|
|
135
|
+
* faked (V2's `snapshot`, lost when this canvas moved onto the scene -- no card had a picture since, user 2026-09-24).
|
|
136
|
+
*/
|
|
137
|
+
async snapshot(width = 480, height = 360): Promise<string | undefined> {
|
|
138
|
+
const layer = this.scene?.root as (SceneLayer & { snapshot3d?: (w?: number, h?: number, type?: string, quality?: number) => Promise<string | undefined> }) | undefined
|
|
139
|
+
if (!layer?.snapshot3d) return undefined
|
|
140
|
+
try {
|
|
141
|
+
/* webp: a card needs no alpha, and png is three or four times heavier in every list response. */
|
|
142
|
+
const url = await layer.snapshot3d(width, height, 'image/webp', 0.85)
|
|
143
|
+
return typeof url === 'string' && url.startsWith('data:') ? url : undefined
|
|
144
|
+
} catch (e) {
|
|
145
|
+
console.warn(`[thumbnail] could not take the picture -- ${(e as Error).message}`)
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
118
150
|
/** The whole figure in view: the board's fit, then the 3D camera's own, as V2 did. */
|
|
119
151
|
fit() {
|
|
120
152
|
this.scene?.fit?.('ratio')
|
|
@@ -129,7 +161,12 @@ export class FigureV3Canvas extends LitElement {
|
|
|
129
161
|
if (!this.stage) return
|
|
130
162
|
const needed = boardFor(this.parts, this.board)
|
|
131
163
|
const outgrown = !this.board || needed.width > this.board.width || needed.height > this.board.height || needed.depth > this.board.depth
|
|
132
|
-
if (this.scene && this.standingFor === this.context
|
|
164
|
+
if (this.scene && this.standingFor === this.context) {
|
|
165
|
+
/*
|
|
166
|
+
A part dragged past the board's edge grows the board where it stands. Standing it up again lost the selection,
|
|
167
|
+
the gizmo and the camera after every such drag -- the gizmo seemed not to work at all (user, 2026-09-24).
|
|
168
|
+
*/
|
|
169
|
+
if (outgrown) this.grow(needed)
|
|
133
170
|
this.sync()
|
|
134
171
|
return
|
|
135
172
|
}
|
|
@@ -167,7 +204,10 @@ export class FigureV3Canvas extends LitElement {
|
|
|
167
204
|
}
|
|
168
205
|
|
|
169
206
|
private components(): Record<string, unknown>[] {
|
|
170
|
-
|
|
207
|
+
const parts = this.parts.map(part => componentOf(part, this.palette, this.board!)).filter((c): c is Record<string, unknown> => !!c)
|
|
208
|
+
/* The fan in the accent colour, translucent; `capturable: false` keeps clicks, the marquee and the gizmo off it. */
|
|
209
|
+
const fan = this.fan ? componentOf(this.fan, { ...this.palette, [JOINT_FAN]: this.palette['palette.accent'] ?? FAN_COLOUR }, this.board!) : null
|
|
210
|
+
return fan ? [...parts, { ...fan, capturable: false, locked: true }] : parts
|
|
171
211
|
}
|
|
172
212
|
|
|
173
213
|
/** The components standing now, by name. Not held between calls: the scene stands its board up again at times. */
|
|
@@ -180,6 +220,19 @@ export class FigureV3Canvas extends LitElement {
|
|
|
180
220
|
return found
|
|
181
221
|
}
|
|
182
222
|
|
|
223
|
+
/** A larger board under the same scene: its size, and the grid that suits it. Components are re-placed by `sync`. */
|
|
224
|
+
private grow(needed: Board & { depth: number }) {
|
|
225
|
+
const layer = this.scene?.root
|
|
226
|
+
this.board = needed
|
|
227
|
+
if (!layer) return
|
|
228
|
+
this.writing = true
|
|
229
|
+
try {
|
|
230
|
+
layer.set({ width: needed.width, height: needed.height, depth: needed.depth, gridStep: niceGridStep(Math.max(needed.width, needed.height)) })
|
|
231
|
+
} finally {
|
|
232
|
+
this.writing = false
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
183
236
|
/** Bring the components to the evaluation: remove the gone, write only what differs, add the new. */
|
|
184
237
|
private sync() {
|
|
185
238
|
const layer = this.scene?.root
|
|
@@ -213,11 +266,11 @@ export class FigureV3Canvas extends LitElement {
|
|
|
213
266
|
private watchScene() {
|
|
214
267
|
const layer = this.scene?.root
|
|
215
268
|
const root = layer?.root
|
|
216
|
-
if (!layer || !root?.on) return
|
|
269
|
+
if (!layer || !root?.on || this.viewOnly) return
|
|
217
270
|
root.on('selected', (after: unknown) => {
|
|
218
271
|
const name = (after as SceneComponent[] | undefined)?.[0]?.state?.name
|
|
219
272
|
const id = typeof name === 'string' ? name : ''
|
|
220
|
-
if (id !== this.selected) this.dispatchEvent(new CustomEvent('part-selected', { detail: id, bubbles: true, composed: true }))
|
|
273
|
+
if (ownerOf(id) !== this.selected) this.dispatchEvent(new CustomEvent('part-selected', { detail: id, bubbles: true, composed: true }))
|
|
221
274
|
})
|
|
222
275
|
/*
|
|
223
276
|
A change comes up to the board with the component in the last argument's `origin` (the scene appends
|
|
@@ -241,10 +294,11 @@ export class FigureV3Canvas extends LitElement {
|
|
|
241
294
|
const layer = this.scene?.root
|
|
242
295
|
const root = layer?.root
|
|
243
296
|
if (!layer || !root) return
|
|
244
|
-
|
|
297
|
+
/* A repeat is selected whole: every copy of it (the editor selects the part the author made). */
|
|
298
|
+
const wanted = this.selected ? [...this.standing()].filter(([name]) => ownerOf(name) === this.selected).map(([, component]) => component) : []
|
|
245
299
|
const now = root.selected ?? []
|
|
246
|
-
if (
|
|
247
|
-
root.selected =
|
|
300
|
+
if (now.length === wanted.length && wanted.every((one, i) => now[i] === one)) return
|
|
301
|
+
root.selected = wanted
|
|
248
302
|
layer.invalidate?.()
|
|
249
303
|
}
|
|
250
304
|
|