@things-factory/figure-ui 10.1.3 → 10.1.4

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 (35) hide show
  1. package/client/modeller/figure-ask.ts +14 -4
  2. package/client/modeller/figure-canvas.ts +53 -0
  3. package/client/modeller/figure-inspector.ts +78 -2
  4. package/client/modeller/figure-side.ts +3 -0
  5. package/client/modeller/figure-thumbnail.ts +78 -0
  6. package/client/pages/figure-list-page.ts +67 -8
  7. package/client/pages/figure-modeller-page.ts +92 -19
  8. package/dist-client/modeller/figure-ask.js +14 -4
  9. package/dist-client/modeller/figure-ask.js.map +1 -1
  10. package/dist-client/modeller/figure-canvas.d.ts +11 -0
  11. package/dist-client/modeller/figure-canvas.js +42 -0
  12. package/dist-client/modeller/figure-canvas.js.map +1 -1
  13. package/dist-client/modeller/figure-inspector.d.ts +17 -0
  14. package/dist-client/modeller/figure-inspector.js +77 -2
  15. package/dist-client/modeller/figure-inspector.js.map +1 -1
  16. package/dist-client/modeller/figure-side.d.ts +1 -0
  17. package/dist-client/modeller/figure-side.js +12 -0
  18. package/dist-client/modeller/figure-side.js.map +1 -1
  19. package/dist-client/modeller/figure-thumbnail.d.ts +3 -0
  20. package/dist-client/modeller/figure-thumbnail.js +59 -0
  21. package/dist-client/modeller/figure-thumbnail.js.map +1 -0
  22. package/dist-client/pages/figure-list-page.d.ts +5 -0
  23. package/dist-client/pages/figure-list-page.js +70 -10
  24. package/dist-client/pages/figure-list-page.js.map +1 -1
  25. package/dist-client/pages/figure-modeller-page.d.ts +18 -0
  26. package/dist-client/pages/figure-modeller-page.js +89 -19
  27. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  28. package/dist-client/tsconfig.tsbuildinfo +1 -0
  29. package/dist-server/tsconfig.tsbuildinfo +1 -0
  30. package/package.json +3 -3
  31. package/translations/en.json +4 -0
  32. package/translations/ja.json +4 -0
  33. package/translations/ko.json +4 -0
  34. package/translations/ms.json +4 -0
  35. package/translations/zh.json +4 -0
@@ -1 +1 @@
1
- {"version":3,"file":"figure-list-page.js","sourceRoot":"","sources":["../../client/pages/figure-list-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '../viewparts/figure-card.js'\n\nimport { css, html } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { navigate, PageView } from '@operato/shell'\nimport { ScrollbarStyles } from '@operato/styles'\n\nimport { deleteFigure, fetchFigureList } from '../graphql/index.js'\nimport type { Figure } from '../types.js'\n\nconst FigureListPageBase = localize(i18next)(PageView) as typeof PageView\n\n/**\n * Figure 목록.\n *\n * 이 화면은 저작면으로 가는 문이다. 여기서 오래 머물 화면이 아니므로 꾸미지 않는다.\n * 대신 **저작자가 목록에서 판단할 수 있어야 하는 것**은 카드에 실는다 — 무게(삼각형\n * 수)와 재활용 점수다. 그것을 저작면에 들어가서야 알면 늦다.\n */\n@customElement('figure-list-page')\nexport class FigureListPage extends FigureListPageBase {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n }\n\n /* 걸러 보기 줄 */\n div[filters] {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n\n input[search] {\n flex: 1;\n min-width: 0;\n max-width: 320px;\n padding: 6px 10px;\n font: var(--input-field-font, inherit);\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n /* 명령 요소의 모서리는 테마가 정한다 — 알약으로 만들지 않는다 */\n border-radius: 6px;\n }\n\n button[state] {\n padding: 5px 12px;\n font: var(--label-font, inherit);\n color: 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[state][selected] {\n color: var(--md-sys-color-on-secondary-container);\n background-color: var(--md-sys-color-secondary-container);\n border-color: transparent;\n }\n\n span[count] {\n margin-left: auto;\n font: var(--label-font, inherit);\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* 카드 격자 */\n div[cards] {\n flex: 1;\n overflow-y: auto;\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-large, 12px);\n align-content: start;\n }\n\n /* ── 아무것도 없을 때 ──────────────────────────────────────────────\n 이 자리가 이 화면에서 가장 중요하다. 처음 온 사람은 여기서 이 도구가\n 무엇을 만드는 것인지 알게 된다. 「데이터 없음」으로 끝내지 않는다. */\n div[empty] {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-large, 12px);\n text-align: center;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n div[empty] md-icon {\n --md-icon-size: 48px;\n color: var(--md-sys-color-outline-variant);\n }\n div[empty] h2 {\n margin: 0;\n font: var(--theme-font, inherit);\n font-size: var(--ui-t-title, 15px);\n color: var(--md-sys-color-on-surface);\n }\n div[empty] p {\n margin: 0;\n max-width: 34em;\n line-height: 1.6;\n }\n div[empty] button {\n margin-top: var(--spacing-medium, 8px);\n padding: 8px 18px;\n font: var(--label-font, inherit);\n color: var(--md-sys-color-on-primary, #fff);\n background-color: var(--md-sys-color-primary);\n border: none;\n border-radius: 6px;\n cursor: pointer;\n }\n `\n ]\n\n @state() private figures: Figure[] = []\n @state() private total = 0\n @state() private search = ''\n @state() private stateFilter = ''\n @state() private loaded = false\n\n get context() {\n return {\n title: i18next.t('title.figures'),\n actions: [\n {\n title: i18next.t('button.new-figure'),\n action: () => this.openModeller()\n }\n ]\n }\n }\n\n render() {\n return html`\n <div filters>\n <input\n search\n type=\"search\"\n .value=${this.search}\n placeholder=${i18next.t('text.search-figure-by-name-or-type')}\n @change=${(e: Event) => {\n this.search = (e.target as HTMLInputElement).value\n this.reload()\n }}\n />\n ${[\n { value: '', label: i18next.t('label.all') },\n { value: 'draft', label: i18next.t('label.draft') },\n { value: 'released', label: i18next.t('label.released') }\n ].map(\n option => html`\n <button\n state\n ?selected=${this.stateFilter === option.value}\n @click=${() => {\n this.stateFilter = option.value\n this.reload()\n }}\n >\n ${option.label}\n </button>\n `\n )}\n ${this.loaded ? html`<span count>${i18next.t('text.figure-count', { count: this.total })}</span>` : ''}\n </div>\n\n ${!this.loaded\n ? html``\n : this.figures.length === 0\n ? this.renderEmpty()\n : html`\n <div cards>\n ${this.figures.map(\n figure => html`\n <figure-card\n .figure=${figure}\n @open=${() => this.openModeller(figure.id)}\n @remove=${() => this.removeFigure(figure)}\n ></figure-card>\n `\n )}\n </div>\n `}\n `\n }\n\n /**\n * 빈 화면.\n *\n * 걸러 보기 때문에 비었는지, 아직 만든 것이 없어서 비었는지를 **가른다.** 같은\n * 문구를 쓰면 사용자가 검색어를 지워 볼 생각을 못 한다.\n */\n private renderEmpty() {\n const filtered = !!this.search || !!this.stateFilter\n\n if (filtered) {\n return html`\n <div empty>\n <md-icon>search_off</md-icon>\n <h2>${i18next.t('text.no-figure-matches-the-filter')}</h2>\n <p>${i18next.t('text.try-clearing-the-search-or-state-filter')}</p>\n <button\n @click=${() => {\n this.search = ''\n this.stateFilter = ''\n this.reload()\n }}\n >\n ${i18next.t('button.clear-filters')}\n </button>\n </div>\n `\n }\n\n return html`\n <div empty>\n <md-icon>deployed_code</md-icon>\n <h2>${i18next.t('text.no-figure-yet')}</h2>\n <p>${i18next.t('text.what-a-figure-is')}</p>\n <button @click=${() => this.openModeller()}>${i18next.t('button.new-figure')}</button>\n </div>\n `\n }\n\n async pageUpdated(changes: Record<string, unknown>, lifecycle: Record<string, unknown>) {\n if (this.active) {\n await this.reload()\n }\n }\n\n private async reload() {\n const { items, total } = await fetchFigureList({\n search: this.search || undefined,\n state: this.stateFilter || undefined\n })\n\n this.figures = items\n this.total = total\n this.loaded = true\n }\n\n private openModeller(id?: string) {\n navigate(id ? `figure-modeller/${id}` : 'figure-modeller')\n }\n\n /** `remove` 는 HTMLElement 의 메서드다 — 가리면 조용한 결함이 된다. */\n private async removeFigure(figure: Figure) {\n // 되돌릴 수 없는 조작이므로 무엇이 사라지는지 이름으로 확인한다\n const message = i18next.t('text.delete-figure-confirm', { name: figure.name })\n if (!confirm(message)) {\n return\n }\n\n await deleteFigure(figure.id)\n await this.reload()\n }\n}\n"]}
1
+ {"version":3,"file":"figure-list-page.js","sourceRoot":"","sources":["../../client/pages/figure-list-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '../viewparts/figure-card.js'\n\nimport { css, html } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { auth } from '@things-factory/auth-base/dist-client'\nimport { navigate, PageView } from '@operato/shell'\nimport { ScrollbarStyles } from '@operato/styles'\n\nimport { deleteFigure, fetchFigureList } from '../graphql/index.js'\nimport type { Figure } from '../types.js'\n\nconst FigureListPageBase = localize(i18next)(PageView) as typeof PageView\n\n/**\n * Figure 목록.\n *\n * 이 화면은 저작면으로 가는 문이다. 여기서 오래 머물 화면이 아니므로 꾸미지 않는다.\n * 대신 **저작자가 목록에서 판단할 수 있어야 하는 것**은 카드에 실는다 — 무게(삼각형\n * 수)와 재활용 점수다. 그것을 저작면에 들어가서야 알면 늦다.\n */\n@customElement('figure-list-page')\nexport class FigureListPage extends FigureListPageBase {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n }\n\n /* 걸러 보기 줄 */\n div[filters] {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n\n input[search] {\n flex: 1;\n min-width: 0;\n max-width: 320px;\n padding: 6px 10px;\n font: var(--input-field-font, inherit);\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n /* 명령 요소의 모서리는 테마가 정한다 — 알약으로 만들지 않는다 */\n border-radius: 6px;\n }\n\n button[state] {\n padding: 5px 12px;\n font: var(--label-font, inherit);\n color: 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[state][selected] {\n color: var(--md-sys-color-on-secondary-container);\n background-color: var(--md-sys-color-secondary-container);\n border-color: transparent;\n }\n\n span[count] {\n margin-left: auto;\n font: var(--label-font, inherit);\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* 카드 격자 */\n div[cards] {\n flex: 1;\n overflow-y: auto;\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-large, 12px);\n align-content: start;\n }\n\n /* ── 아무것도 없을 때 ──────────────────────────────────────────────\n 이 자리가 이 화면에서 가장 중요하다. 처음 온 사람은 여기서 이 도구가\n 무엇을 만드는 것인지 알게 된다. 「데이터 없음」으로 끝내지 않는다. */\n div[empty] {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-large, 12px);\n text-align: center;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n div[empty] md-icon {\n --md-icon-size: 48px;\n color: var(--md-sys-color-outline-variant);\n }\n div[empty] h2 {\n margin: 0;\n font: var(--theme-font, inherit);\n font-size: var(--ui-t-title, 15px);\n color: var(--md-sys-color-on-surface);\n }\n div[empty] p {\n margin: 0;\n max-width: 34em;\n line-height: 1.6;\n }\n div[empty] button {\n margin-top: var(--spacing-medium, 8px);\n padding: 8px 18px;\n font: var(--label-font, inherit);\n color: var(--md-sys-color-on-primary, #fff);\n background-color: var(--md-sys-color-primary);\n border: none;\n border-radius: 6px;\n cursor: pointer;\n }\n `\n ]\n\n @state() private figures: Figure[] = []\n @state() private total = 0\n @state() private search = ''\n @state() private stateFilter = ''\n @state() private loaded = false\n /** 못 불러온 사유. 빈 목록과 구분해서 보여 준다. */\n @state() private failure = ''\n\n private listen?: () => void\n\n get context() {\n return {\n title: i18next.t('title.figures'),\n actions: [\n {\n title: i18next.t('button.new-figure'),\n action: () => this.openModeller()\n }\n ]\n }\n }\n\n render() {\n return html`\n <div filters>\n <input\n search\n type=\"search\"\n .value=${this.search}\n placeholder=${i18next.t('text.search-figure-by-name-or-type')}\n @change=${(e: Event) => {\n this.search = (e.target as HTMLInputElement).value\n this.reload()\n }}\n />\n ${[\n { value: '', label: i18next.t('label.all') },\n { value: 'draft', label: i18next.t('label.draft') },\n { value: 'released', label: i18next.t('label.released') }\n ].map(\n option => html`\n <button\n state\n ?selected=${this.stateFilter === option.value}\n @click=${() => {\n this.stateFilter = option.value\n this.reload()\n }}\n >\n ${option.label}\n </button>\n `\n )}\n ${this.loaded ? html`<span count>${i18next.t('text.figure-count', { count: this.total })}</span>` : ''}\n </div>\n\n ${!this.loaded\n ? html``\n : this.failure\n ? html`\n <div empty>\n <md-icon>error</md-icon>\n <p>${i18next.t('text.could-not-load-figures')}</p>\n <p quiet>${this.failure}</p>\n <button @click=${() => this.reload()}>${i18next.t('button.try-again')}</button>\n </div>\n `\n : this.figures.length === 0\n ? this.renderEmpty()\n : html`\n <div cards>\n ${this.figures.map(\n figure => html`\n <figure-card\n .figure=${figure}\n @open=${() => this.openModeller(figure.id)}\n @remove=${() => this.removeFigure(figure)}\n ></figure-card>\n `\n )}\n </div>\n `}\n `\n }\n\n /**\n * 빈 화면.\n *\n * 걸러 보기 때문에 비었는지, 아직 만든 것이 없어서 비었는지를 **가른다.** 같은\n * 문구를 쓰면 사용자가 검색어를 지워 볼 생각을 못 한다.\n */\n private renderEmpty() {\n const filtered = !!this.search || !!this.stateFilter\n\n if (filtered) {\n return html`\n <div empty>\n <md-icon>search_off</md-icon>\n <h2>${i18next.t('text.no-figure-matches-the-filter')}</h2>\n <p>${i18next.t('text.try-clearing-the-search-or-state-filter')}</p>\n <button\n @click=${() => {\n this.search = ''\n this.stateFilter = ''\n this.reload()\n }}\n >\n ${i18next.t('button.clear-filters')}\n </button>\n </div>\n `\n }\n\n return html`\n <div empty>\n <md-icon>deployed_code</md-icon>\n <h2>${i18next.t('text.no-figure-yet')}</h2>\n <p>${i18next.t('text.what-a-figure-is')}</p>\n <button @click=${() => this.openModeller()}>${i18next.t('button.new-figure')}</button>\n </div>\n `\n }\n\n connectedCallback() {\n super.connectedCallback()\n\n /*\n **로그인 정보가 도착하면 다시 불러온다.**\n\n 화면이 서는 것과 프로필이 오는 것은 순서가 정해져 있지 않고, 목록 질의는 로그인된 사람으로\n 나간다. 첫 질의가 프로필보다 앞서면 그 한 번이 실패하고, 그 뒤로 다시 묻는 사람이 없으면\n 화면은 비어 있는 채로 남는다. lite-menu 도 같은 신호로 같은 순서 문제를 푼다\n (`auth.on('profile')` — 그쪽 주석에 그 사연이 적혀 있다).\n\n **이 화면에서 그 실패를 실제로 본 적은 없다.** 빈 화면을 봤던 것은 숨은 탭에서 조사할 때였고,\n 그때는 셸이 첫 `pageUpdated` 를 requestAnimationFrame 안에서 부르는데 숨은 탭은 rAF 를 돌리지\n 않아 질의가 아예 나가지 않았다 — 원인이 달랐다. 그래도 이 구독은 남긴다: 값이 싸고, 못 불러온\n 경우를 화면이 말하게 한 아래 처리와 짝이다.\n */\n this.listen = () => {\n if (this.active) {\n void this.reload()\n }\n }\n auth.on('profile', this.listen)\n }\n\n disconnectedCallback() {\n if (this.listen) {\n auth.off?.('profile', this.listen)\n this.listen = undefined\n }\n super.disconnectedCallback()\n }\n\n async pageUpdated(changes: Record<string, unknown>, lifecycle: Record<string, unknown>) {\n if (this.active) {\n await this.reload()\n }\n }\n\n private async reload() {\n /*\n **못 불러왔으면 그렇게 말한다.**\n\n 전에는 여기서 던진 오류가 아무도 받지 않아 `loaded` 가 거짓으로 남고, 화면은 아무것도 없이\n 비어 있었다. 목록이 비어 있는 것과 못 불러온 것은 다른 사실이고, 사용자가 할 일도 다르다 —\n 하나는 만들면 되고, 하나는 다시 시도하거나 사람을 불러야 한다.\n */\n try {\n const { items, total } = await fetchFigureList({\n search: this.search || undefined,\n state: this.stateFilter || undefined\n })\n\n this.figures = items\n this.total = total\n this.failure = ''\n } catch (e) {\n this.failure = (e as Error).message\n } finally {\n this.loaded = true\n }\n }\n\n private openModeller(id?: string) {\n navigate(id ? `figure-modeller/${id}` : 'figure-modeller')\n }\n\n /** `remove` 는 HTMLElement 의 메서드다 — 가리면 조용한 결함이 된다. */\n private async removeFigure(figure: Figure) {\n // 되돌릴 수 없는 조작이므로 무엇이 사라지는지 이름으로 확인한다\n const message = i18next.t('text.delete-figure-confirm', { name: figure.name })\n if (!confirm(message)) {\n return\n }\n\n await deleteFigure(figure.id)\n await this.reload()\n }\n}\n"]}
@@ -70,6 +70,7 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
70
70
  willUpdate(changed: Map<string, unknown>): void;
