@things-factory/figure-ui 10.1.16 → 10.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/figure-animations.ts +12 -12
- package/client/modeller/figure-canvas.ts +32 -48
- package/client/modeller/figure-capabilities.ts +10 -10
- package/client/modeller/figure-inspector.ts +39 -28
- package/client/modeller/figure-parts.ts +1 -1
- package/client/modeller/figure-preview.ts +122 -25
- package/client/modeller/figure-report.ts +24 -6
- package/client/modeller/figure-settings.ts +45 -2
- package/client/modeller/figure-side.ts +6 -6
- package/client/modeller/figure-source.ts +52 -32
- package/client/modeller/figure-thumbnail.ts +5 -5
- package/client/modeller/figure-view.ts +53 -0
- package/client/modeller/part-edits.ts +36 -36
- package/client/modeller/proposal.ts +25 -4
- package/client/pages/figure-modeller-page.ts +64 -58
- package/dist-client/modeller/figure-animations.d.ts +4 -4
- package/dist-client/modeller/figure-animations.js +10 -10
- package/dist-client/modeller/figure-animations.js.map +1 -1
- package/dist-client/modeller/figure-canvas.d.ts +7 -7
- package/dist-client/modeller/figure-canvas.js +27 -40
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-capabilities.d.ts +2 -2
- package/dist-client/modeller/figure-capabilities.js +9 -9
- package/dist-client/modeller/figure-capabilities.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +16 -5
- package/dist-client/modeller/figure-inspector.js +38 -27
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-parts.d.ts +1 -1
- package/dist-client/modeller/figure-parts.js +1 -1
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +39 -0
- package/dist-client/modeller/figure-preview.js +112 -23
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-report.d.ts +9 -0
- package/dist-client/modeller/figure-report.js +23 -5
- package/dist-client/modeller/figure-report.js.map +1 -1
- package/dist-client/modeller/figure-settings.js +42 -2
- package/dist-client/modeller/figure-settings.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +2 -2
- package/dist-client/modeller/figure-side.js +5 -5
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +17 -15
- package/dist-client/modeller/figure-source.js +40 -21
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/modeller/figure-thumbnail.d.ts +2 -2
- package/dist-client/modeller/figure-thumbnail.js +3 -3
- package/dist-client/modeller/figure-thumbnail.js.map +1 -1
- package/dist-client/modeller/figure-view.d.ts +45 -0
- package/dist-client/modeller/figure-view.js +27 -0
- package/dist-client/modeller/figure-view.js.map +1 -1
- package/dist-client/modeller/part-edits.d.ts +16 -16
- package/dist-client/modeller/part-edits.js +35 -35
- package/dist-client/modeller/part-edits.js.map +1 -1
- package/dist-client/modeller/proposal.d.ts +1 -1
- package/dist-client/modeller/proposal.js +15 -3
- package/dist-client/modeller/proposal.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +9 -10
- package/dist-client/pages/figure-modeller-page.js +60 -54
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/test/ai-proposal-contract.test.ts +393 -12
- package/test/expect.ts +55 -0
- package/test/figure-source.test.ts +562 -0
- package/test/i18n-prefix-guard.test.ts +1 -1
- package/{client/modeller → test}/part-edits.test.ts +50 -45
- package/translations/en.json +31 -28
- package/translations/ja.json +16 -21
- package/translations/ko.json +41 -38
- package/translations/ms.json +16 -21
- package/translations/zh.json +16 -21
- package/client/modeller/figure-ask.ts +0 -265
- package/client/modeller/figure-source.test.ts +0 -352
- package/dist-client/modeller/figure-ask.d.ts +0 -40
- package/dist-client/modeller/figure-ask.js +0 -282
- package/dist-client/modeller/figure-ask.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"figure-parts.js","sourceRoot":"","sources":["../../client/modeller/figure-parts.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 } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { ScrollbarStyles } from '@operato/styles'\nimport { DETAIL_LEVELS, PART_LIMIT, PRIMITIVE_KINDS } from '@hatiolab/figure-model'\nimport type { DetailLevel, PrimitiveKind } from '@hatiolab/figure-model'\n\nimport type { PartModel } from './figure-source.js'\n\n/** 도형마다의 아이콘. Material Symbols 이름이다. */\nconst SHAPE_ICON: Record<PrimitiveKind, string> = {\n cube: 'deployed_code',\n wall: 'rectangle',\n cylinder: 'database',\n sphere: 'circle',\n rect: 'crop_square',\n polygon: 'pentagon'\n}\n\n/**\n * 도형 팔레트와 부품 목록.\n *\n * ## 팔레트를 목록 위에 둔다\n *\n * 처음 온 사람은 「무엇을 놓을 수 있나」를 먼저 알아야 한다. 빈 목록 아래에 팔레트를\n * 두면 아무것도 없는 화면에서 다음 동작이 안 보인다.\n *\n * ## 한도를 미리 보여 준다\n *\n * 부품 수 한도는 디테일 등급이 정한다(`PART_LIMIT`). 넘긴 뒤에 판정 패널이\n * 나무라는 것보다, **넣기 전에 몇 개 남았는지** 보이는 것이 낫다.\n *\n * ## 고치지 않는다\n *\n * 이 컴포넌트는 정본을 만지지 않는다. 무엇을 하겠다는 것만 알린다 — 정본 한 벌을\n * 저작면이 갖고, 거기서만 바뀐다.\n */\n@customElement('figure-parts')\nexport class FigureParts extends localize(i18next)(LitElement) {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n min-height: 0;\n overflow: hidden;\n background-color: var(--md-sys-color-surface-container-lowest);\n border-right: 1px solid var(--md-sys-color-outline-variant);\n color: var(--md-sys-color-on-surface);\n font: var(--label-font, inherit);\n /*\n 이 패널의 본문 크기를 여기서 한 번 정한다.\n\n 전에는 규칙마다 따로 적었다. 그러면 빠뜨린 규칙이 본문 기본 크기(1rem)를\n 물려받아 그 줄만 제목처럼 커진다. 오류가 아니라서 화면을 띄워 봐야 안다.\n 이 파일에서만 두 번 났다.\n\n form 요소(input · select · button)는 글꼴을 물려받지 않으므로 각자 적어야 한다.\n 그것 말고는 여기 값을 따른다.\n */\n font-size: 0.76rem;\n }\n\n section {\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n section[list] {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n }\n h3 {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 6px;\n margin: 0 0 6px 0;\n font-size: var(--ui-t-micro, 11px);\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n /*\n 부품 수와 한도.\n\n 전에는 이 숫자가 「도형」 절 제목에 붙어 있었다. 부품 수인데 도형 옆에 있으니\n 「도형 8종 중 2종」으로 읽힌다 — 실제로 그렇게 읽혔다. 세는 대상이 있는 절로\n 옮기고, 한도가 어디서 나온 값인지(디테일 등급)를 함께 적는다.\n */\n span[room] {\n font-size: 0.7rem;\n font-weight: 400;\n letter-spacing: 0;\n text-transform: none;\n }\n span[room][full] {\n color: var(--md-sys-color-error);\n }\n\n /* ── 팔레트 ─────────────────────────────────────────────────────── */\n div[palette] {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 4px;\n }\n button[shape] {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 2px;\n padding: 7px 2px 5px 2px;\n font: var(--label-font, inherit);\n font-size: 0.66rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n cursor: pointer;\n }\n button[shape]:hover:not([disabled]) {\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border-color: transparent;\n }\n button[shape][disabled] {\n opacity: 0.35;\n cursor: default;\n }\n button[shape] md-icon {\n --md-icon-size: 20px;\n }\n\n /* ── 부품 목록 ──────────────────────────────────────────────────── */\n ol {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n li {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 6px;\n border-radius: 6px;\n cursor: pointer;\n }\n li:hover {\n background-color: var(--md-sys-color-surface-container);\n }\n li[selected] {\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n }\n li md-icon {\n --md-icon-size: 16px;\n flex: none;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /*\n 숨김 단추.\n\n 고른 줄에서만 보이는 조작(div[ops])과 달리 **늘 자리를 차지한다.** 무엇이 숨겨져\n 있는지는 목록을 훑을 때 한눈에 보여야 한다 — 눌러 봐야 알면 숨겨 둔 것을 잊는다.\n 다만 켜지지 않은 눈은 줄에 마우스를 올렸을 때만 보인다. 여덟 개 줄에 회색 눈이\n 여덟 개 떠 있으면 목록이 시끄럽다.\n */\n /* 머리줄의 것은 늘 보인다 — 줄에 마우스를 올려야 나타나는 것이 아니다 */\n button[eye][all] {\n margin-left: auto;\n opacity: 0.75;\n }\n button[eye][all]:hover {\n opacity: 1;\n }\n button[eye] {\n display: flex;\n flex: none;\n padding: 1px;\n color: inherit;\n background: none;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n opacity: 0;\n }\n li:hover button[eye],\n li[selected] button[eye],\n button[eye][off] {\n opacity: 1;\n }\n button[eye][off] {\n color: var(--md-sys-color-tertiary);\n }\n button[eye] md-icon {\n --md-icon-size: 15px;\n color: inherit;\n }\n\n /* 숨긴 줄은 목록에서도 옅게 — 미리보기에서 faint 로 보이는 것과 같은 뜻이어야 한다 */\n li[off] span[label],\n li[off] > md-icon {\n opacity: 0.45;\n }\n li[selected] md-icon {\n color: inherit;\n }\n span[label] {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 0.78rem;\n }\n\n /* 조작은 고른 줄에서만 보인다 — 늘 보이면 목록이 시끄럽다 */\n div[ops] {\n display: none;\n gap: 1px;\n }\n li[selected] div[ops] {\n display: flex;\n }\n div[ops] button {\n display: flex;\n padding: 2px;\n color: inherit;\n background: none;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n }\n div[ops] button:hover:not([disabled]) {\n background-color: var(--md-sys-color-surface-container-lowest);\n }\n div[ops] button[disabled] {\n opacity: 0.3;\n cursor: default;\n }\n div[ops] md-icon {\n --md-icon-size: 15px;\n color: inherit;\n }\n\n /*\n 디테일 등급 선택.\n\n 전에는 등급이 \"16 / 20 · L 등급\" 안의 글자 하나로만 있었다. S·M·L 이 무엇인지\n 어디에도 없었고, 한도를 다 쓰면 \"등급을 올리세요\" 라고 해 놓고 올릴 칸이 없었다.\n\n 부품 목록 위에 둔다. 등급이 정하는 것이 바로 아래 목록의 한도이고, 팔레트가\n 잠기는 것도 이 값 때문이다. 결과가 보이는 자리에서 고치게 한다.\n */\n div[level] {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0 0 7px 0;\n }\n div[level] label {\n flex: none;\n margin: 0;\n font-size: 0.68rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n div[level] select {\n flex: 1;\n min-width: 0;\n padding: 3px 5px;\n font: var(--input-field-font, inherit);\n font-size: 0.72rem;\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 /* 팔레트가 잠긴 이유를 말한다. 흐린 단추만 두면 왜 안 눌리는지 모른다 */\n p[full] {\n margin: 6px 0 0 0;\n font-size: 0.72rem;\n line-height: 1.5;\n color: var(--md-sys-color-error);\n }\n\n p[quiet] {\n margin: 0;\n font-size: 0.76rem;\n line-height: 1.6;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n `\n ]\n\n /**\n * 보드 위의 부품들. **씬 모델 그대로 받는다.**\n *\n * 이 패널은 이름과 도형만 읽으므로 어느 쪽 형태로 받아도 화면은 같다. 그래도 씬\n * 모델로 받는 것은 모델러 페이지가 데이터를 **한 벌만** 갖게 하기 위해서다. 여기만\n * FigureSource 로 받으면 페이지가 이 패널을 위해 변환본을 따로 들고 있어야 하고,\n * 그 변환본이 언제 낡는지를 또 관리해야 한다.\n */\n @property({ type: Array }) parts: PartModel[] = []\n /** 디테일 등급. 부품 수 한도가 여기서 결정된다. */\n @property({ type: String }) level: DetailLevel = 'M'\n /** 고른 부품의 자리. 고르지 않았으면 -1. */\n @property({ type: Number }) selected = -1\n\n render() {\n const parts = this.parts ?? []\n const level = this.level\n const limit = PART_LIMIT[level]\n const full = parts.length >= limit\n\n return html`\n <section>\n <h3>${i18next.t('figure.label.shapes')}</h3>\n <div palette>\n ${PRIMITIVE_KINDS.map(\n kind => html`\n <button\n shape\n ?disabled=${full}\n title=${full ? i18next.t('figure.text.part-limit-reached') : i18next.t('figure.text.add-a-shape')}\n @click=${() => this.ask('add-part', { primitive: kind })}\n >\n <md-icon>${SHAPE_ICON[kind]}</md-icon>\n ${i18next.t('figure.label.primitive-' + kind)}\n </button>\n `\n )}\n </div>\n ${full\n ? html`<p full>${i18next.t('figure.text.part-limit-reached-short', { level, limit })}</p>`\n : ''}\n </section>\n\n <section list>\n <h3>\n ${i18next.t('figure.label.parts')}\n <span room ?full=${full} title=${i18next.t('figure.text.limit-comes-from-level')}>\n ${i18next.t('figure.text.n-of-limit', { used: parts.length, limit })}\n </span>\n ${this.renderShowAll(parts)}\n </h3>\n\n <div level>\n <label for=\"detail-level\">${i18next.t('figure.label.detail-level')}</label>\n <select\n id=\"detail-level\"\n title=${i18next.t('figure.text.detail-level-explained')}\n @change=${(e: Event) =>\n this.ask('change-detail-level', { level: (e.target as HTMLSelectElement).value })}\n >\n ${DETAIL_LEVELS.map(\n one => html`\n <option value=${one} ?selected=${one === level}>\n ${one} ${i18next.t('figure.label.detail-' + one)} ·\n ${i18next.t('figure.text.n-parts-at-most', { limit: PART_LIMIT[one] })}\n </option>\n `\n )}\n </select>\n </div>\n\n ${parts.length === 0\n ? html`<p quiet>${i18next.t('figure.text.pick-a-shape-above-to-start')}</p>`\n : html`\n <ol>\n ${parts.map((part, index) => this.renderRow(part, index, parts.length))}\n </ol>\n `}\n </section>\n `\n }\n\n /**\n * 한꺼번에 감추고 한꺼번에 되돌린다.\n *\n * 부품 스물짜리를 들여다볼 때 하나만 남기려면 열아홉을 눌러야 했다. 안쪽에 있는 것을\n * 보려고 겉을 걷어내는 일은 저작 중에 늘 하는 일이다.\n *\n * 무엇을 할지는 **지금 상태가 정한다** — 하나라도 감춰져 있으면 다 되돌리고, 다\n * 보이고 있으면 다 감춘다. 단추 하나로 오가는 것이 두 개보다 낫다.\n */\n private renderShowAll(parts: PartModel[]) {\n if (parts.length === 0) return nothing\n\n const anyHidden = parts.some(part => part.hidden)\n\n return html`\n <button\n eye\n all\n ?off=${!anyHidden}\n title=${i18next.t(anyHidden ? 'figure.text.show-all-parts' : 'figure.text.hide-all-parts')}\n @click=${(e: Event) => {\n e.stopPropagation()\n this.ask('set-all-parts-hidden', { hidden: !anyHidden })\n }}\n >\n <md-icon>${anyHidden ? 'visibility' : 'visibility_off'}</md-icon>\n </button>\n `\n }\n\n private renderRow(part: PartModel, index: number, count: number) {\n const selected = index === this.selected\n const off = !!part.hidden\n\n return html`\n <li ?selected=${selected} ?off=${off} @click=${() => this.ask('select-part', { index })}>\n <button\n eye\n ?off=${off}\n title=${i18next.t(off ? 'figure.text.show-this-part' : 'figure.text.hide-this-part')}\n @click=${(e: Event) => {\n // 눌렀을 때 줄 고르기가 함께 일어나지 않게 막는다 — 가리려던 것이지 고르려던 것이 아니다\n e.stopPropagation()\n this.ask('toggle-part-hidden', { index })\n }}\n >\n <md-icon>${off ? 'visibility_off' : 'visibility'}</md-icon>\n </button>\n <md-icon>${SHAPE_ICON[part.primitive]}</md-icon>\n <span label title=${part.name}>${part.name}</span>\n ${selected ? this.renderOps(index, count) : nothing}\n </li>\n `\n }\n\n /**\n * 고른 줄의 조작.\n *\n * 못 하는 것을 **빼지 않고 흐리게 둔다.** 빼면 남은 단추가 그 자리로 밀려서, 첫\n * 줄과 마지막 줄에서 「지우기」가 다른 자리에 온다 — 연달아 지우다 엉뚱한 것을\n * 누르게 된다.\n *\n * 눌렀을 때 줄 고르기가 다시 일어나지 않게 전파를 멈춘다.\n */\n private renderOps(index: number, count: number) {\n const op = (name: string, icon: string, detail: object, enabled = true) => html`\n <button\n op=${name}\n ?disabled=${!enabled}\n title=${i18next.t('figure.button.' + name)}\n @click=${(e: Event) => {\n e.stopPropagation()\n if (enabled) this.ask(name, detail)\n }}\n >\n <md-icon>${icon}</md-icon>\n </button>\n `\n\n return html`\n <div ops>\n ${op('move-part-up', 'arrow_upward', { index, to: index - 1 }, index > 0)}\n ${op('move-part-down', 'arrow_downward', { index, to: index + 1 }, index < count - 1)}\n ${op('duplicate-part', 'content_copy', { index })} ${op('remove-part', 'delete', { index })}\n </div>\n `\n }\n\n /** 정본을 고치지 않는다 — 무엇을 하겠다는 것만 알린다. */\n private ask(what: string, detail: object) {\n this.dispatchEvent(new CustomEvent(what, { detail, bubbles: true, composed: true }))\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"figure-parts.js","sourceRoot":"","sources":["../../client/modeller/figure-parts.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 } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { ScrollbarStyles } from '@operato/styles'\nimport { DETAIL_LEVELS, PART_LIMIT, PRIMITIVE_KINDS } from '@hatiolab/figure-model'\nimport type { DetailLevel, PrimitiveKind } from '@hatiolab/figure-model'\n\nimport type { PartModel } from './figure-source.js'\n\n/** 도형마다의 아이콘. Material Symbols 이름이다. */\nconst SHAPE_ICON: Record<PrimitiveKind, string> = {\n cube: 'deployed_code',\n wall: 'rectangle',\n cylinder: 'database',\n sphere: 'circle',\n rect: 'crop_square',\n polygon: 'pentagon'\n}\n\n/**\n * 도형 팔레트와 부품 목록.\n *\n * ## 팔레트를 목록 위에 둔다\n *\n * 처음 온 사람은 「무엇을 놓을 수 있나」를 먼저 알아야 한다. 빈 목록 아래에 팔레트를\n * 두면 아무것도 없는 화면에서 다음 동작이 안 보인다.\n *\n * ## 한도를 미리 보여 준다\n *\n * 부품 수 한도는 디테일 등급이 정한다(`PART_LIMIT`). 넘긴 뒤에 판정 패널이\n * 나무라는 것보다, **넣기 전에 몇 개 남았는지** 보이는 것이 낫다.\n *\n * ## 고치지 않는다\n *\n * 이 컴포넌트는 정본을 만지지 않는다. 무엇을 하겠다는 것만 알린다 — 정본 한 벌을\n * 저작면이 갖고, 거기서만 바뀐다.\n */\n@customElement('figure-parts')\nexport class FigureParts extends localize(i18next)(LitElement) {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n min-height: 0;\n overflow: hidden;\n background-color: var(--md-sys-color-surface-container-lowest);\n border-right: 1px solid var(--md-sys-color-outline-variant);\n color: var(--md-sys-color-on-surface);\n font: var(--label-font, inherit);\n /*\n 이 패널의 본문 크기를 여기서 한 번 정한다.\n\n 전에는 규칙마다 따로 적었다. 그러면 빠뜨린 규칙이 본문 기본 크기(1rem)를\n 물려받아 그 줄만 제목처럼 커진다. 오류가 아니라서 화면을 띄워 봐야 안다.\n 이 파일에서만 두 번 났다.\n\n form 요소(input · select · button)는 글꼴을 물려받지 않으므로 각자 적어야 한다.\n 그것 말고는 여기 값을 따른다.\n */\n font-size: 0.76rem;\n }\n\n section {\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n section[list] {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n }\n h3 {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 6px;\n margin: 0 0 6px 0;\n font-size: var(--ui-t-micro, 11px);\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n /*\n 부품 수와 한도.\n\n 전에는 이 숫자가 「도형」 절 제목에 붙어 있었다. 부품 수인데 도형 옆에 있으니\n 「도형 8종 중 2종」으로 읽힌다 — 실제로 그렇게 읽혔다. 세는 대상이 있는 절로\n 옮기고, 한도가 어디서 나온 값인지(디테일 등급)를 함께 적는다.\n */\n span[room] {\n font-size: 0.7rem;\n font-weight: 400;\n letter-spacing: 0;\n text-transform: none;\n }\n span[room][full] {\n color: var(--md-sys-color-error);\n }\n\n /* ── 팔레트 ─────────────────────────────────────────────────────── */\n div[palette] {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 4px;\n }\n button[shape] {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 2px;\n padding: 7px 2px 5px 2px;\n font: var(--label-font, inherit);\n font-size: 0.66rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n cursor: pointer;\n }\n button[shape]:hover:not([disabled]) {\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border-color: transparent;\n }\n button[shape][disabled] {\n opacity: 0.35;\n cursor: default;\n }\n button[shape] md-icon {\n --md-icon-size: 20px;\n }\n\n /* ── 부품 목록 ──────────────────────────────────────────────────── */\n ol {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n li {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 6px;\n border-radius: 6px;\n cursor: pointer;\n }\n li:hover {\n background-color: var(--md-sys-color-surface-container);\n }\n li[selected] {\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n }\n li md-icon {\n --md-icon-size: 16px;\n flex: none;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /*\n 숨김 단추.\n\n 고른 줄에서만 보이는 조작(div[ops])과 달리 **늘 자리를 차지한다.** 무엇이 숨겨져\n 있는지는 목록을 훑을 때 한눈에 보여야 한다 — 눌러 봐야 알면 숨겨 둔 것을 잊는다.\n 다만 켜지지 않은 눈은 줄에 마우스를 올렸을 때만 보인다. 여덟 개 줄에 회색 눈이\n 여덟 개 떠 있으면 목록이 시끄럽다.\n */\n /* 머리줄의 것은 늘 보인다 — 줄에 마우스를 올려야 나타나는 것이 아니다 */\n button[eye][all] {\n margin-left: auto;\n opacity: 0.75;\n }\n button[eye][all]:hover {\n opacity: 1;\n }\n button[eye] {\n display: flex;\n flex: none;\n padding: 1px;\n color: inherit;\n background: none;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n opacity: 0;\n }\n li:hover button[eye],\n li[selected] button[eye],\n button[eye][off] {\n opacity: 1;\n }\n button[eye][off] {\n color: var(--md-sys-color-tertiary);\n }\n button[eye] md-icon {\n --md-icon-size: 15px;\n color: inherit;\n }\n\n /* 숨긴 줄은 목록에서도 옅게 — 미리보기에서 faint 로 보이는 것과 같은 뜻이어야 한다 */\n li[off] span[label],\n li[off] > md-icon {\n opacity: 0.45;\n }\n li[selected] md-icon {\n color: inherit;\n }\n span[label] {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 0.78rem;\n }\n\n /* 조작은 고른 줄에서만 보인다 — 늘 보이면 목록이 시끄럽다 */\n div[ops] {\n display: none;\n gap: 1px;\n }\n li[selected] div[ops] {\n display: flex;\n }\n div[ops] button {\n display: flex;\n padding: 2px;\n color: inherit;\n background: none;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n }\n div[ops] button:hover:not([disabled]) {\n background-color: var(--md-sys-color-surface-container-lowest);\n }\n div[ops] button[disabled] {\n opacity: 0.3;\n cursor: default;\n }\n div[ops] md-icon {\n --md-icon-size: 15px;\n color: inherit;\n }\n\n /*\n 디테일 등급 선택.\n\n 전에는 등급이 \"16 / 20 · L 등급\" 안의 글자 하나로만 있었다. S·M·L 이 무엇인지\n 어디에도 없었고, 한도를 다 쓰면 \"등급을 올리세요\" 라고 해 놓고 올릴 칸이 없었다.\n\n 부품 목록 위에 둔다. 등급이 정하는 것이 바로 아래 목록의 한도이고, 팔레트가\n 잠기는 것도 이 값 때문이다. 결과가 보이는 자리에서 고치게 한다.\n */\n div[level] {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0 0 7px 0;\n }\n div[level] label {\n flex: none;\n margin: 0;\n font-size: 0.68rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n div[level] select {\n flex: 1;\n min-width: 0;\n padding: 3px 5px;\n font: var(--input-field-font, inherit);\n font-size: 0.72rem;\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 /* 팔레트가 잠긴 이유를 말한다. 흐린 단추만 두면 왜 안 눌리는지 모른다 */\n p[full] {\n margin: 6px 0 0 0;\n font-size: 0.72rem;\n line-height: 1.5;\n color: var(--md-sys-color-error);\n }\n\n p[quiet] {\n margin: 0;\n font-size: 0.76rem;\n line-height: 1.6;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n `\n ]\n\n /**\n * 초안 위의 부품들. **씬 모델 그대로 받는다.**\n *\n * 이 패널은 이름과 도형만 읽으므로 어느 쪽 형태로 받아도 화면은 같다. 그래도 씬\n * 모델로 받는 것은 모델러 페이지가 데이터를 **한 벌만** 갖게 하기 위해서다. 여기만\n * FigureSource 로 받으면 페이지가 이 패널을 위해 변환본을 따로 들고 있어야 하고,\n * 그 변환본이 언제 낡는지를 또 관리해야 한다.\n */\n @property({ type: Array }) parts: PartModel[] = []\n /** 디테일 등급. 부품 수 한도가 여기서 결정된다. */\n @property({ type: String }) level: DetailLevel = 'M'\n /** 고른 부품의 자리. 고르지 않았으면 -1. */\n @property({ type: Number }) selected = -1\n\n render() {\n const parts = this.parts ?? []\n const level = this.level\n const limit = PART_LIMIT[level]\n const full = parts.length >= limit\n\n return html`\n <section>\n <h3>${i18next.t('figure.label.shapes')}</h3>\n <div palette>\n ${PRIMITIVE_KINDS.map(\n kind => html`\n <button\n shape\n ?disabled=${full}\n title=${full ? i18next.t('figure.text.part-limit-reached') : i18next.t('figure.text.add-a-shape')}\n @click=${() => this.ask('add-part', { primitive: kind })}\n >\n <md-icon>${SHAPE_ICON[kind]}</md-icon>\n ${i18next.t('figure.label.primitive-' + kind)}\n </button>\n `\n )}\n </div>\n ${full\n ? html`<p full>${i18next.t('figure.text.part-limit-reached-short', { level, limit })}</p>`\n : ''}\n </section>\n\n <section list>\n <h3>\n ${i18next.t('figure.label.parts')}\n <span room ?full=${full} title=${i18next.t('figure.text.limit-comes-from-level')}>\n ${i18next.t('figure.text.n-of-limit', { used: parts.length, limit })}\n </span>\n ${this.renderShowAll(parts)}\n </h3>\n\n <div level>\n <label for=\"detail-level\">${i18next.t('figure.label.detail-level')}</label>\n <select\n id=\"detail-level\"\n title=${i18next.t('figure.text.detail-level-explained')}\n @change=${(e: Event) =>\n this.ask('change-detail-level', { level: (e.target as HTMLSelectElement).value })}\n >\n ${DETAIL_LEVELS.map(\n one => html`\n <option value=${one} ?selected=${one === level}>\n ${one} ${i18next.t('figure.label.detail-' + one)} ·\n ${i18next.t('figure.text.n-parts-at-most', { limit: PART_LIMIT[one] })}\n </option>\n `\n )}\n </select>\n </div>\n\n ${parts.length === 0\n ? html`<p quiet>${i18next.t('figure.text.pick-a-shape-above-to-start')}</p>`\n : html`\n <ol>\n ${parts.map((part, index) => this.renderRow(part, index, parts.length))}\n </ol>\n `}\n </section>\n `\n }\n\n /**\n * 한꺼번에 감추고 한꺼번에 되돌린다.\n *\n * 부품 스물짜리를 들여다볼 때 하나만 남기려면 열아홉을 눌러야 했다. 안쪽에 있는 것을\n * 보려고 겉을 걷어내는 일은 저작 중에 늘 하는 일이다.\n *\n * 무엇을 할지는 **지금 상태가 정한다** — 하나라도 감춰져 있으면 다 되돌리고, 다\n * 보이고 있으면 다 감춘다. 단추 하나로 오가는 것이 두 개보다 낫다.\n */\n private renderShowAll(parts: PartModel[]) {\n if (parts.length === 0) return nothing\n\n const anyHidden = parts.some(part => part.hidden)\n\n return html`\n <button\n eye\n all\n ?off=${!anyHidden}\n title=${i18next.t(anyHidden ? 'figure.text.show-all-parts' : 'figure.text.hide-all-parts')}\n @click=${(e: Event) => {\n e.stopPropagation()\n this.ask('set-all-parts-hidden', { hidden: !anyHidden })\n }}\n >\n <md-icon>${anyHidden ? 'visibility' : 'visibility_off'}</md-icon>\n </button>\n `\n }\n\n private renderRow(part: PartModel, index: number, count: number) {\n const selected = index === this.selected\n const off = !!part.hidden\n\n return html`\n <li ?selected=${selected} ?off=${off} @click=${() => this.ask('select-part', { index })}>\n <button\n eye\n ?off=${off}\n title=${i18next.t(off ? 'figure.text.show-this-part' : 'figure.text.hide-this-part')}\n @click=${(e: Event) => {\n // 눌렀을 때 줄 고르기가 함께 일어나지 않게 막는다 — 가리려던 것이지 고르려던 것이 아니다\n e.stopPropagation()\n this.ask('toggle-part-hidden', { index })\n }}\n >\n <md-icon>${off ? 'visibility_off' : 'visibility'}</md-icon>\n </button>\n <md-icon>${SHAPE_ICON[part.primitive]}</md-icon>\n <span label title=${part.name}>${part.name}</span>\n ${selected ? this.renderOps(index, count) : nothing}\n </li>\n `\n }\n\n /**\n * 고른 줄의 조작.\n *\n * 못 하는 것을 **빼지 않고 흐리게 둔다.** 빼면 남은 단추가 그 자리로 밀려서, 첫\n * 줄과 마지막 줄에서 「지우기」가 다른 자리에 온다 — 연달아 지우다 엉뚱한 것을\n * 누르게 된다.\n *\n * 눌렀을 때 줄 고르기가 다시 일어나지 않게 전파를 멈춘다.\n */\n private renderOps(index: number, count: number) {\n const op = (name: string, icon: string, detail: object, enabled = true) => html`\n <button\n op=${name}\n ?disabled=${!enabled}\n title=${i18next.t('figure.button.' + name)}\n @click=${(e: Event) => {\n e.stopPropagation()\n if (enabled) this.ask(name, detail)\n }}\n >\n <md-icon>${icon}</md-icon>\n </button>\n `\n\n return html`\n <div ops>\n ${op('move-part-up', 'arrow_upward', { index, to: index - 1 }, index > 0)}\n ${op('move-part-down', 'arrow_downward', { index, to: index + 1 }, index < count - 1)}\n ${op('duplicate-part', 'content_copy', { index })} ${op('remove-part', 'delete', { index })}\n </div>\n `\n }\n\n /** 정본을 고치지 않는다 — 무엇을 하겠다는 것만 알린다. */\n private ask(what: string, detail: object) {\n this.dispatchEvent(new CustomEvent(what, { detail, bubbles: true, composed: true }))\n }\n}\n"]}
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import { LitElement } from 'lit';
|
|
2
2
|
import type { FigureSource } from '@hatiolab/figure-model';
|
|
3
|
+
import type { ViewSettings } from './figure-view.js';
|
|
4
|
+
/**
|
|
5
|
+
* 저작 중인 것을 **보드에 놓았을 때**의 모습.
|
|
6
|
+
*
|
|
7
|
+
* ## 편집 캔버스로는 볼 수 없다
|
|
8
|
+
*
|
|
9
|
+
* 편집 캔버스는 부품을 컴포넌트로 늘어놓는다. 그래서 늘 기준 크기다. 그런데 이 형식의
|
|
10
|
+
* 값 절반은 **크기가 달라졌을 때 어떻게 반응하는가**에 있다. 기둥만 늘어나는 시그널타워,
|
|
11
|
+
* 롤러 개수가 느는 컨베이어는 기준 크기에서는 아무것도 보여 주지 못한다.
|
|
12
|
+
*
|
|
13
|
+
* 그래서 여기서는 저작한 것을 실제로 컴파일해 `FigureInstance` 로 세운다. 보드가 하는
|
|
14
|
+
* 일을 그대로 한다 — 청사진을 등록하고, 타입 이름만 적은 컴포넌트를 놓는다.
|
|
15
|
+
*
|
|
16
|
+
* ## 여러 크기를 한 번에 놓는다
|
|
17
|
+
*
|
|
18
|
+
* 하나만 놓으면 "이 크기에서는 이렇다" 밖에 알 수 없다. 기준 크기와 늘린 것을 나란히
|
|
19
|
+
* 놓아야 무엇이 따라 늘고 무엇이 그대로인지 보인다.
|
|
20
|
+
*
|
|
21
|
+
* ## 편집하지 않는다
|
|
22
|
+
*
|
|
23
|
+
* 여기서 집거나 끌 수 없다. 편집은 캔버스에서 한다. 보여 주기만 하는 자리라 씬을
|
|
24
|
+
* 뷰 모드로 세운다.
|
|
25
|
+
*/
|
|
3
26
|
/** 얼마나 키워 보나. */
|
|
4
27
|
export declare const PREVIEW_FACTOR = 2;
|
|
5
28
|
/**
|
|
@@ -22,6 +45,13 @@ export declare class FigurePreview extends FigurePreview_base {
|
|
|
22
45
|
static styles: import("lit").CSSResult;
|
|
23
46
|
/** 저작 중인 것. 저장 형식으로 받는다 — 보드가 소비하는 것이 이 형식이다. */
|
|
24
47
|
source?: FigureSource;
|
|
48
|
+
/**
|
|
49
|
+
* 보기 설정. 저장되지 않는다(`figure-view.ts`).
|
|
50
|
+
*
|
|
51
|
+
* 한동안 이 화면만 조명과 환경을 **하드코딩**했다. 저작자가 환경 탭에서 무엇을 바꿔도
|
|
52
|
+
* 편집 캔버스만 달라지고 여기는 그대로였다 — 두 화면이 같은 것을 다르게 보여 주었다.
|
|
53
|
+
*/
|
|
54
|
+
view: ViewSettings;
|
|
25
55
|
private stage;
|
|
26
56
|
/** 세울 수 없는 사유. 없으면 세운 것이다. */
|
|
27
57
|
private cannot;
|
|
@@ -47,6 +77,15 @@ export declare class FigurePreview extends FigurePreview_base {
|
|
|
47
77
|
* 사라진다.
|
|
48
78
|
*/
|
|
49
79
|
private drive;
|
|
80
|
+
/**
|
|
81
|
+
* 보기 설정이 바뀌면 **다시 세우지 않고 씬에 넣는다** — 두 개만 빼고.
|
|
82
|
+
*
|
|
83
|
+
* 조명·환경·바탕은 씬이 스스로 반응한다(`three-capability` 의 `onchange`). 슬라이더를
|
|
84
|
+
* 끄는 동안 다시 세우면 카메라가 매 프레임 처음으로 돌아간다.
|
|
85
|
+
*
|
|
86
|
+
* 좌표 기준과 천장 높이는 다르다. 그 둘은 컴포넌트의 `zPos` 를 만들 때 쓰였으므로,
|
|
87
|
+
* 이미 세워 둔 것에 넣어 봐야 자리가 안 바뀐다. 그것만 다시 세운다.
|
|
88
|
+
*/
|
|
50
89
|
willUpdate(changed: Map<string, unknown>): void;
|
|
51
90
|
firstUpdated(): void;
|
|
52
91
|
disconnectedCallback(): void;
|
|
@@ -4,6 +4,7 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
|
|
4
4
|
import { i18next, localize } from '@operato/i18n';
|
|
5
5
|
import { compile, validate } from '@hatiolab/figure-model';
|
|
6
6
|
import { create, registerFigure } from '@hatiolab/things-scene';
|
|
7
|
+
import { DEFAULT_VIEW, sceneLook, WORKBENCH } from './figure-view.js';
|
|
7
8
|
/**
|
|
8
9
|
* 저작 중인 것을 **보드에 놓았을 때**의 모습.
|
|
9
10
|
*
|
|
@@ -26,8 +27,6 @@ import { create, registerFigure } from '@hatiolab/things-scene';
|
|
|
26
27
|
* 여기서 집거나 끌 수 없다. 편집은 캔버스에서 한다. 보여 주기만 하는 자리라 씬을
|
|
27
28
|
* 뷰 모드로 세운다.
|
|
28
29
|
*/
|
|
29
|
-
/** 배경. 편집 캔버스와 같은 값을 쓴다 — 갈리면 두 화면이 다른 도구처럼 보인다. */
|
|
30
|
-
const WORKBENCH = '#7c7c7c';
|
|
31
30
|
/** 얼마나 키워 보나. */
|
|
32
31
|
export const PREVIEW_FACTOR = 2;
|
|
33
32
|
/**
|
|
@@ -51,9 +50,55 @@ export const PREVIEW_WAYS = [
|
|
|
51
50
|
{ name: 'y·z', axes: ['y', 'z'] },
|
|
52
51
|
{ name: 'x·y·z', axes: ['x', 'y', 'z'] }
|
|
53
52
|
];
|
|
53
|
+
/**
|
|
54
|
+
* 이 도형의 상자 **밑면**이 씬의 어느 높이에 오나.
|
|
55
|
+
*
|
|
56
|
+
* 배치 기준이 하는 일이 이것 하나다 — 좌표를 뒤집는 것이 아니라 **높이를 정하는 것.**
|
|
57
|
+
* 바닥 기반은 땅에 서고, 천정 기반은 천장에서 제 키만큼 내려온 자리에 매달리고,
|
|
58
|
+
* 중심 기반은 그 사이에 뜬다. `operato-scene` 의 archetype 이 같은 말을 한다
|
|
59
|
+
* (`archetypes.ts`: ceiling — *zPos derived from ceiling minus depth*).
|
|
60
|
+
*/
|
|
61
|
+
function bottomOf(placement, height, ceiling) {
|
|
62
|
+
switch (placement) {
|
|
63
|
+
case 'ceiling':
|
|
64
|
+
return ceiling - height;
|
|
65
|
+
case 'center':
|
|
66
|
+
return (ceiling - height) / 2;
|
|
67
|
+
case 'floor':
|
|
68
|
+
default:
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 밑면 높이를 씬이 받는 `zPos` 로 옮긴다.
|
|
74
|
+
*
|
|
75
|
+
* 씬은 부품 중심을 `zPos + geometricOffsetY` 에 놓고, 그 오프셋이 좌표 기준마다 다르다
|
|
76
|
+
* (`real-object.ts`). 그래서 같은 밑면을 말하는 `zPos` 가 기준마다 다른 수다 — 바닥
|
|
77
|
+
* 기준이면 밑면 그대로, 중심 기준이면 중심, 천장 기준이면 윗면.
|
|
78
|
+
*
|
|
79
|
+
* **여기가 유일하게 좌표 기준을 아는 자리다.** 저작한 값에는 이것이 안 들어간다.
|
|
80
|
+
*/
|
|
81
|
+
function zPosOf(bottom, height, reference) {
|
|
82
|
+
switch (reference) {
|
|
83
|
+
case 'space':
|
|
84
|
+
return bottom + height / 2;
|
|
85
|
+
case 'inverted':
|
|
86
|
+
return bottom + height;
|
|
87
|
+
case 'floor':
|
|
88
|
+
default:
|
|
89
|
+
return bottom;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
54
92
|
let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
55
93
|
constructor() {
|
|
56
94
|
super(...arguments);
|
|
95
|
+
/**
|
|
96
|
+
* 보기 설정. 저장되지 않는다(`figure-view.ts`).
|
|
97
|
+
*
|
|
98
|
+
* 한동안 이 화면만 조명과 환경을 **하드코딩**했다. 저작자가 환경 탭에서 무엇을 바꿔도
|
|
99
|
+
* 편집 캔버스만 달라지고 여기는 그대로였다 — 두 화면이 같은 것을 다르게 보여 주었다.
|
|
100
|
+
*/
|
|
101
|
+
this.view = DEFAULT_VIEW;
|
|
57
102
|
/** 세울 수 없는 사유. 없으면 세운 것이다. */
|
|
58
103
|
this.cannot = '';
|
|
59
104
|
/** 지금 민 clip 값. clip 이름 → 수. **저장되지 않는다.** */
|
|
@@ -201,11 +246,31 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
201
246
|
component.set?.('clips', { ...this.driving });
|
|
202
247
|
}
|
|
203
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* 보기 설정이 바뀌면 **다시 세우지 않고 씬에 넣는다** — 두 개만 빼고.
|
|
251
|
+
*
|
|
252
|
+
* 조명·환경·바탕은 씬이 스스로 반응한다(`three-capability` 의 `onchange`). 슬라이더를
|
|
253
|
+
* 끄는 동안 다시 세우면 카메라가 매 프레임 처음으로 돌아간다.
|
|
254
|
+
*
|
|
255
|
+
* 좌표 기준과 천장 높이는 다르다. 그 둘은 컴포넌트의 `zPos` 를 만들 때 쓰였으므로,
|
|
256
|
+
* 이미 세워 둔 것에 넣어 봐야 자리가 안 바뀐다. 그것만 다시 세운다.
|
|
257
|
+
*/
|
|
204
258
|
willUpdate(changed) {
|
|
205
259
|
if (!this.hasUpdated)
|
|
206
260
|
return;
|
|
207
|
-
if (changed.has('source'))
|
|
261
|
+
if (changed.has('source')) {
|
|
262
|
+
this.standUp();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (!changed.has('view'))
|
|
266
|
+
return;
|
|
267
|
+
const before = changed.get('view');
|
|
268
|
+
const now = this.view ?? DEFAULT_VIEW;
|
|
269
|
+
if (before?.placement !== now.placement || before?.ceilingHeight !== now.ceilingHeight) {
|
|
208
270
|
this.standUp();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
this.scene?.root?.set?.(sceneLook(now));
|
|
209
274
|
}
|
|
210
275
|
firstUpdated() {
|
|
211
276
|
this.standUp();
|
|
@@ -231,7 +296,9 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
231
296
|
this.teardown();
|
|
232
297
|
return;
|
|
233
298
|
}
|
|
234
|
-
const
|
|
299
|
+
const view = this.view ?? DEFAULT_VIEW;
|
|
300
|
+
/* 좌표 기준과 천장 높이가 컴포넌트의 `zPos` 를 만든다 — 서명에 안 넣으면 다시 세워도 그대로 선다. */
|
|
301
|
+
const signature = JSON.stringify([source, view.placement, view.ceilingHeight]);
|
|
235
302
|
if (this.scene && this.standing === signature)
|
|
236
303
|
return;
|
|
237
304
|
const { errors } = validate(source);
|
|
@@ -264,23 +331,16 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
264
331
|
const COLUMNS = 4;
|
|
265
332
|
const cell = { x: base.x * biggest * 1.25, z: base.z * biggest * 1.25 };
|
|
266
333
|
const rows = Math.ceil(PREVIEW_WAYS.length / COLUMNS);
|
|
267
|
-
const board = {
|
|
268
|
-
width: Math.round(cell.x * COLUMNS),
|
|
269
|
-
height: Math.round(cell.z * rows),
|
|
270
|
-
/*
|
|
271
|
-
판의 높이도 가장 큰 인스턴스를 담아야 한다.
|
|
272
|
-
|
|
273
|
-
안 주면 카메라가 평면만 보고 자리를 잡아서, 키가 큰 모델은 밑동만 보인다.
|
|
274
|
-
시그널타워를 2 배로 놓으면 2200 인데 판이 그것을 모르면 받침만 화면에 든다.
|
|
275
|
-
*/
|
|
276
|
-
depth: Math.round(base.y * biggest * 1.1)
|
|
277
|
-
};
|
|
278
334
|
/*
|
|
279
335
|
씬의 낱말이 형식과 다르다 — `width` 가 형식의 x, `height` 가 z, `depth` 가 y 다.
|
|
280
336
|
옮기는 일을 여기 한 줄에 모아 둔다.
|
|
281
337
|
|
|
282
338
|
`depth` 를 늘 적는 이유가 있다. 안 적으면 평면 두 축의 기하평균으로 잡혀서,
|
|
283
339
|
「y 만 키운다」가 x·z 를 안 건드리는데도 높이가 안 오른다.
|
|
340
|
+
|
|
341
|
+
높이는 **배치 기준이 정한다.** 바닥 기반은 땅에서 올라오고, 천정 기반은 천장에
|
|
342
|
+
붙어 내려온다. 여덟을 나란히 세우는 자리라 이것이 눈에 바로 보인다 — 바닥 기반이면
|
|
343
|
+
밑면이, 천정 기반이면 윗면이 한 줄이 된다.
|
|
284
344
|
*/
|
|
285
345
|
const components = PREVIEW_WAYS.map((way, at) => {
|
|
286
346
|
const grow = growth(way);
|
|
@@ -288,6 +348,8 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
288
348
|
const row = Math.floor(at / COLUMNS);
|
|
289
349
|
const width = Math.round(base.x * grow.x);
|
|
290
350
|
const height = Math.round(base.z * grow.z);
|
|
351
|
+
const tall = Math.round(base.y * grow.y);
|
|
352
|
+
const bottom = bottomOf(source.placement, tall, view.ceilingHeight);
|
|
291
353
|
return {
|
|
292
354
|
type: source.type,
|
|
293
355
|
id: `at-${way.name}`,
|
|
@@ -296,22 +358,45 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
296
358
|
top: Math.round(row * cell.z + (cell.z - height) / 2),
|
|
297
359
|
width,
|
|
298
360
|
height,
|
|
299
|
-
depth:
|
|
361
|
+
depth: tall,
|
|
362
|
+
zPos: Math.round(zPosOf(bottom, tall, view.placement))
|
|
300
363
|
};
|
|
301
364
|
});
|
|
365
|
+
const draft = {
|
|
366
|
+
width: Math.round(cell.x * COLUMNS),
|
|
367
|
+
height: Math.round(cell.z * rows),
|
|
368
|
+
/*
|
|
369
|
+
판의 높이도 가장 큰 인스턴스를 담아야 한다.
|
|
370
|
+
|
|
371
|
+
안 주면 카메라가 평면만 보고 자리를 잡아서, 키가 큰 모델은 밑동만 보인다.
|
|
372
|
+
시그널타워를 2 배로 놓으면 2200 인데 판이 그것을 모르면 받침만 화면에 든다.
|
|
373
|
+
|
|
374
|
+
가장 높은 **윗면**으로 잡는다. 천정 기반은 천장 높이에 매달리므로 제 키보다
|
|
375
|
+
훨씬 위에 있다 — 키만 보고 잡으면 천장에 걸린 것이 화면 밖으로 나간다.
|
|
376
|
+
*/
|
|
377
|
+
depth: Math.round(Math.max(...components.map(c => bottomOf(source.placement, c.depth, view.ceilingHeight) + c.depth)) * 1.1)
|
|
378
|
+
};
|
|
302
379
|
this.scene = create({
|
|
303
380
|
target: this.stage,
|
|
304
381
|
model: {
|
|
305
382
|
type: 'model-layer',
|
|
306
|
-
width:
|
|
307
|
-
height:
|
|
308
|
-
depth:
|
|
383
|
+
width: draft.width,
|
|
384
|
+
height: draft.height,
|
|
385
|
+
depth: draft.depth,
|
|
309
386
|
threed: true,
|
|
310
|
-
environment: 'studio',
|
|
311
|
-
grid: true,
|
|
312
387
|
sky: 'color',
|
|
313
|
-
|
|
314
|
-
|
|
388
|
+
/*
|
|
389
|
+
보기 설정을 그대로 싣는다.
|
|
390
|
+
|
|
391
|
+
여기만 조명·환경·바탕을 하드코딩하고 있었다. 저작자가 환경 탭에서 조명을
|
|
392
|
+
올려도 편집 캔버스만 밝아지고 이 화면은 그대로여서, 같은 것을 두 화면이
|
|
393
|
+
다르게 보여 주었다.
|
|
394
|
+
|
|
395
|
+
`sceneLook` 이 좌표 기준과 천장 높이를 빼고 준다 — 그 둘은 씬에 그대로 넣을
|
|
396
|
+
값이 아니라 위에서 `zPos` 를 정하는 데 쓴다. 좌표 기준만 여기 따로 싣는다.
|
|
397
|
+
*/
|
|
398
|
+
...sceneLook(view),
|
|
399
|
+
placement: view.placement,
|
|
315
400
|
pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),
|
|
316
401
|
components
|
|
317
402
|
},
|
|
@@ -339,6 +424,10 @@ __decorate([
|
|
|
339
424
|
property({ type: Object }),
|
|
340
425
|
__metadata("design:type", Object)
|
|
341
426
|
], FigurePreview.prototype, "source", void 0);
|
|
427
|
+
__decorate([
|
|
428
|
+
property({ type: Object }),
|
|
429
|
+
__metadata("design:type", Object)
|
|
430
|
+
], FigurePreview.prototype, "view", void 0);
|
|
342
431
|
__decorate([
|
|
343
432
|
query('div[stage]'),
|
|
344
433
|
__metadata("design:type", HTMLDivElement
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"figure-preview.js","sourceRoot":"","sources":["../../client/modeller/figure-preview.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { compile, validate } from '@hatiolab/figure-model'\nimport type { FigureSource } from '@hatiolab/figure-model'\nimport { create, registerFigure } from '@hatiolab/things-scene'\n\n/**\n * 저작 중인 것을 **보드에 놓았을 때**의 모습.\n *\n * ## 편집 캔버스로는 볼 수 없다\n *\n * 편집 캔버스는 부품을 컴포넌트로 늘어놓는다. 그래서 늘 기준 크기다. 그런데 이 형식의\n * 값 절반은 **크기가 달라졌을 때 어떻게 반응하는가**에 있다. 기둥만 늘어나는 시그널타워,\n * 롤러 개수가 느는 컨베이어는 기준 크기에서는 아무것도 보여 주지 못한다.\n *\n * 그래서 여기서는 저작한 것을 실제로 컴파일해 `FigureInstance` 로 세운다. 보드가 하는\n * 일을 그대로 한다 — 청사진을 등록하고, 타입 이름만 적은 컴포넌트를 놓는다.\n *\n * ## 여러 크기를 한 번에 놓는다\n *\n * 하나만 놓으면 \"이 크기에서는 이렇다\" 밖에 알 수 없다. 기준 크기와 늘린 것을 나란히\n * 놓아야 무엇이 따라 늘고 무엇이 그대로인지 보인다.\n *\n * ## 편집하지 않는다\n *\n * 여기서 집거나 끌 수 없다. 편집은 캔버스에서 한다. 보여 주기만 하는 자리라 씬을\n * 뷰 모드로 세운다.\n */\n\n/** 배경. 편집 캔버스와 같은 값을 쓴다 — 갈리면 두 화면이 다른 도구처럼 보인다. */\nconst WORKBENCH = '#7c7c7c'\n\n/** 얼마나 키워 보나. */\nexport const PREVIEW_FACTOR = 2\n\n/**\n * 어느 축을 키워 놓아 보나 — **일곱 조합 전부.**\n *\n * 1 배 · 2 배 · 3 배를 나란히 놓던 자리다. 그러면 세 축이 늘 같이 늘어나므로, 축마다\n * 따로 걸리는 규칙을 눈으로 볼 수가 없었다. 실제로 못 보고 넘어간 것들이 있다 — 컨베이어의\n * 롤러가 폭을 안 따라가던 것, 문의 폭이 높이를 안 따라가던 것이 다 한 축만 늘렸을 때\n * 드러나는 것이었다.\n *\n * 여덟이다. 첫째가 저작한 크기이고 나머지 일곱이 조합이다. 한 줄로 늘어놓으면 화면에\n * 안 담겨서 **네 칸씩 두 줄**로 놓는다.\n */\nexport const PREVIEW_WAYS: readonly { name: string; axes: readonly ('x' | 'y' | 'z')[] }[] = [\n { name: '기준', axes: [] },\n { name: 'x', axes: ['x'] },\n { name: 'y', axes: ['y'] },\n { name: 'z', axes: ['z'] },\n { name: 'x·y', axes: ['x', 'y'] },\n { name: 'x·z', axes: ['x', 'z'] },\n { name: 'y·z', axes: ['y', 'z'] },\n { name: 'x·y·z', axes: ['x', 'y', 'z'] }\n]\n\ninterface SceneHandle {\n dispose?: () => void\n resize?: () => void\n fit?: (type?: string) => void\n root?: {\n invalidate?: () => void\n fitCameraToModel?: (margin?: number) => void\n /** 세워 둔 인스턴스들. clip 을 몰 때 전부에 같은 값을 준다. */\n components?: { set?: (property: string, value: unknown) => void }[]\n }\n}\n\n@customElement('figure-preview')\nexport class FigurePreview extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n position: relative;\n display: flex;\n overflow: hidden;\n background-color: ${unsafeCSS(WORKBENCH)};\n font: var(--label-font, inherit);\n font-size: 0.76rem;\n }\n\n div[stage] {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n }\n\n /* 어느 것이 몇 배인지 — 나란히 놓아도 표가 없으면 읽을 수 없다 */\n div[legend] {\n position: absolute;\n top: 8px;\n left: 8px;\n display: flex;\n gap: 6px;\n padding: 4px 8px;\n color: rgba(255, 255, 255, 0.9);\n background-color: rgba(28, 28, 30, 0.6);\n border-radius: 6px;\n font-size: 0.72rem;\n }\n\n /*\n Clip 조작. **저작자가 제 애니메이션을 만져 볼 유일한 자리다.**\n\n 보드에서는 트윈이 clip 을 몬다 — 「닙이 40% 열렸다」는 사실이 어디선가 온다. 저작\n 중에는 그 사실을 주는 것이 없어서, 손대지 않으면 hold 인 clip 은 0 에 머문 채\n 영영 안 움직인다. 포크가 안 올라가는 것을 저작자가 「안 되는구나」로 읽게 된다.\n\n **미리보기 것이지 자산의 것이 아니다.** 여기서 민 값은 저장되지 않는다.\n */\n div[clips] {\n position: absolute;\n right: 8px;\n bottom: 8px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n color: rgba(255, 255, 255, 0.92);\n background-color: rgba(28, 28, 30, 0.72);\n border-radius: 8px;\n font-size: 0.72rem;\n }\n div[clips] label {\n display: grid;\n grid-template-columns: auto 96px 34px;\n align-items: center;\n gap: 8px;\n }\n div[clips] span[drive] {\n opacity: 0.6;\n font-size: 0.66rem;\n }\n div[clips] input {\n width: 96px;\n accent-color: var(--md-sys-color-primary);\n }\n div[clips] output {\n font-variant-numeric: tabular-nums;\n text-align: right;\n opacity: 0.85;\n }\n\n /* 세울 수 없는 상태를 빈 화면으로 두지 않는다 — 왜 안 보이는지 말한다 */\n div[cannot] {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--spacing-large, 12px);\n text-align: center;\n line-height: 1.6;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: ${unsafeCSS(WORKBENCH)};\n }\n `\n\n /** 저작 중인 것. 저장 형식으로 받는다 — 보드가 소비하는 것이 이 형식이다. */\n @property({ type: Object }) source?: FigureSource\n\n @query('div[stage]') private stage!: HTMLDivElement\n\n /** 세울 수 없는 사유. 없으면 세운 것이다. */\n @state() private cannot = ''\n\n private scene?: SceneHandle\n /** 지금 세워 둔 것의 값. 같으면 다시 세우지 않는다. */\n private standing?: string\n private watcher?: ResizeObserver\n\n /** 지금 민 clip 값. clip 이름 → 수. **저장되지 않는다.** */\n @state() private driving: Record<string, number> = {}\n\n render() {\n const clips = this.source?.animations ?? []\n\n return html`\n <div stage></div>\n ${this.cannot\n ? html`<div cannot>${this.cannot}</div>`\n : html`<div legend>\n ${PREVIEW_WAYS.map(way => html`<span>${way.name}</span>`)}\n </div>\n ${clips.length > 0 ? this.renderClips(clips) : ''}`}\n `\n }\n\n /**\n * Clip 하나에 손잡이 하나.\n *\n * 수의 뜻이 `drive` 에 따라 다르므로 그것을 이름 옆에 적는다. `loop` 는 배속이라 0 이\n * 멈춤이고, `hold` 는 0~1 위치라 0 이 처음이다. 둘을 같은 눈금으로 두면 「1」이 무슨\n * 뜻인지 매번 헷갈린다.\n */\n private renderClips(clips: NonNullable<FigureSource['animations']>) {\n return html`\n <div clips>\n ${clips.map(clip => {\n const loop = (clip.drive ?? 'loop') === 'loop'\n const value = this.driving[clip.name] ?? (loop ? 1 : 0)\n\n return html`\n <label>\n <span>${clip.name} <span drive>${loop ? '배속' : '위치'}</span></span>\n <input\n type=\"range\"\n min=\"0\"\n max=${loop ? 3 : 1}\n step=\"0.05\"\n .value=${String(value)}\n @input=${(e: Event) => this.drive(clip.name, Number((e.target as HTMLInputElement).value))}\n />\n <output>${value.toFixed(2)}</output>\n </label>\n `\n })}\n </div>\n `\n }\n\n /**\n * 세워 둔 인스턴스 전부에 같은 값을 준다.\n *\n * 하나만 몰면 세 배율이 서로 다른 자세로 서서, 크기 반응을 보려고 나란히 놓은 뜻이\n * 사라진다.\n */\n private drive(name: string, value: number): void {\n this.driving = { ...this.driving, [name]: value }\n\n for (const component of this.scene?.root?.components ?? []) {\n component.set?.('clips', { ...this.driving })\n }\n }\n\n willUpdate(changed: Map<string, unknown>) {\n if (!this.hasUpdated) return\n if (changed.has('source')) this.standUp()\n }\n\n firstUpdated() {\n this.standUp()\n this.watcher = new ResizeObserver(() => this.scene?.resize?.())\n this.watcher.observe(this)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.watcher?.disconnect()\n this.watcher = undefined\n this.teardown()\n }\n\n /**\n * 저작한 것을 컴파일해 세운다.\n *\n * **형식을 어겼으면 세우지 않는다.** 반쯤 세운 것을 보여 주면 무엇이 잘못됐는지 알 수\n * 없다. 판정 패널이 이미 사유를 말하고 있으므로 여기서는 세울 수 없다고만 한다.\n */\n private standUp() {\n const source = this.source\n if (!source || source.parts.length === 0) {\n this.cannot = i18next.t('figure.text.preview-needs-a-part')\n this.teardown()\n return\n }\n\n const signature = JSON.stringify(source)\n if (this.scene && this.standing === signature) return\n\n const { errors } = validate(source)\n if (errors.length > 0) {\n this.cannot = i18next.t('figure.text.preview-blocked-by-errors')\n this.teardown()\n return\n }\n\n this.teardown()\n // 표본이 바뀌면 민 값도 처음으로 돌린다 — 다른 자산의 clip 이름이 남으면 안 된다.\n this.driving = {}\n\n try {\n registerFigure(compile(source) as never)\n\n /*\n 보드가 하는 일을 그대로 한다. 컴포넌트에는 타입 이름과 자리·크기만 적는다 —\n 청사진도 원본도 보드에 들어가지 않는다.\n\n 가장 큰 것이 들어갈 만큼 판을 잡고, 세로로 줄을 나눠 나란히 놓는다.\n */\n const { base } = source\n const biggest = PREVIEW_FACTOR\n /** 이 조합에서 축별 배율. 조합에 든 축만 키운다. */\n const growth = (way: (typeof PREVIEW_WAYS)[number]) => ({\n x: way.axes.includes('x') ? PREVIEW_FACTOR : 1,\n y: way.axes.includes('y') ? PREVIEW_FACTOR : 1,\n z: way.axes.includes('z') ? PREVIEW_FACTOR : 1\n })\n // 여백을 넉넉히 두면 화면에서 형상이 작아진다. 가장 큰 것이 겨우 들어갈 만큼만 잡는다.\n /** 네 칸씩 두 줄. */\n const COLUMNS = 4\n const cell = { x: base.x * biggest * 1.25, z: base.z * biggest * 1.25 }\n const rows = Math.ceil(PREVIEW_WAYS.length / COLUMNS)\n const board = {\n width: Math.round(cell.x * COLUMNS),\n height: Math.round(cell.z * rows),\n /*\n 판의 높이도 가장 큰 인스턴스를 담아야 한다.\n\n 안 주면 카메라가 평면만 보고 자리를 잡아서, 키가 큰 모델은 밑동만 보인다.\n 시그널타워를 2 배로 놓으면 2200 인데 판이 그것을 모르면 받침만 화면에 든다.\n */\n depth: Math.round(base.y * biggest * 1.1)\n }\n\n /*\n 씬의 낱말이 형식과 다르다 — `width` 가 형식의 x, `height` 가 z, `depth` 가 y 다.\n 옮기는 일을 여기 한 줄에 모아 둔다.\n\n `depth` 를 늘 적는 이유가 있다. 안 적으면 평면 두 축의 기하평균으로 잡혀서,\n 「y 만 키운다」가 x·z 를 안 건드리는데도 높이가 안 오른다.\n */\n const components = PREVIEW_WAYS.map((way, at) => {\n const grow = growth(way)\n const column = at % COLUMNS\n const row = Math.floor(at / COLUMNS)\n const width = Math.round(base.x * grow.x)\n const height = Math.round(base.z * grow.z)\n\n return {\n type: source.type,\n id: `at-${way.name}`,\n // 칸 가운데에 놓는다 — 크기가 저마다 달라 왼쪽에 맞추면 줄이 안 읽힌다.\n left: Math.round(column * cell.x + (cell.x - width) / 2),\n top: Math.round(row * cell.z + (cell.z - height) / 2),\n width,\n height,\n depth: Math.round(base.y * grow.y)\n }\n })\n\n this.scene = create({\n target: this.stage,\n model: {\n type: 'model-layer',\n width: board.width,\n height: board.height,\n depth: board.depth,\n threed: true,\n environment: 'studio',\n grid: true,\n sky: 'color',\n skyColor: WORKBENCH,\n floor: false,\n pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),\n components\n },\n // 보기만 하는 자리다. 집거나 끌 수 없어야 한다.\n mode: 0\n }) as SceneHandle\n\n this.standing = signature\n this.scene.resize?.()\n this.scene.fit?.('ratio')\n this.scene.root?.fitCameraToModel?.()\n this.cannot = ''\n } catch (e) {\n // 원인을 지어내지 않는다 — 던진 말을 그대로 보여 준다.\n this.cannot = (e as Error).message\n }\n }\n\n private teardown() {\n this.scene?.dispose?.()\n this.scene = undefined\n this.standing = undefined\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"figure-preview.js","sourceRoot":"","sources":["../../client/modeller/figure-preview.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { compile, validate } from '@hatiolab/figure-model'\nimport type { FigureSource } from '@hatiolab/figure-model'\nimport { create, registerFigure } from '@hatiolab/things-scene'\n\nimport { DEFAULT_VIEW, sceneLook, WORKBENCH } from './figure-view.js'\nimport type { ViewSettings } from './figure-view.js'\n\n/**\n * 저작 중인 것을 **보드에 놓았을 때**의 모습.\n *\n * ## 편집 캔버스로는 볼 수 없다\n *\n * 편집 캔버스는 부품을 컴포넌트로 늘어놓는다. 그래서 늘 기준 크기다. 그런데 이 형식의\n * 값 절반은 **크기가 달라졌을 때 어떻게 반응하는가**에 있다. 기둥만 늘어나는 시그널타워,\n * 롤러 개수가 느는 컨베이어는 기준 크기에서는 아무것도 보여 주지 못한다.\n *\n * 그래서 여기서는 저작한 것을 실제로 컴파일해 `FigureInstance` 로 세운다. 보드가 하는\n * 일을 그대로 한다 — 청사진을 등록하고, 타입 이름만 적은 컴포넌트를 놓는다.\n *\n * ## 여러 크기를 한 번에 놓는다\n *\n * 하나만 놓으면 \"이 크기에서는 이렇다\" 밖에 알 수 없다. 기준 크기와 늘린 것을 나란히\n * 놓아야 무엇이 따라 늘고 무엇이 그대로인지 보인다.\n *\n * ## 편집하지 않는다\n *\n * 여기서 집거나 끌 수 없다. 편집은 캔버스에서 한다. 보여 주기만 하는 자리라 씬을\n * 뷰 모드로 세운다.\n */\n\n/** 얼마나 키워 보나. */\nexport const PREVIEW_FACTOR = 2\n\n/**\n * 어느 축을 키워 놓아 보나 — **일곱 조합 전부.**\n *\n * 1 배 · 2 배 · 3 배를 나란히 놓던 자리다. 그러면 세 축이 늘 같이 늘어나므로, 축마다\n * 따로 걸리는 규칙을 눈으로 볼 수가 없었다. 실제로 못 보고 넘어간 것들이 있다 — 컨베이어의\n * 롤러가 폭을 안 따라가던 것, 문의 폭이 높이를 안 따라가던 것이 다 한 축만 늘렸을 때\n * 드러나는 것이었다.\n *\n * 여덟이다. 첫째가 저작한 크기이고 나머지 일곱이 조합이다. 한 줄로 늘어놓으면 화면에\n * 안 담겨서 **네 칸씩 두 줄**로 놓는다.\n */\nexport const PREVIEW_WAYS: readonly { name: string; axes: readonly ('x' | 'y' | 'z')[] }[] = [\n { name: '기준', axes: [] },\n { name: 'x', axes: ['x'] },\n { name: 'y', axes: ['y'] },\n { name: 'z', axes: ['z'] },\n { name: 'x·y', axes: ['x', 'y'] },\n { name: 'x·z', axes: ['x', 'z'] },\n { name: 'y·z', axes: ['y', 'z'] },\n { name: 'x·y·z', axes: ['x', 'y', 'z'] }\n]\n\n/**\n * 이 도형의 상자 **밑면**이 씬의 어느 높이에 오나.\n *\n * 배치 기준이 하는 일이 이것 하나다 — 좌표를 뒤집는 것이 아니라 **높이를 정하는 것.**\n * 바닥 기반은 땅에 서고, 천정 기반은 천장에서 제 키만큼 내려온 자리에 매달리고,\n * 중심 기반은 그 사이에 뜬다. `operato-scene` 의 archetype 이 같은 말을 한다\n * (`archetypes.ts`: ceiling — *zPos derived from ceiling minus depth*).\n */\nfunction bottomOf(placement: FigureSource['placement'], height: number, ceiling: number): number {\n switch (placement) {\n case 'ceiling':\n return ceiling - height\n case 'center':\n return (ceiling - height) / 2\n case 'floor':\n default:\n return 0\n }\n}\n\n/**\n * 밑면 높이를 씬이 받는 `zPos` 로 옮긴다.\n *\n * 씬은 부품 중심을 `zPos + geometricOffsetY` 에 놓고, 그 오프셋이 좌표 기준마다 다르다\n * (`real-object.ts`). 그래서 같은 밑면을 말하는 `zPos` 가 기준마다 다른 수다 — 바닥\n * 기준이면 밑면 그대로, 중심 기준이면 중심, 천장 기준이면 윗면.\n *\n * **여기가 유일하게 좌표 기준을 아는 자리다.** 저작한 값에는 이것이 안 들어간다.\n */\nfunction zPosOf(bottom: number, height: number, reference: ViewSettings['placement']): number {\n switch (reference) {\n case 'space':\n return bottom + height / 2\n case 'inverted':\n return bottom + height\n case 'floor':\n default:\n return bottom\n }\n}\n\ninterface SceneHandle {\n dispose?: () => void\n resize?: () => void\n fit?: (type?: string) => void\n root?: {\n invalidate?: () => void\n fitCameraToModel?: (margin?: number) => void\n /** 보기 설정을 넣는 자리. 씬이 스스로 반응한다. */\n set?: (patch: Record<string, unknown>) => void\n /** 세워 둔 인스턴스들. clip 을 몰 때 전부에 같은 값을 준다. */\n components?: { set?: (property: string, value: unknown) => void }[]\n }\n}\n\n@customElement('figure-preview')\nexport class FigurePreview extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n position: relative;\n display: flex;\n overflow: hidden;\n background-color: ${unsafeCSS(WORKBENCH)};\n font: var(--label-font, inherit);\n font-size: 0.76rem;\n }\n\n div[stage] {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n }\n\n /* 어느 것이 몇 배인지 — 나란히 놓아도 표가 없으면 읽을 수 없다 */\n div[legend] {\n position: absolute;\n top: 8px;\n left: 8px;\n display: flex;\n gap: 6px;\n padding: 4px 8px;\n color: rgba(255, 255, 255, 0.9);\n background-color: rgba(28, 28, 30, 0.6);\n border-radius: 6px;\n font-size: 0.72rem;\n }\n\n /*\n Clip 조작. **저작자가 제 애니메이션을 만져 볼 유일한 자리다.**\n\n 보드에서는 트윈이 clip 을 몬다 — 「닙이 40% 열렸다」는 사실이 어디선가 온다. 저작\n 중에는 그 사실을 주는 것이 없어서, 손대지 않으면 hold 인 clip 은 0 에 머문 채\n 영영 안 움직인다. 포크가 안 올라가는 것을 저작자가 「안 되는구나」로 읽게 된다.\n\n **미리보기 것이지 자산의 것이 아니다.** 여기서 민 값은 저장되지 않는다.\n */\n div[clips] {\n position: absolute;\n right: 8px;\n bottom: 8px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n color: rgba(255, 255, 255, 0.92);\n background-color: rgba(28, 28, 30, 0.72);\n border-radius: 8px;\n font-size: 0.72rem;\n }\n div[clips] label {\n display: grid;\n grid-template-columns: auto 96px 34px;\n align-items: center;\n gap: 8px;\n }\n div[clips] span[drive] {\n opacity: 0.6;\n font-size: 0.66rem;\n }\n div[clips] input {\n width: 96px;\n accent-color: var(--md-sys-color-primary);\n }\n div[clips] output {\n font-variant-numeric: tabular-nums;\n text-align: right;\n opacity: 0.85;\n }\n\n /* 세울 수 없는 상태를 빈 화면으로 두지 않는다 — 왜 안 보이는지 말한다 */\n div[cannot] {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--spacing-large, 12px);\n text-align: center;\n line-height: 1.6;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: ${unsafeCSS(WORKBENCH)};\n }\n `\n\n /** 저작 중인 것. 저장 형식으로 받는다 — 보드가 소비하는 것이 이 형식이다. */\n @property({ type: Object }) source?: FigureSource\n\n /**\n * 보기 설정. 저장되지 않는다(`figure-view.ts`).\n *\n * 한동안 이 화면만 조명과 환경을 **하드코딩**했다. 저작자가 환경 탭에서 무엇을 바꿔도\n * 편집 캔버스만 달라지고 여기는 그대로였다 — 두 화면이 같은 것을 다르게 보여 주었다.\n */\n @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW\n\n @query('div[stage]') private stage!: HTMLDivElement\n\n /** 세울 수 없는 사유. 없으면 세운 것이다. */\n @state() private cannot = ''\n\n private scene?: SceneHandle\n /** 지금 세워 둔 것의 값. 같으면 다시 세우지 않는다. */\n private standing?: string\n private watcher?: ResizeObserver\n\n /** 지금 민 clip 값. clip 이름 → 수. **저장되지 않는다.** */\n @state() private driving: Record<string, number> = {}\n\n render() {\n const clips = this.source?.animations ?? []\n\n return html`\n <div stage></div>\n ${this.cannot\n ? html`<div cannot>${this.cannot}</div>`\n : html`<div legend>\n ${PREVIEW_WAYS.map(way => html`<span>${way.name}</span>`)}\n </div>\n ${clips.length > 0 ? this.renderClips(clips) : ''}`}\n `\n }\n\n /**\n * Clip 하나에 손잡이 하나.\n *\n * 수의 뜻이 `drive` 에 따라 다르므로 그것을 이름 옆에 적는다. `loop` 는 배속이라 0 이\n * 멈춤이고, `hold` 는 0~1 위치라 0 이 처음이다. 둘을 같은 눈금으로 두면 「1」이 무슨\n * 뜻인지 매번 헷갈린다.\n */\n private renderClips(clips: NonNullable<FigureSource['animations']>) {\n return html`\n <div clips>\n ${clips.map(clip => {\n const loop = (clip.drive ?? 'loop') === 'loop'\n const value = this.driving[clip.name] ?? (loop ? 1 : 0)\n\n return html`\n <label>\n <span>${clip.name} <span drive>${loop ? '배속' : '위치'}</span></span>\n <input\n type=\"range\"\n min=\"0\"\n max=${loop ? 3 : 1}\n step=\"0.05\"\n .value=${String(value)}\n @input=${(e: Event) => this.drive(clip.name, Number((e.target as HTMLInputElement).value))}\n />\n <output>${value.toFixed(2)}</output>\n </label>\n `\n })}\n </div>\n `\n }\n\n /**\n * 세워 둔 인스턴스 전부에 같은 값을 준다.\n *\n * 하나만 몰면 세 배율이 서로 다른 자세로 서서, 크기 반응을 보려고 나란히 놓은 뜻이\n * 사라진다.\n */\n private drive(name: string, value: number): void {\n this.driving = { ...this.driving, [name]: value }\n\n for (const component of this.scene?.root?.components ?? []) {\n component.set?.('clips', { ...this.driving })\n }\n }\n\n /**\n * 보기 설정이 바뀌면 **다시 세우지 않고 씬에 넣는다** — 두 개만 빼고.\n *\n * 조명·환경·바탕은 씬이 스스로 반응한다(`three-capability` 의 `onchange`). 슬라이더를\n * 끄는 동안 다시 세우면 카메라가 매 프레임 처음으로 돌아간다.\n *\n * 좌표 기준과 천장 높이는 다르다. 그 둘은 컴포넌트의 `zPos` 를 만들 때 쓰였으므로,\n * 이미 세워 둔 것에 넣어 봐야 자리가 안 바뀐다. 그것만 다시 세운다.\n */\n willUpdate(changed: Map<string, unknown>) {\n if (!this.hasUpdated) return\n\n if (changed.has('source')) {\n this.standUp()\n return\n }\n\n if (!changed.has('view')) return\n\n const before = changed.get('view') as ViewSettings | undefined\n const now = this.view ?? DEFAULT_VIEW\n if (before?.placement !== now.placement || before?.ceilingHeight !== now.ceilingHeight) {\n this.standUp()\n return\n }\n\n this.scene?.root?.set?.(sceneLook(now))\n }\n\n firstUpdated() {\n this.standUp()\n this.watcher = new ResizeObserver(() => this.scene?.resize?.())\n this.watcher.observe(this)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.watcher?.disconnect()\n this.watcher = undefined\n this.teardown()\n }\n\n /**\n * 저작한 것을 컴파일해 세운다.\n *\n * **형식을 어겼으면 세우지 않는다.** 반쯤 세운 것을 보여 주면 무엇이 잘못됐는지 알 수\n * 없다. 판정 패널이 이미 사유를 말하고 있으므로 여기서는 세울 수 없다고만 한다.\n */\n private standUp() {\n const source = this.source\n if (!source || source.parts.length === 0) {\n this.cannot = i18next.t('figure.text.preview-needs-a-part')\n this.teardown()\n return\n }\n\n const view = this.view ?? DEFAULT_VIEW\n /* 좌표 기준과 천장 높이가 컴포넌트의 `zPos` 를 만든다 — 서명에 안 넣으면 다시 세워도 그대로 선다. */\n const signature = JSON.stringify([source, view.placement, view.ceilingHeight])\n if (this.scene && this.standing === signature) return\n\n const { errors } = validate(source)\n if (errors.length > 0) {\n this.cannot = i18next.t('figure.text.preview-blocked-by-errors')\n this.teardown()\n return\n }\n\n this.teardown()\n // 표본이 바뀌면 민 값도 처음으로 돌린다 — 다른 자산의 clip 이름이 남으면 안 된다.\n this.driving = {}\n\n try {\n registerFigure(compile(source) as never)\n\n /*\n 보드가 하는 일을 그대로 한다. 컴포넌트에는 타입 이름과 자리·크기만 적는다 —\n 청사진도 원본도 보드에 들어가지 않는다.\n\n 가장 큰 것이 들어갈 만큼 판을 잡고, 세로로 줄을 나눠 나란히 놓는다.\n */\n const { base } = source\n const biggest = PREVIEW_FACTOR\n /** 이 조합에서 축별 배율. 조합에 든 축만 키운다. */\n const growth = (way: (typeof PREVIEW_WAYS)[number]) => ({\n x: way.axes.includes('x') ? PREVIEW_FACTOR : 1,\n y: way.axes.includes('y') ? PREVIEW_FACTOR : 1,\n z: way.axes.includes('z') ? PREVIEW_FACTOR : 1\n })\n // 여백을 넉넉히 두면 화면에서 형상이 작아진다. 가장 큰 것이 겨우 들어갈 만큼만 잡는다.\n /** 네 칸씩 두 줄. */\n const COLUMNS = 4\n const cell = { x: base.x * biggest * 1.25, z: base.z * biggest * 1.25 }\n const rows = Math.ceil(PREVIEW_WAYS.length / COLUMNS)\n /*\n 씬의 낱말이 형식과 다르다 — `width` 가 형식의 x, `height` 가 z, `depth` 가 y 다.\n 옮기는 일을 여기 한 줄에 모아 둔다.\n\n `depth` 를 늘 적는 이유가 있다. 안 적으면 평면 두 축의 기하평균으로 잡혀서,\n 「y 만 키운다」가 x·z 를 안 건드리는데도 높이가 안 오른다.\n\n 높이는 **배치 기준이 정한다.** 바닥 기반은 땅에서 올라오고, 천정 기반은 천장에\n 붙어 내려온다. 여덟을 나란히 세우는 자리라 이것이 눈에 바로 보인다 — 바닥 기반이면\n 밑면이, 천정 기반이면 윗면이 한 줄이 된다.\n */\n const components = PREVIEW_WAYS.map((way, at) => {\n const grow = growth(way)\n const column = at % COLUMNS\n const row = Math.floor(at / COLUMNS)\n const width = Math.round(base.x * grow.x)\n const height = Math.round(base.z * grow.z)\n const tall = Math.round(base.y * grow.y)\n const bottom = bottomOf(source.placement, tall, view.ceilingHeight)\n\n return {\n type: source.type,\n id: `at-${way.name}`,\n // 칸 가운데에 놓는다 — 크기가 저마다 달라 왼쪽에 맞추면 줄이 안 읽힌다.\n left: Math.round(column * cell.x + (cell.x - width) / 2),\n top: Math.round(row * cell.z + (cell.z - height) / 2),\n width,\n height,\n depth: tall,\n zPos: Math.round(zPosOf(bottom, tall, view.placement))\n }\n })\n\n const draft = {\n width: Math.round(cell.x * COLUMNS),\n height: Math.round(cell.z * rows),\n /*\n 판의 높이도 가장 큰 인스턴스를 담아야 한다.\n\n 안 주면 카메라가 평면만 보고 자리를 잡아서, 키가 큰 모델은 밑동만 보인다.\n 시그널타워를 2 배로 놓으면 2200 인데 판이 그것을 모르면 받침만 화면에 든다.\n\n 가장 높은 **윗면**으로 잡는다. 천정 기반은 천장 높이에 매달리므로 제 키보다\n 훨씬 위에 있다 — 키만 보고 잡으면 천장에 걸린 것이 화면 밖으로 나간다.\n */\n depth: Math.round(\n Math.max(...components.map(c => bottomOf(source.placement, c.depth, view.ceilingHeight) + c.depth)) * 1.1\n )\n }\n\n this.scene = create({\n target: this.stage,\n model: {\n type: 'model-layer',\n width: draft.width,\n height: draft.height,\n depth: draft.depth,\n threed: true,\n sky: 'color',\n /*\n 보기 설정을 그대로 싣는다.\n\n 여기만 조명·환경·바탕을 하드코딩하고 있었다. 저작자가 환경 탭에서 조명을\n 올려도 편집 캔버스만 밝아지고 이 화면은 그대로여서, 같은 것을 두 화면이\n 다르게 보여 주었다.\n\n `sceneLook` 이 좌표 기준과 천장 높이를 빼고 준다 — 그 둘은 씬에 그대로 넣을\n 값이 아니라 위에서 `zPos` 를 정하는 데 쓴다. 좌표 기준만 여기 따로 싣는다.\n */\n ...sceneLook(view),\n placement: view.placement,\n pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),\n components\n },\n // 보기만 하는 자리다. 집거나 끌 수 없어야 한다.\n mode: 0\n }) as SceneHandle\n\n this.standing = signature\n this.scene.resize?.()\n this.scene.fit?.('ratio')\n this.scene.root?.fitCameraToModel?.()\n this.cannot = ''\n } catch (e) {\n // 원인을 지어내지 않는다 — 던진 말을 그대로 보여 준다.\n this.cannot = (e as Error).message\n }\n }\n\n private teardown() {\n this.scene?.dispose?.()\n this.scene = undefined\n this.standing = undefined\n }\n}\n"]}
|
|
@@ -29,6 +29,15 @@ export declare class FigureReport extends FigureReport_base {
|
|
|
29
29
|
/** 추이 그림을 제 크기로 펼쳤나. */
|
|
30
30
|
private expanded;
|
|
31
31
|
private errors;
|
|
32
|
+
/**
|
|
33
|
+
* 형식이 권하지 않는 것 — **그중 둘은 발행을 막는다.**
|
|
34
|
+
*
|
|
35
|
+
* 등급을 형식에서 받는다(`blocksRelease`). 화면이 제 목록을 들면 형식에 코드가 하나 늘 때
|
|
36
|
+
* 조용히 「알림」으로 그려지고, 저작자는 발행이 막힌 것을 **눌러 보고 나서야** 안다.
|
|
37
|
+
*
|
|
38
|
+
* `actual` 은 얼마나 넘쳤나다. 「상자를 벗어난다」만으로는 2mm 인지 250mm 인지 모르고,
|
|
39
|
+
* 그 둘은 고치는 품이 다르다.
|
|
40
|
+
*/
|
|
32
41
|
private violations;
|
|
33
42
|
/**
|
|
34
43
|
* 크기를 바꿔 봤을 때 갈라지는가 — **발행을 막는 것**이다.
|
|
@@ -5,7 +5,7 @@ import { customElement, property, state } from 'lit/decorators.js';
|
|
|
5
5
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
6
6
|
import { i18next, localize } from '@operato/i18n';
|
|
7
7
|
import { ScrollbarStyles } from '@operato/styles';
|
|
8
|
-
import { compile, costCurveSvg, costOf, scoreOf, sizingReport, validate } from '@hatiolab/figure-model';
|
|
8
|
+
import { blocksRelease, compile, costCurveSvg, costOf, scoreOf, sizingReport, validate } from '@hatiolab/figure-model';
|
|
9
9
|
import { capacityOf } from './scene-budget.js';
|
|
10
10
|
import { maturityOf } from './maturity.js';
|
|
11
11
|
/**
|
|
@@ -28,6 +28,15 @@ let FigureReport = class FigureReport extends localize(i18next)(LitElement) {
|
|
|
28
28
|
/** 추이 그림을 제 크기로 펼쳤나. */
|
|
29
29
|
this.expanded = false;
|
|
30
30
|
this.errors = [];
|
|
31
|
+
/**
|
|
32
|
+
* 형식이 권하지 않는 것 — **그중 둘은 발행을 막는다.**
|
|
33
|
+
*
|
|
34
|
+
* 등급을 형식에서 받는다(`blocksRelease`). 화면이 제 목록을 들면 형식에 코드가 하나 늘 때
|
|
35
|
+
* 조용히 「알림」으로 그려지고, 저작자는 발행이 막힌 것을 **눌러 보고 나서야** 안다.
|
|
36
|
+
*
|
|
37
|
+
* `actual` 은 얼마나 넘쳤나다. 「상자를 벗어난다」만으로는 2mm 인지 250mm 인지 모르고,
|
|
38
|
+
* 그 둘은 고치는 품이 다르다.
|
|
39
|
+
*/
|
|
31
40
|
this.violations = [];
|
|
32
41
|
/**
|
|
33
42
|
* 크기를 바꿔 봤을 때 갈라지는가 — **발행을 막는 것**이다.
|
|
@@ -817,9 +826,13 @@ let FigureReport = class FigureReport extends localize(i18next)(LitElement) {
|
|
|
817
826
|
</li>
|
|
818
827
|
`)}
|
|
819
828
|
${this.violations.map(v => html `
|
|
820
|
-
<li violation>
|
|
821
|
-
<md-icon
|
|
822
|
-
<span
|
|
829
|
+
<li ?violation=${!v.blocking}>
|
|
830
|
+
<md-icon>${v.blocking ? 'block' : 'info'}</md-icon>
|
|
831
|
+
<span>
|
|
832
|
+
${v.message}${v.blocking && v.actual !== undefined
|
|
833
|
+
? html ` <code at>${v.actual}</code>`
|
|
834
|
+
: ''}
|
|
835
|
+
</span>
|
|
823
836
|
</li>
|
|
824
837
|
`)}
|
|
825
838
|
</ul>
|
|
@@ -858,7 +871,12 @@ let FigureReport = class FigureReport extends localize(i18next)(LitElement) {
|
|
|
858
871
|
}
|
|
859
872
|
const result = validate(source);
|
|
860
873
|
this.errors = result.errors.map(e => ({ code: e.code, message: e.message, at: e.path }));
|
|
861
|
-
this.violations = result.violations.map(v => ({
|
|
874
|
+
this.violations = result.violations.map(v => ({
|
|
875
|
+
code: v.code,
|
|
876
|
+
message: v.message,
|
|
877
|
+
blocking: blocksRelease(v.code),
|
|
878
|
+
actual: v.actual
|
|
879
|
+
}));
|
|
862
880
|
if (this.errors.length > 0) {
|
|
863
881
|
// 형식을 어겼으면 컴파일이 던진다. 셈을 비우고 오류만 보여 준다.
|
|
864
882
|
this.cost = undefined;
|