@things-factory/figure-ui 10.1.27 → 10.1.28
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 +23 -0
- package/client/modeller/base-box.ts +104 -0
- package/client/modeller/figure-animations.ts +81 -8
- package/client/modeller/figure-canvas.ts +18 -3
- package/client/modeller/figure-inspector.ts +151 -2
- package/client/modeller/figure-preview.ts +55 -0
- package/client/modeller/figure-report.ts +9 -4
- package/client/modeller/figure-side.ts +9 -1
- package/client/modeller/figure-source.ts +8 -0
- package/client/modeller/joint-edits.ts +66 -5
- package/client/modeller/joint-gizmo.ts +118 -0
- package/client/modeller/pose-defaults.ts +67 -0
- package/client/modeller/proposal.ts +9 -3
- package/client/modeller/scene-budget.ts +31 -40
- package/client/pages/figure-list-page.ts +4 -9
- package/client/pages/figure-modeller-page.ts +124 -41
- package/dist-client/graphql/index.d.ts +7 -0
- package/dist-client/graphql/index.js +21 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/base-box.d.ts +47 -0
- package/dist-client/modeller/base-box.js +52 -0
- package/dist-client/modeller/base-box.js.map +1 -0
- package/dist-client/modeller/figure-animations.d.ts +15 -0
- package/dist-client/modeller/figure-animations.js +77 -8
- package/dist-client/modeller/figure-animations.js.map +1 -1
- package/dist-client/modeller/figure-canvas.d.ts +9 -0
- package/dist-client/modeller/figure-canvas.js +14 -3
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +26 -0
- package/dist-client/modeller/figure-inspector.js +139 -2
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +15 -0
- package/dist-client/modeller/figure-preview.js +46 -0
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-report.d.ts +3 -2
- package/dist-client/modeller/figure-report.js +9 -4
- package/dist-client/modeller/figure-report.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +5 -1
- package/dist-client/modeller/figure-side.js +12 -0
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +7 -0
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/modeller/joint-edits.d.ts +23 -3
- package/dist-client/modeller/joint-edits.js +53 -5
- package/dist-client/modeller/joint-edits.js.map +1 -1
- package/dist-client/modeller/joint-gizmo.d.ts +31 -0
- package/dist-client/modeller/joint-gizmo.js +104 -0
- package/dist-client/modeller/joint-gizmo.js.map +1 -0
- package/dist-client/modeller/pose-defaults.d.ts +9 -0
- package/dist-client/modeller/pose-defaults.js +35 -0
- package/dist-client/modeller/pose-defaults.js.map +1 -0
- package/dist-client/modeller/proposal.d.ts +1 -1
- package/dist-client/modeller/proposal.js +8 -3
- package/dist-client/modeller/proposal.js.map +1 -1
- package/dist-client/modeller/scene-budget.d.ts +31 -34
- package/dist-client/modeller/scene-budget.js +28 -37
- package/dist-client/modeller/scene-budget.js.map +1 -1
- package/dist-client/pages/figure-list-page.js +4 -9
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +43 -0
- package/dist-client/pages/figure-modeller-page.js +121 -40
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/test/ai-proposal-contract.test.ts +16 -11
- package/test/base-box.test.ts +154 -0
- package/test/joint-edits.test.ts +37 -2
- package/test/joint-gizmo.test.ts +174 -0
- package/test/pose-defaults.test.ts +71 -0
- package/test/scene-budget.test.ts +53 -0
- package/translations/en.json +13 -2
- package/translations/ja.json +13 -2
- package/translations/ko.json +13 -2
- package/translations/ms.json +13 -2
- package/translations/zh.json +13 -2
package/client/graphql/index.ts
CHANGED
|
@@ -323,6 +323,29 @@ export async function inspectFigure(id: string): Promise<FigureInspection> {
|
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
/** 발행된 판들 — 최근 것부터 열 개. */
|
|
326
|
+
/**
|
|
327
|
+
* 직전 발행판의 정본. 한 번도 발행 안 했으면 `undefined`.
|
|
328
|
+
*
|
|
329
|
+
* 저작 중에 「이 고침이 이미 놓인 것을 깨나」를 그 자리에서 보여 주려면 견줄 것이 필요하다. 저장
|
|
330
|
+
* 전 초안은 서버가 모르므로 판정도 화면에서 한다 — 형식의 `contractChanges` 를 같이 쓴다.
|
|
331
|
+
*/
|
|
332
|
+
export async function fetchReleasedSource(id: string): Promise<string | undefined> {
|
|
333
|
+
const response = await client.query({
|
|
334
|
+
query: gql`
|
|
335
|
+
query FigureReleased($id: String!) {
|
|
336
|
+
figureReleased(id: $id) {
|
|
337
|
+
version
|
|
338
|
+
source
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
`,
|
|
342
|
+
variables: { id },
|
|
343
|
+
fetchPolicy: 'network-only'
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
return unwrap<{ source?: string }>(response, 'figureReleased')?.source ?? undefined
|
|
347
|
+
}
|
|
348
|
+
|
|
326
349
|
export async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {
|
|
327
350
|
const response = await client.query({
|
|
328
351
|
query: gql`
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FigureDraft, PartModel } from './figure-source.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 기준 상자를 고친다 — **부품은 제자리에 둔 채.**
|
|
9
|
+
*
|
|
10
|
+
* ## 왜 화면에 있어야 하나
|
|
11
|
+
*
|
|
12
|
+
* 기준 상자는 도면에 놓일 때의 배율을 정하는 수다(ADR-0044 ①). 부품이 그 상자를 넘으면 발행이
|
|
13
|
+
* 막히고, 고치는 길은 「상자 숫자 하나를 키우는 것」인데 **화면에 그 칸이 없었다.** 저작자는
|
|
14
|
+
* 넘쳤다는 말만 듣고 고칠 자리를 못 찾았다.
|
|
15
|
+
*
|
|
16
|
+
* ## 상자를 키우면 부품이 움직인다 — 그것을 되돌린다
|
|
17
|
+
*
|
|
18
|
+
* 저작면의 부품 자리는 판의 왼쪽 위에서 잰 값이고, 저장 형식의 자리는 **상자의 한가운데**에서 잰
|
|
19
|
+
* 값이다(`toFigureSource` 가 절반을 뺀다). 그래서 상자만 키우면 한가운데가 옮겨 가고 부품이 전부
|
|
20
|
+
* 반대로 밀린다. 여기서 늘어난 절반만큼 부품을 함께 옮겨, 저장되는 자리가 한 값도 안 바뀌게 한다.
|
|
21
|
+
*
|
|
22
|
+
* ## 「부품에 맞추기」는 한가운데를 지킨다
|
|
23
|
+
*
|
|
24
|
+
* 부품을 감싸는 가장 작은 상자를 만들 때, 부품 쪽을 **옮기지 않는다.** 한가운데에서 가장 먼
|
|
25
|
+
* 부품까지의 거리를 양쪽으로 잡는다. 부품이 한쪽으로 치우쳐 있으면 반대쪽에 여유가 남는데,
|
|
26
|
+
* 그것이 사실이다 — 치우친 것을 가운데로 끌어오면 저작자가 놓은 자리가 소리 없이 달라지고,
|
|
27
|
+
* 발행된 도형에서는 문(port)의 자리가 함께 움직인다.
|
|
28
|
+
*
|
|
29
|
+
* 높이는 밑면이 바닥이라 한쪽만 늘린다. 바닥 아래로 내려간 부품이 있으면 그 부품은 상자 밖에
|
|
30
|
+
* 남는다 — 그것을 담으려면 부품을 올려야 하고, 그것은 저작자가 정할 일이다. 발행 관문이 이미
|
|
31
|
+
* `part-outside-base` 로 말한다.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export interface BaseSize {
|
|
35
|
+
/** 저작면의 가로 — 형식의 `base.x`. */
|
|
36
|
+
width: number
|
|
37
|
+
/** 저작면의 세로 — 형식의 `base.z`. */
|
|
38
|
+
height: number
|
|
39
|
+
/** 높이 — 형식의 `base.y`. */
|
|
40
|
+
depth: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 상자는 밀리미터 하나보다 작아질 수 없다. 0 이 되면 배율이 0 으로 나뉜다. */
|
|
44
|
+
const LEAST = 1
|
|
45
|
+
|
|
46
|
+
const round = (value: number) => Math.round(value)
|
|
47
|
+
|
|
48
|
+
export const baseOf = (draft: FigureDraft): BaseSize => ({
|
|
49
|
+
width: draft.width,
|
|
50
|
+
height: draft.height,
|
|
51
|
+
depth: draft.depth
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
/** 부품 전부를 담는 가장 작은 상자. 부품이 없으면 잴 것이 없다. */
|
|
55
|
+
export function baseFittingParts(draft: FigureDraft, parts: readonly PartModel[]): BaseSize | undefined {
|
|
56
|
+
if (parts.length === 0) return undefined
|
|
57
|
+
|
|
58
|
+
const middle = { x: draft.width / 2, y: draft.height / 2 }
|
|
59
|
+
let half = { x: 0, y: 0 }
|
|
60
|
+
let tallest = 0
|
|
61
|
+
|
|
62
|
+
for (const part of parts) {
|
|
63
|
+
half = {
|
|
64
|
+
x: Math.max(half.x, Math.abs(part.left - middle.x), Math.abs(part.left + part.width - middle.x)),
|
|
65
|
+
y: Math.max(half.y, Math.abs(part.top - middle.y), Math.abs(part.top + part.height - middle.y))
|
|
66
|
+
}
|
|
67
|
+
tallest = Math.max(tallest, part.zPos + part.depth)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
width: Math.max(LEAST, round(half.x * 2)),
|
|
72
|
+
height: Math.max(LEAST, round(half.y * 2)),
|
|
73
|
+
depth: Math.max(LEAST, round(tallest))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 상자를 이 크기로. 부품은 저장되는 자리가 그대로이도록 함께 옮긴다.
|
|
79
|
+
*
|
|
80
|
+
* 달라질 것이 없으면 **받은 것을 그대로 돌려준다** — 고친 것이 없는데 「저장 안 됨」이 붙지 않게.
|
|
81
|
+
*/
|
|
82
|
+
export function resizeBase(
|
|
83
|
+
draft: FigureDraft,
|
|
84
|
+
parts: readonly PartModel[],
|
|
85
|
+
next: BaseSize
|
|
86
|
+
): { draft: FigureDraft; parts: readonly PartModel[] } {
|
|
87
|
+
const size = {
|
|
88
|
+
width: Math.max(LEAST, round(next.width)),
|
|
89
|
+
height: Math.max(LEAST, round(next.height)),
|
|
90
|
+
depth: Math.max(LEAST, round(next.depth))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (size.width === draft.width && size.height === draft.height && size.depth === draft.depth) {
|
|
94
|
+
return { draft, parts }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const shift = { x: (size.width - draft.width) / 2, y: (size.height - draft.height) / 2 }
|
|
98
|
+
const moved =
|
|
99
|
+
shift.x === 0 && shift.y === 0
|
|
100
|
+
? parts
|
|
101
|
+
: parts.map(part => ({ ...part, left: part.left + shift.x, top: part.top + shift.y }))
|
|
102
|
+
|
|
103
|
+
return { draft: { ...draft, ...size }, parts: moved }
|
|
104
|
+
}
|
|
@@ -130,6 +130,18 @@ function freeName(taken: readonly { name: string }[], stem: string): string {
|
|
|
130
130
|
for (let n = 2; ; n++) if (!used.has(`${stem}-${n}`)) return `${stem}-${n}`
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* 파라미터 범위에 적히는 단위 — **권하는 것이지 가두는 것이 아니다.**
|
|
135
|
+
*
|
|
136
|
+
* 관절이 만드는 파라미터가 `deg` 와 `mm` 을 쓰고(회전과 직선), 그리퍼·문 같은 값은 `%` 를 쓴다.
|
|
137
|
+
* 속도는 `mm/s` 와 `rpm`, 시간은 `s` 다. 이 여섯이 지금 쓰이는 전부이고, 목록에 없는 말을 적는
|
|
138
|
+
* 길은 그대로 열려 있다.
|
|
139
|
+
*/
|
|
140
|
+
const PARAMETER_UNITS = ['deg', 'mm', '%', 'mm/s', 'rpm', 's'] as const
|
|
141
|
+
|
|
142
|
+
/** 목록 대신 손으로 적겠다는 표. `select` 의 값으로 쓴다. */
|
|
143
|
+
const UNIT_TYPE_IT = '__type-it'
|
|
144
|
+
|
|
133
145
|
@customElement('figure-animations')
|
|
134
146
|
export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
135
147
|
static styles = [
|
|
@@ -225,6 +237,21 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
225
237
|
min-width: 3.2em;
|
|
226
238
|
color: var(--md-sys-color-on-surface-variant);
|
|
227
239
|
}
|
|
240
|
+
/*
|
|
241
|
+
칸이 줄어들 줄 알아야 한다. 숫자 입력칸은 제 기본 너비(20자 남짓)를 갖고 있고 flex 는
|
|
242
|
+
기본 크기 아래로는 안 줄이므로, 범위처럼 한 줄에 셋이 서는 줄은 패널 밖으로 밀려
|
|
243
|
+
오른쪽 칸이 잘렸다.
|
|
244
|
+
*/
|
|
245
|
+
div[row] > input {
|
|
246
|
+
flex: 1 1 0;
|
|
247
|
+
min-width: 0;
|
|
248
|
+
}
|
|
249
|
+
/* 단위는 「mm」 정도의 짧은 말이라 숫자 칸보다 좁게 둔다. 고르는 칸도 같은 폭이다. */
|
|
250
|
+
div[row] > input[unit],
|
|
251
|
+
div[row] > select[unit] {
|
|
252
|
+
flex: 0 1 5.5em;
|
|
253
|
+
min-width: 0;
|
|
254
|
+
}
|
|
228
255
|
|
|
229
256
|
/* ── keyframe ─────────────────────────────────────────────────── */
|
|
230
257
|
|
|
@@ -341,6 +368,9 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
341
368
|
@state() private open = ''
|
|
342
369
|
@state() private recipeTarget = ''
|
|
343
370
|
|
|
371
|
+
/** 단위를 손으로 적는 중인 파라미터. 목록에 없는 말을 적으려고 연 칸이다. */
|
|
372
|
+
@state() private typingUnit = new Set<number>()
|
|
373
|
+
|
|
344
374
|
private get clips(): Clip[] {
|
|
345
375
|
return this.draft?.animations ?? []
|
|
346
376
|
}
|
|
@@ -542,14 +572,7 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
542
572
|
aria-label="max"
|
|
543
573
|
@change=${(e: Event) => this.setRange(at, { max: Number((e.target as HTMLInputElement).value) || 0 })}
|
|
544
574
|
/>
|
|
545
|
-
|
|
546
|
-
unit
|
|
547
|
-
.value=${live(range.unit ?? '')}
|
|
548
|
-
placeholder="mm"
|
|
549
|
-
aria-label="unit"
|
|
550
|
-
title=${i18next.t('figure.text.unit-is-a-word-not-a-rule')}
|
|
551
|
-
@change=${(e: Event) => this.setRange(at, { unit: (e.target as HTMLInputElement).value.trim() })}
|
|
552
|
-
/>
|
|
575
|
+
${this.renderUnit(at, range.unit ?? '')}
|
|
553
576
|
</div>
|
|
554
577
|
${backwards ? html`<p warn>${i18next.t('figure.text.range-needs-room')}</p>` : nothing}
|
|
555
578
|
|
|
@@ -1009,6 +1032,56 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
1009
1032
|
this.patchParameter(at, { name: name.trim() })
|
|
1010
1033
|
}
|
|
1011
1034
|
|
|
1035
|
+
/**
|
|
1036
|
+
* 단위 칸 — **목록이 보이는 채로, 적는 길도 열어 둔다.**
|
|
1037
|
+
*
|
|
1038
|
+
* 처음에는 `datalist` 를 붙였다. 그것은 브라우저가 쥐고 있는 기능이라 **눌러 보기 전에는 목록이
|
|
1039
|
+
* 있는지조차 안 보인다** — 화살표도 없고, 어디에 어떻게 뜨는지도 브라우저가 정한다. 고를 수
|
|
1040
|
+
* 있다는 것을 알리는 것이 이 칸의 목적이므로 `select` 로 바꿨다.
|
|
1041
|
+
*
|
|
1042
|
+
* 형식은 단위를 규칙으로 쓰지 않으므로(표시용 말이다) 목록에 가두지 않는다. 마지막 줄이
|
|
1043
|
+
* 「직접 적기」이고, 목록에 없는 말이 이미 적혀 있으면 처음부터 적는 칸으로 연다.
|
|
1044
|
+
*/
|
|
1045
|
+
private renderUnit(at: number, unit: string) {
|
|
1046
|
+
const listed = (PARAMETER_UNITS as readonly string[]).includes(unit)
|
|
1047
|
+
const typing = this.typingUnit.has(at) || (unit !== '' && !listed)
|
|
1048
|
+
|
|
1049
|
+
if (typing) {
|
|
1050
|
+
return html`<input
|
|
1051
|
+
unit
|
|
1052
|
+
.value=${live(unit)}
|
|
1053
|
+
placeholder="mm"
|
|
1054
|
+
aria-label="unit"
|
|
1055
|
+
title=${i18next.t('figure.text.unit-is-a-word-not-a-rule')}
|
|
1056
|
+
@change=${(e: Event) => this.setRange(at, { unit: (e.target as HTMLInputElement).value.trim() })}
|
|
1057
|
+
/>`
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
return html`<select
|
|
1061
|
+
unit
|
|
1062
|
+
aria-label="unit"
|
|
1063
|
+
title=${i18next.t('figure.text.unit-is-a-word-not-a-rule')}
|
|
1064
|
+
@change=${(e: Event) => this.chooseUnit(at, (e.target as HTMLSelectElement).value)}
|
|
1065
|
+
>
|
|
1066
|
+
<option value="" ?selected=${unit === ''}>${i18next.t('figure.label.unit-none')}</option>
|
|
1067
|
+
${PARAMETER_UNITS.map(one => html`<option value=${one} ?selected=${one === unit}>${one}</option>`)}
|
|
1068
|
+
<option value=${UNIT_TYPE_IT}>${i18next.t('figure.label.unit-type-it')}</option>
|
|
1069
|
+
</select>`
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/** 고른 것을 넣는다. 「직접 적기」는 값이 아니라 **칸을 바꾸는 것**이다. */
|
|
1073
|
+
private chooseUnit(at: number, value: string): void {
|
|
1074
|
+
if (value === UNIT_TYPE_IT) {
|
|
1075
|
+
this.typingUnit = new Set([...this.typingUnit, at])
|
|
1076
|
+
return
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const next = new Set(this.typingUnit)
|
|
1080
|
+
next.delete(at)
|
|
1081
|
+
this.typingUnit = next
|
|
1082
|
+
this.setRange(at, { unit: value })
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1012
1085
|
private setRange(at: number, patch: Partial<Parameter['range']>): void {
|
|
1013
1086
|
const parameter = this.parameters[at]
|
|
1014
1087
|
if (!parameter) return
|
|
@@ -192,6 +192,14 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
192
192
|
/** 지금 고른 부품 이름. 목록에서 고른 것을 캔버스에 알린다. */
|
|
193
193
|
@property({ type: String }) picked?: string
|
|
194
194
|
|
|
195
|
+
/**
|
|
196
|
+
* 관절이 도는 자리를 그리는 부채꼴(`joint-gizmo.ts`). 저작면에만 있고 저장되지 않는다.
|
|
197
|
+
*
|
|
198
|
+
* 부품과 같은 길로 세운다 — 씬에 겹쳐 그리는 통로를 따로 뚫지 않는다. `locked` 라 클릭도
|
|
199
|
+
* 마퀴도 집지 않는다.
|
|
200
|
+
*/
|
|
201
|
+
@property({ type: Object }) gizmo?: PartModel
|
|
202
|
+
|
|
195
203
|
/**
|
|
196
204
|
* 보기 전용 — 씬에서 일어난 일을 밖에 알리지 않는다.
|
|
197
205
|
*
|
|
@@ -278,6 +286,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
278
286
|
|
|
279
287
|
if (changed.has('revision')) this.standing = undefined
|
|
280
288
|
if (changed.has('draft') || changed.has('parts') || changed.has('figureId') || changed.has('revision')) this.standUp()
|
|
289
|
+
else if (changed.has('gizmo')) this.syncParts()
|
|
281
290
|
if (changed.has('view')) this.applyView()
|
|
282
291
|
if (changed.has('picked')) this.showPicked()
|
|
283
292
|
}
|
|
@@ -358,7 +367,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
358
367
|
없어야 한다: 좌표 모눈과 겹쳐 싸우고 원점 아래쪽을 가린다.
|
|
359
368
|
*/
|
|
360
369
|
fillStyle: PAPER,
|
|
361
|
-
components: this.
|
|
370
|
+
components: this.standing3d().map(part => ({ ...part }))
|
|
362
371
|
},
|
|
363
372
|
// **편집 모드**로 만든다. 도면의 3D 모델러가 그 모드에서 카메라 시점 버튼 ·
|
|
364
373
|
// 기즈모 모드 · 도움말 · 단축키를 띄운다.
|
|
@@ -449,11 +458,17 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
449
458
|
* 다시 세우면 고른 것과 카메라가 날아가서, 숫자 하나 고칠 때마다 화면이 처음으로
|
|
450
459
|
* 돌아간다.
|
|
451
460
|
*/
|
|
461
|
+
/** 판에 세울 것 전부 — 저작한 부품과, 있으면 관절 부채꼴. */
|
|
462
|
+
private standing3d(): PartModel[] {
|
|
463
|
+
return this.gizmo ? [...this.parts, this.gizmo] : this.parts
|
|
464
|
+
}
|
|
465
|
+
|
|
452
466
|
private syncParts() {
|
|
453
467
|
const root = this.scene?.root
|
|
454
468
|
if (!root) return
|
|
455
469
|
|
|
456
|
-
const
|
|
470
|
+
const all = this.standing3d()
|
|
471
|
+
const wanted = new Map(all.map(part => [part.name, part]))
|
|
457
472
|
const standing = this.partsOnCanvas()
|
|
458
473
|
|
|
459
474
|
// 없어진 것을 제거한다
|
|
@@ -464,7 +479,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
464
479
|
}
|
|
465
480
|
}
|
|
466
481
|
|
|
467
|
-
for (const part of
|
|
482
|
+
for (const part of all) {
|
|
468
483
|
const found = standing.get(part.name)
|
|
469
484
|
if (found) {
|
|
470
485
|
/*
|
|
@@ -20,6 +20,7 @@ import { ANCHOR_RULES, JOINT_TYPES, PART_ROLES, REPEAT_LIMIT, anchorOfPart, repe
|
|
|
20
20
|
import type {
|
|
21
21
|
AnchorRule,
|
|
22
22
|
Axis,
|
|
23
|
+
ContractChange,
|
|
23
24
|
FigurePart,
|
|
24
25
|
FigurePlacement,
|
|
25
26
|
JointType,
|
|
@@ -34,6 +35,8 @@ import { partToFigure, partFromFigure } from './figure-source.js'
|
|
|
34
35
|
import type { FigureDraft, PartModel } from './figure-source.js'
|
|
35
36
|
import { canMirror } from './part-edits.js'
|
|
36
37
|
import { jointOf, parentChoices, patchJoint, setJointType } from './joint-edits.js'
|
|
38
|
+
import { baseFittingParts, baseOf, resizeBase } from './base-box.js'
|
|
39
|
+
import type { BaseSize } from './base-box.js'
|
|
37
40
|
|
|
38
41
|
/** Axis choices for a joint. Written as a direction the author reads, stored as a vector. */
|
|
39
42
|
const JOINT_AXES: readonly { key: string; axis: Vec3 }[] = [
|
|
@@ -316,9 +319,60 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
316
319
|
grid-template-columns: repeat(3, 1fr);
|
|
317
320
|
gap: 4px;
|
|
318
321
|
}
|
|
322
|
+
/*
|
|
323
|
+
값이 둘인 줄은 셋으로 나누지 않는다. 가동 범위를 세 칸짜리 줄에 넣었더니 칸이 폭의 1/3 이라
|
|
324
|
+
「최소」라는 말과 -120 이 한 칸에 못 들어가 숫자가 잘렸다.
|
|
325
|
+
*/
|
|
326
|
+
div[double] {
|
|
327
|
+
display: grid;
|
|
328
|
+
grid-template-columns: repeat(2, 1fr);
|
|
329
|
+
gap: 4px;
|
|
330
|
+
}
|
|
331
|
+
/* 칸 아래에 붙는 한 줄짜리 행동. 칸과 같은 폭으로 둬야 한 묶음으로 읽힌다. */
|
|
332
|
+
button[fit] {
|
|
333
|
+
display: inline-flex;
|
|
334
|
+
align-items: center;
|
|
335
|
+
justify-content: center;
|
|
336
|
+
gap: 4px;
|
|
337
|
+
width: 100%;
|
|
338
|
+
margin-top: 4px;
|
|
339
|
+
padding: 3px 8px;
|
|
340
|
+
font: inherit;
|
|
341
|
+
font-size: 0.72rem;
|
|
342
|
+
color: var(--md-sys-color-on-surface);
|
|
343
|
+
background-color: var(--md-sys-color-surface-container);
|
|
344
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
345
|
+
border-radius: 6px;
|
|
346
|
+
cursor: pointer;
|
|
347
|
+
}
|
|
348
|
+
/* 고친 칸 바로 아래에 붙는 말. 막는 것은 오류 색, 알리는 것은 조용한 색. */
|
|
349
|
+
p[contract] {
|
|
350
|
+
display: flex;
|
|
351
|
+
gap: 4px;
|
|
352
|
+
margin: 4px 0 0;
|
|
353
|
+
font-size: 0.68rem;
|
|
354
|
+
line-height: 1.4;
|
|
355
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
356
|
+
}
|
|
357
|
+
p[contract][blocking] {
|
|
358
|
+
color: var(--md-sys-color-error);
|
|
359
|
+
}
|
|
360
|
+
p[contract] md-icon {
|
|
361
|
+
--md-icon-size: 14px;
|
|
362
|
+
flex: none;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
button[fit]:hover {
|
|
366
|
+
background-color: var(--md-sys-color-surface-container-high);
|
|
367
|
+
}
|
|
368
|
+
button[fit] md-icon {
|
|
369
|
+
--md-icon-size: 15px;
|
|
370
|
+
}
|
|
319
371
|
div[field] + div[field],
|
|
320
372
|
div[triple] + div[field],
|
|
321
373
|
div[field] + div[triple],
|
|
374
|
+
div[double] + div[field],
|
|
375
|
+
div[field] + div[double],
|
|
322
376
|
div[inline-field] + div[inline-field],
|
|
323
377
|
div[inline-field] + div[field],
|
|
324
378
|
div[field] + div[inline-field],
|
|
@@ -815,6 +869,16 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
815
869
|
@property({ type: Object }) draft?: FigureDraft
|
|
816
870
|
/** 초안 위의 부품들. 씬 모델 그대로다. */
|
|
817
871
|
@property({ type: Array }) parts: PartModel[] = []
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* 이 고침이 이미 놓인 인스턴스에 무엇을 하나 — 발행된 도형에만 있다(ADR-0063 결정 3).
|
|
875
|
+
*
|
|
876
|
+
* 발행 문에서만 말하면 저작자는 열 군데를 고친 뒤에 듣는다. 고치는 칸 옆에서 말한다.
|
|
877
|
+
*/
|
|
878
|
+
@property({ type: Array }) contract: ContractChange[] = []
|
|
879
|
+
|
|
880
|
+
/** 견주지 못한 까닭. 빈 목록과 뜻이 다르다 — 「깨지 않는다」가 아니라 「모른다」다. */
|
|
881
|
+
@property({ type: String }) contractFailure?: string
|
|
818
882
|
@property({ type: Number }) selected = -1
|
|
819
883
|
|
|
820
884
|
/** `part` 는 HTMLElement 의 속성(DOMTokenList)이다 — 가리면 형이 어긋난다. */
|
|
@@ -976,7 +1040,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
976
1040
|
? html`
|
|
977
1041
|
<div vector-row>
|
|
978
1042
|
<label>${i18next.t('figure.label.joint-limits-' + unit)}</label>
|
|
979
|
-
<div
|
|
1043
|
+
<div double>
|
|
980
1044
|
${this.number(i18next.t('figure.label.joint-limit-min'), joint.limits.min, value =>
|
|
981
1045
|
this.tellDraft(patchJoint(this.draft!, joint.name, { limits: { ...joint.limits!, min: value } }))
|
|
982
1046
|
)}
|
|
@@ -1009,9 +1073,73 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
1009
1073
|
})
|
|
1010
1074
|
}
|
|
1011
1075
|
|
|
1076
|
+
/**
|
|
1077
|
+
* 기준 상자를 고친다 — 부품은 저장되는 자리에 그대로 둔 채(`base-box.ts`).
|
|
1078
|
+
*
|
|
1079
|
+
* 초안과 부품이 함께 바뀌므로 둘을 한 번에 올린다. 따로 올리면 되돌리기가 둘로 갈리고, 그 사이
|
|
1080
|
+
* 한 번만 되돌린 화면은 부품이 반쯤 밀린 모습이 된다.
|
|
1081
|
+
*/
|
|
1082
|
+
/**
|
|
1083
|
+
* 기준 상자를 고친 것이 **이미 놓인 인스턴스에 무엇을 하나** — 고친 칸 바로 아래에서 말한다.
|
|
1084
|
+
*
|
|
1085
|
+
* 막는 것과 알리는 것을 가른다. 비율이 달라지면 놓인 것이 찌그러지므로 발행이 막히고, 크기만
|
|
1086
|
+
* 달라지는 것은 실척이 어긋날 뿐이라 알리기만 한다. 까닭은 형식이 적은 것을 그대로 옮긴다 —
|
|
1087
|
+
* 여기서 다시 쓰면 두 벌이 되고 곧 갈린다.
|
|
1088
|
+
*/
|
|
1089
|
+
private renderBaseContract() {
|
|
1090
|
+
if (this.contractFailure) {
|
|
1091
|
+
return html`<p contract>
|
|
1092
|
+
<md-icon>help</md-icon>
|
|
1093
|
+
<span>${i18next.t('figure.text.contract-not-compared', { why: this.contractFailure })}</span>
|
|
1094
|
+
</p>`
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const mine = this.contract.filter(one => one.code === 'base-ratio-changed' || one.code === 'base-size-changed')
|
|
1098
|
+
if (mine.length === 0) return nothing
|
|
1099
|
+
|
|
1100
|
+
return html`${mine.map(
|
|
1101
|
+
one => html`<p contract ?blocking=${one.blocking}>
|
|
1102
|
+
<md-icon>${one.blocking ? 'block' : 'info'}</md-icon>
|
|
1103
|
+
<span><b>${one.message}</b> ${one.why}</span>
|
|
1104
|
+
</p>`
|
|
1105
|
+
)}`
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
private resizeBase(patch: Partial<BaseSize>) {
|
|
1109
|
+
if (!this.draft) return
|
|
1110
|
+
|
|
1111
|
+
const next = resizeBase(this.draft, this.parts, { ...baseOf(this.draft), ...patch })
|
|
1112
|
+
if (next.draft === this.draft) return
|
|
1113
|
+
|
|
1114
|
+
this.dispatchEvent(
|
|
1115
|
+
new CustomEvent('resize-base', {
|
|
1116
|
+
detail: { draft: next.draft, parts: next.parts },
|
|
1117
|
+
bubbles: true,
|
|
1118
|
+
composed: true
|
|
1119
|
+
})
|
|
1120
|
+
)
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/** 부품 전부를 감싸는 가장 작은 상자로. 맞출 것이 없으면 아무 일도 안 한다. */
|
|
1124
|
+
private fitBaseToParts() {
|
|
1125
|
+
if (!this.draft) return
|
|
1126
|
+
const fitted = baseFittingParts(this.draft, this.parts)
|
|
1127
|
+
if (fitted) this.resizeBase(fitted)
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1012
1130
|
private setJointType(part: FigurePart, value: string) {
|
|
1013
1131
|
if (!this.draft) return
|
|
1014
|
-
|
|
1132
|
+
|
|
1133
|
+
/* 붙은 부품을 함께 준다 — 새 관절이 둘이 맞닿는 자리에 서려면 그쪽 치수를 알아야 한다. */
|
|
1134
|
+
const parent = part.parent ? this.parts.find(one => one.name === part.parent) : undefined
|
|
1135
|
+
this.tellDraft(
|
|
1136
|
+
setJointType(
|
|
1137
|
+
this.draft,
|
|
1138
|
+
part,
|
|
1139
|
+
value === '' ? undefined : (value as JointType),
|
|
1140
|
+
parent ? partToFigure(parent, this.draft) : undefined
|
|
1141
|
+
)
|
|
1142
|
+
)
|
|
1015
1143
|
}
|
|
1016
1144
|
|
|
1017
1145
|
private setJointAxis(name: string, key: string) {
|
|
@@ -1147,6 +1275,27 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
1147
1275
|
${this.helpHint('figure.text.placement-explained')}
|
|
1148
1276
|
</div>
|
|
1149
1277
|
|
|
1278
|
+
<!--
|
|
1279
|
+
기준 상자 — **고치는 길이 화면에 있어야 한다.** 부품이 이 상자를 넘으면 발행이 막히고,
|
|
1280
|
+
고치는 방법은 상자 숫자를 키우는 것이다(ADR-0044 ④). 그 칸이 없어서 저작자는 넘쳤다는
|
|
1281
|
+
말만 듣고 고칠 자리를 못 찾았다.
|
|
1282
|
+
-->
|
|
1283
|
+
<div field>
|
|
1284
|
+
<label>${i18next.t('figure.label.base-box')}${this.helpButton('figure.text.base-box-explained')}</label>
|
|
1285
|
+
<div triple>
|
|
1286
|
+
${this.number('X', this.draft?.width ?? 0, value => this.resizeBase({ width: value }), 1)}
|
|
1287
|
+
${this.number('Y', this.draft?.depth ?? 0, value => this.resizeBase({ depth: value }), 1)}
|
|
1288
|
+
${this.number('Z', this.draft?.height ?? 0, value => this.resizeBase({ height: value }), 1)}
|
|
1289
|
+
</div>
|
|
1290
|
+
${this.parts.length > 0
|
|
1291
|
+
? html`<button fit @click=${() => this.fitBaseToParts()} title=${i18next.t('figure.text.fit-base-explained')}>
|
|
1292
|
+
<md-icon>fit_screen</md-icon>${i18next.t('figure.button.fit-base-to-parts')}
|
|
1293
|
+
</button>`
|
|
1294
|
+
: nothing}
|
|
1295
|
+
${this.renderBaseContract()}
|
|
1296
|
+
${this.helpHint('figure.text.base-box-explained')}
|
|
1297
|
+
</div>
|
|
1298
|
+
|
|
1150
1299
|
<div inline-field>
|
|
1151
1300
|
<label>${i18next.t('figure.label.category')}${this.helpButton('figure.text.category-explained')}</label>
|
|
1152
1301
|
<input
|
|
@@ -8,6 +8,8 @@ import { create, registerFigure } from '@hatiolab/things-scene'
|
|
|
8
8
|
|
|
9
9
|
import { DEFAULT_VIEW, previewBoardDepth, sceneLook, WORKBENCH } from './figure-view.js'
|
|
10
10
|
import type { ViewSettings } from './figure-view.js'
|
|
11
|
+
import { poseChanges } from './pose-defaults.js'
|
|
12
|
+
import type { FigureDraft } from './figure-source.js'
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* 저작 중인 것을 **도면에 놓았을 때**의 모습.
|
|
@@ -144,6 +146,24 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
144
146
|
align-items: center;
|
|
145
147
|
gap: 8px;
|
|
146
148
|
}
|
|
149
|
+
/*
|
|
150
|
+
막대 위에 놓는 단추. 자세를 만든 자리에 있어야 「지금 이것을 기본으로」가 읽힌다.
|
|
151
|
+
앉힐 것이 있을 때만 나오므로, 늘 서 있는 칸이 아니다.
|
|
152
|
+
*/
|
|
153
|
+
div[clips] button[pose] {
|
|
154
|
+
align-self: stretch;
|
|
155
|
+
padding: 4px 8px;
|
|
156
|
+
font: inherit;
|
|
157
|
+
font-size: 0.7rem;
|
|
158
|
+
color: rgba(255, 255, 255, 0.92);
|
|
159
|
+
background-color: rgba(255, 255, 255, 0.14);
|
|
160
|
+
border: 1px solid rgba(255, 255, 255, 0.22);
|
|
161
|
+
border-radius: 5px;
|
|
162
|
+
cursor: pointer;
|
|
163
|
+
}
|
|
164
|
+
div[clips] button[pose]:hover {
|
|
165
|
+
background-color: rgba(255, 255, 255, 0.24);
|
|
166
|
+
}
|
|
147
167
|
div[clips] span[drive] {
|
|
148
168
|
opacity: 0.6;
|
|
149
169
|
font-size: 0.66rem;
|
|
@@ -176,6 +196,14 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
176
196
|
/** 저작 중인 것. 저장 형식으로 받는다 — 도면이 소비하는 것이 이 형식이다. */
|
|
177
197
|
@property({ type: Object }) source?: FigureSource
|
|
178
198
|
|
|
199
|
+
/**
|
|
200
|
+
* 저작 중인 초안. **기본값이 지금 무엇인지 알려고만 쥔다** — 고치지 않는다.
|
|
201
|
+
*
|
|
202
|
+
* `source` 로도 파라미터를 읽을 수 있지만, 후보를 미리 볼 때는 `source` 가 후보의 것이고
|
|
203
|
+
* 초안은 열려 있는 도형의 것이다. 「이 자세를 기본으로」는 열려 있는 도형에만 뜻이 있다.
|
|
204
|
+
*/
|
|
205
|
+
@property({ type: Object }) draft?: FigureDraft
|
|
206
|
+
|
|
179
207
|
/**
|
|
180
208
|
* 보기 설정. 저장되지 않는다(`figure-view.ts`).
|
|
181
209
|
*
|
|
@@ -226,8 +254,23 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
226
254
|
clips: NonNullable<FigureSource['animations']>,
|
|
227
255
|
parameters: NonNullable<FigureSource['parameters']>
|
|
228
256
|
) {
|
|
257
|
+
/*
|
|
258
|
+
지금 세운 자세를 기본값으로 앉히는 자리. **막대 옆에 둔다** — 자세를 만든 곳이 여기라,
|
|
259
|
+
값을 읽어 파라미터 칸으로 옮겨 적는 왕복이 없어진다. 앉힐 것이 없으면 아예 안 나온다.
|
|
260
|
+
*/
|
|
261
|
+
const changes = poseChanges(this.draft, this.giving)
|
|
262
|
+
|
|
229
263
|
return html`
|
|
230
264
|
<div clips>
|
|
265
|
+
${changes.length > 0
|
|
266
|
+
? html`<button
|
|
267
|
+
pose
|
|
268
|
+
title=${i18next.t('figure.text.pose-as-default-explained')}
|
|
269
|
+
@click=${() => this.poseAsDefault()}
|
|
270
|
+
>
|
|
271
|
+
${i18next.t('figure.button.pose-as-default')}
|
|
272
|
+
</button>`
|
|
273
|
+
: ''}
|
|
231
274
|
${parameters.map(parameter => {
|
|
232
275
|
const range = parameter.range
|
|
233
276
|
const value = this.giving[parameter.name] ?? parameter.default ?? range?.min ?? 0
|
|
@@ -292,6 +335,18 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
292
335
|
this.tellInstances('parameters', this.giving)
|
|
293
336
|
}
|
|
294
337
|
|
|
338
|
+
/**
|
|
339
|
+
* 세운 자세를 기본값으로 — **셈은 여기서 하지 않는다.**
|
|
340
|
+
*
|
|
341
|
+
* 초안을 쥐고 있는 것은 페이지이고, 되돌리기와 「저장 안 됨」도 거기서 매긴다. 여기서
|
|
342
|
+
* 초안을 만들어 보내면 그 셋이 두 곳으로 갈린다. 민 값만 올린다.
|
|
343
|
+
*/
|
|
344
|
+
private poseAsDefault(): void {
|
|
345
|
+
this.dispatchEvent(
|
|
346
|
+
new CustomEvent('pose-as-default', { detail: { values: { ...this.giving } }, bubbles: true, composed: true })
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
295
350
|
private tellInstances(key: 'clips' | 'parameters', values: Record<string, number>): void {
|
|
296
351
|
for (const component of this.scene?.root?.components ?? []) {
|
|
297
352
|
component.set?.(key, { ...values })
|
|
@@ -9,7 +9,7 @@ import { ScrollbarStyles } from '@operato/styles'
|
|
|
9
9
|
import { blocksRelease, compile, costCurveSvg, costOf, scoreOf, sizingReport, validate } from '@hatiolab/figure-model'
|
|
10
10
|
import type { FigureCost, FigureScore, FigureSource } from '@hatiolab/figure-model'
|
|
11
11
|
|
|
12
|
-
import { capacityOf } from './scene-budget.js'
|
|
12
|
+
import { capacityOf, MEASURED } from './scene-budget.js'
|
|
13
13
|
import { maturityOf } from './maturity.js'
|
|
14
14
|
import type { MaturityAxis } from './maturity.js'
|
|
15
15
|
import type { FigureCapacity } from './scene-budget.js'
|
|
@@ -507,8 +507,9 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
507
507
|
* 직접 답하지 않는다 — 형상이 선언된 한도 안인지를 말할 뿐이다. 그래서 개수를
|
|
508
508
|
* 머리에 올리고 등급은 옆의 badge 로 내린다.
|
|
509
509
|
*
|
|
510
|
-
* 근거를 함께 낸다.
|
|
511
|
-
*
|
|
510
|
+
* 근거를 함께 낸다. 묶음으로 갈리는 개수의 분모는 **실측**이고(2026-09-18, Apple M5), 잰
|
|
511
|
+
* 기계와 날짜를 함께 적는다 — 적지 않으면 어디서나 참인 수로 읽힌다. 삼각형 쪽은 아직 설계
|
|
512
|
+
* 문서의 목표치라 그렇다고 적는다.
|
|
512
513
|
*/
|
|
513
514
|
private renderVerdict() {
|
|
514
515
|
const score = this.score
|
|
@@ -544,7 +545,11 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
544
545
|
per: capacity.per.toLocaleString(),
|
|
545
546
|
budget: capacity.budget.toLocaleString()
|
|
546
547
|
})}
|
|
547
|
-
<em
|
|
548
|
+
<em>
|
|
549
|
+
${capacity.bound === 'groups'
|
|
550
|
+
? i18next.t('figure.text.budget-measured-on', { gpu: MEASURED.gpu, on: MEASURED.on })
|
|
551
|
+
: i18next.t('figure.text.budget-not-measured-yet')}
|
|
552
|
+
</em>
|
|
548
553
|
</p>
|
|
549
554
|
</section>
|
|
550
555
|
`
|
|
@@ -10,7 +10,7 @@ import { css, html, LitElement } from 'lit'
|
|
|
10
10
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
11
11
|
|
|
12
12
|
import { i18next, localize } from '@operato/i18n'
|
|
13
|
-
import type { FigureSource } from '@hatiolab/figure-model'
|
|
13
|
+
import type { ContractChange, FigureSource } from '@hatiolab/figure-model'
|
|
14
14
|
|
|
15
15
|
import type { FigureDraft, PartModel } from './figure-source.js'
|
|
16
16
|
import { DEFAULT_VIEW } from './figure-view.js'
|
|
@@ -159,6 +159,12 @@ export class FigureSide extends localize(i18next)(LitElement) {
|
|
|
159
159
|
@property({ type: Object }) draft?: FigureDraft
|
|
160
160
|
@property({ type: Array }) parts: PartModel[] = []
|
|
161
161
|
@property({ type: Object }) source?: FigureSource
|
|
162
|
+
|
|
163
|
+
/** 이 고침이 이미 놓인 인스턴스에 무엇을 하나(ADR-0063 결정 3). 발행 전 도형에는 비어 있다. */
|
|
164
|
+
@property({ type: Array }) contract: ContractChange[] = []
|
|
165
|
+
|
|
166
|
+
/** 견주지 못한 까닭. 있으면 화면이 「확인하지 못했다」로 말한다. */
|
|
167
|
+
@property({ type: String }) contractFailure?: string
|
|
162
168
|
@property({ type: Number }) selected = -1
|
|
163
169
|
/** 저작 화면의 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다. */
|
|
164
170
|
@property({ type: Object }) view: ViewSettings = DEFAULT_VIEW
|
|
@@ -198,6 +204,8 @@ export class FigureSide extends localize(i18next)(LitElement) {
|
|
|
198
204
|
.figureTags=${this.figureTags}
|
|
199
205
|
.draft=${this.draft}
|
|
200
206
|
.parts=${this.parts}
|
|
207
|
+
.contract=${this.contract}
|
|
208
|
+
.contractFailure=${this.contractFailure}
|
|
201
209
|
.selected=${this.selected}
|
|
202
210
|
></figure-inspector>
|
|
203
211
|
|