71
71
  get context(): {
72
72
  title: string;
73
+ heading: boolean;
73
74
  };
74
75
  render(): import("lit-html").TemplateResult<1>;
75
76
  /**
@@ -214,6 +215,23 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
214
215
  /** 지금 고른 부품 이름. 캔버스가 인덱스가 아니라 이름을 받는다. */
215
216
  private get pickedName();
216
217
  private pickPart;
218
+ /**
219
+ * 그림이 없는 도형을 **열었을 때 한 번** 찍어 채운다.
220
+ *
221
+ * ## 왜 여기인가
222
+ *
223
+ * 보드는 저장할 때 서버가 찍는다. 그런데 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는
224
+ * 것이 자주 실패한다. 도형을 그리는 GPU 는 이미 보는 사람 손에 있고, 씬은 지금 여기 서 있다.
225
+ *
226
+ * 저작면을 거치지 않고 생긴 것들이 그림이 없다 — 씨앗으로 넣은 표본, AI 가 낸 후보, 반입한 자산.
227
+ * 그것들을 한 번 열어 보면 그때 채워진다.
228
+ *
229
+ * ## 저장이 아니다
230
+ *
231
+ * 그림만 보낸다. 정본을 건드리지 않으므로 미저장 표시도 켜지 않는다 — 열어 봤다는 것이 고쳤다는
232
+ * 뜻이 되면 저작자가 저장할지 말지를 매번 고민한다.
233
+ */
234
+ private fillThumbnail;
217
235
  private save;
218
236
  }
219
237
  export {};
@@ -3,6 +3,7 @@ import '@material/web/icon/icon.js';
3
3
  import '../modeller/figure-side.js';
4
4
  import '../modeller/figure-ask.js';
5
5
  import '../modeller/figure-canvas.js';
6
+ import { captureThumbnail } from '../modeller/figure-thumbnail.js';
6
7
  import '../modeller/figure-preview.js';
7
8
  import '../modeller/figure-parts.js';
8
9
  import '../modeller/figure-inspector.js';
@@ -195,7 +196,13 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
195
196
  display: none;
196
197
  }
197
198
 
198
- /* 머리줄 — 무엇을 고치는 중인지 늘 보인다 */
199
+ /*
200
+ 머리줄 — 이름 · 타입 · 미저장 표시 · 말로 시키는 줄 · 저장을 **한 줄에** 담는다.
201
+
202
+ 이름은 페이지 제목에도 나온다(셸이 context.title 로 그린다). 그래도 여기 입력칸을 두는
203
+ 이유는 **고치는 자리**가 필요해서다 — 제목은 읽는 것이고 이 칸은 바꾸는 것이다. 두 줄로
204
+ 벌리는 대신 한 줄에 모아 캔버스에 높이를 넘긴다.
205
+ */
199
206
  div[header] {
200
207
  display: flex;
201
208
  align-items: center;
@@ -203,9 +210,13 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
203
210
  padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
204
211
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
205
212
  }
213
+ div[header] figure-ask {
214
+ flex: 1;
215
+ min-width: 0;
216
+ }
206
217
  input[name] {
207
218
  flex: none;
208
- width: 220px;
219
+ width: 180px;
209
220
  padding: 5px 9px;
210
221
  font: var(--input-field-font, inherit);
211
222
  font-size: 0.82rem;
@@ -483,37 +494,49 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
483
494
  }
484
495
  get context() {
485
496
  return {
486
- title: this.figure?.name ?? i18next.t('title.new-figure')
497
+ title: this.figure?.name ?? i18next.t('title.new-figure'),
498
+ /*
499
+ **제목 줄은 끈다.** 이름은 그대로 둔다.
500
+
501
+ 이 화면의 주인공은 캔버스다. 이름은 속성 패널에서 보고 고치고, 브라우저 탭에도
502
+ 「operato figure · 작업자」로 나온다. 그 위에 제목 줄을 한 겹 더 얹으면 같은 말이 세 번이고,
503
+ 그만큼 캔버스가 낮아진다.
504
+
505
+ `title` 을 비우는 것으로 끄지 않는 이유는 그 값을 MDI 탭도 읽기 때문이다 — 비우면 탭 이름이
506
+ 함께 사라진다. 이름은 하나이고 그리는 자리마다 켜고 끈다(`ox-page-title-bar`).
507
+ */
508
+ heading: false
487
509
  };
488
510
  }
