@things-factory/figure-ui 10.1.38 → 10.1.40
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/graphql/index.ts +53 -0
- package/client/modeller/figure-history-panel.ts +11 -85
- package/client/modeller/figure-v3-properties.ts +254 -0
- package/client/modeller/figure-v3-verdict.ts +247 -0
- package/client/modeller/gate-findings.ts +105 -0
- package/client/modeller/v3-canvas.ts +27 -3
- package/client/modeller/v3-editor.ts +39 -11
- package/client/modeller/v3-figure-facts.ts +53 -0
- package/client/modeller/v3-occupancy.ts +6 -2
- package/client/pages/figure-v3-modeller-page.ts +250 -17
- package/dist-client/graphql/index.d.ts +8 -0
- package/dist-client/graphql/index.js +51 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.d.ts +6 -0
- package/dist-client/modeller/figure-history-panel.js +9 -85
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-v3-properties.d.ts +46 -0
- package/dist-client/modeller/figure-v3-properties.js +259 -0
- package/dist-client/modeller/figure-v3-properties.js.map +1 -0
- package/dist-client/modeller/figure-v3-verdict.d.ts +50 -0
- package/dist-client/modeller/figure-v3-verdict.js +268 -0
- package/dist-client/modeller/figure-v3-verdict.js.map +1 -0
- package/dist-client/modeller/gate-findings.d.ts +12 -0
- package/dist-client/modeller/gate-findings.js +102 -0
- package/dist-client/modeller/gate-findings.js.map +1 -0
- package/dist-client/modeller/v3-canvas.d.ts +6 -0
- package/dist-client/modeller/v3-canvas.js +27 -3
- package/dist-client/modeller/v3-canvas.js.map +1 -1
- package/dist-client/modeller/v3-editor.js +39 -11
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-figure-facts.d.ts +26 -0
- package/dist-client/modeller/v3-figure-facts.js +34 -0
- package/dist-client/modeller/v3-figure-facts.js.map +1 -0
- package/dist-client/modeller/v3-occupancy.d.ts +5 -1
- package/dist-client/modeller/v3-occupancy.js +4 -2
- package/dist-client/modeller/v3-occupancy.js.map +1 -1
- package/dist-client/pages/figure-v3-modeller-page.d.ts +40 -0
- package/dist-client/pages/figure-v3-modeller-page.js +264 -18
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/test/v3-properties.test.ts +93 -0
- package/test/v3-theme.test.ts +67 -0
- package/test/v3-verdict.test.ts +82 -0
- package/translations/en.json +12 -0
- package/translations/ja.json +12 -0
- package/translations/ko.json +12 -0
- package/translations/ms.json +12 -0
- package/translations/zh.json +12 -0
package/client/graphql/index.ts
CHANGED
|
@@ -335,6 +335,59 @@ export async function inspectFigure(id: string): Promise<FigureInspection> {
|
|
|
335
335
|
return unwrap<FigureInspection>(response, 'inspectFigure')
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
/**
|
|
339
|
+
* 편집 중인 원본의 발행 판정 — 저장하지 않은 것을 잰다.
|
|
340
|
+
*
|
|
341
|
+
* 발행 패널의 `inspectFigure` 는 저장된 원본을 재므로 고친 결과를 저장한 뒤에야 보여 준다. 편집하면서 막히는
|
|
342
|
+
* 것을 보려면 편집 중인 원본을 보내야 한다. 판정은 서버의 같은 문(`inspectAnySource`)이 하고 문장도 같다 —
|
|
343
|
+
* 화면이 제 판정을 따로 가지면 두 자리가 다른 말을 한다. `id` 가 있으면 서버가 그 행의 타입과 직전 발행판을 읽는다.
|
|
344
|
+
*/
|
|
345
|
+
export async function inspectFigureDraft(source: string, id?: string): Promise<FigureInspection> {
|
|
346
|
+
const response = await client.query({
|
|
347
|
+
query: gql`
|
|
348
|
+
query InspectFigureDraft($source: String!, $id: String) {
|
|
349
|
+
inspectFigureDraft(source: $source, id: $id) {
|
|
350
|
+
blocked
|
|
351
|
+
findings {
|
|
352
|
+
code
|
|
353
|
+
message
|
|
354
|
+
why
|
|
355
|
+
how
|
|
356
|
+
at
|
|
357
|
+
fixes {
|
|
358
|
+
findingWay
|
|
359
|
+
part
|
|
360
|
+
axis
|
|
361
|
+
from
|
|
362
|
+
to
|
|
363
|
+
resolved
|
|
364
|
+
safe
|
|
365
|
+
}
|
|
366
|
+
fixPlan {
|
|
367
|
+
resolved
|
|
368
|
+
safe
|
|
369
|
+
fixes {
|
|
370
|
+
findingWay
|
|
371
|
+
part
|
|
372
|
+
axis
|
|
373
|
+
from
|
|
374
|
+
to
|
|
375
|
+
resolved
|
|
376
|
+
safe
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
blocking
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
`,
|
|
384
|
+
variables: { source, id },
|
|
385
|
+
fetchPolicy: 'network-only'
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
return unwrap<FigureInspection>(response, 'inspectFigureDraft')
|
|
389
|
+
}
|
|
390
|
+
|
|
338
391
|
/** 발행된 판들 — 최근 것부터 열 개. */
|
|
339
392
|
/**
|
|
340
393
|
* 직전 발행판의 정본. 한 번도 발행 안 했으면 `undefined`.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '@material/web/icon/icon.js'
|
|
2
2
|
|
|
3
|
+
import { gateFindingStyles, renderGateFinding } from './gate-findings.js'
|
|
3
4
|
import { css, html, LitElement, nothing } from 'lit'
|
|
4
5
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
5
6
|
|
|
@@ -31,6 +32,7 @@ import type { FigureGateFinding, FigureGateFix, FigureGateFixPlan, FigureInspect
|
|
|
31
32
|
@customElement('figure-history-panel')
|
|
32
33
|
export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
33
34
|
static styles = [
|
|
35
|
+
gateFindingStyles,
|
|
34
36
|
ScrollbarStyles,
|
|
35
37
|
css`
|
|
36
38
|
:host {
|
|
@@ -191,77 +193,6 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
191
193
|
게이트 사유. 막는 것은 색으로도 말하고, 알리는 것은 조용히 둔다 — 둘을 같은 톤으로 두면
|
|
192
194
|
「이 중 어느 것이 발행을 막았나」를 사람이 다시 세어야 한다.
|
|
193
195
|
*/
|
|
194
|
-
ul[gate] {
|
|
195
|
-
margin: var(--spacing-small, 6px) 0 0;
|
|
196
|
-
padding: 0;
|
|
197
|
-
list-style: none;
|
|
198
|
-
display: flex;
|
|
199
|
-
flex-direction: column;
|
|
200
|
-
gap: 3px;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
li[head] {
|
|
204
|
-
font: var(--label-font, inherit);
|
|
205
|
-
font-size: 0.72rem;
|
|
206
|
-
color: var(--md-sys-color-error);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
li[head][quiet] {
|
|
210
|
-
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
li[finding] {
|
|
214
|
-
display: flex;
|
|
215
|
-
flex-direction: column;
|
|
216
|
-
align-items: flex-start;
|
|
217
|
-
gap: 3px;
|
|
218
|
-
padding: 3px 6px;
|
|
219
|
-
font-size: 0.72rem;
|
|
220
|
-
line-height: 1.5;
|
|
221
|
-
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
222
|
-
background-color: var(--md-sys-color-surface-container);
|
|
223
|
-
border-radius: 4px;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
li[finding][blocking] {
|
|
227
|
-
color: var(--md-sys-color-on-error-container, var(--md-sys-color-on-surface));
|
|
228
|
-
background-color: var(--md-sys-color-error-container);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
li[finding] code[where] {
|
|
232
|
-
font-family: var(--mono-font, monospace);
|
|
233
|
-
font-size: 0.68rem;
|
|
234
|
-
opacity: 0.85;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
li[finding] details[affected] {
|
|
238
|
-
width: 100%;
|
|
239
|
-
color: inherit;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
li[finding] details[affected] summary {
|
|
243
|
-
cursor: pointer;
|
|
244
|
-
font-weight: 600;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/* 왜 나쁜지는 곁글로 — 읽어야 하지만 잰 것보다 먼저 눈에 들 필요는 없다 */
|
|
248
|
-
li[finding] span[why-line] {
|
|
249
|
-
opacity: 0.8;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/* 무엇을 바꾸면 되나 — 이 줄이 실제로 사람이 할 일이다. 연장 표를 달아 눈에 걸리게 한다 */
|
|
253
|
-
li[finding] span[how-line] {
|
|
254
|
-
display: flex;
|
|
255
|
-
align-items: flex-start;
|
|
256
|
-
gap: 4px;
|
|
257
|
-
padding-top: 2px;
|
|
258
|
-
font-weight: 600;
|
|
259
|
-
}
|
|
260
|
-
li[finding] span[how-line] md-icon {
|
|
261
|
-
--md-icon-size: 13px;
|
|
262
|
-
flex: none;
|
|
263
|
-
margin-top: 1px;
|
|
264
|
-
}
|
|
265
196
|
|
|
266
197
|
/* 설명으로 끝내지 않는다. 이 단추는 원본을 바꾸지 않고 모델러에 후보만 연다. */
|
|
267
198
|
li[finding] button[preview-fix] {
|
|
@@ -332,6 +263,12 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
332
263
|
* 화면이 없는 단추를 그리면 눌러도 아무 일이 없다.
|
|
333
264
|
*/
|
|
334
265
|
@property({ type: String }) format: 'v2' | 'v3' = 'v2'
|
|
266
|
+
/**
|
|
267
|
+
* 행의 타입. 판정 하나(`type-is-placeholder`)가 이것을 본다 — V3 자산은 타입을 원본에 적지 않아서, 타입을 바꾸면
|
|
268
|
+
* 원본도 dirty 도 그대로다. 그래서 이 값이 바뀌면 판정을 다시 묻는다. 안 그러면 타입을 정한 뒤에도 「임시 타입」
|
|
269
|
+
* 판정이 남아 발행 단추가 잠긴 채다.
|
|
270
|
+
*/
|
|
271
|
+
@property({ type: String }) figureType?: string
|
|
335
272
|
/** 직전 발행판의 기준 상자. 외형 변화 안내에서 X·Z 복원 선택지에만 쓴다. */
|
|
336
273
|
@property({ attribute: false }) releasedBase?: { x: number; y: number; z: number }
|
|
337
274
|
|
|
@@ -354,7 +291,7 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
354
291
|
저장이 끝난 순간(dirty 가 풀림) 정본이 바뀌었으니 판정을 다시 묻는다. 저작 중(dirty)에는
|
|
355
292
|
묻지 않는다 — 서버에 있는 것은 저장 전 정본이라, 그 답은 화면에 있는 것과 다른 것을 말한다.
|
|
356
293
|
*/
|
|
357
|
-
if (changes.has('figureId') || changes.has('dirty')) {
|
|
294
|
+
if (changes.has('figureId') || changes.has('dirty') || changes.has('figureType')) {
|
|
358
295
|
void this.inspect()
|
|
359
296
|
}
|
|
360
297
|
}
|
|
@@ -474,17 +411,7 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
474
411
|
* 준다 — 화면이 문장을 지으면 서버가 거절할 때와 다른 말을 하게 된다.
|
|
475
412
|
*/
|
|
476
413
|
private renderFinding(finding: FigureGateFinding, blocking: boolean) {
|
|
477
|
-
return html`
|
|
478
|
-
<li finding ?blocking=${blocking}>
|
|
479
|
-
<span msg>${finding.message}</span>
|
|
480
|
-
${finding.at ? html`
|
|
481
|
-
<details affected>
|
|
482
|
-
<summary>${i18next.t('figure.text.show-affected-parts')}</summary>
|
|
483
|
-
<code where>${finding.at}</code>
|
|
484
|
-
</details>
|
|
485
|
-
` : nothing}
|
|
486
|
-
${finding.why ? html`<span why-line>${finding.why}</span>` : nothing}
|
|
487
|
-
${finding.how ? html`<span how-line><md-icon>build</md-icon>${finding.how}</span>` : nothing}
|
|
414
|
+
return renderGateFinding(finding, blocking, html`
|
|
488
415
|
${this.offersFit(finding.code)
|
|
489
416
|
? html`<button fit-base @click=${() => this.fitBase()}>
|
|
490
417
|
${i18next.t('figure.button.fit-base-to-parts')}
|
|
@@ -522,8 +449,7 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
522
449
|
${i18next.t('figure.button.apply-recommended-fix')}
|
|
523
450
|
</button>
|
|
524
451
|
` : nothing}
|
|
525
|
-
|
|
526
|
-
`
|
|
452
|
+
`)
|
|
527
453
|
}
|
|
528
454
|
|
|
529
455
|
/**
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js'
|
|
2
|
+
|
|
3
|
+
import { css, html, LitElement, nothing } from 'lit'
|
|
4
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
5
|
+
|
|
6
|
+
import { i18next, localize } from '@operato/i18n'
|
|
7
|
+
import { V3_FACES } from '@hatiolab/figure-model'
|
|
8
|
+
import type { V3AuthoringAction, V3Face } from '@hatiolab/figure-model'
|
|
9
|
+
|
|
10
|
+
import { occupancyFromParts } from './v3-occupancy.js'
|
|
11
|
+
import { tagsOf, v3FigureFacts, type Placement, type V3FigureFacts } from './v3-figure-facts.js'
|
|
12
|
+
|
|
13
|
+
const PLACEMENTS: Placement[] = ['floor', 'ceiling', 'center']
|
|
14
|
+
const FACES = Object.keys(V3_FACES) as V3Face[]
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 속성 — the V3 modeller's figure properties (parity row ②).
|
|
18
|
+
*
|
|
19
|
+
* The V2 inspector opens with what the figure is: its name, the face it is placed by, the room it takes up, where
|
|
20
|
+
* the catalogue files it. The V3 page had only the type. The same rows, in the same order, drawn the way the V2
|
|
21
|
+
* inspector draws them.
|
|
22
|
+
*
|
|
23
|
+
* Two kinds of change leave this tab, and it does neither itself:
|
|
24
|
+
* - `v3-command` carries an authoring command (rename, declare the room, the mounting face). The page runs it
|
|
25
|
+
* through the session, so it is one step of the same undo history as every other edit, and is saved with it.
|
|
26
|
+
* - `v3-catalog` carries a change to the row (category, tags). Those are not in the asset; the page writes them
|
|
27
|
+
* to the saved row at once, so they need a saved figure.
|
|
28
|
+
*
|
|
29
|
+
* Not here yet, because V3 has no place for them: editing the declared room by hand (V3 declares it from the
|
|
30
|
+
* parts, as graph expressions), and the detail level (the converter records it as lost). Both wait on a V3 ruling.
|
|
31
|
+
*/
|
|
32
|
+
@customElement('figure-v3-properties')
|
|
33
|
+
export class FigureV3Properties extends localize(i18next)(LitElement) {
|
|
34
|
+
static styles = css`
|
|
35
|
+
:host {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
gap: 12px;
|
|
39
|
+
padding: 12px;
|
|
40
|
+
overflow: auto;
|
|
41
|
+
font-size: 0.76rem;
|
|
42
|
+
color: var(--md-sys-color-on-surface);
|
|
43
|
+
}
|
|
44
|
+
div[field] {
|
|
45
|
+
display: grid;
|
|
46
|
+
gap: 4px;
|
|
47
|
+
}
|
|
48
|
+
div[field] > label {
|
|
49
|
+
font-weight: 600;
|
|
50
|
+
}
|
|
51
|
+
div[row] {
|
|
52
|
+
display: flex;
|
|
53
|
+
gap: 6px;
|
|
54
|
+
align-items: center;
|
|
55
|
+
}
|
|
56
|
+
input,
|
|
57
|
+
select,
|
|
58
|
+
button {
|
|
59
|
+
font: inherit;
|
|
60
|
+
min-width: 0;
|
|
61
|
+
}
|
|
62
|
+
input,
|
|
63
|
+
select {
|
|
64
|
+
padding: 5px 8px;
|
|
65
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
66
|
+
border-radius: 4px;
|
|
67
|
+
background: var(--md-sys-color-surface);
|
|
68
|
+
color: var(--md-sys-color-on-surface);
|
|
69
|
+
}
|
|
70
|
+
div[row] > select {
|
|
71
|
+
flex: 1;
|
|
72
|
+
}
|
|
73
|
+
button {
|
|
74
|
+
display: inline-flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: 4px;
|
|
77
|
+
padding: 4px 10px;
|
|
78
|
+
border: none;
|
|
79
|
+
border-radius: 999em;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
82
|
+
background: var(--md-sys-color-secondary-container);
|
|
83
|
+
}
|
|
84
|
+
button:disabled,
|
|
85
|
+
input:disabled,
|
|
86
|
+
select:disabled {
|
|
87
|
+
opacity: 0.5;
|
|
88
|
+
cursor: default;
|
|
89
|
+
}
|
|
90
|
+
button md-icon {
|
|
91
|
+
font-size: 16px;
|
|
92
|
+
width: 16px;
|
|
93
|
+
height: 16px;
|
|
94
|
+
}
|
|
95
|
+
small {
|
|
96
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
97
|
+
}
|
|
98
|
+
`
|
|
99
|
+
|
|
100
|
+
/** The source being edited. */
|
|
101
|
+
@property({ type: String }) source?: string
|
|
102
|
+
/** The saved row, if any. Category and tags live on it. */
|
|
103
|
+
@property({ type: String }) figureId?: string
|
|
104
|
+
@property({ type: String }) figureCategory?: string
|
|
105
|
+
@property({ type: Array }) figureTags?: string[]
|
|
106
|
+
/** The part and face picked for the mounting face, before 「이 면으로」 is pressed. */
|
|
107
|
+
@state() private pickedPart?: string
|
|
108
|
+
@state() private pickedFace?: V3Face
|
|
109
|
+
|
|
110
|
+
private facts(): V3FigureFacts | undefined {
|
|
111
|
+
try {
|
|
112
|
+
return this.source ? v3FigureFacts(this.source) : undefined
|
|
113
|
+
} catch {
|
|
114
|
+
/* A source the format refuses has no facts to show; the verdict tab says why. */
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
render() {
|
|
120
|
+
const facts = this.facts()
|
|
121
|
+
if (!facts) return nothing
|
|
122
|
+
return html`${this.renderName(facts)} ${this.renderPlacement(facts)} ${this.renderSpace(facts)} ${this.renderMounting(facts)}
|
|
123
|
+
${this.renderCatalog()}`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private renderName(facts: V3FigureFacts) {
|
|
127
|
+
return html`
|
|
128
|
+
<div field>
|
|
129
|
+
<label>${i18next.t('figure.label.name')}</label>
|
|
130
|
+
<input
|
|
131
|
+
.value=${facts.name}
|
|
132
|
+
placeholder=${i18next.t('figure.text.figure-display-name')}
|
|
133
|
+
@change=${(event: Event) => {
|
|
134
|
+
const input = event.target as HTMLInputElement
|
|
135
|
+
if (!input.value.trim()) {
|
|
136
|
+
/* The format refuses a blank name; put the old one back rather than send a command that fails. */
|
|
137
|
+
input.value = facts.name
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
if (input.value.trim() !== facts.name) this.command({ kind: 'rename', name: input.value })
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The face the figure is placed by. In V3 it is part of the room declaration, so choosing one declares the room
|
|
149
|
+
* again from the parts — the only way V3 declares it — with the new face. The hint says so.
|
|
150
|
+
*/
|
|
151
|
+
private renderPlacement(facts: V3FigureFacts) {
|
|
152
|
+
const current = facts.placement ?? 'floor'
|
|
153
|
+
return html`
|
|
154
|
+
<div field>
|
|
155
|
+
<label>${i18next.t('figure.label.figure-placement')}</label>
|
|
156
|
+
<select
|
|
157
|
+
?disabled=${!facts.parts.length}
|
|
158
|
+
@change=${(event: Event) => this.declare((event.target as HTMLSelectElement).value as Placement)}
|
|
159
|
+
>
|
|
160
|
+
${PLACEMENTS.map(
|
|
161
|
+
one => html`<option value=${one} ?selected=${one === current}>${i18next.t('figure.label.placement-' + one)}</option>`
|
|
162
|
+
)}
|
|
163
|
+
</select>
|
|
164
|
+
<small>${i18next.t(facts.parts.length ? 'figure.text.placement-redeclares' : 'figure.text.fit-needs-parts')}</small>
|
|
165
|
+
</div>
|
|
166
|
+
`
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private renderSpace(facts: V3FigureFacts) {
|
|
170
|
+
return html`
|
|
171
|
+
<div field>
|
|
172
|
+
<label>${i18next.t('figure.label.declared-space')}</label>
|
|
173
|
+
<div row>
|
|
174
|
+
<span>${i18next.t(facts.declared ? 'figure.text.declared-space-declared' : 'figure.text.declared-space-none')}</span>
|
|
175
|
+
<button fit ?disabled=${!facts.parts.length} @click=${() => this.declare(facts.placement ?? 'floor')}>
|
|
176
|
+
<md-icon>fit_screen</md-icon>${i18next.t('figure.button.fit-base-to-parts')}
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
`
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private renderMounting(facts: V3FigureFacts) {
|
|
184
|
+
const mounting = facts.mounting
|
|
185
|
+
const part = [this.pickedPart, mounting?.part, facts.parts[0]].find(one => one && facts.parts.includes(one))
|
|
186
|
+
const face: V3Face = this.pickedFace ?? mounting?.face ?? 'bottom'
|
|
187
|
+
return html`
|
|
188
|
+
<div field>
|
|
189
|
+
<label>${i18next.t('figure.label.mounting-face')}</label>
|
|
190
|
+
<div row>
|
|
191
|
+
<select ?disabled=${!facts.declared} @change=${(e: Event) => (this.pickedPart = (e.target as HTMLSelectElement).value)}>
|
|
192
|
+
${facts.parts.map(one => html`<option value=${one} ?selected=${one === part}>${one}</option>`)}
|
|
193
|
+
</select>
|
|
194
|
+
<select ?disabled=${!facts.declared} @change=${(e: Event) => (this.pickedFace = (e.target as HTMLSelectElement).value as V3Face)}>
|
|
195
|
+
${FACES.map(one => html`<option value=${one} ?selected=${one === face}>${i18next.t('figure.label.face-' + one)}</option>`)}
|
|
196
|
+
</select>
|
|
197
|
+
</div>
|
|
198
|
+
<div row>
|
|
199
|
+
<button ?disabled=${!facts.declared || !part} @click=${() => this.command({ kind: 'set-mounting-face', part: part!, face })}>
|
|
200
|
+
${i18next.t('figure.button.set-mounting-face')}
|
|
201
|
+
</button>
|
|
202
|
+
<button ?disabled=${!mounting} @click=${() => this.command({ kind: 'clear-mounting-face' })}>
|
|
203
|
+
${i18next.t('figure.button.clear-mounting-face')}
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
<small>
|
|
207
|
+
${facts.declared
|
|
208
|
+
? mounting
|
|
209
|
+
? `${mounting.part} · ${i18next.t('figure.label.face-' + mounting.face)}`
|
|
210
|
+
: i18next.t('figure.label.mounting-face-none')
|
|
211
|
+
: i18next.t('figure.text.mounting-face-needs-space')}
|
|
212
|
+
</small>
|
|
213
|
+
</div>
|
|
214
|
+
`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private renderCatalog() {
|
|
218
|
+
const saved = !!this.figureId
|
|
219
|
+
return html`
|
|
220
|
+
<div field>
|
|
221
|
+
<label>${i18next.t('figure.label.category')}</label>
|
|
222
|
+
<input
|
|
223
|
+
.value=${this.figureCategory ?? ''}
|
|
224
|
+
?disabled=${!saved}
|
|
225
|
+
placeholder=${i18next.t('figure.text.category-placeholder')}
|
|
226
|
+
@change=${(e: Event) => this.catalog({ category: (e.target as HTMLInputElement).value.trim() })}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
<div field>
|
|
230
|
+
<label>${i18next.t('figure.label.tags')}</label>
|
|
231
|
+
<input
|
|
232
|
+
.value=${(this.figureTags ?? []).join(', ')}
|
|
233
|
+
?disabled=${!saved}
|
|
234
|
+
placeholder=${i18next.t('figure.text.tags-placeholder')}
|
|
235
|
+
@change=${(e: Event) => this.catalog({ tags: tagsOf((e.target as HTMLInputElement).value) })}
|
|
236
|
+
/>
|
|
237
|
+
${saved ? nothing : html`<small>${i18next.t('figure.text.catalog-needs-a-save')}</small>`}
|
|
238
|
+
</div>
|
|
239
|
+
`
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private declare(placement: Placement) {
|
|
243
|
+
if (!this.source) return
|
|
244
|
+
this.command(occupancyFromParts(this.source, placement))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private command(action: V3AuthoringAction) {
|
|
248
|
+
this.dispatchEvent(new CustomEvent('v3-command', { detail: { action }, bubbles: true, composed: true }))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private catalog(patch: { category?: string; tags?: string[] }) {
|
|
252
|
+
this.dispatchEvent(new CustomEvent('v3-catalog', { detail: patch, bubbles: true, composed: true }))
|
|
253
|
+
}
|
|
254
|
+
}
|