@things-factory/figure-ui 10.1.40 → 10.1.43

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 (53) hide show
  1. package/client/graphql/index.ts +1 -0
  2. package/client/modeller/figure-v3-material.ts +248 -0
  3. package/client/modeller/figure-v3-parts.ts +16 -1
  4. package/client/modeller/modeller-route.ts +25 -0
  5. package/client/modeller/v3-align.ts +84 -0
  6. package/client/modeller/v3-canvas.ts +140 -2
  7. package/client/modeller/v3-editor.ts +213 -39
  8. package/client/modeller/v3-gizmo.ts +80 -0
  9. package/client/pages/figure-list-page.ts +5 -3
  10. package/client/pages/figure-modeller-page.ts +9 -0
  11. package/client/types.ts +2 -0
  12. package/dist-client/graphql/index.js +1 -0
  13. package/dist-client/graphql/index.js.map +1 -1
  14. package/dist-client/modeller/figure-v3-material.d.ts +36 -0
  15. package/dist-client/modeller/figure-v3-material.js +249 -0
  16. package/dist-client/modeller/figure-v3-material.js.map +1 -0
  17. package/dist-client/modeller/figure-v3-parts.js +16 -1
  18. package/dist-client/modeller/figure-v3-parts.js.map +1 -1
  19. package/dist-client/modeller/modeller-route.d.ts +16 -0
  20. package/dist-client/modeller/modeller-route.js +27 -0
  21. package/dist-client/modeller/modeller-route.js.map +1 -0
  22. package/dist-client/modeller/v3-align.d.ts +26 -0
  23. package/dist-client/modeller/v3-align.js +65 -0
  24. package/dist-client/modeller/v3-align.js.map +1 -0
  25. package/dist-client/modeller/v3-canvas.d.ts +18 -0
  26. package/dist-client/modeller/v3-canvas.js +154 -2
  27. package/dist-client/modeller/v3-canvas.js.map +1 -1
  28. package/dist-client/modeller/v3-editor.d.ts +29 -0
  29. package/dist-client/modeller/v3-editor.js +225 -36
  30. package/dist-client/modeller/v3-editor.js.map +1 -1
  31. package/dist-client/modeller/v3-gizmo.d.ts +48 -0
  32. package/dist-client/modeller/v3-gizmo.js +40 -0
  33. package/dist-client/modeller/v3-gizmo.js.map +1 -0
  34. package/dist-client/pages/figure-list-page.d.ts +1 -0
  35. package/dist-client/pages/figure-list-page.js +5 -3
  36. package/dist-client/pages/figure-list-page.js.map +1 -1
  37. package/dist-client/pages/figure-modeller-page.js +9 -0
  38. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  39. package/dist-client/route.d.ts +1 -1
  40. package/dist-client/tsconfig.tsbuildinfo +1 -1
  41. package/dist-client/types.d.ts +2 -0
  42. package/dist-client/types.js.map +1 -1
  43. package/package.json +4 -4
  44. package/test/modeller-route.test.ts +42 -0
  45. package/test/v3-align.test.ts +64 -0
  46. package/test/v3-gizmo.test.ts +97 -0
  47. package/test/v3-material.test.ts +68 -0
  48. package/test/v3-parts-wiring.test.ts +66 -0
  49. package/translations/en.json +7 -0
  50. package/translations/ja.json +7 -0
  51. package/translations/ko.json +7 -0
  52. package/translations/ms.json +7 -0
  53. package/translations/zh.json +7 -0