489
511
  render() {
490
512
  const source = this.folded;
491
513
  return html `
492
514
  <div header>
493
- <input
494
- name
495
- .value=${this.figure?.name ?? ''}
496
- placeholder=${i18next.t('text.figure-display-name')}
497
- @change=${(e) => this.rename(e.target.value)}
498
- />
499
- <code type title=${i18next.t('text.type-name-cannot-change')}>${this.board?.figureType ?? ''}</code>
515
+ <!--
516
+ 이름과 타입은 **속성 패널**에 있다(figure-inspector 의 도형 절). 여기 있던 이름 칸은
517
+ 하네스 시절의 자리였다 — 앱에서는 셸이 페이지 제목으로 같은 이름을 그리므로 글자가 두 번
518
+ 나오고, 캔버스가 주인공인 화면에서 그만큼 높이를 잃었다.
519
+ -->
500
520
  ${this.dirty ? html `<span dirty>${i18next.t('text.unsaved-changes')}</span>` : ''}
501
- <div spacer></div>
521
+ <!--
522
+ 말로 시키는 줄이 머리줄의 남는 폭을 쓴다. 따로 두면 화면 위가 두 줄이 되고, 그 위 줄은
523
+ 이름을 페이지 제목과 겹쳐 적는다.
524
+ -->
525
+ <figure-ask
526
+ .source=${source}
527
+ .type=${source?.type ?? ''}
528
+ @proposed=${(e) => this.receive(e.detail.proposal)}
529
+ ></figure-ask>
502
530
  <button save ?disabled=${!this.canSave} @click=${() => this.save()}>
503
531
  ${this.saving ? i18next.t('text.saving') : i18next.t('button.save')}
504
532
  </button>
505
533
  </div>
506
534
 
507
- <figure-ask
508
- .source=${source}
509
- .type=${source?.type ?? ''}
510
- @proposed=${(e) => this.receive(e.detail.proposal)}
511
- ></figure-ask>
512
-
513
535
  <div
514
536
  edits
515
537
  ?collapse-left=${this.collapsed.left}
516
538
  ?collapse-right=${this.collapsed.right}
539
+ @figure-rename=${(e) => this.rename(e.detail.name)}
517
540
  @add-part=${(e) => this.addPart(e.detail.primitive)}
518
541
  @select-part=${(e) => (this.selected = e.detail.index)}
519
542
  @remove-part=${(e) => this.removePart(e.detail.index)}
@@ -573,6 +596,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
573
596
  </div>
574
597
 
575
598
  <figure-side
599
+ .figureName=${this.figure?.name ?? ''}
576
600
  .board=${this.board}
577
601
  .parts=${this.parts}
578
602
  .source=${source}
@@ -796,6 +820,10 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
796
820
  async load(id) {
797
821
  const figure = await fetchFigure(id);
798
822
  this.figure = figure;
823
+ /* 그림이 없으면 이 자리에서 채운다(아래 `fillThumbnail`). 기다리지 않는다 — 화면이 먼저다. */
824
+ if (!figure.thumbnail && figure.id) {
825
+ void this.fillThumbnail(figure.id);
826
+ }
799
827
  // 서버가 준 것을 그대로 쓴다. 읽지 못하면 오류보고없이 빈 상태로 바꾸지 않는다.
800
828
  // 그러면 사용자가 저장을 선택해 원본을 덮어쓴다.
801
829
  const model = figure.source ? fromFigureSource(JSON.parse(figure.source)) : undefined;
@@ -948,6 +976,37 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
948
976
  if (at >= 0)
949
977
  this.selected = at;
950
978
  }
979
+ /**
980
+ * 그림이 없는 도형을 **열었을 때 한 번** 찍어 채운다.
981
+ *
982
+ * ## 왜 여기인가
983
+ *
984
+ * 보드는 저장할 때 서버가 찍는다. 그런데 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는
985
+ * 것이 자주 실패한다. 도형을 그리는 GPU 는 이미 보는 사람 손에 있고, 씬은 지금 여기 서 있다.
986
+ *
987
+ * 저작면을 거치지 않고 생긴 것들이 그림이 없다 — 씨앗으로 넣은 표본, AI 가 낸 후보, 반입한 자산.
988
+ * 그것들을 한 번 열어 보면 그때 채워진다.
989
+ *
990
+ * ## 저장이 아니다
991
+ *
992
+ * 그림만 보낸다. 정본을 건드리지 않으므로 미저장 표시도 켜지 않는다 — 열어 봤다는 것이 고쳤다는
993
+ * 뜻이 되면 저작자가 저장할지 말지를 매번 고민한다.
994
+ */
995
+ async fillThumbnail(id) {
996
+ await this.updateComplete;
997
+ const thumbnail = await captureThumbnail(this.board, this.parts);
998
+ if (!thumbnail || this.figure?.id !== id) {
999
+ return;
1000
+ }
1001
+ try {
1002
+ await updateFigure(id, { thumbnail });
1003
+ this.figure = { ...this.figure, thumbnail };
1004
+ }
1005
+ catch (e) {
1006
+ /* 볼 권한만 있는 사람일 수 있다. 화면은 그대로 두고 조용히 넘어간다. */
1007
+ console.warn(`[thumbnail] ${id} 를 채우지 못했습니다 — ${e.message}`);
1008
+ }
1009
+ }
951
1010
  async save() {
952
1011
  if (!this.canSave || !this.folded || !this.figure) {
953
1012
  return;
@@ -955,10 +1014,20 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
955
1014
  this.saving = true;
956
1015
  try {
957
1016
  const sourceJson = JSON.stringify(this.folded);
1017
+ /*
1018
+ 보이는 것을 그대로 찍어 함께 보낸다.
1019
+
1020
+ 카드가 그림 없이 서 있으면 카탈로그에서 무엇인지 못 고른다. 찍는 자리를 저장에 두는 이유는
1021
+ **그 순간이 정본이 바뀌는 순간**이라서다 — 따로 찍는 절차를 두면 그림과 정본이 갈린다.
1022
+
1023
+ 못 찍으면 보내지 않는다(`undefined`). 빈 그림을 넣어 「그림이 있다」고 말하지 않는다.
1024
+ */
1025
+ const thumbnail = await captureThumbnail(this.board, this.parts);
958
1026
  if (this.figure.id) {
959
1027
  const result = await updateFigure(this.figure.id, {
960
1028
  name: this.figure.name,
961
- source: sourceJson
1029
+ source: sourceJson,
1030
+ thumbnail
962
1031
  });
963
1032
  this.figure = result.figure;
964
1033
  this.violations = result.violations;
@@ -973,7 +1042,8 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
973
1042
  const result = await createFigure({
974
1043
  type: this.folded.type,
975
1044
  name: this.figure.name,
976
- source: sourceJson
1045
+ source: sourceJson,
1046
+ thumbnail
977
1047
  });
978
1048
  this.figure = result.figure;
979
1049
  this.violations = result.violations;
@@ -1 +1 @@
1
- {"version":3,"file":"figure-modeller-page.js","sourceRoot":"","sources":["../../client/pages/figure-modeller-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '../modeller/figure-side.js'\nimport '../modeller/figure-ask.js'\nimport '../modeller/figure-canvas.js'\nimport '../modeller/figure-preview.js'\nimport '../modeller/figure-parts.js'\nimport '../modeller/figure-inspector.js'\n\nimport { css, html } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { navigate, PageView } from '@operato/shell'\nimport { validate } from '@hatiolab/figure-model'\nimport type { DetailLevel, FigureSource, PrimitiveKind } from '@hatiolab/figure-model'\n\nimport * as edits from '../modeller/part-edits.js'\nimport * as proposals from '../modeller/proposal.js'\nimport { toFigureSource, fromFigureSource } from '../modeller/figure-source.js'\nimport type { BoardModel, PartModel, PlaneAxis } from '../modeller/figure-source.js'\nimport { DEFAULT_VIEW } from '../modeller/figure-view.js'\nimport type { ViewSettings } from '../modeller/figure-view.js'\nimport { createFigure, fetchFigure, fetchFigureTypeNames, updateFigure } from '../graphql/index.js'\nimport type { Figure, FigureFinding } from '../types.js'\n\nconst FigureModellerPageBase = localize(i18next)(PageView) as typeof PageView\n\n/**\n * 어느 칸이 접혀 있나. 왼쪽은 부품 목록, 오른쪽은 속성·판정·설정이다.\n *\n * 이름에 fold 를 안 쓴다. 이 파일에서 그 낱말은 이미 fold.ts 의 것이다 — 편집 상태를\n * 저장 형식으로 접는다는 뜻이라 칸을 접는 것과 아무 상관이 없다. 한 파일에서 한 낱말이\n * 두 가지를 뜻하면 읽는 사람이 매번 어느 쪽인지 가려야 한다.\n */\ninterface Panes {\n left: boolean\n right: boolean\n}\n\n/**\n * 접어 둔 것은 **이 브라우저에만** 남는다.\n *\n * 저작한 Figure 에 실리는 값이 아니다. 화면을 어떻게 벌려 놓고 일하느냐는 사람마다\n * 다르고, 같은 Figure 를 다른 사람이 열었을 때 내 화면 배치가 따라가면 곤란하다.\n * 화면 설정(`figure-view`)을 저장하지 않는 것과 같은 이유다.\n *\n * 시크릿 창이나 사이트 데이터를 막은 브라우저에서는 **읽는 것 자체가 던진다.** 그때는\n * 둘 다 펴진 채로 시작한다 — 저작면이 안 뜨는 것보다 낫다.\n */\nconst PANES_KEY = 'figure-modeller.collapsed'\n\nfunction recallPanes(): Panes {\n try {\n const said = JSON.parse(localStorage.getItem(PANES_KEY) ?? 'null')\n return { left: said?.left === true, right: said?.right === true }\n } catch {\n return { left: false, right: false }\n }\n}\n\nfunction rememberPanes(panes: Panes): void {\n try {\n localStorage.setItem(PANES_KEY, JSON.stringify(panes))\n } catch {\n // 못 적어도 이번 화면은 그대로 동작한다. 다음에 열 때 펴져 있을 뿐이다.\n }\n}\n\n/**\n * 새로 만든 Figure 의 시작 상태. **씬 모델로 만든다.**\n *\n * 빈 상태로 시작하면 검증이 곧바로 오류를 낸다 — 사용자가 아무것도 하지 않았는데 빨간\n * 화면을 보게 된다. 그래서 부품 하나를 놓아 준다. **처음 열었을 때 바로 무언가 보이는\n * 것**이 이 도구를 익히는 첫 단계다.\n */\nfunction startingModel(type: string): { board: BoardModel; parts: PartModel[] } {\n return {\n board: { width: 300, height: 300, depth: 300, figureType: type, detailLevel: 'M' },\n parts: [\n {\n type: 'figure-part',\n name: 'body',\n primitive: 'cube',\n left: 0,\n top: 0,\n width: 300,\n height: 300,\n zPos: 0,\n depth: 300,\n token: 'palette.primary'\n }\n ]\n }\n}\n\n/**\n * Figure 저작면.\n *\n * ## 세 칸이다\n *\n * 왼쪽 도형 팔레트 · 부품 목록 · 속성\n * 가운데 미리보기 — `FigureInstance` 를 실제로 세운다\n * 오른쪽 판단 — 늘 떠 있다\n *\n * 미리보기가 씬 위에서 실제로 동작하는 것이 이 도구의 전제다. **편집한 것이 곧 실제 결과물**이라야\n * \"모델러에서는 되는데 보드에서는 다르다\"가 생기지 않는다.\n *\n * ## 편집 데이터는 씬 모델 하나뿐이다\n *\n * 이 페이지가 갖는 것은 보드 하나와 부품 목록 — 미리보기에 그대로 세워지는 씬 모델이다.\n * 저장 형식(`FigureSource`)으로 바꾸는 것은 **경계에서만** 한다: 저장할 때, 판단할 때,\n * AI 후보와 비교할 때. 그 변환은 `figure-source.ts` 한 곳에서만 일어난다.\n *\n * 팔레트도 속성 편집기도 이 데이터를 직접 수정하지 않는다. 무엇을 하겠다는 것만 알리고,\n * 바꾸는 것은 이 페이지다. 두 곳에서 고치면 실행 취소도 저장도 어느 쪽이 맞는지 알 수\n * 없게 된다.\n */\n@customElement('figure-modeller-page')\nexport class FigureModellerPage extends FigureModellerPageBase {\n static styles = css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n /*\n 이 화면의 글자 크기를 여기서 정한다.\n\n 안 적어 두면 브라우저 기본 크기(16px)를 물려받는다. 아래 패널들은 전부 12px\n 언저리라 위 두 줄만 커 보이고, 버튼이 화면 전체와 따로 놀았다.\n\n form 요소(input · button)는 글꼴을 물려받지 않으므로 각자 적어야 한다.\n */\n font-size: 0.76rem;\n }\n\n /* 세 칸. 3D 를 가장 크게 둔다 — 이 도구의 주어는 형상이지 속성표가 아니다 */\n div[edits] {\n flex: 1;\n display: grid;\n grid-template-columns: 220px 1fr 320px;\n min-height: 0;\n overflow: hidden;\n }\n\n /*\n **세 칸의 자리를 못 박는다.**\n\n 이것이 없으면 왼쪽을 접었을 때 오른쪽 칸이 가운데 자리로 밀려 들어와 캔버스가\n 통째로 사라진다. display none 인 형제는 그리드 자리를 차지하지 않으므로, 첫째가\n 빠지면 나머지가 한 칸씩 당겨진다. 오른쪽은 마지막이라 빠져도 밀 것이 없어서\n 멀쩡했다 — 한쪽만 고장 나는 종류다.\n\n 이 파일이 이미 한 번 적어 둔 함정이다. 아래 div[lanes] 주석에 같은 말이 있다.\n */\n div[edits] > figure-parts {\n grid-column: 1;\n }\n div[edits] > div[middle] {\n grid-column: 2;\n }\n div[edits] > figure-side {\n grid-column: 3;\n }\n\n /*\n 접힌 칸은 **자리를 아예 안 쓴다.**\n\n 폭만 0 으로 줄이면 안쪽이 넘쳐 나와 가운데를 덮는다. 그렇다고 떼어 내면 오른쪽\n 칸이 펼쳐 둔 추이 그림 같은 상태를 잃는다 — figure-side 가 탭을 감출 때도 DOM 에는\n 남기는 것과 같은 이유다. display none 이 둘을 다 지킨다.\n\n 가운데는 접을 때마다 넓이가 바뀌는데, 편집 캔버스와 미리보기가 각자 ResizeObserver\n 를 갖고 있어 3D 가 알아서 따라온다.\n */\n div[edits][collapse-left] {\n grid-template-columns: 0 1fr 320px;\n }\n div[edits][collapse-right] {\n grid-template-columns: 220px 1fr 0;\n }\n div[edits][collapse-left][collapse-right] {\n grid-template-columns: 0 1fr 0;\n }\n div[edits][collapse-left] > figure-parts,\n div[edits][collapse-right] > figure-side {\n display: none;\n }\n\n /* 머리줄 — 무엇을 고치는 중인지 늘 보인다 */\n div[header] {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n input[name] {\n flex: none;\n width: 220px;\n padding: 5px 9px;\n font: var(--input-field-font, inherit);\n font-size: 0.82rem;\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n }\n /* 타입 이름은 만든 뒤 고칠 수 없다 — 그 사실이 화면에서 보여야 한다 */\n code[type] {\n padding: 4px 8px;\n font-family: var(--mono-font, monospace);\n font-size: 0.76rem;\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: 6px;\n }\n span[dirty] {\n font: var(--label-font, inherit);\n font-size: 0.74rem;\n color: var(--md-sys-color-tertiary);\n }\n div[spacer] {\n flex: 1;\n }\n button[save] {\n padding: 6px 16px;\n font: var(--label-font, inherit);\n font-size: 0.76rem;\n font-weight: 600;\n color: var(--md-sys-color-on-primary, #fff);\n background-color: var(--md-sys-color-primary);\n border: none;\n border-radius: 6px;\n cursor: pointer;\n }\n button[save][disabled] {\n opacity: 0.4;\n cursor: default;\n }\n\n /*\n 칸을 벡터 저작도구의 관례대로 놓는다 — 왼쪽에 무엇이 있나(레이어), 오른쪽에\n 고른 것의 속성. 쓰는 사람이 이미 아는 자리라 배울 것이 하나 줄어든다.\n 차별성은 배치가 아니라 가운데의 로우폴리 3D 에 있으므로, 배치로 낯설게 하지\n 않는다.\n\n 오른쪽은 figure-side 가 속성과 판단을 탭으로 겹쳐 둔다. 위아래로 나누면 둘 다\n 잘려서 속성은 재질까지, 판단은 무게까지 못 내려간다. 개수는 탭 이름표에 남아\n 판단을 덮어 두어도 보인다.\n */\n figure-side {\n min-height: 0;\n }\n\n figure-canvas,\n figure-preview {\n min-width: 0;\n min-height: 0;\n }\n\n /*\n 편집 · 미리보기 전환.\n\n 가운데 칸 위에 얇게 둔다. 무엇을 보고 있는지가 늘 보여야 하고, 두 화면이 같은\n 자리를 쓰므로 어느 쪽인지 모르면 편집이 안 먹는 것처럼 읽힌다.\n */\n div[modes] {\n display: flex;\n flex: none;\n gap: 2px;\n padding: 5px var(--spacing-medium, 8px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n div[modes] button[mode] {\n padding: 3px 12px;\n font: var(--label-font, inherit);\n font-size: 0.74rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background: none;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n }\n div[modes] button[mode]:hover:not([on]) {\n background-color: var(--md-sys-color-surface-container);\n }\n div[modes] button[mode][on] {\n font-weight: 700;\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n }\n\n /*\n 양쪽 칸을 접는 손잡이. 띠의 양 끝, 접히는 칸 바로 옆이다.\n\n 접었을 때 남는 것이 이것 하나뿐이므로 **접는 자리와 펴는 자리가 같아야 한다.**\n 다른 데 두면 접고 나서 되돌릴 곳을 찾아야 한다.\n */\n div[modes] button[collapse] {\n display: flex;\n align-items: center;\n flex: none;\n padding: 2px 3px;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background: none;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n }\n div[modes] button[collapse]:hover {\n background-color: var(--md-sys-color-surface-container);\n }\n div[modes] button[collapse] md-icon {\n --md-icon-size: 18px;\n }\n\n /*\n 후보가 오면 가운데를 둘로 나눈다.\n\n 숫자와 JSON 으로 승낙을 받지 않는다 — 저작한 것이 곧 실물이므로 **둘 다 세워\n 놓고** 고르게 한다. 나란히 두지 않으면 무엇이 달라졌는지 기억으로 견줘야 한다.\n */\n /*\n 세로로 쌓아 둔다. 그리드로 두었더니 후보가 없을 때 미리보기가 첫 줄(auto)에\n 떨어져 **높이가 0** 이 됐다 — 3D 가 통째로 사라졌다. display:none 인 형제는\n 그리드 자리를 차지하지 않는다.\n */\n div[middle] {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n }\n /*\n 가운데를 채운다. 둘 다 적어야 한다 — 하나만 적으면 감춰졌다 나타난 쪽이 높이 0 이\n 되어 화면이 하얗게 뜬다. 실제로 그렇게 났다.\n */\n div[middle] > figure-canvas,\n div[middle] > figure-preview {\n flex: 1;\n }\n div[lanes] {\n flex: 1;\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 1px;\n min-height: 0;\n background-color: var(--md-sys-color-outline-variant);\n }\n div[lane] {\n display: grid;\n grid-template-rows: auto 1fr;\n min-width: 0;\n min-height: 0;\n background-color: var(--md-sys-color-surface);\n }\n div[lane] h4 {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n padding: 5px var(--spacing-medium, 8px);\n font: var(--label-font, inherit);\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 div[lane][candidate] h4 {\n color: var(--md-sys-color-primary);\n }\n div[lane] h4 small {\n font-weight: 400;\n letter-spacing: 0;\n text-transform: none;\n }\n\n /* 받거나 버린다. 그 사이에 다른 것은 없다 */\n div[decide] {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n background-color: var(--md-sys-color-primary-container);\n color: var(--md-sys-color-on-primary-container);\n font-size: 0.76rem;\n }\n div[decide] div[spacer] {\n flex: 1;\n }\n div[decide] button {\n padding: 5px 14px;\n font: var(--label-font, inherit);\n border-radius: 6px;\n cursor: pointer;\n }\n button[take] {\n color: var(--md-sys-color-on-primary, #fff);\n background-color: var(--md-sys-color-primary);\n border: none;\n }\n button[drop] {\n color: inherit;\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n }\n button[take][disabled] {\n opacity: 0.4;\n cursor: default;\n }\n\n /*\n 무엇을 바꾸자는 것인가 — 칩 하나가 변경 하나다.\n\n 통째로만 받게 두면 「몸통은 좋은데 모터 자리가 틀렸다」에 답할 수 없다. 그게\n 가장 흔한 반응이다. 처음에는 전부 골라 둔다 — 통째로 받는 것이 보통의 길이고,\n 빼고 싶은 것만 끄면 된다.\n */\n div[changes] {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n padding: 0 var(--spacing-large, 12px) var(--spacing-medium, 8px) var(--spacing-large, 12px);\n background-color: var(--md-sys-color-primary-container);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n button[change] {\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 2px 9px 2px 5px;\n font: var(--label-font, inherit);\n font-size: 0.72rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 999em;\n cursor: pointer;\n }\n button[change][on] {\n color: var(--md-sys-color-on-primary, #fff);\n background-color: var(--md-sys-color-primary);\n border-color: transparent;\n }\n button[change] md-icon {\n --md-icon-size: 14px;\n }\n /* 안 고른 것은 글자에 줄을 그어 「안 받는다」를 말한다 */\n button[change]:not([on]) span[key] {\n text-decoration: line-through;\n }\n\n /* 고른 조합이 형식을 어기면 미리 알린다. 선택한 뒤에 거절당하는 것보다 낫다 */\n /* 정책을 넘은 것은 막지 않는다 — 알리기만 한다. 색으로 갈라 둔다 */\n p[blocked][warn] {\n color: var(--md-sys-color-tertiary);\n }\n p[blocked] {\n margin: 0;\n padding: 0 var(--spacing-large, 12px) var(--spacing-medium, 8px) var(--spacing-large, 12px);\n background-color: var(--md-sys-color-primary-container);\n font-size: 0.72rem;\n line-height: 1.5;\n color: var(--md-sys-color-error);\n }\n `\n\n /** 편집 중인 Figure. 새로 만드는 중이면 `id` 가 없다. */\n @state() private figure?: Figure\n /** 편집 데이터 — 미리보기에 그대로 세워지는 씬 모델이다. */\n @state() private board?: BoardModel\n @state() private parts: PartModel[] = []\n @state() private selected = -1\n /*\n 저작 화면의 **보기 설정**. 조명 · 바닥 · 축 가이드 같은 것이다.\n\n 자산이 아니라 작업대라 저장되지 않는다 — `toFigureSource` 가 집어 가지 않는다.\n 자세한 것은 `modeller/figure-view.ts` 를 보라.\n */\n @state() private view: ViewSettings = DEFAULT_VIEW\n /**\n * 가운데 칸이 무엇을 보이나.\n *\n * 편집 캔버스는 늘 기준 크기다. 그런데 이 형식의 값 절반은 크기가 달라졌을 때 어떻게\n * 반응하는가에 있다. 그것을 볼 자리가 없었다.\n */\n @state() private mode: 'edit' | 'preview' = 'edit'\n /**\n * 어느 칸이 접혀 있나.\n *\n * 통째로 갈아 끼운다 — 안쪽 값만 바꾸면 Lit 이 같은 객체로 보고 다시 그리지 않는다.\n */\n @state() private collapsed: Panes = recallPanes()\n @state() private dirty = false\n @state() private saving = false\n @state() private violations: FigureFinding[] = []\n /** 저작 보조가 낸 후보. 받기 전에는 원본 데이터가 아니다. */\n @state() private proposal?: FigureSource\n /** 그 후보의 셈. 무엇을 받는 것인지 숫자로도 보여 준다. */\n @state() private proposalOf?: { grade: string; triangles: number; groups: number; attempts: number }\n /** 받기로 고른 변경들. 처음에는 전부 골라 둔다. */\n @state() private picked = new Set<string>()\n\n /**\n * 저장 형식으로 변환한 것. 씬 모델이 바뀔 때만 다시 만든다.\n *\n * 저장·판단·AI 후보 비교가 이것을 쓴다. 렌더링할 때마다 새로 만들면 판단 패널이 매번\n * 다시 계산하므로, 변환은 `willUpdate` 에서 한 번만 한다.\n */\n private folded?: FigureSource\n\n willUpdate(changed: Map<string, unknown>) {\n if (changed.has('board') || changed.has('parts')) {\n this.folded = this.board ? toFigureSource(this.board, this.parts) : undefined\n }\n }\n\n get context() {\n return {\n title: this.figure?.name ?? i18next.t('title.new-figure')\n }\n }\n\n render() {\n const source = this.folded\n\n return html`\n <div header>\n <input\n name\n .value=${this.figure?.name ?? ''}\n placeholder=${i18next.t('text.figure-display-name')}\n @change=${(e: Event) => this.rename((e.target as HTMLInputElement).value)}\n />\n <code type title=${i18next.t('text.type-name-cannot-change')}>${this.board?.figureType ?? ''}</code>\n ${this.dirty ? html`<span dirty>${i18next.t('text.unsaved-changes')}</span>` : ''}\n <div spacer></div>\n <button save ?disabled=${!this.canSave} @click=${() => this.save()}>\n ${this.saving ? i18next.t('text.saving') : i18next.t('button.save')}\n </button>\n </div>\n\n <figure-ask\n .source=${source}\n .type=${source?.type ?? ''}\n @proposed=${(e: CustomEvent) => this.receive(e.detail.proposal)}\n ></figure-ask>\n\n <div\n edits\n ?collapse-left=${this.collapsed.left}\n ?collapse-right=${this.collapsed.right}\n @add-part=${(e: CustomEvent) => this.addPart(e.detail.primitive)}\n @select-part=${(e: CustomEvent) => (this.selected = e.detail.index)}\n @remove-part=${(e: CustomEvent) => this.removePart(e.detail.index)}\n @duplicate-part=${(e: CustomEvent) => this.duplicatePart(e.detail.index)}\n @move-part-up=${(e: CustomEvent) => this.movePart(e.detail.index, e.detail.to)}\n @move-part-down=${(e: CustomEvent) => this.movePart(e.detail.index, e.detail.to)}\n @update-part=${(e: CustomEvent) => this.updatePart(e.detail.index, e.detail.part)}\n @part-changed=${(e: CustomEvent) => this.partChanged(e.detail.name, e.detail.part)}\n @toggle-part-hidden=${(e: CustomEvent) => this.toggleHidden(e.detail.index)}\n @set-all-parts-hidden=${(e: CustomEvent) => this.setAllHidden(e.detail.hidden)}\n @change-detail-level=${(e: CustomEvent) => this.changeDetailLevel(e.detail.level)}\n @part-tool=${(e: CustomEvent) => this.runTool(e.detail.index, e.detail.tool, e.detail.axis)}\n @pick-part=${(e: CustomEvent) => this.pickPart(e.detail.name)}\n >\n <figure-parts\n .parts=${this.parts}\n .level=${(this.board?.detailLevel ?? 'M') as DetailLevel}\n .selected=${this.selected}\n ></figure-parts>\n\n <div middle>\n ${this.renderBar()}\n ${this.proposal ? this.renderDecision() : ''}\n ${this.proposal\n ? html`\n <div lanes>\n <div lane>\n <h4>${i18next.t('label.now')}</h4>\n <figure-canvas\n .board=${this.board}\n .parts=${this.parts}\n .picked=${this.pickedName}\n .view=${this.view}\n ></figure-canvas>\n </div>\n <div lane candidate>\n <h4>\n ${i18next.t('label.proposed')}\n ${this.proposalOf\n ? html`<small>\n ${this.proposalOf.grade} ·\n ${i18next.t('text.n-triangles', { count: this.proposalOf.triangles })}\n </small>`\n : ''}\n </h4>\n ${this.renderProposed()}\n </div>\n </div>\n `\n : this.mode === 'preview'\n ? html`<figure-preview .source=${source}></figure-preview>`\n : html`<figure-canvas\n .board=${this.board}\n .parts=${this.parts}\n .picked=${this.pickedName}\n ></figure-canvas>`}\n </div>\n\n <figure-side\n .board=${this.board}\n .parts=${this.parts}\n .source=${source}\n .selected=${this.selected}\n .view=${this.view}\n @update-view=${(e: CustomEvent) => (this.view = e.detail.view)}\n ></figure-side>\n </div>\n `\n }\n\n /**\n * 가운데 칸 위의 띠 — 무엇을 보고 있나와, 양쪽 칸을 접는 손잡이.\n *\n * ## 무엇을 보고 있나\n *\n * 편집과 미리보기는 같은 자리를 놓고 다투지 않는다. 둘 다 넓이가 필요하고, 동시에\n * 봐야 할 일이 없다. 부품을 고치는 동안에는 편집만 보면 되고, 다 고친 뒤에 보드에서\n * 어떻게 되는지 보면 된다.\n *\n * 후보가 와 있을 때는 그 두 단추를 내지 않는다. 그때는 가운데가 이미 둘로 나뉘어\n * 있다.\n *\n * ## 띠는 늘 있다\n *\n * 손잡이가 여기 있기 때문이다. 후보가 왔다고 띠를 통째로 감추면, 칸을 접어 둔 채로\n * 후보를 받으면 되돌릴 방법이 없어진다.\n */\n private renderBar() {\n const one = (mode: 'edit' | 'preview') => html`\n <button\n mode\n ?on=${this.mode === mode}\n title=${i18next.t('text.mode-' + mode + '-explained')}\n @click=${() => (this.mode = mode)}\n >\n ${i18next.t('label.mode-' + mode)}\n </button>\n `\n\n return html`<div modes>\n ${this.renderCollapse('left')}${this.proposal ? '' : html`${one('edit')}${one('preview')}`}\n <div spacer></div>\n ${this.renderCollapse('right')}\n </div>`\n }\n\n /**\n * 한쪽 칸을 접고 펴는 손잡이.\n *\n * 화살표는 **누르면 무엇이 일어나는지**를 가리킨다 — 펴져 있으면 칸이 사라질 쪽,\n * 접혀 있으면 칸이 돌아올 쪽. 지금 상태를 가리키게 하면 반대로 읽힌다.\n */\n private renderCollapse(side: keyof Panes) {\n const gone = this.collapsed[side]\n const shut = side === 'left' ? 'chevron_left' : 'chevron_right'\n const back = side === 'left' ? 'chevron_right' : 'chevron_left'\n const pane = side === 'left' ? 'parts' : 'properties'\n\n return html`\n <button\n collapse\n title=${i18next.t(`text.${gone ? 'expand' : 'collapse'}-${pane}-pane`)}\n @click=${() => this.toggleCollapse(side)}\n >\n <md-icon>${gone ? back : shut}</md-icon>\n </button>\n `\n }\n\n private toggleCollapse(side: keyof Panes): void {\n this.collapsed = { ...this.collapsed, [side]: !this.collapsed[side] }\n rememberPanes(this.collapsed)\n }\n\n /**\n * AI 후보를 나란히 세운다.\n *\n * 후보는 저장 형식으로 온다. 미리보기는 씬 모델을 받으므로 여기서 한 번 변환한다 —\n * 후보를 받아들이기 전까지는 편집 데이터가 아니므로 상태로 두지 않는다.\n */\n private renderProposed() {\n if (!this.proposal) return ''\n\n const { board, parts } = fromFigureSource(this.proposal)\n return html`<figure-canvas .board=${board} .parts=${parts} .viewOnly=${true}></figure-canvas>`\n }\n\n /**\n * 무엇을 받을지 고른다.\n *\n * 통째로만 받게 두면 「몸통은 좋은데 모터 자리가 틀렸다」에 답할 수 없다 — 그게\n * 가장 흔한 반응이다. 변경 하나가 칩 하나이고, 처음에는 전부 골라 둔다. 통째로\n * 받는 것이 보통의 길이므로 그것이 한 번 누르기여야 한다.\n *\n * 고른 조합을 **받기 전에 재 본다.** 형식을 어기면 못 누르게 하고, 정책을 넘으면\n * 알린다. 골라 받으면 후보를 낼 때와 다른 조합이 되므로 후보의 점수를 그대로\n * 믿을 수 없다 — 예를 들어 등급은 안 받고 부품만 받으면 부품 수 한도를 넘는다.\n *\n * 형식 오류가 실제로 나는 조합은 아직 보지 못했다. 그래도 두는 것은 값이 거의\n * 안 들고, 우리가 생각 못 한 조합을 막아 주기 때문이다.\n */\n private renderDecision() {\n if (!this.proposal) return ''\n\n const changes = proposals.diffProposal(this.folded, this.proposal)\n const picked = proposals.applyProposal(this.folded, this.proposal, this.picked)\n const { errors, violations } = validate(picked)\n\n return html`\n <div decide>\n <span>${i18next.t('text.assistant-proposed-a-figure')}</span>\n <div spacer></div>\n <button drop @click=${() => this.discard()}>${i18next.t('button.discard')}</button>\n <button take ?disabled=${errors.length > 0 || this.picked.size === 0} @click=${() => this.take()}>\n ${i18next.t('button.take-n-changes', { n: this.picked.size })}\n </button>\n </div>\n\n ${changes.length > 0\n ? html`\n <div changes>\n ${changes.map(change => this.renderChange(change))}\n </div>\n `\n : ''}\n ${errors.length > 0\n ? html`<p blocked>${i18next.t('text.picked-changes-do-not-validate')} — ${errors[0].message}</p>`\n : violations.length > 0\n ? html`<p blocked warn>${violations[0].message}</p>`\n : ''}\n `\n }\n\n /** 변경 하나. 무엇을 하겠다는 것인지 부호로 먼저 말한다. */\n private renderChange(change: proposals.ProposalChange) {\n const icon = {\n added: 'add',\n changed: 'edit',\n removed: 'remove',\n base: 'aspect_ratio',\n detail: 'tune',\n anchor: 'my_location',\n styleKit: 'palette'\n }[change.kind]\n\n const on = this.picked.has(change.key)\n\n return html`\n <button\n change\n ?on=${on}\n title=${i18next.t('text.change-' + change.kind)}\n @click=${() => this.toggle(change.key)}\n >\n <md-icon>${icon}</md-icon>\n <span key>${change.key}</span>\n </button>\n `\n }\n\n private toggle(key: string) {\n const next = new Set(this.picked)\n if (next.has(key)) next.delete(key)\n else next.add(key)\n this.picked = next\n }\n\n /**\n * 후보가 왔다.\n *\n * 원본 데이터에 넣지 않는다. 나란히 두고 저작자가 고른다. AI 가 원본 데이터에 직접 쓰면\n * 무엇이 자기 것이고 무엇이 기계 것인지 모르게 된다.\n */\n private receive(proposal: {\n source: string\n grade: string\n triangles: number\n groups: number\n attempts: number\n }) {\n this.proposal = JSON.parse(proposal.source) as FigureSource\n // 처음에는 전부 골라 둔다 — 통째로 받는 것이 보통의 길이다.\n this.picked = new Set(proposals.diffProposal(this.folded, this.proposal).map(change => change.key))\n this.proposalOf = {\n grade: proposal.grade,\n triangles: proposal.triangles,\n groups: proposal.groups,\n attempts: proposal.attempts\n }\n }\n\n /**\n * 고른 변경을 받아들인다.\n *\n * 후보 비교는 저장 형식으로 한다 — 부품 이름으로 짝짓고 기준 치수·등급까지 함께 봐야\n * 하므로 그쪽이 자연스럽다. 받아들인 결과는 다시 씬 모델로 되돌려 편집을 이어간다.\n */\n private take() {\n if (!this.proposal) return\n\n const taken = proposals.applyProposal(this.folded, this.proposal, this.picked)\n const { board, parts } = fromFigureSource(taken)\n\n this.board = board\n this.parts = parts\n this.selected = parts.length ? 0 : -1\n this.dirty = true\n this.discard()\n }\n\n private discard() {\n this.proposal = undefined\n this.proposalOf = undefined\n this.picked = new Set()\n }\n\n /**\n * 저장할 수 있나.\n *\n * 형식을 어긴 원본 데이터로는 저장을 누를 수 없게 한다. 눌렀다가 서버가 받아주지 않는 것보다,\n * 누를 수 없는 이유가 판단 패널에 떠 있는 것이 낫다.\n */\n private get canSave(): boolean {\n if (this.saving || !this.folded || !this.figure?.name) {\n return false\n }\n return validate(this.folded).errors.length === 0\n }\n\n async pageUpdated(changes: Record<string, unknown>, lifecycle: { resourceId?: string }) {\n if (!this.active) {\n return\n }\n\n const id = lifecycle?.resourceId\n if (id && id !== this.figure?.id) {\n await this.load(id)\n } else if (!id && !this.figure) {\n this.startNew()\n }\n }\n\n private async load(id: string) {\n const figure = await fetchFigure(id)\n\n this.figure = figure\n\n // 서버가 준 것을 그대로 쓴다. 읽지 못하면 오류보고없이 빈 상태로 바꾸지 않는다.\n // 그러면 사용자가 저장을 선택해 원본을 덮어쓴다.\n const model = figure.source ? fromFigureSource(JSON.parse(figure.source) as FigureSource) : undefined\n this.board = model?.board\n this.parts = model?.parts ?? []\n\n /*\n **아무것도 고르지 않은 채로 연다.**\n\n 전에는 첫 부품을 잡아 두었다. 그러면 사용자가 고른 적 없는 부품의 속성이 떠 있고,\n 캔버스에는 그 부품의 윤곽선이 그려진다. 모델 전체를 보려면 먼저 선택을 풀어야 했다.\n\n 속성 패널의 빈 상태도 그래서 **열었을 때 한 번도 안 보였다.** 처음 여는 사람에게\n 「왼쪽에서 부품을 고르는 도구」라고 알려 줄 자리가 그 화면이다.\n\n 새로 만들 때(`startNew`)는 다르다. 방금 부품을 만들었으니 바로 편집하려는 것이 맞다.\n */\n this.selected = -1\n this.dirty = false\n this.violations = []\n }\n\n private startNew() {\n // 타입 이름을 사람이 정하기 전까지는 임시 이름을 쓴다. 저장할 때 확정한다.\n const draftType = `FIGURE_${Date.now().toString(36).toUpperCase()}`\n const { board, parts } = startingModel(draftType)\n\n this.figure = { id: '', type: draftType, name: '' } as Figure\n this.board = board\n this.parts = parts\n this.selected = 0\n this.dirty = true\n this.violations = []\n }\n\n private rename(name: string) {\n if (!this.figure) return\n\n this.figure = { ...this.figure, name }\n this.dirty = true\n }\n\n /**\n * 부품 목록을 갈아 끼운다.\n *\n * 편집 규칙은 `part-edits` 가 갖는다 — 화면과 테스트가 같은 것을 쓰게 하려고 떼어\n * 놓았다. 여기서는 결과를 담고 선택 인덱스를 정리한다.\n */\n private put(parts: PartModel[], selected = this.selected) {\n this.parts = parts\n this.selected = Math.min(selected, parts.length - 1)\n this.dirty = true\n }\n\n private addPart(primitive: PrimitiveKind) {\n if (!this.board) return\n // 추가한 것을 바로 수정할 수 있게 선택 상태로 둔다.\n this.put(edits.addPart(this.board, this.parts, primitive), this.parts.length)\n }\n\n private removePart(index: number) {\n this.put(edits.removePart(this.parts, index), Math.max(0, index - 1))\n }\n\n private duplicatePart(index: number) {\n this.put(edits.duplicatePart(this.parts, index), index + 1)\n }\n\n private movePart(index: number, to: number) {\n this.put(edits.movePart(this.parts, index, to), to)\n }\n\n private updatePart(index: number, part: PartModel) {\n this.put(edits.updatePart(this.parts, index, part))\n }\n\n /**\n * 디테일 등급을 바꾼다.\n *\n * 이 값은 부품 수 한도를 정하고 성숙도의 한 축이 된다. 부품보다 위에 있는 값이라\n * 보드에 담는다.\n *\n * 한도를 내려서 이미 넣은 부품 수보다 작아질 수 있다. 그때 부품을 지우지 않는다.\n * 검증이 위반으로 알리고, 성숙도가 등급에 안 맞는다고 말한다. 저작자가 무엇을 뺄지\n * 정할 일이지 도구가 정할 일이 아니다.\n */\n private changeDetailLevel(level: DetailLevel) {\n if (!this.board) return\n\n this.board = { ...this.board, detailLevel: level }\n this.dirty = true\n }\n\n /**\n * 부품을 잠깐 가린다.\n *\n * 씬이 이미 갖고 있는 상태를 켜고 끄는 것뿐이다 — 편집 모드에서는 faint 로 보이고\n * 뷰 모드에서는 안 그려진다. 저장 형식에는 실리지 않으므로 다시 열면 전부 보인다.\n *\n * `dirty` 로 치지 않는다. 저장될 것이 하나도 바뀌지 않았는데 \"저장하지 않은 변경\"이\n * 뜨면, 저장 버튼이 무엇을 저장하겠다는 것인지 알 수 없다.\n */\n private toggleHidden(index: number) {\n const part = this.parts[index]\n if (!part) return\n\n this.parts = edits.updatePart(this.parts, index, { ...part, hidden: !part.hidden })\n }\n\n /**\n * 한꺼번에 감추거나 되돌린다.\n *\n * 부품 스물짜리에서 안쪽 하나를 보려고 열아홉을 누르던 것을 없앤다. 한 번에 바꾸므로\n * 목록을 한 번만 갈아 끼운다 — 하나씩 스무 번 갈면 미리보기가 스무 번 다시 선다.\n */\n private setAllHidden(hidden: boolean) {\n this.parts = this.parts.map(part => (part.hidden === hidden ? part : { ...part, hidden }))\n }\n\n /**\n * 미리보기에서 기즈모로 옮기거나 2D 에서 끌었다.\n *\n * 알려 오는 것은 부품 **이름**이다. 목록의 몇 번째인지는 여기서 찾는다 — 이벤트가\n * 오는 사이에 목록이 바뀌었으면 찾지 못하고, 그때는 아무것도 하지 않는다.\n */\n private partChanged(name: string, part: PartModel) {\n const at = this.parts.findIndex(item => item.name === name)\n if (at >= 0) this.put(edits.updatePart(this.parts, at, part))\n }\n\n /**\n * 정밀 배치 도구.\n *\n * 규칙은 `part-edits` 가 갖는다. 여기서는 어느 것을 부를지만 정한다.\n */\n private runTool(index: number, tool: string, axis: PlaneAxis) {\n const board = this.board\n if (!board) return\n\n if (tool === 'center') this.put(edits.centerPart(board, this.parts, index, axis))\n else if (tool === 'floor') this.put(edits.alignPartToStart(this.parts, index, axis))\n else if (tool === 'snap') this.put(edits.snapPart(board, this.parts, index))\n // 대칭 복제는 부품이 하나 늘어난다 — 새로 생긴 것을 선택 상태로 둔다\n else if (tool === 'mirror') this.put(edits.mirrorPart(board, this.parts, index, axis), index + 1)\n }\n\n /**\n * 캔버스에서 부품을 선택했다.\n *\n * 캔버스가 알리는 것은 **이름**이다. 목록의 몇 번째인지는 여기서 찾는다 — 알림이\n * 오는 사이에 목록이 바뀌었으면 못 찾고, 그때는 고른 것을 바꾸지 않는다.\n * 없는 자리를 고르면 속성 칸이 빈 채로 열려 「사라졌나」로 읽힌다.\n */\n /** 지금 고른 부품 이름. 캔버스가 인덱스가 아니라 이름을 받는다. */\n private get pickedName(): string | undefined {\n return this.parts[this.selected]?.name\n }\n\n private pickPart(name: string) {\n const at = this.parts.findIndex(part => part.name === name)\n if (at >= 0) this.selected = at\n }\n\n private async save() {\n if (!this.canSave || !this.folded || !this.figure) {\n return\n }\n\n this.saving = true\n try {\n const sourceJson = JSON.stringify(this.folded)\n\n if (this.figure.id) {\n const result = await updateFigure(this.figure.id, {\n name: this.figure.name,\n source: sourceJson\n })\n this.figure = result.figure\n this.violations = result.violations\n } else {\n // 새로 만들 때만 타입 이름이 정해진다. 이미 쓰이는 이름이면 저장 전에 막는다 —\n // 만든 뒤에는 고칠 수 없으므로 여기서 걸러야 한다.\n const taken = await fetchFigureTypeNames()\n if (taken.includes(this.folded.type)) {\n throw new Error(i18next.t('text.type-name-already-taken', { type: this.folded.type }))\n }\n\n const result = await createFigure({\n type: this.folded.type,\n name: this.figure.name,\n source: sourceJson\n })\n this.figure = result.figure\n this.violations = result.violations\n navigate(`figure-modeller/${result.figure.id}`)\n }\n\n this.dirty = false\n } finally {\n this.saving = false\n }\n }\n}\n"]}
1
+ {"version":3,"file":"figure-modeller-page.js","sourceRoot":"","sources":["../../client/pages/figure-modeller-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '../modeller/figure-side.js'\nimport '../modeller/figure-ask.js'\nimport '../modeller/figure-canvas.js'\nimport { captureThumbnail } from '../modeller/figure-thumbnail.js'\nimport '../modeller/figure-preview.js'\nimport '../modeller/figure-parts.js'\nimport '../modeller/figure-inspector.js'\n\nimport { css, html } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { navigate, PageView } from '@operato/shell'\nimport { validate } from '@hatiolab/figure-model'\nimport type { DetailLevel, FigureSource, PrimitiveKind } from '@hatiolab/figure-model'\n\nimport * as edits from '../modeller/part-edits.js'\nimport * as proposals from '../modeller/proposal.js'\nimport { toFigureSource, fromFigureSource } from '../modeller/figure-source.js'\nimport type { BoardModel, PartModel, PlaneAxis } from '../modeller/figure-source.js'\nimport { DEFAULT_VIEW } from '../modeller/figure-view.js'\nimport type { ViewSettings } from '../modeller/figure-view.js'\nimport { createFigure, fetchFigure, fetchFigureTypeNames, updateFigure } from '../graphql/index.js'\nimport type { Figure, FigureFinding } from '../types.js'\n\nconst FigureModellerPageBase = localize(i18next)(PageView) as typeof PageView\n\n/**\n * 어느 칸이 접혀 있나. 왼쪽은 부품 목록, 오른쪽은 속성·판정·설정이다.\n *\n * 이름에 fold 를 안 쓴다. 이 파일에서 그 낱말은 이미 fold.ts 의 것이다 — 편집 상태를\n * 저장 형식으로 접는다는 뜻이라 칸을 접는 것과 아무 상관이 없다. 한 파일에서 한 낱말이\n * 두 가지를 뜻하면 읽는 사람이 매번 어느 쪽인지 가려야 한다.\n */\ninterface Panes {\n left: boolean\n right: boolean\n}\n\n/**\n * 접어 둔 것은 **이 브라우저에만** 남는다.\n *\n * 저작한 Figure 에 실리는 값이 아니다. 화면을 어떻게 벌려 놓고 일하느냐는 사람마다\n * 다르고, 같은 Figure 를 다른 사람이 열었을 때 내 화면 배치가 따라가면 곤란하다.\n * 화면 설정(`figure-view`)을 저장하지 않는 것과 같은 이유다.\n *\n * 시크릿 창이나 사이트 데이터를 막은 브라우저에서는 **읽는 것 자체가 던진다.** 그때는\n * 둘 다 펴진 채로 시작한다 — 저작면이 안 뜨는 것보다 낫다.\n */\nconst PANES_KEY = 'figure-modeller.collapsed'\n\nfunction recallPanes(): Panes {\n try {\n const said = JSON.parse(localStorage.getItem(PANES_KEY) ?? 'null')\n return { left: said?.left === true, right: said?.right === true }\n } catch {\n return { left: false, right: false }\n }\n}\n\nfunction rememberPanes(panes: Panes): void {\n try {\n localStorage.setItem(PANES_KEY, JSON.stringify(panes))\n } catch {\n // 못 적어도 이번 화면은 그대로 동작한다. 다음에 열 때 펴져 있을 뿐이다.\n }\n}\n\n/**\n * 새로 만든 Figure 의 시작 상태. **씬 모델로 만든다.**\n *\n * 빈 상태로 시작하면 검증이 곧바로 오류를 낸다 — 사용자가 아무것도 하지 않았는데 빨간\n * 화면을 보게 된다. 그래서 부품 하나를 놓아 준다. **처음 열었을 때 바로 무언가 보이는\n * 것**이 이 도구를 익히는 첫 단계다.\n */\nfunction startingModel(type: string): { board: BoardModel; parts: PartModel[] } {\n return {\n board: { width: 300, height: 300, depth: 300, figureType: type, detailLevel: 'M' },\n parts: [\n {\n type: 'figure-part',\n name: 'body',\n primitive: 'cube',\n left: 0,\n top: 0,\n width: 300,\n height: 300,\n zPos: 0,\n depth: 300,\n token: 'palette.primary'\n }\n ]\n }\n}\n\n/**\n * Figure 저작면.\n *\n * ## 세 칸이다\n *\n * 왼쪽 도형 팔레트 · 부품 목록 · 속성\n * 가운데 미리보기 — `FigureInstance` 를 실제로 세운다\n * 오른쪽 판단 — 늘 떠 있다\n *\n * 미리보기가 씬 위에서 실제로 동작하는 것이 이 도구의 전제다. **편집한 것이 곧 실제 결과물**이라야\n * \"모델러에서는 되는데 보드에서는 다르다\"가 생기지 않는다.\n *\n * ## 편집 데이터는 씬 모델 하나뿐이다\n *\n * 이 페이지가 갖는 것은 보드 하나와 부품 목록 — 미리보기에 그대로 세워지는 씬 모델이다.\n * 저장 형식(`FigureSource`)으로 바꾸는 것은 **경계에서만** 한다: 저장할 때, 판단할 때,\n * AI 후보와 비교할 때. 그 변환은 `figure-source.ts` 한 곳에서만 일어난다.\n *\n * 팔레트도 속성 편집기도 이 데이터를 직접 수정하지 않는다. 무엇을 하겠다는 것만 알리고,\n * 바꾸는 것은 이 페이지다. 두 곳에서 고치면 실행 취소도 저장도 어느 쪽이 맞는지 알 수\n * 없게 된다.\n */\n@customElement('figure-modeller-page')\nexport class FigureModellerPage extends FigureModellerPageBase {\n static styles = css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n /*\n 이 화면의 글자 크기를 여기서 정한다.\n\n 안 적어 두면 브라우저 기본 크기(16px)를 물려받는다. 아래 패널들은 전부 12px\n 언저리라 위 두 줄만 커 보이고, 버튼이 화면 전체와 따로 놀았다.\n\n form 요소(input · button)는 글꼴을 물려받지 않으므로 각자 적어야 한다.\n */\n font-size: 0.76rem;\n }\n\n /* 세 칸. 3D 를 가장 크게 둔다 — 이 도구의 주어는 형상이지 속성표가 아니다 */\n div[edits] {\n flex: 1;\n display: grid;\n grid-template-columns: 220px 1fr 320px;\n min-height: 0;\n overflow: hidden;\n }\n\n /*\n **세 칸의 자리를 못 박는다.**\n\n 이것이 없으면 왼쪽을 접었을 때 오른쪽 칸이 가운데 자리로 밀려 들어와 캔버스가\n 통째로 사라진다. display none 인 형제는 그리드 자리를 차지하지 않으므로, 첫째가\n 빠지면 나머지가 한 칸씩 당겨진다. 오른쪽은 마지막이라 빠져도 밀 것이 없어서\n 멀쩡했다 — 한쪽만 고장 나는 종류다.\n\n 이 파일이 이미 한 번 적어 둔 함정이다. 아래 div[lanes] 주석에 같은 말이 있다.\n */\n div[edits] > figure-parts {\n grid-column: 1;\n }\n div[edits] > div[middle] {\n grid-column: 2;\n }\n div[edits] > figure-side {\n grid-column: 3;\n }\n\n /*\n 접힌 칸은 **자리를 아예 안 쓴다.**\n\n 폭만 0 으로 줄이면 안쪽이 넘쳐 나와 가운데를 덮는다. 그렇다고 떼어 내면 오른쪽\n 칸이 펼쳐 둔 추이 그림 같은 상태를 잃는다 — figure-side 가 탭을 감출 때도 DOM 에는\n 남기는 것과 같은 이유다. display none 이 둘을 다 지킨다.\n\n 가운데는 접을 때마다 넓이가 바뀌는데, 편집 캔버스와 미리보기가 각자 ResizeObserver\n 를 갖고 있어 3D 가 알아서 따라온다.\n */\n div[edits][collapse-left] {\n grid-template-columns: 0 1fr 320px;\n }\n div[edits][collapse-right] {\n grid-template-columns: 220px 1fr 0;\n }\n div[edits][collapse-left][collapse-right] {\n grid-template-columns: 0 1fr 0;\n }\n div[edits][collapse-left] > figure-parts,\n div[edits][collapse-right] > figure-side {\n display: none;\n }\n\n /*\n 머리줄 — 이름 · 타입 · 미저장 표시 · 말로 시키는 줄 · 저장을 **한 줄에** 담는다.\n\n 이름은 페이지 제목에도 나온다(셸이 context.title 로 그린다). 그래도 여기 입력칸을 두는\n 이유는 **고치는 자리**가 필요해서다 — 제목은 읽는 것이고 이 칸은 바꾸는 것이다. 두 줄로\n 벌리는 대신 한 줄에 모아 캔버스에 높이를 넘긴다.\n */\n div[header] {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n div[header] figure-ask {\n flex: 1;\n min-width: 0;\n }\n input[name] {\n flex: none;\n width: 180px;\n padding: 5px 9px;\n font: var(--input-field-font, inherit);\n font-size: 0.82rem;\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n }\n /* 타입 이름은 만든 뒤 고칠 수 없다 — 그 사실이 화면에서 보여야 한다 */\n code[type] {\n padding: 4px 8px;\n font-family: var(--mono-font, monospace);\n font-size: 0.76rem;\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: 6px;\n }\n span[dirty] {\n font: var(--label-font, inherit);\n font-size: 0.74rem;\n color: var(--md-sys-color-tertiary);\n }\n div[spacer] {\n flex: 1;\n }\n button[save] {\n padding: 6px 16px;\n font: var(--label-font, inherit);\n font-size: 0.76rem;\n font-weight: 600;\n color: var(--md-sys-color-on-primary, #fff);\n background-color: var(--md-sys-color-primary);\n border: none;\n border-radius: 6px;\n cursor: pointer;\n }\n button[save][disabled] {\n opacity: 0.4;\n cursor: default;\n }\n\n /*\n 칸을 벡터 저작도구의 관례대로 놓는다 — 왼쪽에 무엇이 있나(레이어), 오른쪽에\n 고른 것의 속성. 쓰는 사람이 이미 아는 자리라 배울 것이 하나 줄어든다.\n 차별성은 배치가 아니라 가운데의 로우폴리 3D 에 있으므로, 배치로 낯설게 하지\n 않는다.\n\n 오른쪽은 figure-side 가 속성과 판단을 탭으로 겹쳐 둔다. 위아래로 나누면 둘 다\n 잘려서 속성은 재질까지, 판단은 무게까지 못 내려간다. 개수는 탭 이름표에 남아\n 판단을 덮어 두어도 보인다.\n */\n figure-side {\n min-height: 0;\n }\n\n figure-canvas,\n figure-preview {\n min-width: 0;\n min-height: 0;\n }\n\n /*\n 편집 · 미리보기 전환.\n\n 가운데 칸 위에 얇게 둔다. 무엇을 보고 있는지가 늘 보여야 하고, 두 화면이 같은\n 자리를 쓰므로 어느 쪽인지 모르면 편집이 안 먹는 것처럼 읽힌다.\n */\n div[modes] {\n display: flex;\n flex: none;\n gap: 2px;\n padding: 5px var(--spacing-medium, 8px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n div[modes] button[mode] {\n padding: 3px 12px;\n font: var(--label-font, inherit);\n font-size: 0.74rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background: none;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n }\n div[modes] button[mode]:hover:not([on]) {\n background-color: var(--md-sys-color-surface-container);\n }\n div[modes] button[mode][on] {\n font-weight: 700;\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n }\n\n /*\n 양쪽 칸을 접는 손잡이. 띠의 양 끝, 접히는 칸 바로 옆이다.\n\n 접었을 때 남는 것이 이것 하나뿐이므로 **접는 자리와 펴는 자리가 같아야 한다.**\n 다른 데 두면 접고 나서 되돌릴 곳을 찾아야 한다.\n */\n div[modes] button[collapse] {\n display: flex;\n align-items: center;\n flex: none;\n padding: 2px 3px;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background: none;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n }\n div[modes] button[collapse]:hover {\n background-color: var(--md-sys-color-surface-container);\n }\n div[modes] button[collapse] md-icon {\n --md-icon-size: 18px;\n }\n\n /*\n 후보가 오면 가운데를 둘로 나눈다.\n\n 숫자와 JSON 으로 승낙을 받지 않는다 — 저작한 것이 곧 실물이므로 **둘 다 세워\n 놓고** 고르게 한다. 나란히 두지 않으면 무엇이 달라졌는지 기억으로 견줘야 한다.\n */\n /*\n 세로로 쌓아 둔다. 그리드로 두었더니 후보가 없을 때 미리보기가 첫 줄(auto)에\n 떨어져 **높이가 0** 이 됐다 — 3D 가 통째로 사라졌다. display:none 인 형제는\n 그리드 자리를 차지하지 않는다.\n */\n div[middle] {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n }\n /*\n 가운데를 채운다. 둘 다 적어야 한다 — 하나만 적으면 감춰졌다 나타난 쪽이 높이 0 이\n 되어 화면이 하얗게 뜬다. 실제로 그렇게 났다.\n */\n div[middle] > figure-canvas,\n div[middle] > figure-preview {\n flex: 1;\n }\n div[lanes] {\n flex: 1;\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 1px;\n min-height: 0;\n background-color: var(--md-sys-color-outline-variant);\n }\n div[lane] {\n display: grid;\n grid-template-rows: auto 1fr;\n min-width: 0;\n min-height: 0;\n background-color: var(--md-sys-color-surface);\n }\n div[lane] h4 {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n padding: 5px var(--spacing-medium, 8px);\n font: var(--label-font, inherit);\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 div[lane][candidate] h4 {\n color: var(--md-sys-color-primary);\n }\n div[lane] h4 small {\n font-weight: 400;\n letter-spacing: 0;\n text-transform: none;\n }\n\n /* 받거나 버린다. 그 사이에 다른 것은 없다 */\n div[decide] {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n background-color: var(--md-sys-color-primary-container);\n color: var(--md-sys-color-on-primary-container);\n font-size: 0.76rem;\n }\n div[decide] div[spacer] {\n flex: 1;\n }\n div[decide] button {\n padding: 5px 14px;\n font: var(--label-font, inherit);\n border-radius: 6px;\n cursor: pointer;\n }\n button[take] {\n color: var(--md-sys-color-on-primary, #fff);\n background-color: var(--md-sys-color-primary);\n border: none;\n }\n button[drop] {\n color: inherit;\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n }\n button[take][disabled] {\n opacity: 0.4;\n cursor: default;\n }\n\n /*\n 무엇을 바꾸자는 것인가 — 칩 하나가 변경 하나다.\n\n 통째로만 받게 두면 「몸통은 좋은데 모터 자리가 틀렸다」에 답할 수 없다. 그게\n 가장 흔한 반응이다. 처음에는 전부 골라 둔다 — 통째로 받는 것이 보통의 길이고,\n 빼고 싶은 것만 끄면 된다.\n */\n div[changes] {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n padding: 0 var(--spacing-large, 12px) var(--spacing-medium, 8px) var(--spacing-large, 12px);\n background-color: var(--md-sys-color-primary-container);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n button[change] {\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 2px 9px 2px 5px;\n font: var(--label-font, inherit);\n font-size: 0.72rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 999em;\n cursor: pointer;\n }\n button[change][on] {\n color: var(--md-sys-color-on-primary, #fff);\n background-color: var(--md-sys-color-primary);\n border-color: transparent;\n }\n button[change] md-icon {\n --md-icon-size: 14px;\n }\n /* 안 고른 것은 글자에 줄을 그어 「안 받는다」를 말한다 */\n button[change]:not([on]) span[key] {\n text-decoration: line-through;\n }\n\n /* 고른 조합이 형식을 어기면 미리 알린다. 선택한 뒤에 거절당하는 것보다 낫다 */\n /* 정책을 넘은 것은 막지 않는다 — 알리기만 한다. 색으로 갈라 둔다 */\n p[blocked][warn] {\n color: var(--md-sys-color-tertiary);\n }\n p[blocked] {\n margin: 0;\n padding: 0 var(--spacing-large, 12px) var(--spacing-medium, 8px) var(--spacing-large, 12px);\n background-color: var(--md-sys-color-primary-container);\n font-size: 0.72rem;\n line-height: 1.5;\n color: var(--md-sys-color-error);\n }\n `\n\n /** 편집 중인 Figure. 새로 만드는 중이면 `id` 가 없다. */\n @state() private figure?: Figure\n /** 편집 데이터 — 미리보기에 그대로 세워지는 씬 모델이다. */\n @state() private board?: BoardModel\n @state() private parts: PartModel[] = []\n @state() private selected = -1\n /*\n 저작 화면의 **보기 설정**. 조명 · 바닥 · 축 가이드 같은 것이다.\n\n 자산이 아니라 작업대라 저장되지 않는다 — `toFigureSource` 가 집어 가지 않는다.\n 자세한 것은 `modeller/figure-view.ts` 를 보라.\n */\n @state() private view: ViewSettings = DEFAULT_VIEW\n /**\n * 가운데 칸이 무엇을 보이나.\n *\n * 편집 캔버스는 늘 기준 크기다. 그런데 이 형식의 값 절반은 크기가 달라졌을 때 어떻게\n * 반응하는가에 있다. 그것을 볼 자리가 없었다.\n */\n @state() private mode: 'edit' | 'preview' = 'edit'\n /**\n * 어느 칸이 접혀 있나.\n *\n * 통째로 갈아 끼운다 — 안쪽 값만 바꾸면 Lit 이 같은 객체로 보고 다시 그리지 않는다.\n */\n @state() private collapsed: Panes = recallPanes()\n @state() private dirty = false\n @state() private saving = false\n @state() private violations: FigureFinding[] = []\n /** 저작 보조가 낸 후보. 받기 전에는 원본 데이터가 아니다. */\n @state() private proposal?: FigureSource\n /** 그 후보의 셈. 무엇을 받는 것인지 숫자로도 보여 준다. */\n @state() private proposalOf?: { grade: string; triangles: number; groups: number; attempts: number }\n /** 받기로 고른 변경들. 처음에는 전부 골라 둔다. */\n @state() private picked = new Set<string>()\n\n /**\n * 저장 형식으로 변환한 것. 씬 모델이 바뀔 때만 다시 만든다.\n *\n * 저장·판단·AI 후보 비교가 이것을 쓴다. 렌더링할 때마다 새로 만들면 판단 패널이 매번\n * 다시 계산하므로, 변환은 `willUpdate` 에서 한 번만 한다.\n */\n private folded?: FigureSource\n\n willUpdate(changed: Map<string, unknown>) {\n if (changed.has('board') || changed.has('parts')) {\n this.folded = this.board ? toFigureSource(this.board, this.parts) : undefined\n }\n }\n\n get context() {\n return {\n title: this.figure?.name ?? i18next.t('title.new-figure'),\n /*\n **제목 줄은 끈다.** 이름은 그대로 둔다.\n\n 이 화면의 주인공은 캔버스다. 이름은 속성 패널에서 보고 고치고, 브라우저 탭에도\n 「operato figure · 작업자」로 나온다. 그 위에 제목 줄을 한 겹 더 얹으면 같은 말이 세 번이고,\n 그만큼 캔버스가 낮아진다.\n\n `title` 을 비우는 것으로 끄지 않는 이유는 그 값을 MDI 탭도 읽기 때문이다 — 비우면 탭 이름이\n 함께 사라진다. 이름은 하나이고 그리는 자리마다 켜고 끈다(`ox-page-title-bar`).\n */\n heading: false\n }\n }\n\n render() {\n const source = this.folded\n\n return html`\n <div header>\n <!--\n 이름과 타입은 **속성 패널**에 있다(figure-inspector 의 도형 절). 여기 있던 이름 칸은\n 하네스 시절의 자리였다 — 앱에서는 셸이 페이지 제목으로 같은 이름을 그리므로 글자가 두 번\n 나오고, 캔버스가 주인공인 화면에서 그만큼 높이를 잃었다.\n -->\n ${this.dirty ? html`<span dirty>${i18next.t('text.unsaved-changes')}</span>` : ''}\n <!--\n 말로 시키는 줄이 머리줄의 남는 폭을 쓴다. 따로 두면 화면 위가 두 줄이 되고, 그 위 줄은\n 이름을 페이지 제목과 겹쳐 적는다.\n -->\n <figure-ask\n .source=${source}\n .type=${source?.type ?? ''}\n @proposed=${(e: CustomEvent) => this.receive(e.detail.proposal)}\n ></figure-ask>\n <button save ?disabled=${!this.canSave} @click=${() => this.save()}>\n ${this.saving ? i18next.t('text.saving') : i18next.t('button.save')}\n </button>\n </div>\n\n <div\n edits\n ?collapse-left=${this.collapsed.left}\n ?collapse-right=${this.collapsed.right}\n @figure-rename=${(e: CustomEvent) => this.rename(e.detail.name)}\n @add-part=${(e: CustomEvent) => this.addPart(e.detail.primitive)}\n @select-part=${(e: CustomEvent) => (this.selected = e.detail.index)}\n @remove-part=${(e: CustomEvent) => this.removePart(e.detail.index)}\n @duplicate-part=${(e: CustomEvent) => this.duplicatePart(e.detail.index)}\n @move-part-up=${(e: CustomEvent) => this.movePart(e.detail.index, e.detail.to)}\n @move-part-down=${(e: CustomEvent) => this.movePart(e.detail.index, e.detail.to)}\n @update-part=${(e: CustomEvent) => this.updatePart(e.detail.index, e.detail.part)}\n @part-changed=${(e: CustomEvent) => this.partChanged(e.detail.name, e.detail.part)}\n @toggle-part-hidden=${(e: CustomEvent) => this.toggleHidden(e.detail.index)}\n @set-all-parts-hidden=${(e: CustomEvent) => this.setAllHidden(e.detail.hidden)}\n @change-detail-level=${(e: CustomEvent) => this.changeDetailLevel(e.detail.level)}\n @part-tool=${(e: CustomEvent) => this.runTool(e.detail.index, e.detail.tool, e.detail.axis)}\n @pick-part=${(e: CustomEvent) => this.pickPart(e.detail.name)}\n >\n <figure-parts\n .parts=${this.parts}\n .level=${(this.board?.detailLevel ?? 'M') as DetailLevel}\n .selected=${this.selected}\n ></figure-parts>\n\n <div middle>\n ${this.renderBar()}\n ${this.proposal ? this.renderDecision() : ''}\n ${this.proposal\n ? html`\n <div lanes>\n <div lane>\n <h4>${i18next.t('label.now')}</h4>\n <figure-canvas\n .board=${this.board}\n .parts=${this.parts}\n .picked=${this.pickedName}\n .view=${this.view}\n ></figure-canvas>\n </div>\n <div lane candidate>\n <h4>\n ${i18next.t('label.proposed')}\n ${this.proposalOf\n ? html`<small>\n ${this.proposalOf.grade} ·\n ${i18next.t('text.n-triangles', { count: this.proposalOf.triangles })}\n </small>`\n : ''}\n </h4>\n ${this.renderProposed()}\n </div>\n </div>\n `\n : this.mode === 'preview'\n ? html`<figure-preview .source=${source}></figure-preview>`\n : html`<figure-canvas\n .board=${this.board}\n .parts=${this.parts}\n .picked=${this.pickedName}\n ></figure-canvas>`}\n </div>\n\n <figure-side\n .figureName=${this.figure?.name ?? ''}\n .board=${this.board}\n .parts=${this.parts}\n .source=${source}\n .selected=${this.selected}\n .view=${this.view}\n @update-view=${(e: CustomEvent) => (this.view = e.detail.view)}\n ></figure-side>\n </div>\n `\n }\n\n /**\n * 가운데 칸 위의 띠 — 무엇을 보고 있나와, 양쪽 칸을 접는 손잡이.\n *\n * ## 무엇을 보고 있나\n *\n * 편집과 미리보기는 같은 자리를 놓고 다투지 않는다. 둘 다 넓이가 필요하고, 동시에\n * 봐야 할 일이 없다. 부품을 고치는 동안에는 편집만 보면 되고, 다 고친 뒤에 보드에서\n * 어떻게 되는지 보면 된다.\n *\n * 후보가 와 있을 때는 그 두 단추를 내지 않는다. 그때는 가운데가 이미 둘로 나뉘어\n * 있다.\n *\n * ## 띠는 늘 있다\n *\n * 손잡이가 여기 있기 때문이다. 후보가 왔다고 띠를 통째로 감추면, 칸을 접어 둔 채로\n * 후보를 받으면 되돌릴 방법이 없어진다.\n */\n private renderBar() {\n const one = (mode: 'edit' | 'preview') => html`\n <button\n mode\n ?on=${this.mode === mode}\n title=${i18next.t('text.mode-' + mode + '-explained')}\n @click=${() => (this.mode = mode)}\n >\n ${i18next.t('label.mode-' + mode)}\n </button>\n `\n\n return html`<div modes>\n ${this.renderCollapse('left')}${this.proposal ? '' : html`${one('edit')}${one('preview')}`}\n <div spacer></div>\n ${this.renderCollapse('right')}\n </div>`\n }\n\n /**\n * 한쪽 칸을 접고 펴는 손잡이.\n *\n * 화살표는 **누르면 무엇이 일어나는지**를 가리킨다 — 펴져 있으면 칸이 사라질 쪽,\n * 접혀 있으면 칸이 돌아올 쪽. 지금 상태를 가리키게 하면 반대로 읽힌다.\n */\n private renderCollapse(side: keyof Panes) {\n const gone = this.collapsed[side]\n const shut = side === 'left' ? 'chevron_left' : 'chevron_right'\n const back = side === 'left' ? 'chevron_right' : 'chevron_left'\n const pane = side === 'left' ? 'parts' : 'properties'\n\n return html`\n <button\n collapse\n title=${i18next.t(`text.${gone ? 'expand' : 'collapse'}-${pane}-pane`)}\n @click=${() => this.toggleCollapse(side)}\n >\n <md-icon>${gone ? back : shut}</md-icon>\n </button>\n `\n }\n\n private toggleCollapse(side: keyof Panes): void {\n this.collapsed = { ...this.collapsed, [side]: !this.collapsed[side] }\n rememberPanes(this.collapsed)\n }\n\n /**\n * AI 후보를 나란히 세운다.\n *\n * 후보는 저장 형식으로 온다. 미리보기는 씬 모델을 받으므로 여기서 한 번 변환한다 —\n * 후보를 받아들이기 전까지는 편집 데이터가 아니므로 상태로 두지 않는다.\n */\n private renderProposed() {\n if (!this.proposal) return ''\n\n const { board, parts } = fromFigureSource(this.proposal)\n return html`<figure-canvas .board=${board} .parts=${parts} .viewOnly=${true}></figure-canvas>`\n }\n\n /**\n * 무엇을 받을지 고른다.\n *\n * 통째로만 받게 두면 「몸통은 좋은데 모터 자리가 틀렸다」에 답할 수 없다 — 그게\n * 가장 흔한 반응이다. 변경 하나가 칩 하나이고, 처음에는 전부 골라 둔다. 통째로\n * 받는 것이 보통의 길이므로 그것이 한 번 누르기여야 한다.\n *\n * 고른 조합을 **받기 전에 재 본다.** 형식을 어기면 못 누르게 하고, 정책을 넘으면\n * 알린다. 골라 받으면 후보를 낼 때와 다른 조합이 되므로 후보의 점수를 그대로\n * 믿을 수 없다 — 예를 들어 등급은 안 받고 부품만 받으면 부품 수 한도를 넘는다.\n *\n * 형식 오류가 실제로 나는 조합은 아직 보지 못했다. 그래도 두는 것은 값이 거의\n * 안 들고, 우리가 생각 못 한 조합을 막아 주기 때문이다.\n */\n private renderDecision() {\n if (!this.proposal) return ''\n\n const changes = proposals.diffProposal(this.folded, this.proposal)\n const picked = proposals.applyProposal(this.folded, this.proposal, this.picked)\n const { errors, violations } = validate(picked)\n\n return html`\n <div decide>\n <span>${i18next.t('text.assistant-proposed-a-figure')}</span>\n <div spacer></div>\n <button drop @click=${() => this.discard()}>${i18next.t('button.discard')}</button>\n <button take ?disabled=${errors.length > 0 || this.picked.size === 0} @click=${() => this.take()}>\n ${i18next.t('button.take-n-changes', { n: this.picked.size })}\n </button>\n </div>\n\n ${changes.length > 0\n ? html`\n <div changes>\n ${changes.map(change => this.renderChange(change))}\n </div>\n `\n : ''}\n ${errors.length > 0\n ? html`<p blocked>${i18next.t('text.picked-changes-do-not-validate')} — ${errors[0].message}</p>`\n : violations.length > 0\n ? html`<p blocked warn>${violations[0].message}</p>`\n : ''}\n `\n }\n\n /** 변경 하나. 무엇을 하겠다는 것인지 부호로 먼저 말한다. */\n private renderChange(change: proposals.ProposalChange) {\n const icon = {\n added: 'add',\n changed: 'edit',\n removed: 'remove',\n base: 'aspect_ratio',\n detail: 'tune',\n anchor: 'my_location',\n styleKit: 'palette'\n }[change.kind]\n\n const on = this.picked.has(change.key)\n\n return html`\n <button\n change\n ?on=${on}\n title=${i18next.t('text.change-' + change.kind)}\n @click=${() => this.toggle(change.key)}\n >\n <md-icon>${icon}</md-icon>\n <span key>${change.key}</span>\n </button>\n `\n }\n\n private toggle(key: string) {\n const next = new Set(this.picked)\n if (next.has(key)) next.delete(key)\n else next.add(key)\n this.picked = next\n }\n\n /**\n * 후보가 왔다.\n *\n * 원본 데이터에 넣지 않는다. 나란히 두고 저작자가 고른다. AI 가 원본 데이터에 직접 쓰면\n * 무엇이 자기 것이고 무엇이 기계 것인지 모르게 된다.\n */\n private receive(proposal: {\n source: string\n grade: string\n triangles: number\n groups: number\n attempts: number\n }) {\n this.proposal = JSON.parse(proposal.source) as FigureSource\n // 처음에는 전부 골라 둔다 — 통째로 받는 것이 보통의 길이다.\n this.picked = new Set(proposals.diffProposal(this.folded, this.proposal).map(change => change.key))\n this.proposalOf = {\n grade: proposal.grade,\n triangles: proposal.triangles,\n groups: proposal.groups,\n attempts: proposal.attempts\n }\n }\n\n /**\n * 고른 변경을 받아들인다.\n *\n * 후보 비교는 저장 형식으로 한다 — 부품 이름으로 짝짓고 기준 치수·등급까지 함께 봐야\n * 하므로 그쪽이 자연스럽다. 받아들인 결과는 다시 씬 모델로 되돌려 편집을 이어간다.\n */\n private take() {\n if (!this.proposal) return\n\n const taken = proposals.applyProposal(this.folded, this.proposal, this.picked)\n const { board, parts } = fromFigureSource(taken)\n\n this.board = board\n this.parts = parts\n this.selected = parts.length ? 0 : -1\n this.dirty = true\n this.discard()\n }\n\n private discard() {\n this.proposal = undefined\n this.proposalOf = undefined\n this.picked = new Set()\n }\n\n /**\n * 저장할 수 있나.\n *\n * 형식을 어긴 원본 데이터로는 저장을 누를 수 없게 한다. 눌렀다가 서버가 받아주지 않는 것보다,\n * 누를 수 없는 이유가 판단 패널에 떠 있는 것이 낫다.\n */\n private get canSave(): boolean {\n if (this.saving || !this.folded || !this.figure?.name) {\n return false\n }\n return validate(this.folded).errors.length === 0\n }\n\n async pageUpdated(changes: Record<string, unknown>, lifecycle: { resourceId?: string }) {\n if (!this.active) {\n return\n }\n\n const id = lifecycle?.resourceId\n if (id && id !== this.figure?.id) {\n await this.load(id)\n } else if (!id && !this.figure) {\n this.startNew()\n }\n }\n\n private async load(id: string) {\n const figure = await fetchFigure(id)\n\n this.figure = figure\n\n /* 그림이 없으면 이 자리에서 채운다(아래 `fillThumbnail`). 기다리지 않는다 — 화면이 먼저다. */\n if (!figure.thumbnail && figure.id) {\n void this.fillThumbnail(figure.id)\n }\n\n // 서버가 준 것을 그대로 쓴다. 읽지 못하면 오류보고없이 빈 상태로 바꾸지 않는다.\n // 그러면 사용자가 저장을 선택해 원본을 덮어쓴다.\n const model = figure.source ? fromFigureSource(JSON.parse(figure.source) as FigureSource) : undefined\n this.board = model?.board\n this.parts = model?.parts ?? []\n\n /*\n **아무것도 고르지 않은 채로 연다.**\n\n 전에는 첫 부품을 잡아 두었다. 그러면 사용자가 고른 적 없는 부품의 속성이 떠 있고,\n 캔버스에는 그 부품의 윤곽선이 그려진다. 모델 전체를 보려면 먼저 선택을 풀어야 했다.\n\n 속성 패널의 빈 상태도 그래서 **열었을 때 한 번도 안 보였다.** 처음 여는 사람에게\n 「왼쪽에서 부품을 고르는 도구」라고 알려 줄 자리가 그 화면이다.\n\n 새로 만들 때(`startNew`)는 다르다. 방금 부품을 만들었으니 바로 편집하려는 것이 맞다.\n */\n this.selected = -1\n this.dirty = false\n this.violations = []\n }\n\n private startNew() {\n // 타입 이름을 사람이 정하기 전까지는 임시 이름을 쓴다. 저장할 때 확정한다.\n const draftType = `FIGURE_${Date.now().toString(36).toUpperCase()}`\n const { board, parts } = startingModel(draftType)\n\n this.figure = { id: '', type: draftType, name: '' } as Figure\n this.board = board\n this.parts = parts\n this.selected = 0\n this.dirty = true\n this.violations = []\n }\n\n private rename(name: string) {\n if (!this.figure) return\n\n this.figure = { ...this.figure, name }\n this.dirty = true\n }\n\n /**\n * 부품 목록을 갈아 끼운다.\n *\n * 편집 규칙은 `part-edits` 가 갖는다 — 화면과 테스트가 같은 것을 쓰게 하려고 떼어\n * 놓았다. 여기서는 결과를 담고 선택 인덱스를 정리한다.\n */\n private put(parts: PartModel[], selected = this.selected) {\n this.parts = parts\n this.selected = Math.min(selected, parts.length - 1)\n this.dirty = true\n }\n\n private addPart(primitive: PrimitiveKind) {\n if (!this.board) return\n // 추가한 것을 바로 수정할 수 있게 선택 상태로 둔다.\n this.put(edits.addPart(this.board, this.parts, primitive), this.parts.length)\n }\n\n private removePart(index: number) {\n this.put(edits.removePart(this.parts, index), Math.max(0, index - 1))\n }\n\n private duplicatePart(index: number) {\n this.put(edits.duplicatePart(this.parts, index), index + 1)\n }\n\n private movePart(index: number, to: number) {\n this.put(edits.movePart(this.parts, index, to), to)\n }\n\n private updatePart(index: number, part: PartModel) {\n this.put(edits.updatePart(this.parts, index, part))\n }\n\n /**\n * 디테일 등급을 바꾼다.\n *\n * 이 값은 부품 수 한도를 정하고 성숙도의 한 축이 된다. 부품보다 위에 있는 값이라\n * 보드에 담는다.\n *\n * 한도를 내려서 이미 넣은 부품 수보다 작아질 수 있다. 그때 부품을 지우지 않는다.\n * 검증이 위반으로 알리고, 성숙도가 등급에 안 맞는다고 말한다. 저작자가 무엇을 뺄지\n * 정할 일이지 도구가 정할 일이 아니다.\n */\n private changeDetailLevel(level: DetailLevel) {\n if (!this.board) return\n\n this.board = { ...this.board, detailLevel: level }\n this.dirty = true\n }\n\n /**\n * 부품을 잠깐 가린다.\n *\n * 씬이 이미 갖고 있는 상태를 켜고 끄는 것뿐이다 — 편집 모드에서는 faint 로 보이고\n * 뷰 모드에서는 안 그려진다. 저장 형식에는 실리지 않으므로 다시 열면 전부 보인다.\n *\n * `dirty` 로 치지 않는다. 저장될 것이 하나도 바뀌지 않았는데 \"저장하지 않은 변경\"이\n * 뜨면, 저장 버튼이 무엇을 저장하겠다는 것인지 알 수 없다.\n */\n private toggleHidden(index: number) {\n const part = this.parts[index]\n if (!part) return\n\n this.parts = edits.updatePart(this.parts, index, { ...part, hidden: !part.hidden })\n }\n\n /**\n * 한꺼번에 감추거나 되돌린다.\n *\n * 부품 스물짜리에서 안쪽 하나를 보려고 열아홉을 누르던 것을 없앤다. 한 번에 바꾸므로\n * 목록을 한 번만 갈아 끼운다 — 하나씩 스무 번 갈면 미리보기가 스무 번 다시 선다.\n */\n private setAllHidden(hidden: boolean) {\n this.parts = this.parts.map(part => (part.hidden === hidden ? part : { ...part, hidden }))\n }\n\n /**\n * 미리보기에서 기즈모로 옮기거나 2D 에서 끌었다.\n *\n * 알려 오는 것은 부품 **이름**이다. 목록의 몇 번째인지는 여기서 찾는다 — 이벤트가\n * 오는 사이에 목록이 바뀌었으면 찾지 못하고, 그때는 아무것도 하지 않는다.\n */\n private partChanged(name: string, part: PartModel) {\n const at = this.parts.findIndex(item => item.name === name)\n if (at >= 0) this.put(edits.updatePart(this.parts, at, part))\n }\n\n /**\n * 정밀 배치 도구.\n *\n * 규칙은 `part-edits` 가 갖는다. 여기서는 어느 것을 부를지만 정한다.\n */\n private runTool(index: number, tool: string, axis: PlaneAxis) {\n const board = this.board\n if (!board) return\n\n if (tool === 'center') this.put(edits.centerPart(board, this.parts, index, axis))\n else if (tool === 'floor') this.put(edits.alignPartToStart(this.parts, index, axis))\n else if (tool === 'snap') this.put(edits.snapPart(board, this.parts, index))\n // 대칭 복제는 부품이 하나 늘어난다 — 새로 생긴 것을 선택 상태로 둔다\n else if (tool === 'mirror') this.put(edits.mirrorPart(board, this.parts, index, axis), index + 1)\n }\n\n /**\n * 캔버스에서 부품을 선택했다.\n *\n * 캔버스가 알리는 것은 **이름**이다. 목록의 몇 번째인지는 여기서 찾는다 — 알림이\n * 오는 사이에 목록이 바뀌었으면 못 찾고, 그때는 고른 것을 바꾸지 않는다.\n * 없는 자리를 고르면 속성 칸이 빈 채로 열려 「사라졌나」로 읽힌다.\n */\n /** 지금 고른 부품 이름. 캔버스가 인덱스가 아니라 이름을 받는다. */\n private get pickedName(): string | undefined {\n return this.parts[this.selected]?.name\n }\n\n private pickPart(name: string) {\n const at = this.parts.findIndex(part => part.name === name)\n if (at >= 0) this.selected = at\n }\n\n /**\n * 그림이 없는 도형을 **열었을 때 한 번** 찍어 채운다.\n *\n * ## 왜 여기인가\n *\n * 보드는 저장할 때 서버가 찍는다. 그런데 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는\n * 것이 자주 실패한다. 도형을 그리는 GPU 는 이미 보는 사람 손에 있고, 씬은 지금 여기 서 있다.\n *\n * 저작면을 거치지 않고 생긴 것들이 그림이 없다 — 씨앗으로 넣은 표본, AI 가 낸 후보, 반입한 자산.\n * 그것들을 한 번 열어 보면 그때 채워진다.\n *\n * ## 저장이 아니다\n *\n * 그림만 보낸다. 정본을 건드리지 않으므로 미저장 표시도 켜지 않는다 — 열어 봤다는 것이 고쳤다는\n * 뜻이 되면 저작자가 저장할지 말지를 매번 고민한다.\n */\n private async fillThumbnail(id: string): Promise<void> {\n await this.updateComplete\n\n const thumbnail = await captureThumbnail(this.board, this.parts)\n if (!thumbnail || this.figure?.id !== id) {\n return\n }\n\n try {\n await updateFigure(id, { thumbnail })\n this.figure = { ...this.figure, thumbnail }\n } catch (e) {\n /* 볼 권한만 있는 사람일 수 있다. 화면은 그대로 두고 조용히 넘어간다. */\n console.warn(`[thumbnail] ${id} 를 채우지 못했습니다 — ${(e as Error).message}`)\n }\n }\n\n private async save() {\n if (!this.canSave || !this.folded || !this.figure) {\n return\n }\n\n this.saving = true\n try {\n const sourceJson = JSON.stringify(this.folded)\n /*\n 보이는 것을 그대로 찍어 함께 보낸다.\n\n 카드가 그림 없이 서 있으면 카탈로그에서 무엇인지 못 고른다. 찍는 자리를 저장에 두는 이유는\n **그 순간이 정본이 바뀌는 순간**이라서다 — 따로 찍는 절차를 두면 그림과 정본이 갈린다.\n\n 못 찍으면 보내지 않는다(`undefined`). 빈 그림을 넣어 「그림이 있다」고 말하지 않는다.\n */\n const thumbnail = await captureThumbnail(this.board, this.parts)\n\n if (this.figure.id) {\n const result = await updateFigure(this.figure.id, {\n name: this.figure.name,\n source: sourceJson,\n thumbnail\n })\n this.figure = result.figure\n this.violations = result.violations\n } else {\n // 새로 만들 때만 타입 이름이 정해진다. 이미 쓰이는 이름이면 저장 전에 막는다 —\n // 만든 뒤에는 고칠 수 없으므로 여기서 걸러야 한다.\n const taken = await fetchFigureTypeNames()\n if (taken.includes(this.folded.type)) {\n throw new Error(i18next.t('text.type-name-already-taken', { type: this.folded.type }))\n }\n\n const result = await createFigure({\n type: this.folded.type,\n name: this.figure.name,\n source: sourceJson,\n thumbnail\n })\n this.figure = result.figure\n this.violations = result.violations\n navigate(`figure-modeller/${result.figure.id}`)\n }\n\n this.dirty = false\n } finally {\n this.saving = false\n }\n }\n}\n"]}