@things-factory/figure-ui 10.1.38 → 10.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/client/graphql/index.ts +53 -0
  2. package/client/modeller/figure-history-panel.ts +11 -85
  3. package/client/modeller/figure-v3-properties.ts +254 -0
  4. package/client/modeller/figure-v3-verdict.ts +247 -0
  5. package/client/modeller/gate-findings.ts +105 -0
  6. package/client/modeller/v3-canvas.ts +27 -3
  7. package/client/modeller/v3-editor.ts +39 -11
  8. package/client/modeller/v3-figure-facts.ts +53 -0
  9. package/client/modeller/v3-occupancy.ts +6 -2
  10. package/client/pages/figure-v3-modeller-page.ts +250 -17
  11. package/dist-client/graphql/index.d.ts +8 -0
  12. package/dist-client/graphql/index.js +51 -0
  13. package/dist-client/graphql/index.js.map +1 -1
  14. package/dist-client/modeller/figure-history-panel.d.ts +6 -0
  15. package/dist-client/modeller/figure-history-panel.js +9 -85
  16. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  17. package/dist-client/modeller/figure-v3-properties.d.ts +46 -0
  18. package/dist-client/modeller/figure-v3-properties.js +259 -0
  19. package/dist-client/modeller/figure-v3-properties.js.map +1 -0
  20. package/dist-client/modeller/figure-v3-verdict.d.ts +50 -0
  21. package/dist-client/modeller/figure-v3-verdict.js +268 -0
  22. package/dist-client/modeller/figure-v3-verdict.js.map +1 -0
  23. package/dist-client/modeller/gate-findings.d.ts +12 -0
  24. package/dist-client/modeller/gate-findings.js +102 -0
  25. package/dist-client/modeller/gate-findings.js.map +1 -0
  26. package/dist-client/modeller/v3-canvas.d.ts +6 -0
  27. package/dist-client/modeller/v3-canvas.js +27 -3
  28. package/dist-client/modeller/v3-canvas.js.map +1 -1
  29. package/dist-client/modeller/v3-editor.js +39 -11
  30. package/dist-client/modeller/v3-editor.js.map +1 -1
  31. package/dist-client/modeller/v3-figure-facts.d.ts +26 -0
  32. package/dist-client/modeller/v3-figure-facts.js +34 -0
  33. package/dist-client/modeller/v3-figure-facts.js.map +1 -0
  34. package/dist-client/modeller/v3-occupancy.d.ts +5 -1
  35. package/dist-client/modeller/v3-occupancy.js +4 -2
  36. package/dist-client/modeller/v3-occupancy.js.map +1 -1
  37. package/dist-client/pages/figure-v3-modeller-page.d.ts +40 -0
  38. package/dist-client/pages/figure-v3-modeller-page.js +264 -18
  39. package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
  40. package/dist-client/route.d.ts +1 -1
  41. package/dist-client/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +4 -4
  43. package/test/v3-properties.test.ts +93 -0
  44. package/test/v3-theme.test.ts +67 -0
  45. package/test/v3-verdict.test.ts +82 -0
  46. package/translations/en.json +12 -0
  47. package/translations/ja.json +12 -0
  48. package/translations/ko.json +12 -0
  49. package/translations/ms.json +12 -0
  50. package/translations/zh.json +12 -0