@@ -0,0 +1,249 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import { css, html, LitElement, nothing } from 'lit';
3
+ import { customElement, property } from 'lit/decorators.js';
4
+ import { i18next, localize } from '@operato/i18n';
5
+ import { MATERIAL_PRESETS, SEGMENT_PRESETS, v3TrianglesOf } from '@hatiolab/figure-model';
6
+ import { activePalette } from '@hatiolab/things-scene';
7
+ /** The purpose each palette role is for, by its token — the V2 inspector's words for the same roles. */
8
+ const PURPOSE = {
9
+ 'palette.primary': 'primary',
10
+ 'palette.accent': 'accent',
11
+ 'palette.neutral': 'neutral',
12
+ 'palette.dark': 'dark',
13
+ 'palette.light': 'light',
14
+ 'palette.surface': 'surface',
15
+ 'palette.status-danger': 'danger',
16
+ 'palette.status-warning': 'warning',
17
+ 'palette.status-info': 'info',
18
+ 'palette.status-success': 'success'
19
+ };
20
+ export const purposeOf = (token) => i18next.t(`figure.label.palette-purpose-${PURPOSE[token ?? ''] ?? 'registered'}`);
21
+ /** Bodies with a rim — the ones `set-segments` accepts. */
22
+ const ROUND = new Set(['cylinder', 'ellipticCylinder', 'sphere', 'frustum']);
23
+ const DEFAULT_SEGMENTS = 12;
24
+ /**
25
+ * 재질 — a V3 part's material, drawn as the V2 inspector draws it (V3 parity row ③).
26
+ *
27
+ * The V3 editor showed the colour role as a raw token (`palette.dark`) in a select and nothing else. The V3 material
28
+ * holds the colour role, a preset, transparency and a glow; a round body also has how finely its rim is drawn. This
29
+ * shows those, with the V2 words: painted colours and lit colours as two groups of swatches, each named for what it
30
+ * is for.
31
+ *
32
+ * Not here, because a V3 material has no place for them: flat shading and casting shadows (V2 fields the converter
33
+ * does not carry). The glow's colour comes from the lit colours, as in V2.
34
+ *
35
+ * Every change leaves as one `v3-part-edit`; the editor runs it through the session, one step of the undo history.
36
+ */
37
+ let FigureV3Material = class FigureV3Material extends localize(i18next)(LitElement) {
38
+ constructor() {
39
+ super(...arguments);
40
+ /** The asset's palette, token → colour. */
41
+ this.palette = {};
42
+ this.locked = false;
43
+ }
44
+ static { this.styles = css `
45
+ :host {
46
+ display: grid;
47
+ gap: 10px;
48
+ }
49
+ h3 {
50
+ font-size: var(--ui-t-body, 13px);
51
+ margin: 12px 0 0;
52
+ }
53
+ div[field] {
54
+ display: grid;
55
+ gap: 4px;
56
+ }
57
+ div[field] > label {
58
+ font-weight: 600;
59
+ }
60
+ div[swatches] {
61
+ display: flex;
62
+ flex-wrap: wrap;
63
+ gap: 4px;
64
+ }
65
+ button[swatch] {
66
+ width: 22px;
67
+ height: 22px;
68
+ padding: 0;
69
+ border-radius: 50%;
70
+ border: 1px solid var(--md-sys-color-outline-variant);
71
+ background: var(--swatch);
72
+ cursor: pointer;
73
+ }
74
+ button[swatch][on] {
75
+ outline: 2px solid var(--md-sys-color-primary);
76
+ outline-offset: 1px;
77
+ }
78
+ small {
79
+ color: var(--md-sys-color-on-surface-variant);
80
+ }
81
+ select,
82
+ input[type='range'] {
83
+ font: inherit;
84
+ min-width: 0;
85
+ }
86
+ label[toggle] {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 6px;
90
+ }
91
+ div[segments] {
92
+ display: grid;
93
+ grid-template-columns: repeat(3, 1fr);
94
+ gap: 4px;
95
+ }
96
+ div[segments] button {
97
+ display: grid;
98
+ padding: 4px;
99
+ font: inherit;
100
+ border: 1px solid var(--md-sys-color-outline-variant);
101
+ border-radius: 6px;
102
+ background: var(--md-sys-color-surface);
103
+ color: var(--md-sys-color-on-surface);
104
+ cursor: pointer;
105
+ }
106
+ div[segments] button[on] {
107
+ background: var(--md-sys-color-secondary-container);
108
+ color: var(--md-sys-color-on-secondary-container);
109
+ border-color: var(--md-sys-color-primary);
110
+ }
111
+ `; }
112
+ render() {
113
+ const part = this.body;
114
+ if (!part)
115
+ return nothing;
116
+ const material = part.material;
117
+ /*
118
+ The roles offered are the app's palette, as in V2, with any the asset carries beyond it. A new asset carries
119
+ only its primary colour; a role it lacks is sent with its colour and added in the same edit (`put`).
120
+ */
121
+ const entries = Object.entries(this.offered());
122
+ const painted = entries.filter(([token]) => !token.startsWith('palette.status-'));
123
+ const lit = entries.filter(([token]) => token.startsWith('palette.status-'));
124
+ const glow = material.emissive;
125
+ return html `
126
+ <h3>${i18next.t('figure.label.part-material')}</h3>
127
+ <div field>
128
+ <label>${i18next.t('figure.label.palette-role')}</label>
129
+ <div swatches>${this.swatches(painted, material.token, token => this.put({ token }))}</div>
130
+ ${lit.length
131
+ ? html `<small>${i18next.t('figure.label.status-colors')}</small>
132
+ <div swatches>${this.swatches(lit, material.token, token => this.put({ token }))}</div>`
133
+ : nothing}
134
+ <small><b>${purposeOf(material.token)}</b> — ${i18next.t('figure.text.palette-purpose-explained')}</small>
135
+ </div>
136
+ <div field>
137
+ <label>${i18next.t('figure.label.preset')}</label>
138
+ <select
139
+ ?disabled=${this.locked}
140
+ @change=${(e) => this.put({ preset: (e.target.value || undefined) })}
141
+ >
142
+ <option value="" ?selected=${!material.preset}>${i18next.t('figure.label.none')}</option>
143
+ ${MATERIAL_PRESETS.map(preset => html `<option value=${preset} ?selected=${preset === material.preset}>${i18next.t('figure.label.preset-' + preset)}</option>`)}
144
+ </select>
145
+ </div>
146
+ <label toggle>
147
+ <input
148
+ type="checkbox"
149
+ ?disabled=${this.locked}
150
+ .checked=${!!material.transparent}
151
+ @change=${(e) => this.put({ transparent: e.target.checked || undefined })}
152
+ />
153
+ ${i18next.t('figure.label.transparent')}
154
+ </label>
155
+ <div field>
156
+ <label>${i18next.t('figure.label.emissive')}</label>
157
+ <input
158
+ type="range"
159
+ min="0"
160
+ max="1"
161
+ step="0.05"
162
+ ?disabled=${this.locked}
163
+ .value=${String(glow?.intensity ?? 0)}
164
+ @change=${(e) => this.glow(Number(e.target.value))}
165
+ />
166
+ <small>${(glow?.intensity ?? 0).toFixed(2)}</small>
167
+ </div>
168
+ ${glow
169
+ ? html `<div field>
170
+ <label>${i18next.t('figure.label.emissive-color')}</label>
171
+ <div swatches>${this.swatches(lit.length ? lit : painted, glow.token, token => this.put({ emissive: { ...glow, token } }))}</div>
172
+ </div>`
173
+ : nothing}
174
+ ${this.renderSegments(part)}
175
+ `;
176
+ }
177
+ renderSegments(part) {
178
+ const provider = part.shape?.provider ?? '';
179
+ if (!ROUND.has(provider))
180
+ return nothing;
181
+ const current = part.segments ?? DEFAULT_SEGMENTS;
182
+ return html `
183
+ <div field>
184
+ <label>${i18next.t('figure.label.segments')}</label>
185
+ <div segments>
186
+ ${SEGMENT_PRESETS.map(preset => html `<button
187
+ ?on=${preset === current}
188
+ ?disabled=${this.locked}
189
+ @click=${() => this.edit({ kind: 'set-segments', target: part.id, segments: preset })}
190
+ >
191
+ <b>${preset}</b>
192
+ <small>${i18next.t('figure.text.n-triangles', { count: v3TrianglesOf({ ...part, segments: preset }) })}</small>
193
+ </button>`)}
194
+ </div>
195
+ </div>
196
+ `;
197
+ }
198
+ swatches(entries, chosen, pick) {
199
+ return entries.map(([token, colour]) => html `<button
200
+ swatch
201
+ ?on=${token === chosen}
202
+ ?disabled=${this.locked}
203
+ title=${purposeOf(token)}
204
+ aria-label=${purposeOf(token)}
205
+ style="--swatch:${colour}"
206
+ @click=${() => pick(token)}
207
+ ></button>`);
208
+ }
209
+ offered() {
210
+ return { ...activePalette(), ...this.palette };
211
+ }
212
+ /** Intensity 0 is no glow at all, as in V2; a new glow takes the part's own colour until another is picked. */
213
+ glow(intensity) {
214
+ const material = this.body.material;
215
+ this.put({ emissive: intensity > 0 ? { token: material.emissive?.token ?? material.token, intensity, on: true } : undefined });
216
+ }
217
+ put(patch) {
218
+ const material = { ...this.body.material, ...patch };
219
+ for (const key of Object.keys(material))
220
+ if (material[key] === undefined)
221
+ delete material[key];
222
+ /* The colours this material names that the asset does not carry yet. */
223
+ const offered = this.offered();
224
+ const palette = Object.fromEntries([material.token, material.emissive?.token]
225
+ .filter((token) => !!token && this.palette[token] === undefined && offered[token] !== undefined)
226
+ .map(token => [token, offered[token]]));
227
+ this.edit({ kind: 'set-material', target: this.body.id, material, ...(Object.keys(palette).length ? { palette } : {}) });
228
+ }
229
+ edit(edit) {
230
+ this.dispatchEvent(new CustomEvent('v3-part-edit', { detail: { edit }, bubbles: true, composed: true }));
231
+ }
232
+ };
233
+ __decorate([
234
+ property({ attribute: false }),
235
+ __metadata("design:type", Object)
236
+ ], FigureV3Material.prototype, "body", void 0);
237
+ __decorate([
238
+ property({ attribute: false }),
239
+ __metadata("design:type", Object)
240
+ ], FigureV3Material.prototype, "palette", void 0);
241
+ __decorate([
242
+ property({ type: Boolean }),
243
+ __metadata("design:type", Object)
244
+ ], FigureV3Material.prototype, "locked", void 0);
245
+ FigureV3Material = __decorate([
246
+ customElement('figure-v3-material')
247
+ ], FigureV3Material);
248
+ export { FigureV3Material };
249
+ //# sourceMappingURL=figure-v3-material.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figure-v3-material.js","sourceRoot":"","sources":["../../client/modeller/figure-v3-material.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { MATERIAL_PRESETS, SEGMENT_PRESETS, v3TrianglesOf } from '@hatiolab/figure-model'\nimport { activePalette } from '@hatiolab/things-scene'\nimport type { V3AssetGeometry, V3Material, V3PartEdit } from '@hatiolab/figure-model'\n\n/** The purpose each palette role is for, by its token — the V2 inspector's words for the same roles. */\nconst PURPOSE: Record<string, string> = {\n 'palette.primary': 'primary',\n 'palette.accent': 'accent',\n 'palette.neutral': 'neutral',\n 'palette.dark': 'dark',\n 'palette.light': 'light',\n 'palette.surface': 'surface',\n 'palette.status-danger': 'danger',\n 'palette.status-warning': 'warning',\n 'palette.status-info': 'info',\n 'palette.status-success': 'success'\n}\nexport const purposeOf = (token?: string) => i18next.t(`figure.label.palette-purpose-${PURPOSE[token ?? ''] ?? 'registered'}`)\n\n/** Bodies with a rim — the ones `set-segments` accepts. */\nconst ROUND = new Set(['cylinder', 'ellipticCylinder', 'sphere', 'frustum'])\nconst DEFAULT_SEGMENTS = 12\n\n/**\n * 재질 — a V3 part's material, drawn as the V2 inspector draws it (V3 parity row ③).\n *\n * The V3 editor showed the colour role as a raw token (`palette.dark`) in a select and nothing else. The V3 material\n * holds the colour role, a preset, transparency and a glow; a round body also has how finely its rim is drawn. This\n * shows those, with the V2 words: painted colours and lit colours as two groups of swatches, each named for what it\n * is for.\n *\n * Not here, because a V3 material has no place for them: flat shading and casting shadows (V2 fields the converter\n * does not carry). The glow's colour comes from the lit colours, as in V2.\n *\n * Every change leaves as one `v3-part-edit`; the editor runs it through the session, one step of the undo history.\n */\n@customElement('figure-v3-material')\nexport class FigureV3Material extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n display: grid;\n gap: 10px;\n }\n h3 {\n font-size: var(--ui-t-body, 13px);\n margin: 12px 0 0;\n }\n div[field] {\n display: grid;\n gap: 4px;\n }\n div[field] > label {\n font-weight: 600;\n }\n div[swatches] {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n }\n button[swatch] {\n width: 22px;\n height: 22px;\n padding: 0;\n border-radius: 50%;\n border: 1px solid var(--md-sys-color-outline-variant);\n background: var(--swatch);\n cursor: pointer;\n }\n button[swatch][on] {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: 1px;\n }\n small {\n color: var(--md-sys-color-on-surface-variant);\n }\n select,\n input[type='range'] {\n font: inherit;\n min-width: 0;\n }\n label[toggle] {\n display: flex;\n align-items: center;\n gap: 6px;\n }\n div[segments] {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 4px;\n }\n div[segments] button {\n display: grid;\n padding: 4px;\n font: inherit;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n background: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n cursor: pointer;\n }\n div[segments] button[on] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n border-color: var(--md-sys-color-primary);\n }\n `\n\n /** The selected body as evaluated — its material, its segments and its shape. */\n @property({ attribute: false }) body?: V3AssetGeometry & { id: string }\n /** The asset's palette, token → colour. */\n @property({ attribute: false }) palette: Record<string, string> = {}\n @property({ type: Boolean }) locked = false\n\n render() {\n const part = this.body\n if (!part) return nothing\n const material = part.material\n /*\n The roles offered are the app's palette, as in V2, with any the asset carries beyond it. A new asset carries\n only its primary colour; a role it lacks is sent with its colour and added in the same edit (`put`).\n */\n const entries = Object.entries(this.offered())\n const painted = entries.filter(([token]) => !token.startsWith('palette.status-'))\n const lit = entries.filter(([token]) => token.startsWith('palette.status-'))\n const glow = material.emissive\n return html`\n <h3>${i18next.t('figure.label.part-material')}</h3>\n <div field>\n <label>${i18next.t('figure.label.palette-role')}</label>\n <div swatches>${this.swatches(painted, material.token, token => this.put({ token }))}</div>\n ${lit.length\n ? html`<small>${i18next.t('figure.label.status-colors')}</small>\n <div swatches>${this.swatches(lit, material.token, token => this.put({ token }))}</div>`\n : nothing}\n <small><b>${purposeOf(material.token)}</b> — ${i18next.t('figure.text.palette-purpose-explained')}</small>\n </div>\n <div field>\n <label>${i18next.t('figure.label.preset')}</label>\n <select\n ?disabled=${this.locked}\n @change=${(e: Event) => this.put({ preset: ((e.target as HTMLSelectElement).value || undefined) as V3Material['preset'] })}\n >\n <option value=\"\" ?selected=${!material.preset}>${i18next.t('figure.label.none')}</option>\n ${MATERIAL_PRESETS.map(\n preset => html`<option value=${preset} ?selected=${preset === material.preset}>${i18next.t('figure.label.preset-' + preset)}</option>`\n )}\n </select>\n </div>\n <label toggle>\n <input\n type=\"checkbox\"\n ?disabled=${this.locked}\n .checked=${!!material.transparent}\n @change=${(e: Event) => this.put({ transparent: (e.target as HTMLInputElement).checked || undefined })}\n />\n ${i18next.t('figure.label.transparent')}\n </label>\n <div field>\n <label>${i18next.t('figure.label.emissive')}</label>\n <input\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.05\"\n ?disabled=${this.locked}\n .value=${String(glow?.intensity ?? 0)}\n @change=${(e: Event) => this.glow(Number((e.target as HTMLInputElement).value))}\n />\n <small>${(glow?.intensity ?? 0).toFixed(2)}</small>\n </div>\n ${glow\n ? html`<div field>\n <label>${i18next.t('figure.label.emissive-color')}</label>\n <div swatches>${this.swatches(lit.length ? lit : painted, glow.token, token => this.put({ emissive: { ...glow, token } }))}</div>\n </div>`\n : nothing}\n ${this.renderSegments(part)}\n `\n }\n\n private renderSegments(part: V3AssetGeometry & { id: string }) {\n const provider = (part as { shape?: { provider?: string } }).shape?.provider ?? ''\n if (!ROUND.has(provider)) return nothing\n const current = part.segments ?? DEFAULT_SEGMENTS\n return html`\n <div field>\n <label>${i18next.t('figure.label.segments')}</label>\n <div segments>\n ${SEGMENT_PRESETS.map(\n preset => html`<button\n ?on=${preset === current}\n ?disabled=${this.locked}\n @click=${() => this.edit({ kind: 'set-segments', target: part.id, segments: preset })}\n >\n <b>${preset}</b>\n <small>${i18next.t('figure.text.n-triangles', { count: v3TrianglesOf({ ...part, segments: preset }) })}</small>\n </button>`\n )}\n </div>\n </div>\n `\n }\n\n private swatches(entries: [string, string][], chosen: string | undefined, pick: (token: string) => void) {\n return entries.map(\n ([token, colour]) => html`<button\n swatch\n ?on=${token === chosen}\n ?disabled=${this.locked}\n title=${purposeOf(token)}\n aria-label=${purposeOf(token)}\n style=\"--swatch:${colour}\"\n @click=${() => pick(token)}\n ></button>`\n )\n }\n\n private offered(): Record<string, string> {\n return { ...(activePalette() as Record<string, string>), ...this.palette }\n }\n\n /** Intensity 0 is no glow at all, as in V2; a new glow takes the part's own colour until another is picked. */\n private glow(intensity: number) {\n const material = this.body!.material\n this.put({ emissive: intensity > 0 ? { token: material.emissive?.token ?? material.token, intensity, on: true } : undefined })\n }\n\n private put(patch: Partial<V3Material>) {\n const material: V3Material = { ...this.body!.material, ...patch }\n for (const key of Object.keys(material) as (keyof V3Material)[]) if (material[key] === undefined) delete material[key]\n /* The colours this material names that the asset does not carry yet. */\n const offered = this.offered()\n const palette = Object.fromEntries(\n [material.token, material.emissive?.token]\n .filter((token): token is string => !!token && this.palette[token] === undefined && offered[token] !== undefined)\n .map(token => [token, offered[token]!])\n )\n this.edit({ kind: 'set-material', target: this.body!.id, material, ...(Object.keys(palette).length ? { palette } : {}) })\n }\n\n private edit(edit: V3PartEdit) {\n this.dispatchEvent(new CustomEvent('v3-part-edit', { detail: { edit }, bubbles: true, composed: true }))\n }\n}\n"]}
@@ -22,7 +22,7 @@ const PROVIDER_ICON = {
22
22
  const RECIPES = [
23
23
  { kind: 'box', icon: 'deployed_code', label: 'figure.label.primitive-cube' },
24
24
  { kind: 'cylinder', icon: 'database', label: 'figure.label.primitive-cylinder' },
25
- { kind: 'rounded-box', icon: 'crop_square', label: 'figure.label.primitive-rect' },
25
+ { kind: 'rounded-box', icon: 'crop_square', label: 'figure.label.primitive-rounded-box' },
26
26
  { kind: 'portal', icon: 'door_front', label: 'figure.label.primitive-portal' }
27
27
  ];
28
28
  let FigureV3Parts = class FigureV3Parts extends localize(i18next)(LitElement) {
@@ -47,6 +47,8 @@ let FigureV3Parts = class FigureV3Parts extends localize(i18next)(LitElement) {
47
47
  color: var(--md-sys-color-on-surface);
48
48
  font: var(--label-font, inherit);
49
49
  font-size: 0.76rem;
50
+ /* Folds by its own width: the editor gives this column 140 px when it has two columns (UX, 2026-09-24). */
51
+ container-type: inline-size;
50
52
  }
51
53
  section {
52
54
  padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
@@ -213,6 +215,19 @@ let FigureV3Parts = class FigureV3Parts extends localize(i18next)(LitElement) {
213
215
  --md-icon-size: 15px;
214
216
  color: inherit;
215
217
  }
218
+ /*
219
+ Under 200 px the selected row's four buttons pushed its name out and the last one past the edge (seen at 140,
220
+ 2026-09-24). They go to a line of their own under the name.
221
+ */
222
+ @container (max-width: 199px) {
223
+ li[selected] {
224
+ flex-wrap: wrap;
225
+ }
226
+ li[selected] div[ops] {
227
+ flex-basis: 100%;
228
+ justify-content: flex-end;
229
+ }
230
+ }
216
231
  p[quiet] {
217
232
  margin: 0;
218
233
  font-size: 0.76rem;
@@ -1 +1 @@
1
- {"version":3,"file":"figure-v3-parts.js","sourceRoot":"","sources":["../../client/modeller/figure-v3-parts.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport { i18next, localize } from '@operato/i18n'\nimport { ScrollbarStyles } from '@operato/styles'\nimport type { V3PrimitiveCreation } from '@hatiolab/figure-model'\n\n/**\n * 도형 팔레트와 부품 목록 — V3 자산의 것.\n *\n * `figure-parts`(V2) 와 **같은 자리, 같은 조작, 같은 낱말**이다. 저작자가 V2 에서 익힌 손이 그대로\n * 가야 한다: 팔레트는 목록 위에, 눈 단추는 늘 자리를 차지하고, 조작은 고른 줄에서만 보인다.\n *\n * 다른 것은 읽는 대상뿐이다. V2 는 부품의 `primitive` 를 읽었고 여기서는 V3 평가가 준 `provider` 를\n * 읽는다. 「숨김」은 여기서도 **작업대의 것**이다 — 저장되지 않고, 자산의 `visibility`(상태에 따라\n * 보이고 안 보이는 것)와는 다른 것이다. 그것은 속성 패널의 몫이다.\n *\n * 이 컴포넌트는 정본을 만지지 않는다. 무엇을 하겠다는 것만 알린다 — V2 와 같은 규칙, 같은 사건 이름.\n */\n\n/** 한 줄 — V3 평가에서 이 패널이 읽는 것만. */\nexport interface V3PartRow {\n id: string\n provider: string\n /** 작업대에서 잠깐 가린 것. 저장되지 않는다. */\n hidden?: boolean\n /** 자산이 지금 상태에서 보이지 않는다고 한 것. 저장되는 것이고, 여기서는 표시만 한다. */\n invisible?: boolean\n}\n\n/** 제공자마다의 아이콘. Material Symbols 이름이다. */\nconst PROVIDER_ICON: Record<string, string> = {\n box: 'deployed_code',\n cylinder: 'database',\n ellipticCylinder: 'database',\n frustum: 'change_history',\n sphere: 'circle',\n roundedBox: 'crop_square',\n roundedBoxXZ: 'crop_square',\n hollowBox: 'check_box_outline_blank',\n hollowBoxXZ: 'check_box_outline_blank',\n polygon: 'pentagon',\n portal: 'door_front'\n}\n\n/** 팔레트 — 저작 API 가 만들 수 있는 것만 보인다. */\nconst RECIPES: Array<{ kind: V3PrimitiveCreation['kind']; icon: string; label: string }> = [\n { kind: 'box', icon: 'deployed_code', label: 'figure.label.primitive-cube' },\n { kind: 'cylinder', icon: 'database', label: 'figure.label.primitive-cylinder' },\n { kind: 'rounded-box', icon: 'crop_square', label: 'figure.label.primitive-rect' },\n { kind: 'portal', icon: 'door_front', label: 'figure.label.primitive-portal' }\n]\n\n@customElement('figure-v3-parts')\nexport class FigureV3Parts extends localize(i18next)(LitElement) {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n min-height: 0;\n overflow: hidden;\n background-color: var(--md-sys-color-surface-container-lowest);\n border-right: 1px solid var(--md-sys-color-outline-variant);\n color: var(--md-sys-color-on-surface);\n font: var(--label-font, inherit);\n font-size: 0.76rem;\n }\n section {\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n section[list] {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n }\n h3 {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 6px;\n margin: 0 0 6px 0;\n font-size: var(--ui-t-micro, 11px);\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n span[room] {\n font-size: 0.7rem;\n font-weight: 400;\n letter-spacing: 0;\n text-transform: none;\n }\n div[palette] {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 4px;\n }\n button[shape] {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 2px;\n padding: 7px 2px 5px 2px;\n font: var(--label-font, inherit);\n font-size: 0.66rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n cursor: pointer;\n }\n button[shape]:hover:not([disabled]) {\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border-color: transparent;\n }\n button[shape][disabled] {\n opacity: 0.35;\n cursor: default;\n }\n button[shape] md-icon {\n --md-icon-size: 20px;\n }\n ol {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n li {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 6px;\n border-radius: 6px;\n cursor: pointer;\n }\n li:hover {\n background-color: var(--md-sys-color-surface-container);\n }\n li[selected] {\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n }\n li md-icon {\n --md-icon-size: 16px;\n flex: none;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n button[eye][all] {\n margin-left: auto;\n opacity: 0.75;\n }\n button[eye][all]:hover {\n opacity: 1;\n }\n button[eye] {\n display: flex;\n flex: none;\n padding: 1px;\n color: inherit;\n background: none;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n opacity: 0;\n }\n li:hover button[eye],\n li[selected] button[eye],\n button[eye][off] {\n opacity: 1;\n }\n button[eye][off] {\n color: var(--md-sys-color-tertiary);\n }\n button[eye] md-icon {\n --md-icon-size: 15px;\n color: inherit;\n }\n li[off] span[label],\n li[off] > md-icon,\n li[invisible] span[label],\n li[invisible] > md-icon {\n opacity: 0.45;\n }\n li[selected] md-icon {\n color: inherit;\n }\n span[label] {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 0.78rem;\n }\n span[label] small {\n margin-left: 4px;\n font-size: 0.66rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n div[ops] {\n display: none;\n gap: 1px;\n }\n li[selected] div[ops] {\n display: flex;\n }\n div[ops] button {\n display: flex;\n padding: 2px;\n color: inherit;\n background: none;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n }\n div[ops] button:hover:not([disabled]) {\n background-color: var(--md-sys-color-surface-container-lowest);\n }\n div[ops] button[disabled] {\n opacity: 0.3;\n cursor: default;\n }\n div[ops] md-icon {\n --md-icon-size: 15px;\n color: inherit;\n }\n p[quiet] {\n margin: 0;\n font-size: 0.76rem;\n line-height: 1.6;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n `\n ]\n\n @property({ type: Array }) parts: V3PartRow[] = []\n /** 고른 부품의 id. 자리(index)가 아니다 — V3 부품은 id 로 안정하게 가리킨다. */\n @property({ type: String }) selected = ''\n /** 편집할 수 없을 때(불러오는 중 · 저장 중) 팔레트와 조작을 잠근다. */\n @property({ type: Boolean }) locked = false\n\n render() {\n return html`\n <section>\n <h3>${i18next.t('figure.label.shapes')}</h3>\n <div palette>\n ${RECIPES.map(\n recipe => html`\n <button\n shape\n ?disabled=${this.locked}\n title=${i18next.t(recipe.label)}\n @click=${() => this.ask('add-part', { kind: recipe.kind })}\n >\n <md-icon>${recipe.icon}</md-icon>\n <span>${i18next.t(recipe.label)}</span>\n </button>\n `\n )}\n </div>\n </section>\n\n <section list>\n <h3>\n <span>${i18next.t('figure.label.parts')}</span>\n <span room>${this.parts.length}</span>\n ${this.renderShowAll()}\n </h3>\n ${this.parts.length === 0\n ? html`<p quiet>${i18next.t('figure.text.no-parts-yet')}</p>`\n : html`<ol>\n ${this.parts.map((part, index) => this.renderRow(part, index))}\n </ol>`}\n </section>\n `\n }\n\n /** 한꺼번에 감추고 한꺼번에 되돌린다 — 하나라도 감춰져 있으면 다 되돌리고, 다 보이면 다 감춘다. */\n private renderShowAll() {\n if (this.parts.length === 0) return nothing\n const anyHidden = this.parts.some(part => part.hidden)\n return html`\n <button\n eye\n all\n ?off=${!anyHidden}\n title=${i18next.t(anyHidden ? 'figure.text.show-all-parts' : 'figure.text.hide-all-parts')}\n @click=${(e: Event) => {\n e.stopPropagation()\n this.ask('set-all-parts-hidden', { hidden: !anyHidden })\n }}\n >\n <md-icon>${anyHidden ? 'visibility' : 'visibility_off'}</md-icon>\n </button>\n `\n }\n\n private renderRow(part: V3PartRow, index: number) {\n const selected = part.id === this.selected\n const off = !!part.hidden\n return html`\n <li\n ?selected=${selected}\n ?off=${off}\n ?invisible=${!!part.invisible}\n @click=${() => this.ask('select-part', { id: part.id })}\n >\n <button\n eye\n ?off=${off}\n title=${i18next.t(off ? 'figure.text.show-this-part' : 'figure.text.hide-this-part')}\n @click=${(e: Event) => {\n e.stopPropagation()\n this.ask('toggle-part-hidden', { id: part.id })\n }}\n >\n <md-icon>${off ? 'visibility_off' : 'visibility'}</md-icon>\n </button>\n <md-icon>${PROVIDER_ICON[part.provider] ?? 'deployed_code'}</md-icon>\n <span label title=${part.id}\n >${part.id}${part.invisible ? html`<small>${i18next.t('figure.label.state-hidden')}</small>` : nothing}</span\n >\n ${selected ? this.renderOps(part.id, index) : nothing}\n </li>\n `\n }\n\n /** 고른 줄의 조작. 못 하는 것은 빼지 않고 흐리게 둔다 — 자리가 밀리면 연달아 지우다 엉뚱한 것을 누른다. */\n private renderOps(id: string, index: number) {\n const count = this.parts.length\n const op = (name: string, icon: string, detail: object, enabled = true) => html`\n <button\n op=${name}\n ?disabled=${!enabled || this.locked}\n title=${i18next.t('figure.button.' + name)}\n @click=${(e: Event) => {\n e.stopPropagation()\n if (enabled && !this.locked) this.ask(name, detail)\n }}\n >\n <md-icon>${icon}</md-icon>\n </button>\n `\n return html`\n <div ops>\n ${op('move-part-up', 'arrow_upward', { id, to: index - 1 }, index > 0)}\n ${op('move-part-down', 'arrow_downward', { id, to: index + 1 }, index < count - 1)}\n ${op('duplicate-part', 'content_copy', { id })} ${op('remove-part', 'delete', { id })}\n </div>\n `\n }\n\n /** 정본을 고치지 않는다 — 무엇을 하겠다는 것만 알린다. */\n private ask(what: string, detail: object) {\n this.dispatchEvent(new CustomEvent(what, { detail, bubbles: true, composed: true }))\n }\n}\n"]}
1
+ {"version":3,"file":"figure-v3-parts.js","sourceRoot":"","sources":["../../client/modeller/figure-v3-parts.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport { i18next, localize } from '@operato/i18n'\nimport { ScrollbarStyles } from '@operato/styles'\nimport type { V3PrimitiveCreation } from '@hatiolab/figure-model'\n\n/**\n * 도형 팔레트와 부품 목록 — V3 자산의 것.\n *\n * `figure-parts`(V2) 와 **같은 자리, 같은 조작, 같은 낱말**이다. 저작자가 V2 에서 익힌 손이 그대로\n * 가야 한다: 팔레트는 목록 위에, 눈 단추는 늘 자리를 차지하고, 조작은 고른 줄에서만 보인다.\n *\n * 다른 것은 읽는 대상뿐이다. V2 는 부품의 `primitive` 를 읽었고 여기서는 V3 평가가 준 `provider` 를\n * 읽는다. 「숨김」은 여기서도 **작업대의 것**이다 — 저장되지 않고, 자산의 `visibility`(상태에 따라\n * 보이고 안 보이는 것)와는 다른 것이다. 그것은 속성 패널의 몫이다.\n *\n * 이 컴포넌트는 정본을 만지지 않는다. 무엇을 하겠다는 것만 알린다 — V2 와 같은 규칙, 같은 사건 이름.\n */\n\n/** 한 줄 — V3 평가에서 이 패널이 읽는 것만. */\nexport interface V3PartRow {\n id: string\n provider: string\n /** 작업대에서 잠깐 가린 것. 저장되지 않는다. */\n hidden?: boolean\n /** 자산이 지금 상태에서 보이지 않는다고 한 것. 저장되는 것이고, 여기서는 표시만 한다. */\n invisible?: boolean\n}\n\n/** 제공자마다의 아이콘. Material Symbols 이름이다. */\nconst PROVIDER_ICON: Record<string, string> = {\n box: 'deployed_code',\n cylinder: 'database',\n ellipticCylinder: 'database',\n frustum: 'change_history',\n sphere: 'circle',\n roundedBox: 'crop_square',\n roundedBoxXZ: 'crop_square',\n hollowBox: 'check_box_outline_blank',\n hollowBoxXZ: 'check_box_outline_blank',\n polygon: 'pentagon',\n portal: 'door_front'\n}\n\n/** 팔레트 — 저작 API 가 만들 수 있는 것만 보인다. */\nconst RECIPES: Array<{ kind: V3PrimitiveCreation['kind']; icon: string; label: string }> = [\n { kind: 'box', icon: 'deployed_code', label: 'figure.label.primitive-cube' },\n { kind: 'cylinder', icon: 'database', label: 'figure.label.primitive-cylinder' },\n { kind: 'rounded-box', icon: 'crop_square', label: 'figure.label.primitive-rounded-box' },\n { kind: 'portal', icon: 'door_front', label: 'figure.label.primitive-portal' }\n]\n\n@customElement('figure-v3-parts')\nexport class FigureV3Parts extends localize(i18next)(LitElement) {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n min-height: 0;\n overflow: hidden;\n background-color: var(--md-sys-color-surface-container-lowest);\n border-right: 1px solid var(--md-sys-color-outline-variant);\n color: var(--md-sys-color-on-surface);\n font: var(--label-font, inherit);\n font-size: 0.76rem;\n /* Folds by its own width: the editor gives this column 140 px when it has two columns (UX, 2026-09-24). */\n container-type: inline-size;\n }\n section {\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n section[list] {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n }\n h3 {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 6px;\n margin: 0 0 6px 0;\n font-size: var(--ui-t-micro, 11px);\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n span[room] {\n font-size: 0.7rem;\n font-weight: 400;\n letter-spacing: 0;\n text-transform: none;\n }\n div[palette] {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 4px;\n }\n button[shape] {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 2px;\n padding: 7px 2px 5px 2px;\n font: var(--label-font, inherit);\n font-size: 0.66rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n cursor: pointer;\n }\n button[shape]:hover:not([disabled]) {\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border-color: transparent;\n }\n button[shape][disabled] {\n opacity: 0.35;\n cursor: default;\n }\n button[shape] md-icon {\n --md-icon-size: 20px;\n }\n ol {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n li {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 6px;\n border-radius: 6px;\n cursor: pointer;\n }\n li:hover {\n background-color: var(--md-sys-color-surface-container);\n }\n li[selected] {\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n }\n li md-icon {\n --md-icon-size: 16px;\n flex: none;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n button[eye][all] {\n margin-left: auto;\n opacity: 0.75;\n }\n button[eye][all]:hover {\n opacity: 1;\n }\n button[eye] {\n display: flex;\n flex: none;\n padding: 1px;\n color: inherit;\n background: none;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n opacity: 0;\n }\n li:hover button[eye],\n li[selected] button[eye],\n button[eye][off] {\n opacity: 1;\n }\n button[eye][off] {\n color: var(--md-sys-color-tertiary);\n }\n button[eye] md-icon {\n --md-icon-size: 15px;\n color: inherit;\n }\n li[off] span[label],\n li[off] > md-icon,\n li[invisible] span[label],\n li[invisible] > md-icon {\n opacity: 0.45;\n }\n li[selected] md-icon {\n color: inherit;\n }\n span[label] {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 0.78rem;\n }\n span[label] small {\n margin-left: 4px;\n font-size: 0.66rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n div[ops] {\n display: none;\n gap: 1px;\n }\n li[selected] div[ops] {\n display: flex;\n }\n div[ops] button {\n display: flex;\n padding: 2px;\n color: inherit;\n background: none;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n }\n div[ops] button:hover:not([disabled]) {\n background-color: var(--md-sys-color-surface-container-lowest);\n }\n div[ops] button[disabled] {\n opacity: 0.3;\n cursor: default;\n }\n div[ops] md-icon {\n --md-icon-size: 15px;\n color: inherit;\n }\n /*\n Under 200 px the selected row's four buttons pushed its name out and the last one past the edge (seen at 140,\n 2026-09-24). They go to a line of their own under the name.\n */\n @container (max-width: 199px) {\n li[selected] {\n flex-wrap: wrap;\n }\n li[selected] div[ops] {\n flex-basis: 100%;\n justify-content: flex-end;\n }\n }\n p[quiet] {\n margin: 0;\n font-size: 0.76rem;\n line-height: 1.6;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n `\n ]\n\n @property({ type: Array }) parts: V3PartRow[] = []\n /** 고른 부품의 id. 자리(index)가 아니다 — V3 부품은 id 로 안정하게 가리킨다. */\n @property({ type: String }) selected = ''\n /** 편집할 수 없을 때(불러오는 중 · 저장 중) 팔레트와 조작을 잠근다. */\n @property({ type: Boolean }) locked = false\n\n render() {\n return html`\n <section>\n <h3>${i18next.t('figure.label.shapes')}</h3>\n <div palette>\n ${RECIPES.map(\n recipe => html`\n <button\n shape\n ?disabled=${this.locked}\n title=${i18next.t(recipe.label)}\n @click=${() => this.ask('add-part', { kind: recipe.kind })}\n >\n <md-icon>${recipe.icon}</md-icon>\n <span>${i18next.t(recipe.label)}</span>\n </button>\n `\n )}\n </div>\n </section>\n\n <section list>\n <h3>\n <span>${i18next.t('figure.label.parts')}</span>\n <span room>${this.parts.length}</span>\n ${this.renderShowAll()}\n </h3>\n ${this.parts.length === 0\n ? html`<p quiet>${i18next.t('figure.text.no-parts-yet')}</p>`\n : html`<ol>\n ${this.parts.map((part, index) => this.renderRow(part, index))}\n </ol>`}\n </section>\n `\n }\n\n /** 한꺼번에 감추고 한꺼번에 되돌린다 — 하나라도 감춰져 있으면 다 되돌리고, 다 보이면 다 감춘다. */\n private renderShowAll() {\n if (this.parts.length === 0) return nothing\n const anyHidden = this.parts.some(part => part.hidden)\n return html`\n <button\n eye\n all\n ?off=${!anyHidden}\n title=${i18next.t(anyHidden ? 'figure.text.show-all-parts' : 'figure.text.hide-all-parts')}\n @click=${(e: Event) => {\n e.stopPropagation()\n this.ask('set-all-parts-hidden', { hidden: !anyHidden })\n }}\n >\n <md-icon>${anyHidden ? 'visibility' : 'visibility_off'}</md-icon>\n </button>\n `\n }\n\n private renderRow(part: V3PartRow, index: number) {\n const selected = part.id === this.selected\n const off = !!part.hidden\n return html`\n <li\n ?selected=${selected}\n ?off=${off}\n ?invisible=${!!part.invisible}\n @click=${() => this.ask('select-part', { id: part.id })}\n >\n <button\n eye\n ?off=${off}\n title=${i18next.t(off ? 'figure.text.show-this-part' : 'figure.text.hide-this-part')}\n @click=${(e: Event) => {\n e.stopPropagation()\n this.ask('toggle-part-hidden', { id: part.id })\n }}\n >\n <md-icon>${off ? 'visibility_off' : 'visibility'}</md-icon>\n </button>\n <md-icon>${PROVIDER_ICON[part.provider] ?? 'deployed_code'}</md-icon>\n <span label title=${part.id}\n >${part.id}${part.invisible ? html`<small>${i18next.t('figure.label.state-hidden')}</small>` : nothing}</span\n >\n ${selected ? this.renderOps(part.id, index) : nothing}\n </li>\n `\n }\n\n /** 고른 줄의 조작. 못 하는 것은 빼지 않고 흐리게 둔다 — 자리가 밀리면 연달아 지우다 엉뚱한 것을 누른다. */\n private renderOps(id: string, index: number) {\n const count = this.parts.length\n const op = (name: string, icon: string, detail: object, enabled = true) => html`\n <button\n op=${name}\n ?disabled=${!enabled || this.locked}\n title=${i18next.t('figure.button.' + name)}\n @click=${(e: Event) => {\n e.stopPropagation()\n if (enabled && !this.locked) this.ask(name, detail)\n }}\n >\n <md-icon>${icon}</md-icon>\n </button>\n `\n return html`\n <div ops>\n ${op('move-part-up', 'arrow_upward', { id, to: index - 1 }, index > 0)}\n ${op('move-part-down', 'arrow_downward', { id, to: index + 1 }, index < count - 1)}\n ${op('duplicate-part', 'content_copy', { id })} ${op('remove-part', 'delete', { id })}\n </div>\n `\n }\n\n /** 정본을 고치지 않는다 — 무엇을 하겠다는 것만 알린다. */\n private ask(what: string, detail: object) {\n this.dispatchEvent(new CustomEvent(what, { detail, bubbles: true, composed: true }))\n }\n}\n"]}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Which modeller opens a figure — by the format of its source, which the server reports (`format`).
3
+ *
4
+ * The catalogue sent every figure to the V2 modeller, so a converted figure could only be opened by typing the V3
5
+ * page's address (2026-09-24). A new figure starts in V3: new V2 figures would only add to what the cutover has to
6
+ * convert (ADR-0087). A figure is opened in V3 only when the server says it is V3 — an unknown format goes to the
7
+ * V2 page, which reports what it could not read.
8
+ */
9
+ export declare const V3_MODELLER = "figure-v3-modeller";
10
+ export declare const V2_MODELLER = "figure-modeller";
11
+ export declare function modellerPathOf(figure?: {
12
+ id?: string;
13
+ format?: string | null;
14
+ }): string;
15
+ /** The version a V3 asset stamps on itself. The V2 page forwards such a figure rather than failing on it. */
16
+ export declare function isV3Source(source?: string | null): boolean;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Which modeller opens a figure — by the format of its source, which the server reports (`format`).
3
+ *
4
+ * The catalogue sent every figure to the V2 modeller, so a converted figure could only be opened by typing the V3
5
+ * page's address (2026-09-24). A new figure starts in V3: new V2 figures would only add to what the cutover has to
6
+ * convert (ADR-0087). A figure is opened in V3 only when the server says it is V3 — an unknown format goes to the
7
+ * V2 page, which reports what it could not read.
8
+ */
9
+ export const V3_MODELLER = 'figure-v3-modeller';
10
+ export const V2_MODELLER = 'figure-modeller';
11
+ export function modellerPathOf(figure) {
12
+ if (!figure?.id)
13
+ return V3_MODELLER;
14
+ return figure.format === 'v3' ? `${V3_MODELLER}/${figure.id}` : `${V2_MODELLER}/${figure.id}`;
15
+ }
16
+ /** The version a V3 asset stamps on itself. The V2 page forwards such a figure rather than failing on it. */
17
+ export function isV3Source(source) {
18
+ if (!source)
19
+ return false;
20
+ try {
21
+ return JSON.parse(source)?.version === 'figure-v3-asset-1';
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ //# sourceMappingURL=modeller-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modeller-route.js","sourceRoot":"","sources":["../../client/modeller/modeller-route.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,oBAAoB,CAAA;AAC/C,MAAM,CAAC,MAAM,WAAW,GAAG,iBAAiB,CAAA;AAE5C,MAAM,UAAU,cAAc,CAAC,MAAgD;IAC7E,IAAI,CAAC,MAAM,EAAE,EAAE;QAAE,OAAO,WAAW,CAAA;IACnC,OAAO,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,WAAW,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,WAAW,IAAI,MAAM,CAAC,EAAE,EAAE,CAAA;AAC/F,CAAC;AAED,6GAA6G;AAC7G,MAAM,UAAU,UAAU,CAAC,MAAsB;IAC/C,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACzB,IAAI,CAAC;QACH,OAAQ,IAAI,CAAC,KAAK,CAAC,MAAM,CAA2B,EAAE,OAAO,KAAK,mBAAmB,CAAA;IACvF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC","sourcesContent":["/**\n * Which modeller opens a figure — by the format of its source, which the server reports (`format`).\n *\n * The catalogue sent every figure to the V2 modeller, so a converted figure could only be opened by typing the V3\n * page's address (2026-09-24). A new figure starts in V3: new V2 figures would only add to what the cutover has to\n * convert (ADR-0087). A figure is opened in V3 only when the server says it is V3 — an unknown format goes to the\n * V2 page, which reports what it could not read.\n */\nexport const V3_MODELLER = 'figure-v3-modeller'\nexport const V2_MODELLER = 'figure-modeller'\n\nexport function modellerPathOf(figure?: { id?: string; format?: string | null }): string {\n if (!figure?.id) return V3_MODELLER\n return figure.format === 'v3' ? `${V3_MODELLER}/${figure.id}` : `${V2_MODELLER}/${figure.id}`\n}\n\n/** The version a V3 asset stamps on itself. The V2 page forwards such a figure rather than failing on it. */\nexport function isV3Source(source?: string | null): boolean {\n if (!source) return false\n try {\n return (JSON.parse(source) as { version?: unknown })?.version === 'figure-v3-asset-1'\n } catch {\n return false\n }\n}\n"]}
@@ -0,0 +1,26 @@
1
+ import type { V3Asset, V3InputChange, V3WorldBox } from '@hatiolab/figure-model';
2
+ import type { GizmoReach } from './v3-gizmo.js';
3
+ /**
4
+ * 맞추기 — the V2 inspector's align tools on a V3 part (parity row ③): centre on X, centre on Z, stand on the floor,
5
+ * mirror on X or Z, snap to the grid.
6
+ *
7
+ * V2 measured against its base box, which ran from 0 to its size on each axis. A V3 asset's frame has the floor at
8
+ * y 0 and X and Z centred on 0, so centring puts the part's box centre on 0 and the floor puts its lowest point on
9
+ * y 0. The part's box is its evaluated, rotated extent (`v3OccupiedBoxesOf`), so a turned part lands by what it
10
+ * actually covers, not by its unturned size.
11
+ *
12
+ * Every tool writes the part's own position (and, for snap, size) inputs — the same ones the gizmo writes, and only
13
+ * where the gizmo could (`GizmoReach`).
14
+ */
15
+ export type AlignTool = 'centre-x' | 'centre-z' | 'floor' | 'snap';
16
+ export declare function boxOf(asset: V3Asset, part: string): V3WorldBox | undefined;
17
+ /** The grid V2 snaps to: about five steps across the figure, on 1 · 2 · 5 multiples. */
18
+ export declare function gridStepOf(asset: V3Asset): number;
19
+ export declare function alignChanges(asset: V3Asset, part: string, reach: GizmoReach, tool: AlignTool): V3InputChange[];
20
+ /**
21
+ * Where a mirrored copy goes: the same distance on the other side of the centre plane. The copy is made by
22
+ * `duplicate-part` first; this is the position the copy is then given.
23
+ */
24
+ export declare function mirrorChanges(asset: V3Asset, part: string, copy: string, axis: 'x' | 'z'): V3InputChange[];
25
+ /** Mirroring a part that already sits on the centre plane would put the copy inside it — V2 refuses it too. */
26
+ export declare function canMirror(asset: V3Asset, part: string, axis: 'x' | 'z', reach: GizmoReach): boolean;
@@ -0,0 +1,65 @@
1
+ import { compileV3Asset, v3OccupiedBoxesOf, v3WritersOf } from '@hatiolab/figure-model';
2
+ import { niceStep } from './part-edits.js';
3
+ const AXES = ['x', 'y', 'z'];
4
+ export function boxOf(asset, part) {
5
+ const geometry = compileV3Asset(asset).evaluate().geometry;
6
+ return v3OccupiedBoxesOf(geometry, v3WritersOf(asset)).find(box => box.part === part);
7
+ }
8
+ /** The grid V2 snaps to: about five steps across the figure, on 1 · 2 · 5 multiples. */
9
+ export function gridStepOf(asset) {
10
+ const geometry = compileV3Asset(asset).evaluate().geometry;
11
+ const boxes = v3OccupiedBoxesOf(geometry, v3WritersOf(asset));
12
+ if (!boxes.length)
13
+ return 1;
14
+ const span = Math.max(...AXES.map(axis => {
15
+ const lo = Math.min(...boxes.map(b => b.centre[axis] - b.extent[axis] / 2));
16
+ const hi = Math.max(...boxes.map(b => b.centre[axis] + b.extent[axis] / 2));
17
+ return hi - lo;
18
+ }));
19
+ return niceStep(span);
20
+ }
21
+ export function alignChanges(asset, part, reach, tool) {
22
+ if (!reach.translate)
23
+ return [];
24
+ const box = boxOf(asset, part);
25
+ if (!box)
26
+ return [];
27
+ const value = (id) => asset.designInputs[id] ?? 0;
28
+ const set = (input, next) => ({ kind: 'set-design', input, unit: 'mm', value: Math.round(next) });
29
+ const at = (axis) => `${part}.position.${axis}`;
30
+ switch (tool) {
31
+ case 'centre-x':
32
+ return [set(at('x'), value(at('x')) - box.centre.x)];
33
+ case 'centre-z':
34
+ return [set(at('z'), value(at('z')) - box.centre.z)];
35
+ case 'floor':
36
+ return [set(at('y'), value(at('y')) - (box.centre.y - box.extent.y / 2))];
37
+ case 'snap': {
38
+ const step = gridStepOf(asset);
39
+ const round = (n) => Math.round(n / step) * step;
40
+ const changes = AXES.map(axis => set(at(axis), round(value(at(axis)))));
41
+ /* A size never snaps to 0 — the part would look gone. */
42
+ if (reach.scale && reach.sizes)
43
+ for (const id of reach.sizes)
44
+ changes.push(set(id, Math.max(step, round(value(id)))));
45
+ return changes;
46
+ }
47
+ }
48
+ }
49
+ /**
50
+ * Where a mirrored copy goes: the same distance on the other side of the centre plane. The copy is made by
51
+ * `duplicate-part` first; this is the position the copy is then given.
52
+ */
53
+ export function mirrorChanges(asset, part, copy, axis) {
54
+ const box = boxOf(asset, part);
55
+ if (!box)
56
+ return [];
57
+ const id = `${part}.position.${axis}`;
58
+ return [{ kind: 'set-design', input: `${copy}.position.${axis}`, unit: 'mm', value: Math.round((asset.designInputs[id] ?? 0) - 2 * box.centre[axis]) }];
59
+ }
60
+ /** Mirroring a part that already sits on the centre plane would put the copy inside it — V2 refuses it too. */
61
+ export function canMirror(asset, part, axis, reach) {
62
+ const box = reach.translate ? boxOf(asset, part) : undefined;
63
+ return !!box && Math.abs(box.centre[axis]) >= 0.5;
64
+ }
65
+ //# sourceMappingURL=v3-align.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v3-align.js","sourceRoot":"","sources":["../../client/modeller/v3-align.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAGvF,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAiB1C,MAAM,IAAI,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU,CAAA;AAErC,MAAM,UAAU,KAAK,CAAC,KAAc,EAAE,IAAY;IAChD,MAAM,QAAQ,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAA;IAC1D,OAAO,iBAAiB,CAAC,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;AACvF,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,MAAM,QAAQ,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAA;IAC1D,MAAM,KAAK,GAAG,iBAAiB,CAAC,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAA;IAC7D,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,CAAC,CAAA;IAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CACnB,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QACjB,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QAC3E,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QAC3E,OAAO,EAAE,GAAG,EAAE,CAAA;IAChB,CAAC,CAAC,CACH,CAAA;IACD,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAA;AACvB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAc,EAAE,IAAY,EAAE,KAAiB,EAAE,IAAe;IAC3F,IAAI,CAAC,KAAK,CAAC,SAAS;QAAE,OAAO,EAAE,CAAA;IAC/B,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IAC9B,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAA;IACnB,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,CAAC,CAAA;IACzD,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,IAAY,EAAiB,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAChI,MAAM,EAAE,GAAG,CAAC,IAAqB,EAAE,EAAE,CAAC,GAAG,IAAI,aAAa,IAAI,EAAE,CAAA;IAChE,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,UAAU;YACb,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;QACtD,KAAK,UAAU;YACb,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;QACtD,KAAK,OAAO;YACV,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;QAC3E,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;YAC9B,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,IAAI,CAAA;YACxD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YACvE,yDAAyD;YACzD,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK;gBAC5B,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,KAAK;oBAAE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YACvF,OAAO,OAAO,CAAA;QAChB,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc,EAAE,IAAY,EAAE,IAAY,EAAE,IAAe;IACvF,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IAC9B,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAA;IACnB,MAAM,EAAE,GAAG,GAAG,IAAI,aAAa,IAAI,EAAE,CAAA;IACrC,OAAO,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,GAAG,IAAI,aAAa,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAA;AACzJ,CAAC;AAED,+GAA+G;AAC/G,MAAM,UAAU,SAAS,CAAC,KAAc,EAAE,IAAY,EAAE,IAAe,EAAE,KAAiB;IACxF,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAC5D,OAAO,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,CAAA;AACnD,CAAC","sourcesContent":["import { compileV3Asset, v3OccupiedBoxesOf, v3WritersOf } from '@hatiolab/figure-model'\nimport type { V3Asset, V3InputChange, V3WorldBox } from '@hatiolab/figure-model'\n\nimport { niceStep } from './part-edits.js'\nimport type { GizmoReach } from './v3-gizmo.js'\n\n/**\n * 맞추기 — the V2 inspector's align tools on a V3 part (parity row ③): centre on X, centre on Z, stand on the floor,\n * mirror on X or Z, snap to the grid.\n *\n * V2 measured against its base box, which ran from 0 to its size on each axis. A V3 asset's frame has the floor at\n * y 0 and X and Z centred on 0, so centring puts the part's box centre on 0 and the floor puts its lowest point on\n * y 0. The part's box is its evaluated, rotated extent (`v3OccupiedBoxesOf`), so a turned part lands by what it\n * actually covers, not by its unturned size.\n *\n * Every tool writes the part's own position (and, for snap, size) inputs — the same ones the gizmo writes, and only\n * where the gizmo could (`GizmoReach`).\n */\nexport type AlignTool = 'centre-x' | 'centre-z' | 'floor' | 'snap'\n\nconst AXES = ['x', 'y', 'z'] as const\n\nexport function boxOf(asset: V3Asset, part: string): V3WorldBox | undefined {\n const geometry = compileV3Asset(asset).evaluate().geometry\n return v3OccupiedBoxesOf(geometry, v3WritersOf(asset)).find(box => box.part === part)\n}\n\n/** The grid V2 snaps to: about five steps across the figure, on 1 · 2 · 5 multiples. */\nexport function gridStepOf(asset: V3Asset): number {\n const geometry = compileV3Asset(asset).evaluate().geometry\n const boxes = v3OccupiedBoxesOf(geometry, v3WritersOf(asset))\n if (!boxes.length) return 1\n const span = Math.max(\n ...AXES.map(axis => {\n const lo = Math.min(...boxes.map(b => b.centre[axis] - b.extent[axis] / 2))\n const hi = Math.max(...boxes.map(b => b.centre[axis] + b.extent[axis] / 2))\n return hi - lo\n })\n )\n return niceStep(span)\n}\n\nexport function alignChanges(asset: V3Asset, part: string, reach: GizmoReach, tool: AlignTool): V3InputChange[] {\n if (!reach.translate) return []\n const box = boxOf(asset, part)\n if (!box) return []\n const value = (id: string) => asset.designInputs[id] ?? 0\n const set = (input: string, next: number): V3InputChange => ({ kind: 'set-design', input, unit: 'mm', value: Math.round(next) })\n const at = (axis: 'x' | 'y' | 'z') => `${part}.position.${axis}`\n switch (tool) {\n case 'centre-x':\n return [set(at('x'), value(at('x')) - box.centre.x)]\n case 'centre-z':\n return [set(at('z'), value(at('z')) - box.centre.z)]\n case 'floor':\n return [set(at('y'), value(at('y')) - (box.centre.y - box.extent.y / 2))]\n case 'snap': {\n const step = gridStepOf(asset)\n const round = (n: number) => Math.round(n / step) * step\n const changes = AXES.map(axis => set(at(axis), round(value(at(axis)))))\n /* A size never snaps to 0 — the part would look gone. */\n if (reach.scale && reach.sizes)\n for (const id of reach.sizes) changes.push(set(id, Math.max(step, round(value(id)))))\n return changes\n }\n }\n}\n\n/**\n * Where a mirrored copy goes: the same distance on the other side of the centre plane. The copy is made by\n * `duplicate-part` first; this is the position the copy is then given.\n */\nexport function mirrorChanges(asset: V3Asset, part: string, copy: string, axis: 'x' | 'z'): V3InputChange[] {\n const box = boxOf(asset, part)\n if (!box) return []\n const id = `${part}.position.${axis}`\n return [{ kind: 'set-design', input: `${copy}.position.${axis}`, unit: 'mm', value: Math.round((asset.designInputs[id] ?? 0) - 2 * box.centre[axis]) }]\n}\n\n/** Mirroring a part that already sits on the centre plane would put the copy inside it — V2 refuses it too. */\nexport function canMirror(asset: V3Asset, part: string, axis: 'x' | 'z', reach: GizmoReach): boolean {\n const box = reach.translate ? boxOf(asset, part) : undefined\n return !!box && Math.abs(box.centre[axis]) >= 0.5\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { LitElement, type PropertyValues } from 'lit';
2
+ import type { GizmoReach } from './v3-gizmo.js';
2
3
  import { type V3RenderPart } from './v3-renderer.js';
3
4
  /** Renders evaluated V3 geometry directly. There is no parts/base or things-scene conversion. */
4
5
  export declare class FigureV3Canvas extends LitElement {
@@ -6,6 +7,17 @@ export declare class FigureV3Canvas extends LitElement {
6
7
  parts: V3RenderPart[];
7
8
  palette: Record<string, string>;
8
9
  context: string;
10
+ /** The selected part, and what the gizmo may write for it (`gizmoReachOf`). Nothing reachable, no gizmo. */
11
+ selected: string;
12
+ reach: GizmoReach;
13
+ /** Move · turn · size — W · E · R, as in the V2 modeller. */
14
+ mode: 'translate' | 'rotate' | 'scale';
15
+ private transform?;
16
+ /** What the gizmo holds: a stand-in at the body's origin, so the body's own matrix stays the evaluation's. */
17
+ private readonly proxy;
18
+ private dragStart?;
19
+ /** The pointerup that ends a drag is not a pick — releasing off the body would otherwise deselect it. */
20
+ private justDragged;
9
21
  private renderer?;
10
22
  private readonly scene;
11
23
  private readonly group;
@@ -16,10 +28,16 @@ export declare class FigureV3Canvas extends LitElement {
16
28
  private initialized;
17
29
  private contextAtRender;
18
30
  render(): import("lit-html").TemplateResult<1>;
31
+ private key;
19
32
  protected firstUpdated(): void;
20
33
  connectedCallback(): void;
21
34
  private initialize;
22
35
  protected updated(changed: PropertyValues): void;
36
+ /** Puts the gizmo on the selected body, in a mode it may write — or takes it away. */
37
+ private placeGizmo;
38
+ private dragging;
39
+ /** While dragging, the body follows the stand-in; the next evaluation replaces it with where the inputs put it. */
40
+ private follow;
23
41
  private updateGeometry;
24
42
  fit(): void;
25
43
  /**