@things-factory/figure-ui 10.1.34 → 10.1.36

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.
Files changed (49) hide show
  1. package/client/graphql/index.ts +13 -0
  2. package/client/graphql/v3-store.ts +30 -0
  3. package/client/modeller/figure-history-panel.ts +29 -2
  4. package/client/modeller/v3-canvas.ts +145 -0
  5. package/client/modeller/v3-editor.ts +317 -0
  6. package/client/modeller/v3-product-session.ts +141 -0
  7. package/client/modeller/v3-renderer.ts +61 -0
  8. package/client/pages/figure-modeller-page.ts +50 -3
  9. package/client/pages/figure-v3-modeller-page.ts +117 -0
  10. package/client/types.ts +8 -0
  11. package/dist-client/graphql/index.js +13 -0
  12. package/dist-client/graphql/index.js.map +1 -1
  13. package/dist-client/graphql/v3-store.d.ts +3 -0
  14. package/dist-client/graphql/v3-store.js +32 -0
  15. package/dist-client/graphql/v3-store.js.map +1 -0
  16. package/dist-client/modeller/figure-history-panel.d.ts +2 -0
  17. package/dist-client/modeller/figure-history-panel.js +28 -1
  18. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  19. package/dist-client/modeller/v3-canvas.d.ts +28 -0
  20. package/dist-client/modeller/v3-canvas.js +157 -0
  21. package/dist-client/modeller/v3-canvas.js.map +1 -0
  22. package/dist-client/modeller/v3-editor.d.ts +19 -0
  23. package/dist-client/modeller/v3-editor.js +332 -0
  24. package/dist-client/modeller/v3-editor.js.map +1 -0
  25. package/dist-client/modeller/v3-product-session.d.ts +366 -0
  26. package/dist-client/modeller/v3-product-session.js +130 -0
  27. package/dist-client/modeller/v3-product-session.js.map +1 -0
  28. package/dist-client/modeller/v3-renderer.d.ts +31 -0
  29. package/dist-client/modeller/v3-renderer.js +81 -0
  30. package/dist-client/modeller/v3-renderer.js.map +1 -0
  31. package/dist-client/pages/figure-modeller-page.d.ts +7 -0
  32. package/dist-client/pages/figure-modeller-page.js +55 -2
  33. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  34. package/dist-client/pages/figure-v3-modeller-page.d.ts +26 -0
  35. package/dist-client/pages/figure-v3-modeller-page.js +140 -0
  36. package/dist-client/pages/figure-v3-modeller-page.js.map +1 -0
  37. package/dist-client/tsconfig.tsbuildinfo +1 -1
  38. package/dist-client/types.d.ts +7 -0
  39. package/dist-client/types.js.map +1 -1
  40. package/docs/v3-product-cutover.md +35 -0
  41. package/package.json +4 -4
  42. package/test/v3-product-session.test.ts +133 -0
  43. package/test/v3-product-write.test.ts +48 -0
  44. package/test/v3-renderer.test.ts +24 -0
  45. package/translations/en.json +3 -0
  46. package/translations/ja.json +3 -0
  47. package/translations/ko.json +3 -0
  48. package/translations/ms.json +3 -0
  49. package/translations/zh.json +3 -0