@@ -0,0 +1,268 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import { css, html, LitElement, nothing } from 'lit';
4
+ import { customElement, property, state } from 'lit/decorators.js';
5
+ import { i18next, localize } from '@operato/i18n';
6
+ import { parseV3Asset, v3CostOf } from '@hatiolab/figure-model';
7
+ import { inspectFigureDraft } from '../graphql/index.js';
8
+ import { gateFindingStyles, renderGateFinding } from './gate-findings.js';
9
+ import { capacityFrom, MEASURED } from './scene-budget.js';
10
+ /** How long an edit settles before the server is asked again. One question per pause, not one per keystroke. */
11
+ export const VERDICT_SETTLE_MS = 400;
12
+ /**
13
+ * 판정 — the V3 modeller's verdict while editing (parity row ①).
14
+ *
15
+ * The V2 modeller shows, as the author edits, how many of the figure fit on one screen and what would stop it
16
+ * being published. The V3 page showed neither: the author learned what blocked a release only after saving, in the
17
+ * release panel. This tab answers both from the source being edited.
18
+ *
19
+ * The count is computed here, from `v3CostOf` and the same `capacityFrom` the catalogue cards use, so the tab and
20
+ * the card cannot disagree. The findings are not: they come from the server's release door (`inspectFigureDraft`),
21
+ * the same door and the same sentences as the release panel. A second judgement on the screen would say something
22
+ * different the first time either changed.
23
+ *
24
+ * No grade. V2's grade measures V2 reuse; nothing measures it for V3, so the tab shows no letter rather than one
25
+ * nothing computed.
26
+ */
27
+ let FigureV3Verdict = class FigureV3Verdict extends localize(i18next)(LitElement) {
28
+ constructor() {
29
+ super(...arguments);
30
+ /** Bodies in the asset, drawn or hidden. With none there is nothing to fit the space to, so the button is not offered. */
31
+ this.bodies = 0;
32
+ this.failure = '';
33
+ this.asking = false;
34
+ /** Answers can come back out of order; only the newest question's answer is shown. */
35
+ this.asked = 0;
36
+ this.fit = () => {
37
+ this.dispatchEvent(new CustomEvent('fit-base-to-parts', { bubbles: true, composed: true }));
38
+ };
39
+ }
40
+ static { this.styles = [
41
+ gateFindingStyles,
42
+ css `
43
+ :host {
44
+ display: flex;
45
+ flex-direction: column;
46
+ gap: 14px;
47
+ padding: 12px;
48
+ overflow: auto;
49
+ font-size: 0.76rem;
50
+ color: var(--md-sys-color-on-surface);
51
+ }
52
+ section > h3 {
53
+ margin: 0 0 6px 0;
54
+ font-size: var(--ui-t-micro, 11px);
55
+ font-weight: 700;
56
+ letter-spacing: 0.04em;
57
+ text-transform: uppercase;
58
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
59
+ }
60
+ p {
61
+ margin: 4px 0 0;
62
+ }
63
+ p[lead],
64
+ p[basis],
65
+ p[quiet] {
66
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
67
+ }
68
+ div[count] {
69
+ display: flex;
70
+ align-items: baseline;
71
+ gap: 6px;
72
+ }
73
+ span[capacity] {
74
+ font-size: 2.1rem;
75
+ font-weight: 800;
76
+ line-height: 1;
77
+ letter-spacing: -0.01em;
78
+ color: var(--md-sys-color-primary);
79
+ }
80
+ span[unit] {
81
+ font-size: 0.9rem;
82
+ font-weight: 600;
83
+ color: var(--md-sys-color-primary);
84
+ }
85
+ p[clear] {
86
+ color: var(--md-sys-color-primary);
87
+ font-weight: 600;
88
+ }
89
+ p[failed] {
90
+ color: var(--md-sys-color-error);
91
+ }
92
+ button[fit] {
93
+ align-self: flex-start;
94
+ margin-top: 4px;
95
+ padding: 4px 10px;
96
+ font: inherit;
97
+ color: var(--md-sys-color-on-secondary-container);
98
+ background: var(--md-sys-color-secondary-container);
99
+ border: none;
100
+ border-radius: 999em;
101
+ cursor: pointer;
102
+ }
103
+ `
104
+ ]; }
105
+ disconnectedCallback() {
106
+ clearTimeout(this.timer);
107
+ super.disconnectedCallback();
108
+ }
109
+ updated(changes) {
110
+ if (changes.has('source'))
111
+ this.measure();
112
+ if (changes.has('source') || changes.has('figureId') || changes.has('figureType'))
113
+ this.schedule();
114
+ }
115
+ measure() {
116
+ let capacity;
117
+ let bodies = 0;
118
+ try {
119
+ const cost = this.source ? v3CostOf(parseV3Asset(this.source)) : undefined;
120
+ capacity = cost ? capacityFrom(cost.groups, cost.triangles) : undefined;
121
+ bodies = cost ? cost.groups + cost.hidden : 0;
122
+ }
123
+ catch {
124
+ /* A source the format refuses has no count; the server's finding below says why. */
125
+ capacity = undefined;
126
+ }
127
+ this.capacity = capacity;
128
+ this.bodies = bodies;
129
+ this.dispatchEvent(new CustomEvent('capacity-changed', { detail: { instances: capacity?.instances } }));
130
+ }
131
+ schedule() {
132
+ clearTimeout(this.timer);
133
+ if (!this.source) {
134
+ this.inspection = undefined;
135
+ return;
136
+ }
137
+ this.timer = setTimeout(() => this.ask(), VERDICT_SETTLE_MS);
138
+ }
139
+ async ask() {
140
+ const question = ++this.asked;
141
+ const source = this.source;
142
+ this.asking = true;
143
+ try {
144
+ const inspection = await inspectFigureDraft(source, this.figureId);
145
+ if (question !== this.asked)
146
+ return;
147
+ this.inspection = inspection;
148
+ this.failure = '';
149
+ }
150
+ catch (error) {
151
+ if (question !== this.asked)
152
+ return;
153
+ /* The server's reason as it gave it — the screen does not make one up. The last answer is dropped with it. */
154
+ this.inspection = undefined;
155
+ this.failure = error instanceof Error ? error.message : String(error);
156
+ }
157
+ finally {
158
+ if (question === this.asked)
159
+ this.asking = false;
160
+ }
161
+ }
162
+ render() {
163
+ return html `${this.renderCount()} ${this.renderGate()}`;
164
+ }
165
+ renderCount() {
166
+ const capacity = this.capacity;
167
+ return html `
168
+ <section>
169
+ <h3>${i18next.t('figure.label.maturity')}</h3>
170
+ <p lead>${i18next.t('figure.text.how-many-fit')}</p>
171
+ ${capacity
172
+ ? html `
173
+ <div count>
174
+ <span capacity>${capacity.instances.toLocaleString()}</span>
175
+ <span unit>${i18next.t('figure.text.instances-unit')}</span>
176
+ </div>
177
+ <p basis>
178
+ ${i18next.t('figure.text.bound-by-' + capacity.bound, {
179
+ per: capacity.per.toLocaleString(),
180
+ budget: capacity.budget.toLocaleString()
181
+ })}
182
+ <em>
183
+ ${capacity.bound === 'groups'
184
+ ? i18next.t('figure.text.budget-measured-on', { gpu: MEASURED.gpu, on: MEASURED.on })
185
+ : i18next.t('figure.text.budget-not-measured-yet')}
186
+ </em>
187
+ </p>
188
+ `
189
+ : html `<p quiet>${i18next.t('figure.text.maturity-blocked-no-parts')}</p>`}
190
+ </section>
191
+ `;
192
+ }
193
+ renderGate() {
194
+ const findings = this.inspection?.findings ?? [];
195
+ const blocking = findings.filter(finding => finding.blocking);
196
+ const advisory = findings.filter(finding => !finding.blocking);
197
+ return html `
198
+ <section>
199
+ <h3>${i18next.t('figure.label.release-verdict')}</h3>
200
+ ${this.failure
201
+ ? html `<p failed role="alert">${i18next.t('figure.text.verdict-not-received', { reason: this.failure })}</p>`
202
+ : !this.inspection
203
+ ? html `<p quiet>${i18next.t(this.asking ? 'figure.text.verdict-asking' : 'figure.text.verdict-waiting')}</p>`
204
+ : blocking.length
205
+ ? html `<p>${i18next.t('figure.text.cannot-release-now')}</p>`
206
+ : html `<p clear>${i18next.t('figure.text.nothing-blocks-release')}</p>`}
207
+ ${blocking.length
208
+ ? html `<ul gate>
209
+ <li head>${i18next.t('figure.text.must-fix-to-release')}</li>
210
+ ${blocking.map(finding => renderGateFinding(finding, true, this.actionsFor(finding.code)))}
211
+ </ul>`
212
+ : nothing}
213
+ ${advisory.length
214
+ ? html `<ul gate>
215
+ <li head quiet>${i18next.t('figure.text.does-not-block')}</li>
216
+ ${advisory.map(finding => renderGateFinding(finding, false))}
217
+ </ul>`
218
+ : nothing}
219
+ </section>
220
+ `;
221
+ }
222
+ /** The same way out the release panel offers for the same two findings — declare the space from the parts. */
223
+ actionsFor(code) {
224
+ if (code !== 'part-outside-occupancy' && code !== 'occupancy-undeclared')
225
+ return nothing;
226
+ /* A new figure with no parts is refused as undeclared too, but fitting needs parts to measure (seen 2026-09-24). */
227
+ if (!this.bodies)
228
+ return nothing;
229
+ return html `<button fit @click=${this.fit}>${i18next.t('figure.button.fit-base-to-parts')}</button>`;
230
+ }
231
+ };
232
+ __decorate([
233
+ property({ type: String }),
234
+ __metadata("design:type", String)
235
+ ], FigureV3Verdict.prototype, "source", void 0);
236
+ __decorate([
237
+ property({ type: String }),
238
+ __metadata("design:type", String)
239
+ ], FigureV3Verdict.prototype, "figureId", void 0);
240
+ __decorate([
241
+ property({ type: String }),
242
+ __metadata("design:type", String)
243
+ ], FigureV3Verdict.prototype, "figureType", void 0);
244
+ __decorate([
245
+ state(),
246
+ __metadata("design:type", Object)
247
+ ], FigureV3Verdict.prototype, "capacity", void 0);
248
+ __decorate([
249
+ state(),
250
+ __metadata("design:type", Object)
251
+ ], FigureV3Verdict.prototype, "bodies", void 0);
252
+ __decorate([
253
+ state(),
254
+ __metadata("design:type", Object)
255
+ ], FigureV3Verdict.prototype, "inspection", void 0);
256
+ __decorate([
257
+ state(),
258
+ __metadata("design:type", Object)
259
+ ], FigureV3Verdict.prototype, "failure", void 0);
260
+ __decorate([
261
+ state(),
262
+ __metadata("design:type", Object)
263
+ ], FigureV3Verdict.prototype, "asking", void 0);
264
+ FigureV3Verdict = __decorate([
265
+ customElement('figure-v3-verdict')
266
+ ], FigureV3Verdict);
267
+ export { FigureV3Verdict };
268
+ //# sourceMappingURL=figure-v3-verdict.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figure-v3-verdict.js","sourceRoot":"","sources":["../../client/modeller/figure-v3-verdict.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { parseV3Asset, v3CostOf } from '@hatiolab/figure-model'\n\nimport { inspectFigureDraft } from '../graphql/index.js'\nimport type { FigureInspection } from '../types.js'\nimport { gateFindingStyles, renderGateFinding } from './gate-findings.js'\nimport { capacityFrom, MEASURED } from './scene-budget.js'\nimport type { FigureCapacity } from './scene-budget.js'\n\n/** How long an edit settles before the server is asked again. One question per pause, not one per keystroke. */\nexport const VERDICT_SETTLE_MS = 400\n\n/**\n * 판정 — the V3 modeller's verdict while editing (parity row ①).\n *\n * The V2 modeller shows, as the author edits, how many of the figure fit on one screen and what would stop it\n * being published. The V3 page showed neither: the author learned what blocked a release only after saving, in the\n * release panel. This tab answers both from the source being edited.\n *\n * The count is computed here, from `v3CostOf` and the same `capacityFrom` the catalogue cards use, so the tab and\n * the card cannot disagree. The findings are not: they come from the server's release door (`inspectFigureDraft`),\n * the same door and the same sentences as the release panel. A second judgement on the screen would say something\n * different the first time either changed.\n *\n * No grade. V2's grade measures V2 reuse; nothing measures it for V3, so the tab shows no letter rather than one\n * nothing computed.\n */\n@customElement('figure-v3-verdict')\nexport class FigureV3Verdict extends localize(i18next)(LitElement) {\n static styles = [\n gateFindingStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n gap: 14px;\n padding: 12px;\n overflow: auto;\n font-size: 0.76rem;\n color: var(--md-sys-color-on-surface);\n }\n section > h3 {\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 p {\n margin: 4px 0 0;\n }\n p[lead],\n p[basis],\n p[quiet] {\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n div[count] {\n display: flex;\n align-items: baseline;\n gap: 6px;\n }\n span[capacity] {\n font-size: 2.1rem;\n font-weight: 800;\n line-height: 1;\n letter-spacing: -0.01em;\n color: var(--md-sys-color-primary);\n }\n span[unit] {\n font-size: 0.9rem;\n font-weight: 600;\n color: var(--md-sys-color-primary);\n }\n p[clear] {\n color: var(--md-sys-color-primary);\n font-weight: 600;\n }\n p[failed] {\n color: var(--md-sys-color-error);\n }\n button[fit] {\n align-self: flex-start;\n margin-top: 4px;\n padding: 4px 10px;\n font: inherit;\n color: var(--md-sys-color-on-secondary-container);\n background: var(--md-sys-color-secondary-container);\n border: none;\n border-radius: 999em;\n cursor: pointer;\n }\n `\n ]\n\n /** The source being edited, serialised. Every change of it is a new question. */\n @property({ type: String }) source?: string\n /** The saved row, if any — the server reads its type and last release from it. */\n @property({ type: String }) figureId?: string\n /** Changing the type changes the verdict (a temporary type is refused), so it is a reason to ask again. */\n @property({ type: String }) figureType?: string\n\n @state() private capacity?: FigureCapacity\n /** Bodies in the asset, drawn or hidden. With none there is nothing to fit the space to, so the button is not offered. */\n @state() private bodies = 0\n @state() private inspection?: FigureInspection\n @state() private failure = ''\n @state() private asking = false\n\n private timer?: ReturnType<typeof setTimeout>\n /** Answers can come back out of order; only the newest question's answer is shown. */\n private asked = 0\n\n disconnectedCallback() {\n clearTimeout(this.timer)\n super.disconnectedCallback()\n }\n\n updated(changes: Map<string, unknown>) {\n if (changes.has('source')) this.measure()\n if (changes.has('source') || changes.has('figureId') || changes.has('figureType')) this.schedule()\n }\n\n private measure() {\n let capacity: FigureCapacity | undefined\n let bodies = 0\n try {\n const cost = this.source ? v3CostOf(parseV3Asset(this.source)) : undefined\n capacity = cost ? capacityFrom(cost.groups, cost.triangles) : undefined\n bodies = cost ? cost.groups + cost.hidden : 0\n } catch {\n /* A source the format refuses has no count; the server's finding below says why. */\n capacity = undefined\n }\n this.capacity = capacity\n this.bodies = bodies\n this.dispatchEvent(new CustomEvent('capacity-changed', { detail: { instances: capacity?.instances } }))\n }\n\n private schedule() {\n clearTimeout(this.timer)\n if (!this.source) {\n this.inspection = undefined\n return\n }\n this.timer = setTimeout(() => this.ask(), VERDICT_SETTLE_MS)\n }\n\n private async ask() {\n const question = ++this.asked\n const source = this.source!\n this.asking = true\n try {\n const inspection = await inspectFigureDraft(source, this.figureId)\n if (question !== this.asked) return\n this.inspection = inspection\n this.failure = ''\n } catch (error) {\n if (question !== this.asked) return\n /* The server's reason as it gave it — the screen does not make one up. The last answer is dropped with it. */\n this.inspection = undefined\n this.failure = error instanceof Error ? error.message : String(error)\n } finally {\n if (question === this.asked) this.asking = false\n }\n }\n\n render() {\n return html`${this.renderCount()} ${this.renderGate()}`\n }\n\n private renderCount() {\n const capacity = this.capacity\n return html`\n <section>\n <h3>${i18next.t('figure.label.maturity')}</h3>\n <p lead>${i18next.t('figure.text.how-many-fit')}</p>\n ${capacity\n ? html`\n <div count>\n <span capacity>${capacity.instances.toLocaleString()}</span>\n <span unit>${i18next.t('figure.text.instances-unit')}</span>\n </div>\n <p basis>\n ${i18next.t('figure.text.bound-by-' + capacity.bound, {\n per: capacity.per.toLocaleString(),\n budget: capacity.budget.toLocaleString()\n })}\n <em>\n ${capacity.bound === 'groups'\n ? i18next.t('figure.text.budget-measured-on', { gpu: MEASURED.gpu, on: MEASURED.on })\n : i18next.t('figure.text.budget-not-measured-yet')}\n </em>\n </p>\n `\n : html`<p quiet>${i18next.t('figure.text.maturity-blocked-no-parts')}</p>`}\n </section>\n `\n }\n\n private renderGate() {\n const findings = this.inspection?.findings ?? []\n const blocking = findings.filter(finding => finding.blocking)\n const advisory = findings.filter(finding => !finding.blocking)\n return html`\n <section>\n <h3>${i18next.t('figure.label.release-verdict')}</h3>\n ${this.failure\n ? html`<p failed role=\"alert\">${i18next.t('figure.text.verdict-not-received', { reason: this.failure })}</p>`\n : !this.inspection\n ? html`<p quiet>${i18next.t(this.asking ? 'figure.text.verdict-asking' : 'figure.text.verdict-waiting')}</p>`\n : blocking.length\n ? html`<p>${i18next.t('figure.text.cannot-release-now')}</p>`\n : html`<p clear>${i18next.t('figure.text.nothing-blocks-release')}</p>`}\n ${blocking.length\n ? html`<ul gate>\n <li head>${i18next.t('figure.text.must-fix-to-release')}</li>\n ${blocking.map(finding => renderGateFinding(finding, true, this.actionsFor(finding.code)))}\n </ul>`\n : nothing}\n ${advisory.length\n ? html`<ul gate>\n <li head quiet>${i18next.t('figure.text.does-not-block')}</li>\n ${advisory.map(finding => renderGateFinding(finding, false))}\n </ul>`\n : nothing}\n </section>\n `\n }\n\n /** The same way out the release panel offers for the same two findings — declare the space from the parts. */\n private actionsFor(code: string) {\n if (code !== 'part-outside-occupancy' && code !== 'occupancy-undeclared') return nothing\n /* A new figure with no parts is refused as undeclared too, but fitting needs parts to measure (seen 2026-09-24). */\n if (!this.bodies) return nothing\n return html`<button fit @click=${this.fit}>${i18next.t('figure.button.fit-base-to-parts')}</button>`\n }\n\n private fit = () => {\n this.dispatchEvent(new CustomEvent('fit-base-to-parts', { bubbles: true, composed: true }))\n }\n}\n"]}
@@ -0,0 +1,12 @@
1
+ import { type TemplateResult } from 'lit';
2
+ import type { FigureGateFinding } from '../types.js';
3
+ /**
4
+ * One release-gate finding as a line on screen — the measured fact, where, why it matters, and what to change.
5
+ *
6
+ * Two places draw these: the release panel (`figure-history-panel`) and the V3 modeller's verdict while editing
7
+ * (`figure-v3-verdict`). They show the same findings from the same server door, so they draw them one way; a copy
8
+ * in each would drift the first time one of them is touched. The caller passes its own action buttons, because
9
+ * what can be done about a finding depends on the screen it is shown on.
10
+ */
11
+ export declare function renderGateFinding(finding: FigureGateFinding, blocking: boolean, actions?: unknown): TemplateResult;
12
+ export declare const gateFindingStyles: import("lit").CSSResult;
@@ -0,0 +1,102 @@
1
+ import { css, html, nothing } from 'lit';
2
+ import { i18next } from '@operato/i18n';
3
+ /**
4
+ * One release-gate finding as a line on screen — the measured fact, where, why it matters, and what to change.
5
+ *
6
+ * Two places draw these: the release panel (`figure-history-panel`) and the V3 modeller's verdict while editing
7
+ * (`figure-v3-verdict`). They show the same findings from the same server door, so they draw them one way; a copy
8
+ * in each would drift the first time one of them is touched. The caller passes its own action buttons, because
9
+ * what can be done about a finding depends on the screen it is shown on.
10
+ */
11
+ export function renderGateFinding(finding, blocking, actions = nothing) {
12
+ return html `
13
+ <li finding ?blocking=${blocking}>
14
+ <span msg>${finding.message}</span>
15
+ ${finding.at
16
+ ? html `
17
+ <details affected>
18
+ <summary>${i18next.t('figure.text.show-affected-parts')}</summary>
19
+ <code where>${finding.at}</code>
20
+ </details>
21
+ `
22
+ : nothing}
23
+ ${finding.why ? html `<span why-line>${finding.why}</span>` : nothing}
24
+ ${finding.how ? html `<span how-line><md-icon>build</md-icon>${finding.how}</span>` : nothing}
25
+ ${actions}
26
+ </li>
27
+ `;
28
+ }
29
+ export const gateFindingStyles = css `
30
+ ul[gate] {
31
+ margin: var(--spacing-small, 6px) 0 0;
32
+ padding: 0;
33
+ list-style: none;
34
+ display: flex;
35
+ flex-direction: column;
36
+ gap: 3px;
37
+ }
38
+
39
+ li[head] {
40
+ font: var(--label-font, inherit);
41
+ font-size: 0.72rem;
42
+ color: var(--md-sys-color-error);
43
+ }
44
+
45
+ li[head][quiet] {
46
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
47
+ }
48
+
49
+ li[finding] {
50
+ display: flex;
51
+ flex-direction: column;
52
+ align-items: flex-start;
53
+ gap: 3px;
54
+ padding: 3px 6px;
55
+ font-size: 0.72rem;
56
+ line-height: 1.5;
57
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
58
+ background-color: var(--md-sys-color-surface-container);
59
+ border-radius: 4px;
60
+ }
61
+
62
+ li[finding][blocking] {
63
+ color: var(--md-sys-color-on-error-container, var(--md-sys-color-on-surface));
64
+ background-color: var(--md-sys-color-error-container);
65
+ }
66
+
67
+ li[finding] code[where] {
68
+ font-family: var(--mono-font, monospace);
69
+ font-size: 0.68rem;
70
+ opacity: 0.85;
71
+ }
72
+
73
+ li[finding] details[affected] {
74
+ width: 100%;
75
+ color: inherit;
76
+ }
77
+
78
+ li[finding] details[affected] summary {
79
+ cursor: pointer;
80
+ font-weight: 600;
81
+ }
82
+
83
+ /* 왜 나쁜지는 곁글로 — 읽어야 하지만 잰 것보다 먼저 눈에 들 필요는 없다 */
84
+ li[finding] span[why-line] {
85
+ opacity: 0.8;
86
+ }
87
+
88
+ /* 무엇을 바꾸면 되나 — 이 줄이 실제로 사람이 할 일이다. 연장 표를 달아 눈에 걸리게 한다 */
89
+ li[finding] span[how-line] {
90
+ display: flex;
91
+ align-items: flex-start;
92
+ gap: 4px;
93
+ padding-top: 2px;
94
+ font-weight: 600;
95
+ }
96
+ li[finding] span[how-line] md-icon {
97
+ --md-icon-size: 13px;
98
+ flex: none;
99
+ margin-top: 1px;
100
+ }
101
+ `;
102
+ //# sourceMappingURL=gate-findings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-findings.js","sourceRoot":"","sources":["../../client/modeller/gate-findings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAuB,MAAM,KAAK,CAAA;AAC7D,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAIvC;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAA0B,EAAE,QAAiB,EAAE,UAAmB,OAAO;IACzG,OAAO,IAAI,CAAA;4BACe,QAAQ;kBAClB,OAAO,CAAC,OAAO;QACzB,OAAO,CAAC,EAAE;QACV,CAAC,CAAC,IAAI,CAAA;;yBAEW,OAAO,CAAC,CAAC,CAAC,iCAAiC,CAAC;4BACzC,OAAO,CAAC,EAAE;;WAE3B;QACH,CAAC,CAAC,OAAO;QACT,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAA,kBAAkB,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,OAAO;QAClE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAA,0CAA0C,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,OAAO;QAC1F,OAAO;;GAEZ,CAAA;AACH,CAAC;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwEnC,CAAA","sourcesContent":["import { css, html, nothing, type TemplateResult } from 'lit'\nimport { i18next } from '@operato/i18n'\n\nimport type { FigureGateFinding } from '../types.js'\n\n/**\n * One release-gate finding as a line on screen — the measured fact, where, why it matters, and what to change.\n *\n * Two places draw these: the release panel (`figure-history-panel`) and the V3 modeller's verdict while editing\n * (`figure-v3-verdict`). They show the same findings from the same server door, so they draw them one way; a copy\n * in each would drift the first time one of them is touched. The caller passes its own action buttons, because\n * what can be done about a finding depends on the screen it is shown on.\n */\nexport function renderGateFinding(finding: FigureGateFinding, blocking: boolean, actions: unknown = nothing): TemplateResult {\n return html`\n <li finding ?blocking=${blocking}>\n <span msg>${finding.message}</span>\n ${finding.at\n ? html`\n <details affected>\n <summary>${i18next.t('figure.text.show-affected-parts')}</summary>\n <code where>${finding.at}</code>\n </details>\n `\n : nothing}\n ${finding.why ? html`<span why-line>${finding.why}</span>` : nothing}\n ${finding.how ? html`<span how-line><md-icon>build</md-icon>${finding.how}</span>` : nothing}\n ${actions}\n </li>\n `\n}\n\nexport const gateFindingStyles = css`\n ul[gate] {\n margin: var(--spacing-small, 6px) 0 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 3px;\n }\n\n li[head] {\n font: var(--label-font, inherit);\n font-size: 0.72rem;\n color: var(--md-sys-color-error);\n }\n\n li[head][quiet] {\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n li[finding] {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 3px;\n padding: 3px 6px;\n font-size: 0.72rem;\n line-height: 1.5;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: var(--md-sys-color-surface-container);\n border-radius: 4px;\n }\n\n li[finding][blocking] {\n color: var(--md-sys-color-on-error-container, var(--md-sys-color-on-surface));\n background-color: var(--md-sys-color-error-container);\n }\n\n li[finding] code[where] {\n font-family: var(--mono-font, monospace);\n font-size: 0.68rem;\n opacity: 0.85;\n }\n\n li[finding] details[affected] {\n width: 100%;\n color: inherit;\n }\n\n li[finding] details[affected] summary {\n cursor: pointer;\n font-weight: 600;\n }\n\n /* 왜 나쁜지는 곁글로 — 읽어야 하지만 잰 것보다 먼저 눈에 들 필요는 없다 */\n li[finding] span[why-line] {\n opacity: 0.8;\n }\n\n /* 무엇을 바꾸면 되나 — 이 줄이 실제로 사람이 할 일이다. 연장 표를 달아 눈에 걸리게 한다 */\n li[finding] span[how-line] {\n display: flex;\n align-items: flex-start;\n gap: 4px;\n padding-top: 2px;\n font-weight: 600;\n }\n li[finding] span[how-line] md-icon {\n --md-icon-size: 13px;\n flex: none;\n margin-top: 1px;\n }\n`\n"]}
@@ -12,6 +12,7 @@ export declare class FigureV3Canvas extends LitElement {
12
12
  private readonly camera;
13
13
  private controls?;
14
14
  private observer?;
15
+ private themeObserver?;
15
16
  private initialized;
16
17
  private contextAtRender;
17
18
  render(): import("lit-html").TemplateResult<1>;
@@ -21,6 +22,11 @@ export declare class FigureV3Canvas extends LitElement {
21
22
  protected updated(changed: PropertyValues): void;
22
23
  private updateGeometry;
23
24
  fit(): void;
25
+ /**
26
+ * The scene's background, taken from the host's computed background — the token resolved and normalised to
27
+ * `rgb(r, g, b)`, which THREE.Color reads. The token's own text may use the space-separated form it does not.
28
+ */
29
+ private paintBackground;
24
30
  private draw;
25
31
  private pick;
26
32
  thumbnail(): any;
@@ -33,7 +33,12 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
33
33
  min-width: 0;
34
34
  min-height: 0;
35
35
  position: relative;
36
- background: #eef2f4;
36
+ /*
37
+ The working board is one step above the surface in both themes (UX, 2026-09-24): surface-container is 238
38
+ in light and 31 in dark. The 3D background is read from this same value (paintBackground), so the
39
+ first paint and the scene agree and there is no flash between them.
40
+ */
41
+ background: var(--md-sys-color-surface-container);
37
42
  }
38
43
  canvas {
39
44
  display: block;
@@ -45,7 +50,7 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
45
50
  inset: 45% 1rem auto;
46
51
  text-align: center;
47
52
  pointer-events: none;
48
- color: #526773;
53
+ color: var(--md-sys-color-on-surface-variant);
49
54
  }
50
55
  `; }
51
56
  render() {
@@ -66,7 +71,16 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
66
71
  const canvas = this.renderRoot.querySelector('canvas');
67
72
  this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, preserveDrawingBuffer: true });
68
73
  this.renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
69
- this.scene.background = new THREE.Color('#eef2f4');
74
+ this.paintBackground();
75
+ /*
76
+ The app switches theme by the body's class (light · dark). Reading the colour once at start left a light
77
+ board in the middle of a dark screen after every switch (seen 2026-09-24), so it is read again then.
78
+ */
79
+ this.themeObserver = new MutationObserver(() => {
80
+ this.paintBackground();
81
+ this.draw();
82
+ });
83
+ this.themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
70
84
  this.scene.add(this.group, new THREE.HemisphereLight(0xffffff, 0x62717d, 2.5));
71
85
  const light = new THREE.DirectionalLight(0xffffff, 3);
72
86
  light.position.set(1, 2, 3);
@@ -123,12 +137,22 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
123
137
  this.controls?.update();
124
138
  this.draw();
125
139
  }
140
+ /**
141
+ * The scene's background, taken from the host's computed background — the token resolved and normalised to
142
+ * `rgb(r, g, b)`, which THREE.Color reads. The token's own text may use the space-separated form it does not.
143
+ */
144
+ paintBackground() {
145
+ const colour = getComputedStyle(this).backgroundColor;
146
+ if (colour && colour !== 'rgba(0, 0, 0, 0)' && colour !== 'transparent')
147
+ this.scene.background = new THREE.Color(colour);
148
+ }
126
149
  thumbnail() {
127
150
  this.draw();
128
151
  return this.renderer?.domElement.toDataURL('image/png');
129
152
  }
130
153
  disconnectedCallback() {
131
154
  this.observer?.disconnect();
155
+ this.themeObserver?.disconnect();
132
156
  this.controls?.dispose();
133
157
  disposeV3Meshes(this.group);
134
158
  this.renderer?.dispose();
@@ -1 +1 @@
1
- {"version":3,"file":"v3-canvas.js","sourceRoot":"","sources":["../../client/modeller/v3-canvas.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css, type PropertyValues } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport * as THREE from 'three'\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'\nimport { replaceV3Meshes, disposeV3Meshes, type V3RenderPart } from './v3-renderer.js'\n\n/** Renders evaluated V3 geometry directly. There is no parts/base or things-scene conversion. */\n@customElement('figure-v3-canvas')\nexport class FigureV3Canvas extends LitElement {\n static styles = css`\n :host {\n display: block;\n min-width: 0;\n min-height: 0;\n position: relative;\n background: #eef2f4;\n }\n canvas {\n display: block;\n width: 100%;\n height: 100%;\n }\n p {\n position: absolute;\n inset: 45% 1rem auto;\n text-align: center;\n pointer-events: none;\n color: #526773;\n }\n `\n @property({ attribute: false }) parts: V3RenderPart[] = []\n @property({ attribute: false }) palette: Record<string, string> = {}\n @property() context = ''\n private renderer?: THREE.WebGLRenderer\n private readonly scene = new THREE.Scene()\n private readonly group = new THREE.Group()\n private readonly camera = new THREE.PerspectiveCamera(40, 1, 0.1, 1000000)\n private controls?: OrbitControls\n private observer?: ResizeObserver\n private initialized = false\n private contextAtRender = ''\n render() {\n return html`<canvas aria-label=\"Figure 3D 모델\" @pointerup=${this.pick}></canvas\n >${this.parts.length ? null : html`<p>도형을 추가해 모델링을 시작하세요.</p>`}`\n }\n protected firstUpdated() {\n this.initialize()\n }\n connectedCallback() {\n super.connectedCallback()\n if (this.hasUpdated) void this.updateComplete.then(() => this.initialize())\n }\n private initialize() {\n if (this.initialized || !this.isConnected) return\n const canvas = this.renderRoot.querySelector('canvas')!\n this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, preserveDrawingBuffer: true })\n this.renderer.setPixelRatio(Math.min(devicePixelRatio, 2))\n this.scene.background = new THREE.Color('#eef2f4')\n this.scene.add(this.group, new THREE.HemisphereLight(0xffffff, 0x62717d, 2.5))\n const light = new THREE.DirectionalLight(0xffffff, 3)\n light.position.set(1, 2, 3)\n this.scene.add(light)\n this.controls = new OrbitControls(this.camera, canvas)\n this.controls.addEventListener('change', this.draw)\n this.observer = new ResizeObserver(() => {\n const r = this.getBoundingClientRect()\n if (r.width && r.height) {\n this.renderer!.setSize(r.width, r.height, false)\n this.camera.aspect = r.width / r.height\n this.camera.updateProjectionMatrix()\n this.draw()\n }\n })\n this.observer.observe(this)\n this.initialized = true\n this.updateGeometry(true)\n }\n protected updated(changed: PropertyValues) {\n if (this.initialized && (changed.has('parts') || changed.has('palette') || changed.has('context')))\n this.updateGeometry(this.contextAtRender !== this.context)\n }\n private updateGeometry(fit: boolean) {\n try {\n const wasEmpty = this.group.children.length === 0\n replaceV3Meshes(this.group, this.parts, this.palette)\n this.contextAtRender = this.context\n if (fit || wasEmpty) this.fit()\n this.draw()\n } catch (error) {\n // Do not leave another model's geometry visible after a failed context switch.\n if (this.contextAtRender !== this.context) {\n disposeV3Meshes(this.group)\n this.draw()\n }\n this.dispatchEvent(new CustomEvent('render-error', { detail: error, bubbles: true, composed: true }))\n }\n }\n fit() {\n const bounds = new THREE.Box3().setFromObject(this.group),\n center = new THREE.Vector3(),\n size = new THREE.Vector3()\n if (!bounds.isEmpty()) {\n bounds.getCenter(center)\n bounds.getSize(size)\n }\n const radius = Math.max(size.length() / 2, 100),\n distance = (radius / Math.sin(THREE.MathUtils.degToRad(this.camera.fov / 2))) * 1.3\n this.camera.position.copy(center).add(new THREE.Vector3(1, 0.8, 1).normalize().multiplyScalar(distance))\n this.camera.near = Math.max(0.01, distance / 10000)\n this.camera.far = Math.max(10000, distance * 100)\n this.camera.updateProjectionMatrix()\n this.controls?.target.copy(center)\n this.controls?.update()\n this.draw()\n }\n private draw = () => {\n if (this.renderer) this.renderer.render(this.scene, this.camera)\n }\n private pick = (event: PointerEvent) => {\n const r = (event.currentTarget as HTMLCanvasElement).getBoundingClientRect(),\n ray = new THREE.Raycaster()\n ray.setFromCamera(\n new THREE.Vector2(((event.clientX - r.left) / r.width) * 2 - 1, (-(event.clientY - r.top) / r.height) * 2 + 1),\n this.camera\n )\n const hit = ray.intersectObjects(this.group.children)[0]\n this.dispatchEvent(\n new CustomEvent('part-selected', { detail: hit?.object.name ?? '', bubbles: true, composed: true })\n )\n }\n thumbnail() {\n this.draw()\n return this.renderer?.domElement.toDataURL('image/png')\n }\n disconnectedCallback() {\n this.observer?.disconnect()\n this.controls?.dispose()\n disposeV3Meshes(this.group)\n this.renderer?.dispose()\n this.renderer = undefined\n this.scene.clear()\n this.initialized = false\n super.disconnectedCallback()\n }\n}\n"]}
1
+ {"version":3,"file":"v3-canvas.js","sourceRoot":"","sources":["../../client/modeller/v3-canvas.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css, type PropertyValues } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport * as THREE from 'three'\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'\nimport { replaceV3Meshes, disposeV3Meshes, type V3RenderPart } from './v3-renderer.js'\n\n/** Renders evaluated V3 geometry directly. There is no parts/base or things-scene conversion. */\n@customElement('figure-v3-canvas')\nexport class FigureV3Canvas extends LitElement {\n static styles = css`\n :host {\n display: block;\n min-width: 0;\n min-height: 0;\n position: relative;\n /*\n The working board is one step above the surface in both themes (UX, 2026-09-24): surface-container is 238\n in light and 31 in dark. The 3D background is read from this same value (paintBackground), so the\n first paint and the scene agree and there is no flash between them.\n */\n background: var(--md-sys-color-surface-container);\n }\n canvas {\n display: block;\n width: 100%;\n height: 100%;\n }\n p {\n position: absolute;\n inset: 45% 1rem auto;\n text-align: center;\n pointer-events: none;\n color: var(--md-sys-color-on-surface-variant);\n }\n `\n @property({ attribute: false }) parts: V3RenderPart[] = []\n @property({ attribute: false }) palette: Record<string, string> = {}\n @property() context = ''\n private renderer?: THREE.WebGLRenderer\n private readonly scene = new THREE.Scene()\n private readonly group = new THREE.Group()\n private readonly camera = new THREE.PerspectiveCamera(40, 1, 0.1, 1000000)\n private controls?: OrbitControls\n private observer?: ResizeObserver\n private themeObserver?: MutationObserver\n private initialized = false\n private contextAtRender = ''\n render() {\n return html`<canvas aria-label=\"Figure 3D 모델\" @pointerup=${this.pick}></canvas\n >${this.parts.length ? null : html`<p>도형을 추가해 모델링을 시작하세요.</p>`}`\n }\n protected firstUpdated() {\n this.initialize()\n }\n connectedCallback() {\n super.connectedCallback()\n if (this.hasUpdated) void this.updateComplete.then(() => this.initialize())\n }\n private initialize() {\n if (this.initialized || !this.isConnected) return\n const canvas = this.renderRoot.querySelector('canvas')!\n this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, preserveDrawingBuffer: true })\n this.renderer.setPixelRatio(Math.min(devicePixelRatio, 2))\n this.paintBackground()\n /*\n The app switches theme by the body's class (light · dark). Reading the colour once at start left a light\n board in the middle of a dark screen after every switch (seen 2026-09-24), so it is read again then.\n */\n this.themeObserver = new MutationObserver(() => {\n this.paintBackground()\n this.draw()\n })\n this.themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] })\n this.scene.add(this.group, new THREE.HemisphereLight(0xffffff, 0x62717d, 2.5))\n const light = new THREE.DirectionalLight(0xffffff, 3)\n light.position.set(1, 2, 3)\n this.scene.add(light)\n this.controls = new OrbitControls(this.camera, canvas)\n this.controls.addEventListener('change', this.draw)\n this.observer = new ResizeObserver(() => {\n const r = this.getBoundingClientRect()\n if (r.width && r.height) {\n this.renderer!.setSize(r.width, r.height, false)\n this.camera.aspect = r.width / r.height\n this.camera.updateProjectionMatrix()\n this.draw()\n }\n })\n this.observer.observe(this)\n this.initialized = true\n this.updateGeometry(true)\n }\n protected updated(changed: PropertyValues) {\n if (this.initialized && (changed.has('parts') || changed.has('palette') || changed.has('context')))\n this.updateGeometry(this.contextAtRender !== this.context)\n }\n private updateGeometry(fit: boolean) {\n try {\n const wasEmpty = this.group.children.length === 0\n replaceV3Meshes(this.group, this.parts, this.palette)\n this.contextAtRender = this.context\n if (fit || wasEmpty) this.fit()\n this.draw()\n } catch (error) {\n // Do not leave another model's geometry visible after a failed context switch.\n if (this.contextAtRender !== this.context) {\n disposeV3Meshes(this.group)\n this.draw()\n }\n this.dispatchEvent(new CustomEvent('render-error', { detail: error, bubbles: true, composed: true }))\n }\n }\n fit() {\n const bounds = new THREE.Box3().setFromObject(this.group),\n center = new THREE.Vector3(),\n size = new THREE.Vector3()\n if (!bounds.isEmpty()) {\n bounds.getCenter(center)\n bounds.getSize(size)\n }\n const radius = Math.max(size.length() / 2, 100),\n distance = (radius / Math.sin(THREE.MathUtils.degToRad(this.camera.fov / 2))) * 1.3\n this.camera.position.copy(center).add(new THREE.Vector3(1, 0.8, 1).normalize().multiplyScalar(distance))\n this.camera.near = Math.max(0.01, distance / 10000)\n this.camera.far = Math.max(10000, distance * 100)\n this.camera.updateProjectionMatrix()\n this.controls?.target.copy(center)\n this.controls?.update()\n this.draw()\n }\n /**\n * The scene's background, taken from the host's computed background — the token resolved and normalised to\n * `rgb(r, g, b)`, which THREE.Color reads. The token's own text may use the space-separated form it does not.\n */\n private paintBackground() {\n const colour = getComputedStyle(this).backgroundColor\n if (colour && colour !== 'rgba(0, 0, 0, 0)' && colour !== 'transparent') this.scene.background = new THREE.Color(colour)\n }\n private draw = () => {\n if (this.renderer) this.renderer.render(this.scene, this.camera)\n }\n private pick = (event: PointerEvent) => {\n const r = (event.currentTarget as HTMLCanvasElement).getBoundingClientRect(),\n ray = new THREE.Raycaster()\n ray.setFromCamera(\n new THREE.Vector2(((event.clientX - r.left) / r.width) * 2 - 1, (-(event.clientY - r.top) / r.height) * 2 + 1),\n this.camera\n )\n const hit = ray.intersectObjects(this.group.children)[0]\n this.dispatchEvent(\n new CustomEvent('part-selected', { detail: hit?.object.name ?? '', bubbles: true, composed: true })\n )\n }\n thumbnail() {\n this.draw()\n return this.renderer?.domElement.toDataURL('image/png')\n }\n disconnectedCallback() {\n this.observer?.disconnect()\n this.themeObserver?.disconnect()\n this.controls?.dispose()\n disposeV3Meshes(this.group)\n this.renderer?.dispose()\n this.renderer = undefined\n this.scene.clear()\n this.initialized = false\n super.disconnectedCallback()\n }\n}\n"]}