@things-factory/figure-ui 10.1.169 → 10.1.171
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/affected-parts.ts +11 -0
- package/client/modeller/canvas.ts +15 -1
- package/client/modeller/editor-styles.ts +98 -6
- package/client/modeller/editor.ts +212 -53
- package/client/modeller/fasten-geometry.ts +19 -0
- package/client/modeller/figure-fasten-drawing.ts +4 -3
- package/client/modeller/figure-links.ts +80 -37
- package/client/modeller/figure-material-panel.ts +20 -1
- package/client/modeller/figure-preview.ts +121 -8
- package/client/modeller/figure-properties.ts +1 -2
- package/client/modeller/figure-section-panel.ts +5 -0
- package/client/modeller/figure-verdict.ts +9 -2
- package/client/modeller/gate-findings.ts +12 -4
- package/client/modeller/group-menu.ts +57 -0
- package/client/modeller/links.ts +22 -1
- package/client/modeller/modeller-keys.ts +6 -0
- package/client/modeller/preview-size.ts +32 -0
- package/client/modeller/stage-model.ts +11 -0
- package/client/pages/figure-modeller-page.ts +8 -2
- package/dist-client/modeller/affected-parts.d.ts +6 -0
- package/dist-client/modeller/affected-parts.js +12 -0
- package/dist-client/modeller/affected-parts.js.map +1 -0
- package/dist-client/modeller/canvas.d.ts +3 -0
- package/dist-client/modeller/canvas.js +12 -1
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +1 -1
- package/dist-client/modeller/editor-styles.js +98 -6
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +42 -1
- package/dist-client/modeller/editor.js +220 -51
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +27 -0
- package/dist-client/modeller/fasten-geometry.js +14 -0
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.js +4 -3
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +2 -0
- package/dist-client/modeller/figure-links.js +84 -38
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +7 -0
- package/dist-client/modeller/figure-material-panel.js +24 -1
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +12 -0
- package/dist-client/modeller/figure-preview.js +120 -5
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.js +1 -2
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-section-panel.js +5 -0
- package/dist-client/modeller/figure-section-panel.js.map +1 -1
- package/dist-client/modeller/figure-verdict.d.ts +1 -0
- package/dist-client/modeller/figure-verdict.js +8 -2
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/gate-findings.d.ts +3 -1
- package/dist-client/modeller/gate-findings.js +8 -4
- package/dist-client/modeller/gate-findings.js.map +1 -1
- package/dist-client/modeller/group-menu.d.ts +18 -0
- package/dist-client/modeller/group-menu.js +52 -0
- package/dist-client/modeller/group-menu.js.map +1 -0
- package/dist-client/modeller/links.d.ts +5 -0
- package/dist-client/modeller/links.js +21 -1
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/modeller-keys.js +7 -0
- package/dist-client/modeller/modeller-keys.js.map +1 -1
- package/dist-client/modeller/preview-size.d.ts +23 -0
- package/dist-client/modeller/preview-size.js +32 -0
- package/dist-client/modeller/preview-size.js.map +1 -0
- package/dist-client/modeller/stage-model.d.ts +11 -0
- package/dist-client/modeller/stage-model.js +11 -0
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +3 -0
- package/dist-client/pages/figure-modeller-page.js +11 -2
- 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/package.json +8 -8
- package/test/browser/editing-page.js +3 -1
- package/test/browser/editing.check.mjs +2 -1
- package/test/browser/links-page.js +1 -0
- package/test/browser/links.check.mjs +20 -5
- package/test/browser/modeller.check.mjs +9 -1
- package/test/browser/more-open.js +11 -0
- package/test/browser/parts.check.mjs +11 -1
- package/test/browser/places-mesh.check.mjs +9 -3
- package/test/browser/product-page.check.mjs +9 -4
- package/test/browser/props.check.mjs +224 -0
- package/test/browser/screen-page.js +18 -0
- package/test/browser/screen.check.mjs +172 -29
- package/test/browser/section-page.js +1 -0
- package/test/browser/section.check.mjs +6 -0
- package/test/command-preview.test.ts +2 -2
- package/test/fasten-geometry.test.ts +15 -0
- package/test/fields.test.ts +4 -2
- package/test/first-minute.test.ts +15 -2
- package/test/layout.test.ts +1 -1
- package/test/preview-size.test.ts +33 -0
- package/test/refusals.test.ts +1 -1
- package/test/resize-with-placement.test.ts +1 -1
- package/test/stage-model.test.ts +11 -1
- package/test/verdict.test.ts +7 -0
- package/translations/en.json +37 -27
- package/translations/es.json +18 -8
- package/translations/ja.json +19 -9
- package/translations/ko.json +50 -40
- package/translations/ms.json +19 -9
- package/translations/zh.json +19 -9
|
@@ -4,7 +4,7 @@ import { customElement, property, state } from 'lit/decorators.js'
|
|
|
4
4
|
import { i18next, localize } from '@operato/i18n'
|
|
5
5
|
import type { FigureFace } from '@hatiolab/figure-model'
|
|
6
6
|
|
|
7
|
-
import { alignmentAt, offsetOf, overhangOf, screenOf, type FaceView, type FastenMode } from './fasten-geometry.js'
|
|
7
|
+
import { alignmentAt, fastenLabelsOf, offsetOf, overhangOf, screenOf, type FaceView, type FastenMode } from './fasten-geometry.js'
|
|
8
8
|
import type { Alignment } from './links.js'
|
|
9
9
|
|
|
10
10
|
type Axis = 'x' | 'y' | 'z'
|
|
@@ -299,6 +299,7 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
299
299
|
}
|
|
300
300
|
const faceName = this.target ? `${this.target} ${i18next.t('figure.label.face-' + this.face)}` : this.planeWord
|
|
301
301
|
const sizeWords = faceSized ? ` ${Math.round(view.face[h]!)} × ${Math.round(view.face[v]!)}` : ''
|
|
302
|
+
const labels = fastenLabelsOf(faceSized ? { x: fx, y: fy, w: fw, h: fh } : { x: 6, y: 4, w: W - 12, h: H - 8 }, { x: px, y: py, w: pw, h: ph }, this.partName, H)
|
|
302
303
|
return html`<div where tabindex="0" @keydown=${(ev: KeyboardEvent) => this.keys(ev, view)}>
|
|
303
304
|
${svg`<svg viewBox="0 0 ${W} ${H}" role="img" aria-label=${faceName}
|
|
304
305
|
@pointermove=${(ev: PointerEvent) => this.drag(ev, view, cx, cy)}
|
|
@@ -306,7 +307,7 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
306
307
|
${faceSized
|
|
307
308
|
? svg`<rect x=${fx} y=${fy} width=${fw} height=${fh} rx="4" style="fill:var(--md-sys-color-primary-container);stroke:var(--md-sys-color-primary);stroke-width:2" />`
|
|
308
309
|
: 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" />`}
|
|
309
|
-
<text x=${
|
|
310
|
+
<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>
|
|
310
311
|
${guide(h)}${guide(v)}
|
|
311
312
|
<g footprint style=${`cursor:${this.locked ? 'default' : 'grab'}`} @pointerdown=${(ev: PointerEvent) => this.grab(ev, at, caught)}>
|
|
312
313
|
<rect x=${px} y=${py} width=${pw} height=${ph} rx=${round ? pw / 2 : 2} style="fill:var(--md-sys-color-tertiary-container)" />
|
|
@@ -317,7 +318,7 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
|
|
|
317
318
|
: nothing}
|
|
318
319
|
<rect x=${px} y=${py} width=${pw} height=${ph} rx=${round ? pw / 2 : 2}
|
|
319
320
|
style=${`fill:none;stroke:var(--md-sys-color-on-surface);stroke-width:1.5;stroke-dasharray:${touching ? '' : '4 3'}`} />
|
|
320
|
-
<text x=${
|
|
321
|
+
<text part-name x=${labels.part.x} y=${labels.part.y} text-anchor="middle" font-size="12" pointer-events="none" style="fill:var(--md-sys-color-on-surface)">${this.partName}</text>
|
|
321
322
|
</g>
|
|
322
323
|
${dim(h)}${dim(v)}
|
|
323
324
|
</svg>`}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
followableAxes,
|
|
21
21
|
positionIsShare,
|
|
22
22
|
choiceOf,
|
|
23
|
+
followWordsOf,
|
|
23
24
|
fastenedTo,
|
|
24
25
|
heldBy,
|
|
25
26
|
followOf,
|
|
@@ -60,6 +61,7 @@ import type { FastenChange } from './figure-fasten-drawing.js'
|
|
|
60
61
|
import { faceViewOf, insideAlignmentOf, modeOf, myFaceFor, turnsInPlace, type FaceView, type FastenMode } from './fasten-geometry.js'
|
|
61
62
|
import type { FigureRevision } from './revision.js'
|
|
62
63
|
import { HelpToggles } from './help.js'
|
|
64
|
+
import { groupMenu, GroupMenuStyles } from './group-menu.js'
|
|
63
65
|
import { jointAxisWord } from './joint-axis.js'
|
|
64
66
|
import { placeTerms } from '@things-factory/figure-viewer/dist-client/detail-facts.js'
|
|
65
67
|
|
|
@@ -100,10 +102,28 @@ const freshPlace = () => ({ role: 'grip' as 'seat' | 'grip', face: 'top' as Figu
|
|
|
100
102
|
@customElement('figure-links')
|
|
101
103
|
export class FigureLinks extends localize(i18next)(LitElement) {
|
|
102
104
|
private readonly help = new HelpToggles(this)
|
|
103
|
-
static styles = [SidePanelStyles, css`
|
|
105
|
+
static styles = [SidePanelStyles, GroupMenuStyles, css`
|
|
104
106
|
:host {
|
|
105
107
|
display: block;
|
|
106
108
|
}
|
|
109
|
+
/* A group's sub-question, under its title: quieter than the title, as a label is. */
|
|
110
|
+
p[group-question] {
|
|
111
|
+
margin: 4px 0 0;
|
|
112
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
113
|
+
}
|
|
114
|
+
/* The fastening a part has, in one line, and 「바꾸기」 at its end. */
|
|
115
|
+
div[fastened-summary] {
|
|
116
|
+
display: flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
gap: 8px;
|
|
119
|
+
}
|
|
120
|
+
div[fastened-summary] > p {
|
|
121
|
+
flex: 1;
|
|
122
|
+
margin: 0;
|
|
123
|
+
}
|
|
124
|
+
div[fastened-summary] > button[quiet] {
|
|
125
|
+
margin-left: 0;
|
|
126
|
+
}
|
|
107
127
|
/* The measure's name, then its rule: X, or a cylinder's 반지름 · 길이, on one line. */
|
|
108
128
|
div[row] > b {
|
|
109
129
|
white-space: nowrap;
|
|
@@ -121,9 +141,9 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
121
141
|
div[resize-now] { display: grid; gap: 4px; margin: 12px 0; }
|
|
122
142
|
div[resize-now] div[row] { grid-template-columns: 1fr auto auto auto; align-items: center; gap: 8px; }
|
|
123
143
|
div[resize-now] span[measure] { font-variant-numeric: tabular-nums; }
|
|
124
|
-
button[link] { background: none; border: none; padding: 0; color: var(--md-sys-color-primary, #477b9e); text-decoration: underline; cursor: pointer; font: inherit; }
|
|
125
144
|
/* Size behaviour is a choice about dimensions only. It never implies a placement or a fastening. */
|
|
126
|
-
|
|
145
|
+
/* One of the size group's rows: the group's gap spaces it, as it does the rest (a margin on top of it left a hole). */
|
|
146
|
+
details[resize-choice] { margin: 0; padding: 0; border: 0; }
|
|
127
147
|
details[resize-choice] > div { display: grid; gap: 8px; padding: 8px 0; }
|
|
128
148
|
p[fasten-mode] { margin: 0; font-weight: 600; }
|
|
129
149
|
section[fastening][spotlit] { outline: 2px solid var(--md-sys-color-primary, #477b9e); outline-offset: 4px; border-radius: 6px; transition: outline-color 0.3s; }
|
|
@@ -186,6 +206,8 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
186
206
|
|
|
187
207
|
/** A new fastening starts with a target. Its face and alignment are asked only after that target is chosen. */
|
|
188
208
|
@state() private choosingFastening = false
|
|
209
|
+
/** The fastening the part has, opened to be changed: until then the place group says it in one line. */
|
|
210
|
+
@state() private changingFastening = false
|
|
189
211
|
/** The fastening section, marked for a moment when "open attachment settings" brings it into view. */
|
|
190
212
|
@state() private spotlit = false
|
|
191
213
|
@state() private fasten = freshFasten()
|
|
@@ -219,6 +241,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
219
241
|
this.row = freshRow()
|
|
220
242
|
this.editingMotion = false
|
|
221
243
|
this.choosingFastening = false
|
|
244
|
+
this.changingFastening = false
|
|
222
245
|
}
|
|
223
246
|
}
|
|
224
247
|
|
|
@@ -270,17 +293,29 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
270
293
|
? this.renderPlaces(asset)
|
|
271
294
|
: html`<p hint>${i18next.t('figure.text.places-need-a-box')}</p>`}</section>`
|
|
272
295
|
}
|
|
296
|
+
/*
|
|
297
|
+
Three groups in one order whatever the part's state (chief architect 2026-10-16): its size, its place, its row. The
|
|
298
|
+
editor's own numbers go in through the slots, so a group keeps its place when a rule or a fastening takes them over.
|
|
299
|
+
*/
|
|
300
|
+
/* Fastened, by a face or by an origin: either can be taken off, from the place group's 「⋯」. */
|
|
301
|
+
const held = !!heldBy(asset, this.partId) || !!(this.revision && fasteningOf(this.revision, this.partId)?.atOrigin)
|
|
273
302
|
const shown = html`
|
|
274
|
-
<section>
|
|
275
|
-
<h3>${i18next.t('figure.label.
|
|
303
|
+
<section size>
|
|
304
|
+
<h3>${i18next.t('figure.label.group-size')}</h3>
|
|
305
|
+
<slot name="size"></slot>
|
|
306
|
+
<p group-question>${i18next.t('figure.label.resize-intent')}</p>
|
|
276
307
|
${canFollow(asset, this.partId) ? this.renderDimensions(asset) : html`<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
|
|
277
308
|
</section>
|
|
278
309
|
<section fastening ?spotlit=${this.spotlit} @focusin=${() => (this.working = 'fasten')}>
|
|
279
|
-
<h3
|
|
310
|
+
<h3>
|
|
311
|
+
${i18next.t('figure.label.group-place')}${this.help.button('figure.text.fastening-explained')}
|
|
312
|
+
${groupMenu([{ label: i18next.t('figure.button.detach'), icon: 'link_off', disabled: this.locked || !held, run: () => this.command({ kind: 'detach', part: this.partId }) }])}
|
|
313
|
+
</h3>${this.help.hint('figure.text.fastening-explained')}
|
|
314
|
+
<slot name="place"></slot>
|
|
280
315
|
${this.renderPositions(asset)}
|
|
281
316
|
${canFasten(asset, this.partId) ? this.renderFastening(asset) : html`<p hint>${i18next.t('figure.text.links-need-a-box')}</p>`}
|
|
282
317
|
</section>
|
|
283
|
-
${this.renderRow(asset)}
|
|
318
|
+
${rowOf(asset, this.partId) ? html`<slot name="row"></slot>` : this.renderRow(asset)}
|
|
284
319
|
`
|
|
285
320
|
this.candidate = this.working === 'row' && this.rowCandidate ? { action: this.rowCandidate } : this.fastenCandidate
|
|
286
321
|
return shown
|
|
@@ -301,7 +336,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
301
336
|
<b bound>${one.label}</b>
|
|
302
337
|
<small>${one.face ? i18next.t('figure.label.face-' + one.face) + ' · ' : ''}${placeTerms(one, (key, params) => i18next.t(key, params))}</small>
|
|
303
338
|
</span>
|
|
304
|
-
<button
|
|
339
|
+
<button quiet ?disabled=${this.locked} @click=${() => this.command({ kind: 'remove-binding', id: one.id })}>
|
|
305
340
|
<md-icon>close</md-icon>${i18next.t('figure.button.remove-place')}
|
|
306
341
|
</button>
|
|
307
342
|
</div>`
|
|
@@ -373,9 +408,9 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
373
408
|
return html`<div row>
|
|
374
409
|
<b>${word(axis)}</b>
|
|
375
410
|
<span measure>${mm === undefined ? '—' : `${Math.round(mm)} mm`}</span>
|
|
376
|
-
<span>${i18next.t(
|
|
411
|
+
<span>${custom ? followWordsOf(now, (key, params) => i18next.t(key, params)) : i18next.t(following ? 'figure.label.resize-following' : 'figure.label.follows-nothing')}</span>
|
|
377
412
|
${following
|
|
378
|
-
? html`<button
|
|
413
|
+
? html`<button quiet resize-unfollow=${axis} ?disabled=${this.locked}
|
|
379
414
|
@click=${() => {
|
|
380
415
|
this.follow(axis, 'own', now)
|
|
381
416
|
}}>${i18next.t('figure.button.resize-unfollow')}</button>`
|
|
@@ -383,7 +418,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
383
418
|
</div>`
|
|
384
419
|
})}</div>`}
|
|
385
420
|
${followsAll
|
|
386
|
-
?
|
|
421
|
+
? nothing
|
|
387
422
|
: html`<button act follow-size ?disabled=${this.locked || asset.sizing === 'uniform'} @click=${() => this.command({ kind: 'follow-size', part: this.partId })}>
|
|
388
423
|
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.follow-size')}
|
|
389
424
|
</button>`}
|
|
@@ -398,15 +433,15 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
398
433
|
<option value="own" ?selected=${value === 'own'}>${i18next.t('figure.label.follows-nothing')}</option>
|
|
399
434
|
${AXES.filter(one => hasInstanceSize(asset, one)).map(
|
|
400
435
|
one => html`<option value=${`share:${one}`} ?selected=${value === `share:${one}`}>
|
|
401
|
-
${i18next.t('figure.label.follows-instance-share'
|
|
436
|
+
${i18next.t('figure.label.follows-instance-share', { axis: one.toUpperCase() })}
|
|
402
437
|
</option>
|
|
403
438
|
<option value=${`instance:${one}`} ?selected=${value === `instance:${one}`}>
|
|
404
|
-
${i18next.t('figure.label.follows-instance'
|
|
439
|
+
${i18next.t('figure.label.follows-instance', { axis: one.toUpperCase() })}
|
|
405
440
|
</option>`
|
|
406
441
|
)}
|
|
407
442
|
${others.flatMap(other =>
|
|
408
443
|
AXES.map(
|
|
409
|
-
one => html`<option value=${`part:${other}:${one}`} ?selected=${value === `part:${other}:${one}`}>${other
|
|
444
|
+
one => html`<option value=${`part:${other}:${one}`} ?selected=${value === `part:${other}:${one}`}>${i18next.t('figure.label.follows-part-axis', { part: other, axis: one.toUpperCase() })}</option>`
|
|
410
445
|
)
|
|
411
446
|
)}
|
|
412
447
|
${now.of === 'expression' || now.of === 'input'
|
|
@@ -468,6 +503,8 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
468
503
|
private async openFastening(asset: FigureAsset) {
|
|
469
504
|
const own = fasteningOf(this.revision!, this.partId)
|
|
470
505
|
this.fasten = own ? ({ ...freshFasten(), ...own } as ReturnType<typeof freshFasten>) : freshFasten()
|
|
506
|
+
/* Asked for from elsewhere: the form itself, not the one-line summary of the fastening it has. */
|
|
507
|
+
this.changingFastening = true
|
|
471
508
|
this.spotlit = true
|
|
472
509
|
await this.updateComplete
|
|
473
510
|
const section = this.renderRoot.querySelector<HTMLElement>('section[fastening]')
|
|
@@ -548,9 +585,6 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
548
585
|
this.fastenCandidate = undefined
|
|
549
586
|
return html`<div fastened fasten-at-origin>
|
|
550
587
|
<p>${this.heldWords(asset)}</p>
|
|
551
|
-
<button act ?disabled=${this.locked} @click=${() => this.command({ kind: 'detach', part: this.partId })}>
|
|
552
|
-
<md-icon>link_off</md-icon>${i18next.t('figure.button.detach')}
|
|
553
|
-
</button>
|
|
554
588
|
${this.renderDetachWords()}
|
|
555
589
|
</div>`
|
|
556
590
|
}
|
|
@@ -610,17 +644,13 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
610
644
|
*/
|
|
611
645
|
const current = asset.joints?.find(j => j.body1.part === this.partId)
|
|
612
646
|
const editing = i18next.t('figure.text.fasten-new')
|
|
613
|
-
/*
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
`
|
|
621
|
-
return html`
|
|
622
|
-
${current ? nothing : html`<p fasten-mode>${editing}</p>`}
|
|
623
|
-
${this.overhangNote.length ? html`<p hint overhang-note>${this.overhangNote.join(' ')}</p>` : nothing}
|
|
647
|
+
/*
|
|
648
|
+
A fastening the part has, said in one line -- what, on which face, how -- and 「바꾸기」 to open the drawing and the
|
|
649
|
+
details (chief architect 2026-10-16). They were open on every fastened part, near a screen of the panel, for a
|
|
650
|
+
choice made once. Taking it off is in the group's 「⋯」. What would hang over still shows: it is about now.
|
|
651
|
+
*/
|
|
652
|
+
/* What hangs over the face, and the way to fix it: about the fastening as it is now, so said open or folded. */
|
|
653
|
+
const overhang = html`${this.overhangNote.length ? html`<p hint overhang-note>${this.overhangNote.join(' ')}</p>` : nothing}
|
|
624
654
|
${this.overhangWords.length
|
|
625
655
|
? html`<div overhang role="status">
|
|
626
656
|
${this.overhangWords.map(line => html`<p>${line}</p>`)}
|
|
@@ -631,15 +661,28 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
631
661
|
${this.fixNote ? html`<p hint fix-note>${this.fixNote}</p>` : nothing}`
|
|
632
662
|
: nothing}
|
|
633
663
|
</div>`
|
|
634
|
-
: nothing}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
664
|
+
: nothing}`
|
|
665
|
+
if (current && held && !this.changingFastening && (f.toPlane || f.target))
|
|
666
|
+
return html`
|
|
667
|
+
${overhang}
|
|
668
|
+
<div fastened-summary>
|
|
669
|
+
<p fasten-sentence>${this.sentenceOf(myFace, f.toPlane ? planeWord : `${f.target}의 ${i18next.t('figure.label.face-' + targetFace)}`, mode)}</p>
|
|
670
|
+
<button quiet change-fastening ?disabled=${this.locked} @click=${() => (this.changingFastening = true)}>${i18next.t('figure.button.change-fastening')}</button>
|
|
671
|
+
</div>
|
|
672
|
+
${this.renderDetachWords()}
|
|
673
|
+
`
|
|
674
|
+
/* A blank attachment is not a form to decipher. Ask for the thing to attach to first. */
|
|
675
|
+
if (!current && !this.choosingFastening && !f.target)
|
|
676
|
+
return html`
|
|
677
|
+
<p hint>${i18next.t('figure.text.fasten-start')}</p>
|
|
678
|
+
<button act ?disabled=${this.locked} @click=${() => (this.choosingFastening = true)}>
|
|
679
|
+
<md-icon>link</md-icon>${i18next.t('figure.button.fasten-start')}
|
|
680
|
+
</button>
|
|
681
|
+
`
|
|
682
|
+
return html`
|
|
683
|
+
${current ? nothing : html`<p fasten-mode>${editing}</p>`}
|
|
684
|
+
${overhang}
|
|
685
|
+
${held ? this.renderDetachWords() : nothing}
|
|
643
686
|
<div form>
|
|
644
687
|
<span>${i18next.t('figure.label.target-part')}</span>
|
|
645
688
|
<select @change=${(e: Event) => {
|
|
@@ -46,6 +46,11 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
|
|
|
46
46
|
:host {
|
|
47
47
|
display: block;
|
|
48
48
|
}
|
|
49
|
+
/* The rarer half, drawn inside the 「더 보기」 fold: the fold gives the padding and the line. */
|
|
50
|
+
div[more] {
|
|
51
|
+
display: grid;
|
|
52
|
+
gap: 8px;
|
|
53
|
+
}
|
|
49
54
|
div[field] {
|
|
50
55
|
display: grid;
|
|
51
56
|
gap: 4px;
|
|
@@ -164,6 +169,11 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
|
|
|
164
169
|
/** The asset's palette, token → colour. */
|
|
165
170
|
@property({ attribute: false }) palette: Record<string, string> = {}
|
|
166
171
|
@property({ type: Boolean }) locked = false
|
|
172
|
+
/**
|
|
173
|
+
* Which half to draw. `look` is what most parts need -- the colour role and the preset; `more` is what few parts
|
|
174
|
+
* need -- transparency, glow and the segments of a round part -- drawn in the properties' 「더 보기」 fold.
|
|
175
|
+
*/
|
|
176
|
+
@property({ type: String }) show: 'look' | 'more' = 'look'
|
|
167
177
|
/** The glow while its slider is dragged: shown beside it and drawn on the canvas, recorded on release. */
|
|
168
178
|
@state() private glowing?: number
|
|
169
179
|
|
|
@@ -179,6 +189,7 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
|
|
|
179
189
|
const painted = entries.filter(([token]) => !token.startsWith('palette.status-'))
|
|
180
190
|
const lit = entries.filter(([token]) => token.startsWith('palette.status-'))
|
|
181
191
|
const glow = material.emissive
|
|
192
|
+
if (this.show === 'more') return this.renderMore(part, painted, lit)
|
|
182
193
|
return html`<section>
|
|
183
194
|
<h3>${i18next.t('figure.label.part-material')}</h3>
|
|
184
195
|
<div field>
|
|
@@ -201,6 +212,14 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
|
|
|
201
212
|
)}
|
|
202
213
|
</select>
|
|
203
214
|
</div>
|
|
215
|
+
</section>`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Transparency, glow and segments: few parts need them, so they wait in the 「더 보기」 fold. */
|
|
219
|
+
private renderMore(part: FigureAssetGeometry & { id: string }, painted: [string, string][], lit: [string, string][]) {
|
|
220
|
+
const material = part.material
|
|
221
|
+
const glow = material.emissive
|
|
222
|
+
return html`<div more>
|
|
204
223
|
<label toggle>
|
|
205
224
|
<input
|
|
206
225
|
type="checkbox"
|
|
@@ -233,7 +252,7 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
|
|
|
233
252
|
</div>`
|
|
234
253
|
: nothing}
|
|
235
254
|
${this.renderSegments(part)}
|
|
236
|
-
</
|
|
255
|
+
</div>`
|
|
237
256
|
}
|
|
238
257
|
|
|
239
258
|
private renderSegments(part: FigureAssetGeometry & { id: string }) {
|
|
@@ -10,15 +10,18 @@ import { DEFAULT_VIEW, previewBoardDepth, sceneLook, WORKBENCH, type ViewSetting
|
|
|
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, PREVIEW_FACTOR, PREVIEW_WAYS, previewClampsOf, previewInstances, previewTypeOf, setupPartOf } from '@things-factory/figure-viewer/dist-client/preview-model.js'
|
|
13
|
+
import { authoredSizeOf, occupancyAtSize, PREVIEW_FACTOR, PREVIEW_WAYS, previewClampsOf, previewInstances, previewOneInstance, previewTypeOf, setupPartOf, sizeRangeOf, sizeWithinRange } from '@things-factory/figure-viewer/dist-client/preview-model.js'
|
|
14
|
+
import { previewSizeWordsOf } from './preview-size.js'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* The figure placed on a board.
|
|
17
18
|
*
|
|
18
19
|
* The editing canvas shows the figure at the size it was made. Half of what a figure is lies in how it answers a
|
|
19
|
-
* different size, so here the figure being authored is registered as a board registers it and placed as
|
|
20
|
-
* `FigureInstance`
|
|
21
|
-
*
|
|
20
|
+
* different size, so here the figure being authored is registered as a board registers it and placed as a
|
|
21
|
+
* `FigureInstance` the author stretches with a slider per axis the figure lets change, its size and its rows' counts
|
|
22
|
+
* said beside it (first 15 minutes, 2026-10-16: eight variants side by side at one scale made a 60 mm roller on a 3 m
|
|
23
|
+
* conveyor a speck). The eight -- the size it was made, and each of the seven ways of growing one, two or three axes
|
|
24
|
+
* twice as large, four to a row -- are a choice in the legend. The board draws them as it will on a real board --
|
|
22
25
|
* placement, lights, environment, the same renderer.
|
|
23
26
|
*
|
|
24
27
|
* The controls: each value that poses the figure with a slider (and a dial for an angle), each clip's
|
|
@@ -92,6 +95,55 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
92
95
|
flex-wrap: wrap;
|
|
93
96
|
max-width: calc(100% - 16px);
|
|
94
97
|
}
|
|
98
|
+
/* One figure or eight: two words in the legend, the one shown pressed. */
|
|
99
|
+
div[legend] button[way] {
|
|
100
|
+
padding: 0 6px;
|
|
101
|
+
font: inherit;
|
|
102
|
+
color: inherit;
|
|
103
|
+
background: none;
|
|
104
|
+
border: none;
|
|
105
|
+
border-radius: 4px;
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
opacity: 0.7;
|
|
108
|
+
}
|
|
109
|
+
div[legend] button[way][aria-pressed='true'] {
|
|
110
|
+
opacity: 1;
|
|
111
|
+
background-color: rgba(255, 255, 255, 0.18);
|
|
112
|
+
}
|
|
113
|
+
/* The one figure's size: under the legend, in the controls' look. */
|
|
114
|
+
div[sizes] {
|
|
115
|
+
position: absolute;
|
|
116
|
+
top: 40px;
|
|
117
|
+
left: 8px;
|
|
118
|
+
display: grid;
|
|
119
|
+
gap: 6px;
|
|
120
|
+
padding: 8px 10px;
|
|
121
|
+
max-width: calc(100% - 16px);
|
|
122
|
+
box-sizing: border-box;
|
|
123
|
+
color: rgba(255, 255, 255, 0.92);
|
|
124
|
+
background-color: rgba(28, 28, 30, 0.72);
|
|
125
|
+
border-radius: 8px;
|
|
126
|
+
font-size: 0.72rem;
|
|
127
|
+
}
|
|
128
|
+
div[sizes] label {
|
|
129
|
+
display: grid;
|
|
130
|
+
grid-template-columns: 32px 160px 52px;
|
|
131
|
+
align-items: center;
|
|
132
|
+
gap: 8px;
|
|
133
|
+
}
|
|
134
|
+
div[sizes] input {
|
|
135
|
+
width: 100%;
|
|
136
|
+
accent-color: var(--md-sys-color-primary);
|
|
137
|
+
}
|
|
138
|
+
div[sizes] output {
|
|
139
|
+
font-variant-numeric: tabular-nums;
|
|
140
|
+
text-align: right;
|
|
141
|
+
}
|
|
142
|
+
div[sizes] p[said] {
|
|
143
|
+
margin: 0;
|
|
144
|
+
font-variant-numeric: tabular-nums;
|
|
145
|
+
font-weight: 600;
|
|
146
|
+
}
|
|
95
147
|
/* A variant drawn at another size than it asks: the figure does not declare that size. */
|
|
96
148
|
div[legend] small[clamp] {
|
|
97
149
|
margin-left: 4px;
|
|
@@ -216,6 +268,11 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
216
268
|
/** The part chosen in the editor. The size help starts on it, so the help speaks of the part the author was working on. */
|
|
217
269
|
@property() selected = ''
|
|
218
270
|
@state() private setupPart = ''
|
|
271
|
+
/** One figure stretched by sliders, or the eight ways of growing side by side. */
|
|
272
|
+
@state() private way: 'one' | 'eight' = 'one'
|
|
273
|
+
/** The size the one figure is tried at; the authored size when the figure stands up. */
|
|
274
|
+
@state() private trying?: { x: number; y: number; z: number }
|
|
275
|
+
private board = { width: 0, height: 0 }
|
|
219
276
|
|
|
220
277
|
/** What is being tried here now -- clip speeds and control values -- for the AI dock to hear with a request; none when nothing is. */
|
|
221
278
|
tried(): { clips?: Record<string, number>; controls?: Record<string, number> } | undefined {
|
|
@@ -247,7 +304,10 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
247
304
|
${this.cannot
|
|
248
305
|
? html`<div cannot>${this.cannot}</div>`
|
|
249
306
|
: html`<div legend>
|
|
250
|
-
${
|
|
307
|
+
${(['one', 'eight'] as const).map(
|
|
308
|
+
way => html`<button way=${way} aria-pressed=${String(this.way === way)} @click=${() => this.chooseWay(way)}>${i18next.t(`figure.label.preview-way-${way}`)}</button>`
|
|
309
|
+
)}
|
|
310
|
+
${this.way === 'one' ? nothing : PREVIEW_WAYS.map(way => {
|
|
251
311
|
const clamps = clampsByWay[way.name] ?? []
|
|
252
312
|
return html`<span>
|
|
253
313
|
${way.axes.length ? way.name : i18next.t('figure.label.preview-base')}
|
|
@@ -257,6 +317,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
257
317
|
</span>`
|
|
258
318
|
})}
|
|
259
319
|
</div>
|
|
320
|
+
${this.way === 'one' && asset && this.trying ? this.renderSizes(asset, this.trying) : nothing}
|
|
260
321
|
<div foot>${asset?.appearance.length ? html`<div setup>
|
|
261
322
|
<p>${i18next.t('figure.text.preview-setup')}</p>
|
|
262
323
|
<select aria-label=${i18next.t('figure.label.resize-part')} @change=${(e: Event) => this.setupPart = (e.target as HTMLSelectElement).value}>
|
|
@@ -267,6 +328,53 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
267
328
|
${controls.length + clips.length + (asset?.motionGroups?.length ?? 0) + (asset ? valueRowsOf(asset).length : 0) ? this.renderDrivers(asset!, controls, clips) : nothing}</div>`}`
|
|
268
329
|
}
|
|
269
330
|
|
|
331
|
+
/** The one figure's size: a slider per axis the figure lets change, and what it is at the size tried. */
|
|
332
|
+
private renderSizes(asset: FigureAsset, size: { x: number; y: number; z: number }) {
|
|
333
|
+
const say = (key: string, params?: Record<string, string>) => i18next.t(key, params)
|
|
334
|
+
/* A board sizes a figure by the space it takes: with none declared, its box folds and no size can be tried. */
|
|
335
|
+
if (!asset.occupancy) return html`<div sizes><p said>${i18next.t('figure.text.preview-needs-space')}</p></div>`
|
|
336
|
+
return html`<div sizes>
|
|
337
|
+
${(['x', 'y', 'z'] as const).map(axis => {
|
|
338
|
+
const range = sizeRangeOf(asset, axis)
|
|
339
|
+
if (!range) return nothing
|
|
340
|
+
return html`<label>
|
|
341
|
+
<span>${i18next.t(`figure.label.measure-${axis}`)}</span>
|
|
342
|
+
<input type="range" aria-label=${i18next.t(`figure.label.measure-${axis}`)} min=${range.min} max=${range.max} step="any" .value=${String(size[axis])}
|
|
343
|
+
@input=${(e: Event) => this.tryAxis(axis, (e.target as HTMLInputElement).valueAsNumber)}
|
|
344
|
+
@change=${() => this.fitView()} />
|
|
345
|
+
<output>${Math.round(size[axis]).toLocaleString()}</output>
|
|
346
|
+
</label>`
|
|
347
|
+
})}
|
|
348
|
+
<p said>${previewSizeWordsOf(asset, size, say)}</p>
|
|
349
|
+
</div>`
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private chooseWay(way: 'one' | 'eight') {
|
|
353
|
+
if (way === this.way) return
|
|
354
|
+
this.way = way
|
|
355
|
+
this.standUp()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** The one figure at another size on an axis, held to what the figure declares, its centre kept on the board's. */
|
|
359
|
+
private tryAxis(axis: 'x' | 'y' | 'z', value: number) {
|
|
360
|
+
const asset = this.asset
|
|
361
|
+
const used = asset ? sizeWithinRange(asset, axis, value) : undefined
|
|
362
|
+
if (used === undefined || !this.trying) return
|
|
363
|
+
this.trying = { ...this.trying, [axis]: used }
|
|
364
|
+
const instance = this.scene?.root?.components?.[0] as { model?: { width?: number; height?: number }; set(props: Record<string, unknown>): void } | undefined
|
|
365
|
+
/* The instance's box is the space the figure takes at the sizes tried, not the sizes (occupancyAtSize). */
|
|
366
|
+
const box = occupancyAtSize(asset!, this.trying) ?? this.trying
|
|
367
|
+
instance?.set({ width: box.x, depth: box.y, height: box.z })
|
|
368
|
+
const { width = 0, height = 0 } = instance?.model ?? {}
|
|
369
|
+
instance?.set({ left: this.board.width / 2 - width / 2, top: this.board.height / 2 - height / 2 })
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** The figure as stretched, whole in view: done once the slider is let go, so the ground does not slide under a drag. */
|
|
373
|
+
private fitView() {
|
|
374
|
+
this.scene?.fit?.('ratio')
|
|
375
|
+
this.scene?.root?.fitCameraToModel?.()
|
|
376
|
+
}
|
|
377
|
+
|
|
270
378
|
private renderDrivers(asset: FigureAsset, controls: FigureAsset['document']['model']['inputs'], clips: string[]) {
|
|
271
379
|
const changed = controls.filter(c => this.giving[c.id] !== undefined && this.giving[c.id] !== asset.stateDefaults[c.id])
|
|
272
380
|
return html`<div clips>
|
|
@@ -464,7 +572,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
464
572
|
this.teardown()
|
|
465
573
|
return
|
|
466
574
|
}
|
|
467
|
-
const signature = JSON.stringify([asset, view.placement, view.ceilingHeight])
|
|
575
|
+
const signature = JSON.stringify([asset, view.placement, view.ceilingHeight, this.way])
|
|
468
576
|
if (this.scene && this.standing === signature) return
|
|
469
577
|
try {
|
|
470
578
|
compileFigureAsset(asset)
|
|
@@ -490,12 +598,17 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
490
598
|
this.valuing = {}
|
|
491
599
|
registerFigure(type, asset)
|
|
492
600
|
this.registered = type
|
|
493
|
-
|
|
601
|
+
/* Each instance stands in the space the figure takes at the size it is asked (occupancyAtSize), as a board reads it. */
|
|
602
|
+
const boxOf = (asked: { x: number; y: number; z: number }) => occupancyAtSize(asset, asked) ?? asked
|
|
603
|
+
const { components, board } = this.way === 'one' ? previewOneInstance(type, boxOf(size)) : previewInstances(type, size, boxOf)
|
|
604
|
+
this.board = board
|
|
605
|
+
this.trying = this.way === 'one' ? { ...size } : undefined
|
|
494
606
|
const model = {
|
|
495
607
|
type: 'model-layer',
|
|
496
608
|
width: board.width,
|
|
497
609
|
height: board.height,
|
|
498
|
-
|
|
610
|
+
/* As tall as the figure may grow: twice for the eight, the declared height range's top for the one. */
|
|
611
|
+
depth: previewBoardDepth({ placement: asset.placement, base: { y: size.y } }, view.ceilingHeight, this.way === 'one' ? Math.max(1, (sizeRangeOf(asset, 'y')?.max ?? size.y) / size.y) : PREVIEW_FACTOR),
|
|
499
612
|
threed: true,
|
|
500
613
|
sky: 'color',
|
|
501
614
|
...sceneLook(view),
|
|
@@ -174,7 +174,6 @@ export class FigureProperties extends localize(i18next)(LitElement) {
|
|
|
174
174
|
one => html`<option value=${one} ?selected=${one === current}>${i18next.t('figure.label.placement-' + one)}</option>`
|
|
175
175
|
)}
|
|
176
176
|
</select>
|
|
177
|
-
<small>${i18next.t('figure.text.placement-is-intent')}</small>
|
|
178
177
|
</div>
|
|
179
178
|
`
|
|
180
179
|
}
|
|
@@ -212,7 +211,7 @@ export class FigureProperties extends localize(i18next)(LitElement) {
|
|
|
212
211
|
<button act ?disabled=${!facts.declared || !part} @click=${() => this.command({ kind: 'set-mounting-face', part: part!, face })}>
|
|
213
212
|
<md-icon>check</md-icon>${i18next.t('figure.button.set-mounting-face')}
|
|
214
213
|
</button>
|
|
215
|
-
<button
|
|
214
|
+
<button quiet ?disabled=${!mounting} @click=${() => this.command({ kind: 'clear-mounting-face' })}>
|
|
216
215
|
<md-icon>close</md-icon>${i18next.t('figure.button.clear-mounting-face')}
|
|
217
216
|
</button>
|
|
218
217
|
</div>
|
|
@@ -30,6 +30,11 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
|
|
|
30
30
|
:host {
|
|
31
31
|
display: block;
|
|
32
32
|
}
|
|
33
|
+
/* Inside the properties' 「더 보기」 fold: one group among the fold's, not a section of the side of its own. */
|
|
34
|
+
:host([embedded]) section {
|
|
35
|
+
padding: 0;
|
|
36
|
+
border-bottom: 0;
|
|
37
|
+
}
|
|
33
38
|
div[row] {
|
|
34
39
|
display: flex;
|
|
35
40
|
gap: 6px;
|
|
@@ -421,6 +421,9 @@ export class FigureVerdict extends localize(i18next)(LitElement) {
|
|
|
421
421
|
else this.measureTimer = setTimeout(() => this.measure(), MEASURE_SETTLE_MS)
|
|
422
422
|
}
|
|
423
423
|
if (changed || changes.has('figureId') || changes.has('figureType')) this.schedule()
|
|
424
|
+
/* What blocks release, told to the page so the bar can say it beside 「발행」 (first 15 minutes, 2026-10-16). */
|
|
425
|
+
if (changes.has('inspection'))
|
|
426
|
+
this.dispatchEvent(new CustomEvent('release-verdict', { detail: { blocking: (this.inspection?.findings ?? []).filter(finding => finding.blocking) } }))
|
|
424
427
|
}
|
|
425
428
|
|
|
426
429
|
private measure() {
|
|
@@ -762,19 +765,23 @@ export class FigureVerdict extends localize(i18next)(LitElement) {
|
|
|
762
765
|
${blocking.length
|
|
763
766
|
? html`<ul gate>
|
|
764
767
|
<li head>${i18next.t('figure.text.must-fix-to-release')}</li>
|
|
765
|
-
${blocking.map(finding => renderGateFinding(finding, true, this.actionsFor(finding.code)))}
|
|
768
|
+
${blocking.map(finding => renderGateFinding(finding, true, this.actionsFor(finding.code), this.partNames()))}
|
|
766
769
|
</ul>`
|
|
767
770
|
: nothing}
|
|
768
771
|
${advisory.length
|
|
769
772
|
? html`<ul gate>
|
|
770
773
|
<li head quiet>${i18next.t('figure.text.does-not-block')}</li>
|
|
771
|
-
${advisory.map(finding => renderGateFinding(finding, false))}
|
|
774
|
+
${advisory.map(finding => renderGateFinding(finding, false, nothing, this.partNames()))}
|
|
772
775
|
</ul>`
|
|
773
776
|
: nothing}
|
|
774
777
|
</section>
|
|
775
778
|
`
|
|
776
779
|
}
|
|
777
780
|
|
|
781
|
+
private partNames(): string[] {
|
|
782
|
+
return (this.revision?.asset.appearance ?? []).map(look => look.target)
|
|
783
|
+
}
|
|
784
|
+
|
|
778
785
|
/** The same way out the release panel offers for the same two findings — declare the space from the parts. */
|
|
779
786
|
private actionsFor(code: string) {
|
|
780
787
|
if (code !== 'part-outside-occupancy' && code !== 'occupancy-undeclared') return nothing
|
|
@@ -2,6 +2,7 @@ import { css, html, nothing, type TemplateResult } from 'lit'
|
|
|
2
2
|
import { i18next } from '@operato/i18n'
|
|
3
3
|
|
|
4
4
|
import type { FigureGateFinding } from '../types.js'
|
|
5
|
+
import { affectedPartsOf } from './affected-parts.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* One release-gate finding as a line on screen — the measured fact, where, why it matters, and what to change.
|
|
@@ -11,15 +12,22 @@ import type { FigureGateFinding } from '../types.js'
|
|
|
11
12
|
* in each would drift the first time one of them is touched. The caller passes its own action buttons, because
|
|
12
13
|
* what can be done about a finding depends on the screen it is shown on.
|
|
13
14
|
*/
|
|
14
|
-
export function renderGateFinding(
|
|
15
|
+
export function renderGateFinding(
|
|
16
|
+
finding: FigureGateFinding,
|
|
17
|
+
blocking: boolean,
|
|
18
|
+
actions: unknown = nothing,
|
|
19
|
+
/** The figure's part names, when the screen has the figure: only these are shown as affected. */
|
|
20
|
+
parts?: readonly string[]
|
|
21
|
+
): TemplateResult {
|
|
22
|
+
const affected = finding.at ? (parts ? affectedPartsOf(finding.at, parts) : [finding.at]) : []
|
|
15
23
|
return html`
|
|
16
24
|
<li finding ?blocking=${blocking}>
|
|
17
25
|
<span msg>${finding.message}</span>
|
|
18
|
-
${
|
|
26
|
+
${affected.length
|
|
19
27
|
? html`
|
|
20
28
|
<details affected>
|
|
21
29
|
<summary>${i18next.t('figure.text.show-affected-parts')}</summary>
|
|
22
|
-
<
|
|
30
|
+
<span where>${affected.join(' · ')}</span>
|
|
23
31
|
</details>
|
|
24
32
|
`
|
|
25
33
|
: nothing}
|
|
@@ -68,7 +76,7 @@ export const gateFindingStyles = css`
|
|
|
68
76
|
background-color: var(--md-sys-color-error-container);
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
li[finding]
|
|
79
|
+
li[finding] [where] {
|
|
72
80
|
font-family: var(--mono-font, monospace);
|
|
73
81
|
font-size: 0.68rem;
|
|
74
82
|
opacity: 0.85;
|