@@ -0,0 +1,332 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import { LitElement, html, css } from 'lit';
3
+ import { customElement, property, state } from 'lit/decorators.js';
4
+ import './v3-canvas.js';
5
+ const names = {
6
+ position: '위치',
7
+ rotation: '회전',
8
+ size: '크기',
9
+ dimension: '치수',
10
+ width: '너비',
11
+ height: '높이',
12
+ depth: '깊이',
13
+ radius: '반지름',
14
+ length: '길이',
15
+ round: '모서리 반경',
16
+ left: '왼쪽',
17
+ right: '오른쪽',
18
+ bottom: '아랫면',
19
+ top: '윗면',
20
+ openingLeft: '개구부 왼쪽',
21
+ openingRight: '개구부 오른쪽',
22
+ openingTop: '개구부 윗면'
23
+ };
24
+ function fieldLabel(id) {
25
+ return id
26
+ .split('.')
27
+ .map(p => names[p] ?? p.toUpperCase())
28
+ .join(' · ');
29
+ }
30
+ /** Product editor surface. The injected session is shared by page persistence and AI commands. */
31
+ let FigureV3Editor = class FigureV3Editor extends LitElement {
32
+ constructor() {
33
+ super(...arguments);
34
+ this.revision = 0;
35
+ this.selected = '';
36
+ this.error = '';
37
+ this.lastContext = null;
38
+ }
39
+ static { this.styles = css `
40
+ :host {
41
+ display: flex;
42
+ flex-direction: column;
43
+ min-height: 0;
44
+ height: 100%;
45
+ font: 13px sans-serif;
46
+ color: var(--md-sys-color-on-surface, #22343f);
47
+ }
48
+ header {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: 8px;
52
+ padding: 10px;
53
+ border-bottom: 1px solid #d4dde2;
54
+ flex-wrap: wrap;
55
+ }
56
+ main {
57
+ flex: 1;
58
+ min-height: 0;
59
+ display: grid;
60
+ grid-template-columns: 180px minmax(180px, 1fr) 300px;
61
+ }
62
+ aside {
63
+ padding: 12px;
64
+ overflow: auto;
65
+ background: var(--md-sys-color-surface, #fff);
66
+ }
67
+ aside:first-child {
68
+ border-right: 1px solid #d4dde2;
69
+ }
70
+ aside:last-child {
71
+ border-left: 1px solid #d4dde2;
72
+ }
73
+ h3 {
74
+ font-size: var(--ui-t-body, 13px);
75
+ margin: 22px 0 10px;
76
+ }
77
+ h3:first-child {
78
+ margin-top: 0;
79
+ }
80
+ button,
81
+ input,
82
+ select {
83
+ font: inherit;
84
+ box-sizing: border-box;
85
+ height: 34px;
86
+ border: 1px solid #cbd5dc;
87
+ border-radius: 6px;
88
+ background: var(--md-sys-color-surface, #fff);
89
+ color: inherit;
90
+ padding: 0 8px;
91
+ min-width: 0;
92
+ }
93
+ button {
94
+ cursor: pointer;
95
+ }
96
+ button:disabled {
97
+ opacity: 0.45;
98
+ cursor: default;
99
+ }
100
+ button[aria-pressed='true'] {
101
+ background: #d6edf0;
102
+ border-color: #337783;
103
+ }
104
+ .parts {
105
+ display: grid;
106
+ gap: 6px;
107
+ }
108
+ .recipes {
109
+ display: grid;
110
+ grid-template-columns: 1fr 1fr;
111
+ gap: 6px;
112
+ }
113
+ label {
114
+ display: grid;
115
+ grid-template-columns: 112px minmax(0, 1fr);
116
+ align-items: center;
117
+ gap: 8px;
118
+ margin: 8px 0;
119
+ }
120
+ label span {
121
+ overflow-wrap: anywhere;
122
+ }
123
+ .error {
124
+ padding: 10px;
125
+ background: #fff0eb;
126
+ color: #922716;
127
+ white-space: pre-wrap;
128
+ }
129
+ .status {
130
+ margin-left: auto;
131
+ }
132
+ .hint {
133
+ color: #637783;
134
+ line-height: 1.5;
135
+ }
136
+ @media (max-width: 800px) {
137
+ main {
138
+ grid-template-columns: 140px minmax(160px, 1fr);
139
+ }
140
+ aside:last-child {
141
+ grid-column: 1/-1;
142
+ max-height: 260px;
143
+ border-top: 1px solid #d4dde2;
144
+ }
145
+ }
146
+ `; }
147
+ willUpdate(_changed) {
148
+ const view = this.session?.read();
149
+ if (view?.context !== this.lastContext) {
150
+ this.lastContext = view?.context ?? null;
151
+ this.selected = '';
152
+ this.error = '';
153
+ }
154
+ if (view && !view.geometry.some(g => g.id === this.selected))
155
+ this.selected = '';
156
+ }
157
+ run(action) {
158
+ try {
159
+ action();
160
+ this.error = '';
161
+ this.changed();
162
+ }
163
+ catch (error) {
164
+ this.error = error instanceof Error ? error.message : String(error);
165
+ }
166
+ }
167
+ changed() {
168
+ this.requestUpdate();
169
+ this.dispatchEvent(new CustomEvent('v3-changed', { bubbles: true, composed: true }));
170
+ }
171
+ async save() {
172
+ if (!this.session)
173
+ return;
174
+ try {
175
+ const promise = this.session.save();
176
+ this.changed();
177
+ await promise;
178
+ this.error = '';
179
+ }
180
+ catch (error) {
181
+ this.error = error instanceof Error ? error.message : String(error);
182
+ }
183
+ finally {
184
+ this.changed();
185
+ }
186
+ }
187
+ add(kind) {
188
+ if (!this.session)
189
+ return;
190
+ this.run(() => {
191
+ const ids = new Set(this.session.read().geometry.map(g => g.id));
192
+ let n = 1;
193
+ while (ids.has(`${kind}-${n}`))
194
+ n++;
195
+ const common = { id: `${kind}-${n}`, position: { x: 0, y: 50, z: 0 } };
196
+ const part = kind === 'box'
197
+ ? { ...common, kind, dimensions: { width: 100, height: 100, depth: 100 } }
198
+ : kind === 'cylinder'
199
+ ? { ...common, kind, dimensions: { radius: 50, length: 100 } }
200
+ : kind === 'rounded-box'
201
+ ? { ...common, kind, dimensions: { width: 100, height: 100, depth: 100, round: 10 } }
202
+ : {
203
+ ...common,
204
+ position: { x: 0, y: 0, z: 0 },
205
+ kind: 'portal',
206
+ dimensions: {
207
+ left: -100,
208
+ right: 100,
209
+ bottom: 0,
210
+ top: 200,
211
+ openingLeft: -70,
212
+ openingRight: 70,
213
+ openingTop: 170,
214
+ depth: 30
215
+ }
216
+ };
217
+ this.session.add(part);
218
+ this.selected = part.id;
219
+ });
220
+ }
221
+ changeField(field, event) {
222
+ const input = event.target, value = input.valueAsNumber;
223
+ this.run(() => this.session.apply([
224
+ { kind: field.role === 'state' ? 'set-state' : 'set-design', input: field.id, unit: field.unit, value }
225
+ ]));
226
+ // Rejected edits must also restore the displayed numeric value.
227
+ input.value = String(this.session.read().fields.find(i => i.id === field.id).value);
228
+ }
229
+ render() {
230
+ const view = this.session?.read();
231
+ if (!view)
232
+ return html `<p>모델을 여는 중입니다.</p>`;
233
+ const unavailable = view.loading || !!view.error || view.saving || !view.context;
234
+ const part = view.geometry.find(g => g.id === this.selected);
235
+ // Shared/root parameters remain available even when a part is selected.
236
+ const own = view.fields.filter(i => this.selected && i.id.startsWith(this.selected + '.'));
237
+ const common = view.fields.filter(i => !view.geometry.some(g => i.id.startsWith(g.id + '.')));
238
+ const fields = this.selected ? [...own, ...common] : view.fields;
239
+ return html ` <header>
240
+ <strong>V3 모델링</strong>
241
+ <button ?disabled=${unavailable || !view.canUndo} @click=${() => this.run(() => this.session.undo())}>
242
+ 실행 취소
243
+ </button>
244
+ <button ?disabled=${unavailable || !view.canRedo} @click=${() => this.run(() => this.session.redo())}>
245
+ 다시 실행
246
+ </button>
247
+ <button @click=${() => this.renderRoot.querySelector('figure-v3-canvas')?.fit()}>
248
+ 전체 보기
249
+ </button>
250
+ <span class="status" role="status"
251
+ >${view.loading ? '불러오는 중' : view.saving ? '저장 중' : view.dirty || !view.id ? '저장 안 됨' : '저장됨'}</span
252
+ >
253
+ <button ?disabled=${unavailable || (!view.dirty && !!view.id)} @click=${this.save}>저장</button>
254
+ </header>
255
+ ${this.error || view.error ? html `<div role="alert" class="error">${this.error || view.error}</div>` : null}
256
+ <main>
257
+ <aside>
258
+ <h3>도형 추가</h3>
259
+ <div class="recipes">
260
+ ${['box', 'cylinder', 'rounded-box', 'portal'].map((kind, i) => html `<button ?disabled=${unavailable} @click=${() => this.add(kind)}>${['상자', '원기둥', '둥근 상자', '문틀'][i]}</button>`)}
261
+ </div>
262
+ <h3>부품 ${view.geometry.length}</h3>
263
+ <div class="parts">
264
+ ${view.geometry.map(g => html `<button
265
+ aria-pressed=${g.id === this.selected}
266
+ @click=${() => {
267
+ this.selected = g.id;
268
+ }}
269
+ >
270
+ ${g.id}
271
+ </button>`)}
272
+ </div>
273
+ </aside>
274
+ <figure-v3-canvas
275
+ .parts=${view.geometry}
276
+ .palette=${view.palette ?? {}}
277
+ .context=${view.context ?? ''}
278
+ @part-selected=${(e) => {
279
+ this.selected = e.detail;
280
+ }}
281
+ @render-error=${(e) => {
282
+ this.error = e.detail.message;
283
+ }}
284
+ ></figure-v3-canvas>
285
+ <aside>
286
+ <h3>${this.selected || '모델 입력'}</h3>
287
+ ${fields.map(field => html `<label><span>${fieldLabel(field.id)} (${field.unit})${field.role === 'state' ? ' · 동작' : ''}</span><input aria-label=${field.id} type="number" step="any" .value=${String(field.value)} ?disabled=${unavailable} @change=${(e) => this.changeField(field, e)} /></label>`)}
288
+ ${part
289
+ ? html `<h3>재질</h3>
290
+ <label
291
+ ><span>색 역할</span
292
+ ><select
293
+ .value=${part.material.token}
294
+ ?disabled=${unavailable}
295
+ @change=${(e) => this.run(() => this.session.edit({ kind: 'set-material', target: this.selected, material: { ...part.material, token: e.target.value } }))}
296
+ >
297
+ ${Object.keys(view.palette ?? {}).map(token => html `<option value=${token}>${token === 'palette.primary' ? '기본 몸체' : token}</option>`)}
298
+ </select></label
299
+ >
300
+ <p class="hint">
301
+ 도형과 위치를 결정하는 입력을 편집합니다. 다른 부품과 공유하는 입력은 함께 영향을 줄 수 있습니다.
302
+ </p>`
303
+ : null}
304
+ ${view.bindings.length
305
+ ? html `<h3>배치 연결점</h3>
306
+ ${view.bindings.map(b => html `<p>${b.label} · ${b.role}</p>`)}`
307
+ : null}
308
+ </aside>
309
+ </main>`;
310
+ }
311
+ };
312
+ __decorate([
313
+ property({ attribute: false }),
314
+ __metadata("design:type", Object)
315
+ ], FigureV3Editor.prototype, "session", void 0);
316
+ __decorate([
317
+ property({ type: Number }),
318
+ __metadata("design:type", Object)
319
+ ], FigureV3Editor.prototype, "revision", void 0);
320
+ __decorate([
321
+ state(),
322
+ __metadata("design:type", Object)
323
+ ], FigureV3Editor.prototype, "selected", void 0);
324
+ __decorate([
325
+ state(),
326
+ __metadata("design:type", Object)
327
+ ], FigureV3Editor.prototype, "error", void 0);
328
+ FigureV3Editor = __decorate([
329
+ customElement('figure-v3-editor')
330
+ ], FigureV3Editor);
331
+ export { FigureV3Editor };
332
+ //# sourceMappingURL=v3-editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v3-editor.js","sourceRoot":"","sources":["../../client/modeller/v3-editor.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css, type PropertyValues } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport type { V3InputChange, V3PrimitiveCreation } from '@hatiolab/figure-model'\nimport type { V3ProductSession } from './v3-product-session.js'\nimport './v3-canvas.js'\nimport type { FigureV3Canvas } from './v3-canvas.js'\n\nconst names: Record<string, string> = {\n position: '위치',\n rotation: '회전',\n size: '크기',\n dimension: '치수',\n width: '너비',\n height: '높이',\n depth: '깊이',\n radius: '반지름',\n length: '길이',\n round: '모서리 반경',\n left: '왼쪽',\n right: '오른쪽',\n bottom: '아랫면',\n top: '윗면',\n openingLeft: '개구부 왼쪽',\n openingRight: '개구부 오른쪽',\n openingTop: '개구부 윗면'\n}\nfunction fieldLabel(id: string) {\n return id\n .split('.')\n .map(p => names[p] ?? p.toUpperCase())\n .join(' · ')\n}\n\n/** Product editor surface. The injected session is shared by page persistence and AI commands. */\n@customElement('figure-v3-editor')\nexport class FigureV3Editor extends LitElement {\n static styles = css`\n :host {\n display: flex;\n flex-direction: column;\n min-height: 0;\n height: 100%;\n font: 13px sans-serif;\n color: var(--md-sys-color-on-surface, #22343f);\n }\n header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px;\n border-bottom: 1px solid #d4dde2;\n flex-wrap: wrap;\n }\n main {\n flex: 1;\n min-height: 0;\n display: grid;\n grid-template-columns: 180px minmax(180px, 1fr) 300px;\n }\n aside {\n padding: 12px;\n overflow: auto;\n background: var(--md-sys-color-surface, #fff);\n }\n aside:first-child {\n border-right: 1px solid #d4dde2;\n }\n aside:last-child {\n border-left: 1px solid #d4dde2;\n }\n h3 {\n font-size: var(--ui-t-body, 13px);\n margin: 22px 0 10px;\n }\n h3:first-child {\n margin-top: 0;\n }\n button,\n input,\n select {\n font: inherit;\n box-sizing: border-box;\n height: 34px;\n border: 1px solid #cbd5dc;\n border-radius: 6px;\n background: var(--md-sys-color-surface, #fff);\n color: inherit;\n padding: 0 8px;\n min-width: 0;\n }\n button {\n cursor: pointer;\n }\n button:disabled {\n opacity: 0.45;\n cursor: default;\n }\n button[aria-pressed='true'] {\n background: #d6edf0;\n border-color: #337783;\n }\n .parts {\n display: grid;\n gap: 6px;\n }\n .recipes {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 6px;\n }\n label {\n display: grid;\n grid-template-columns: 112px minmax(0, 1fr);\n align-items: center;\n gap: 8px;\n margin: 8px 0;\n }\n label span {\n overflow-wrap: anywhere;\n }\n .error {\n padding: 10px;\n background: #fff0eb;\n color: #922716;\n white-space: pre-wrap;\n }\n .status {\n margin-left: auto;\n }\n .hint {\n color: #637783;\n line-height: 1.5;\n }\n @media (max-width: 800px) {\n main {\n grid-template-columns: 140px minmax(160px, 1fr);\n }\n aside:last-child {\n grid-column: 1/-1;\n max-height: 260px;\n border-top: 1px solid #d4dde2;\n }\n }\n `\n @property({ attribute: false }) session?: V3ProductSession\n @property({ type: Number }) revision = 0\n @state() private selected = ''\n @state() private error = ''\n private lastContext: string | null = null\n protected willUpdate(_changed: PropertyValues) {\n const view = this.session?.read()\n if (view?.context !== this.lastContext) {\n this.lastContext = view?.context ?? null\n this.selected = ''\n this.error = ''\n }\n if (view && !view.geometry.some(g => g.id === this.selected)) this.selected = ''\n }\n private run(action: () => unknown) {\n try {\n action()\n this.error = ''\n this.changed()\n } catch (error) {\n this.error = error instanceof Error ? error.message : String(error)\n }\n }\n private changed() {\n this.requestUpdate()\n this.dispatchEvent(new CustomEvent('v3-changed', { bubbles: true, composed: true }))\n }\n private async save() {\n if (!this.session) return\n try {\n const promise = this.session.save()\n this.changed()\n await promise\n this.error = ''\n } catch (error) {\n this.error = error instanceof Error ? error.message : String(error)\n } finally {\n this.changed()\n }\n }\n private add(kind: V3PrimitiveCreation['kind']) {\n if (!this.session) return\n this.run(() => {\n const ids = new Set(this.session!.read().geometry.map(g => g.id))\n let n = 1\n while (ids.has(`${kind}-${n}`)) n++\n const common = { id: `${kind}-${n}`, position: { x: 0, y: 50, z: 0 } }\n const part: V3PrimitiveCreation =\n kind === 'box'\n ? { ...common, kind, dimensions: { width: 100, height: 100, depth: 100 } }\n : kind === 'cylinder'\n ? { ...common, kind, dimensions: { radius: 50, length: 100 } }\n : kind === 'rounded-box'\n ? { ...common, kind, dimensions: { width: 100, height: 100, depth: 100, round: 10 } }\n : {\n ...common,\n position: { x: 0, y: 0, z: 0 },\n kind: 'portal',\n dimensions: {\n left: -100,\n right: 100,\n bottom: 0,\n top: 200,\n openingLeft: -70,\n openingRight: 70,\n openingTop: 170,\n depth: 30\n }\n }\n this.session!.add(part)\n this.selected = part.id\n })\n }\n private changeField(field: { id: string; unit: V3InputChange['unit']; role?: string }, event: Event) {\n const input = event.target as HTMLInputElement,\n value = input.valueAsNumber\n this.run(() =>\n this.session!.apply([\n { kind: field.role === 'state' ? 'set-state' : 'set-design', input: field.id, unit: field.unit, value }\n ])\n )\n // Rejected edits must also restore the displayed numeric value.\n input.value = String(this.session!.read().fields.find(i => i.id === field.id)!.value)\n }\n render() {\n const view = this.session?.read()\n if (!view) return html`<p>모델을 여는 중입니다.</p>`\n const unavailable = view.loading || !!view.error || view.saving || !view.context\n const part = view.geometry.find(g => g.id === this.selected)\n // Shared/root parameters remain available even when a part is selected.\n const own = view.fields.filter(i => this.selected && i.id.startsWith(this.selected + '.'))\n const common = view.fields.filter(i => !view.geometry.some(g => i.id.startsWith(g.id + '.')))\n const fields = this.selected ? [...own, ...common] : view.fields\n return html` <header>\n <strong>V3 모델링</strong>\n <button ?disabled=${unavailable || !view.canUndo} @click=${() => this.run(() => this.session!.undo())}>\n 실행 취소\n </button>\n <button ?disabled=${unavailable || !view.canRedo} @click=${() => this.run(() => this.session!.redo())}>\n 다시 실행\n </button>\n <button @click=${() => this.renderRoot.querySelector<FigureV3Canvas>('figure-v3-canvas')?.fit()}>\n 전체 보기\n </button>\n <span class=\"status\" role=\"status\"\n >${view.loading ? '불러오는 중' : view.saving ? '저장 중' : view.dirty || !view.id ? '저장 안 됨' : '저장됨'}</span\n >\n <button ?disabled=${unavailable || (!view.dirty && !!view.id)} @click=${this.save}>저장</button>\n </header>\n ${this.error || view.error ? html`<div role=\"alert\" class=\"error\">${this.error || view.error}</div>` : null}\n <main>\n <aside>\n <h3>도형 추가</h3>\n <div class=\"recipes\">\n ${(['box', 'cylinder', 'rounded-box', 'portal'] as const).map((kind, i) => html`<button ?disabled=${unavailable} @click=${() => this.add(kind)}>${['상자', '원기둥', '둥근 상자', '문틀'][i]}</button>`)}\n </div>\n <h3>부품 ${view.geometry.length}</h3>\n <div class=\"parts\">\n ${view.geometry.map(\n g =>\n html`<button\n aria-pressed=${g.id === this.selected}\n @click=${() => {\n this.selected = g.id\n }}\n >\n ${g.id}\n </button>`\n )}\n </div>\n </aside>\n <figure-v3-canvas\n .parts=${view.geometry}\n .palette=${view.palette ?? {}}\n .context=${view.context ?? ''}\n @part-selected=${(e: CustomEvent<string>) => {\n this.selected = e.detail\n }}\n @render-error=${(e: CustomEvent<Error>) => {\n this.error = e.detail.message\n }}\n ></figure-v3-canvas>\n <aside>\n <h3>${this.selected || '모델 입력'}</h3>\n ${fields.map(field => html`<label><span>${fieldLabel(field.id)} (${field.unit})${field.role === 'state' ? ' · 동작' : ''}</span><input aria-label=${field.id} type=\"number\" step=\"any\" .value=${String(field.value)} ?disabled=${unavailable} @change=${(e: Event) => this.changeField(field, e)} /></label>`)}\n ${\n part\n ? html`<h3>재질</h3>\n <label\n ><span>색 역할</span\n ><select\n .value=${part.material.token}\n ?disabled=${unavailable}\n @change=${(e: Event) => this.run(() => this.session!.edit({ kind: 'set-material', target: this.selected, material: { ...part.material, token: (e.target as HTMLSelectElement).value } }))}\n >\n ${Object.keys(view.palette ?? {}).map(token => html`<option value=${token}>${token === 'palette.primary' ? '기본 몸체' : token}</option>`)}\n </select></label\n >\n <p class=\"hint\">\n 도형과 위치를 결정하는 입력을 편집합니다. 다른 부품과 공유하는 입력은 함께 영향을 줄 수 있습니다.\n </p>`\n : null\n }\n ${\n view.bindings.length\n ? html`<h3>배치 연결점</h3>\n ${view.bindings.map(b => html`<p>${b.label} · ${b.role}</p>`)}`\n : null\n }\n </aside>\n </main>`\n }\n}\n"]}