@things-factory/figure-ui 10.1.22 → 10.1.23

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 (47) hide show
  1. package/client/modeller/figure-canvas.ts +5 -5
  2. package/client/modeller/figure-inspector.ts +2 -2
  3. package/client/modeller/figure-preview.ts +7 -7
  4. package/client/modeller/figure-settings.ts +4 -4
  5. package/client/modeller/figure-thumbnail.ts +1 -1
  6. package/client/modeller/figure-view.ts +6 -6
  7. package/client/modeller/part-edits.ts +1 -1
  8. package/client/modeller/scene-budget.ts +1 -1
  9. package/client/pages/figure-list-page.ts +1 -1
  10. package/client/pages/figure-modeller-page.ts +4 -4
  11. package/client/types.ts +2 -2
  12. package/dist-client/modeller/figure-canvas.d.ts +1 -1
  13. package/dist-client/modeller/figure-canvas.js +5 -5
  14. package/dist-client/modeller/figure-canvas.js.map +1 -1
  15. package/dist-client/modeller/figure-inspector.d.ts +1 -1
  16. package/dist-client/modeller/figure-inspector.js +2 -2
  17. package/dist-client/modeller/figure-inspector.js.map +1 -1
  18. package/dist-client/modeller/figure-preview.d.ts +3 -3
  19. package/dist-client/modeller/figure-preview.js +5 -5
  20. package/dist-client/modeller/figure-preview.js.map +1 -1
  21. package/dist-client/modeller/figure-settings.js +4 -4
  22. package/dist-client/modeller/figure-settings.js.map +1 -1
  23. package/dist-client/modeller/figure-thumbnail.js +1 -1
  24. package/dist-client/modeller/figure-thumbnail.js.map +1 -1
  25. package/dist-client/modeller/figure-view.d.ts +6 -6
  26. package/dist-client/modeller/figure-view.js.map +1 -1
  27. package/dist-client/modeller/part-edits.js +1 -1
  28. package/dist-client/modeller/part-edits.js.map +1 -1
  29. package/dist-client/modeller/scene-budget.d.ts +1 -1
  30. package/dist-client/modeller/scene-budget.js +1 -1
  31. package/dist-client/modeller/scene-budget.js.map +1 -1
  32. package/dist-client/pages/figure-list-page.js +1 -1
  33. package/dist-client/pages/figure-list-page.js.map +1 -1
  34. package/dist-client/pages/figure-modeller-page.d.ts +3 -3
  35. package/dist-client/pages/figure-modeller-page.js +4 -4
  36. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  37. package/dist-client/tsconfig.tsbuildinfo +1 -1
  38. package/dist-client/types.d.ts +2 -2
  39. package/dist-client/types.js.map +1 -1
  40. package/package.json +2 -2
  41. package/test/figure-source.test.ts +3 -3
  42. package/test/i18n-prefix-guard.test.ts +19 -1
  43. package/translations/en.json +25 -14
  44. package/translations/ja.json +8 -8
  45. package/translations/ko.json +13 -13
  46. package/translations/ms.json +8 -8
  47. package/translations/zh.json +8 -8
@@ -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\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 * `FigureRealObject.effectiveZPos` 가 청사진의 `placement` 와 레이어의 `heights` 로 정한다\n * (ADR-0045, `operato-twin/design/04-decisions.md`). 보드가 그렇게 세우므로 「보드에 놓았을\n * 때의 모습」을 말하는 이 화면도 그렇게 세워야 한다 — 여기서 `zPos` 를 적으면 **적은 값이\n * 이겨서**, 씬이 무엇을 할지가 이 화면에서만 안 보인다.\n *\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 움직임 조작. **저작자가 제 애니메이션과 파라미터를 만져 볼 유일한 자리다.**\n\n 도면에서는 트윈이 이 값을 준다 — 「닙이 40% 닫혔다」는 사실이 어디선가 온다. 저작\n 중에는 그 사실을 주는 것이 없어서, 손대지 않으면 파라미터는 기본값에 머문 채 영영\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 /** 지금 민 파라미터 값. 이름 → `range` 의 단위로 잰 수. **저장되지 않는다.** */\n @state() private giving: Record<string, number> = {}\n\n render() {\n const clips = this.source?.animations ?? []\n const parameters = this.source?.parameters ?? []\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 + parameters.length > 0 ? this.renderDrivers(clips, parameters) : ''}`}\n `\n }\n\n /**\n * 움직임 하나에 손잡이 하나.\n *\n * 두 갈래를 **눈금이 다른 채로** 낸다. clip 은 배속이라 0 이 멈춤이고 0~3 이 뜻이\n * 있으며, 파라미터는 물리량이라 그 `range` 가 곧 눈금이고 단위가 따라 붙는다. 하나의\n * 0~1 로 뭉뚱그리면 「1」이 무슨 뜻인지 매번 헷갈리고, 1200mm 를 넣어 보려던 저작자가\n * 넣을 자리를 못 찾는다.\n */\n private renderDrivers(\n clips: NonNullable<FigureSource['animations']>,\n parameters: NonNullable<FigureSource['parameters']>\n ) {\n return html`\n <div clips>\n ${parameters.map(parameter => {\n const range = parameter.range\n const value = this.giving[parameter.name] ?? parameter.default ?? range?.min ?? 0\n const step = range ? Math.max((range.max - range.min) / 100, 0.01) : 1\n\n return html`\n <label>\n <span>${parameter.label ?? parameter.name} <span drive>${range?.unit ?? ''}</span></span>\n <input\n type=\"range\"\n min=${range?.min ?? 0}\n max=${range?.max ?? 1}\n step=${step}\n .value=${String(value)}\n @input=${(e: Event) => this.give(parameter.name, Number((e.target as HTMLInputElement).value))}\n />\n <output>${Math.round(value)}</output>\n </label>\n `\n })}\n ${clips.map(clip => {\n const value = this.driving[clip.name] ?? 1\n\n return html`\n <label>\n <span>${clip.name} <span drive>배속</span></span>\n <input\n type=\"range\"\n min=\"0\"\n max=\"3\"\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 this.tellInstances('clips', this.driving)\n }\n\n /**\n * 파라미터 값을 준다 — **`range` 의 단위 그대로다.**\n *\n * 0~1 로 바꿔서 주지 않는다. 그 환산은 런타임의 일이고, 여기서 한 번 더 하면 두 곳이\n * 갈릴 수 있다. 저작자가 화면에서 보는 1200 이 씬에 그대로 간다.\n */\n private give(name: string, value: number): void {\n this.giving = { ...this.giving, [name]: value }\n this.tellInstances('parameters', this.giving)\n }\n\n private tellInstances(key: 'clips' | 'parameters', values: Record<string, number>): void {\n for (const component of this.scene?.root?.components ?? []) {\n component.set?.(key, { ...values })\n }\n }\n\n /**\n * 보기 설정이 바뀌면 **다시 세우지 않고 씬에 넣는다** — 두 개만 빼고.\n *\n * 조명·환경·바탕은 씬이 스스로 반응한다(`three-capability` 의 `onchange`). 슬라이더를\n * 끄는 동안 다시 세우면 카메라가 매 프레임 처음으로 돌아간다.\n *\n * 좌표 기준과 천장 높이는 다르다. 씬이 그 둘로 인스턴스 높이를 정하는데, 그 계산은 세울\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 /* 씬이 이 둘로 인스턴스 높이를 정한다 — 서명에 안 넣으면 다시 세워도 그대로 선다. */\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\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 }\n })\n\n const draft = {\n width: Math.round(cell.x * COLUMNS),\n height: Math.round(cell.z * rows),\n /*\n 판의 높이도 가장 큰 인스턴스를 담아야 한다. 안 주면 카메라가 평면만 보고 자리를\n 잡아서, 키가 큰 모델은 밑동만 보인다 — 시그널타워를 2 배로 놓으면 2200 인데 판이\n 그것을 모르면 받침만 화면에 든다.\n\n 어디에 올려 놓을지는 **씬이 정하므로**(`FigureRealObject.effectiveZPos`), 여기서는\n 어느 경우에도 담기는 경계만 잡는다. 바닥 기반은 가장 큰 것의 키까지, 천정 기반은\n 천장까지다. 배치를 높이로 옮기는 계산을 여기서 다시 쓰면 그것이 두 벌째가 된다.\n */\n depth: Math.round(Math.max(view.ceilingHeight, base.y * biggest) * 1.1)\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 (`ceilingHeight`) 편집 캔버스에 새면 안 되는 것(`placement`)이라서다. 둘 다 아래에\n 씬의 낱말로 따로 싣는다.\n */\n ...sceneLook(view),\n placement: view.placement,\n /*\n 부착면의 높이. 씬이 이 자리를 갖고(`real-object.ts` 의 `sceneHeights`), 천정 기반\n 도형이 여기서 제 높이를 얻는다. 보드에서는 현장이 이 값을 준다.\n */\n heights: { floor: 0, ceiling: view.ceilingHeight },\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 * `FigureRealObject.effectiveZPos` 가 청사진의 `placement` 와 레이어의 `heights` 로 정한다\n * (ADR-0045, `operato-twin/design/04-decisions.md`). 도면이 그렇게 세우므로 「도면에 놓았을\n * 때의 모습」을 말하는 이 화면도 그렇게 세워야 한다 — 여기서 `zPos` 를 적으면 **적은 값이\n * 이겨서**, 씬이 무엇을 할지가 이 화면에서만 안 보인다.\n *\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 움직임 조작. **저작자가 제 애니메이션과 파라미터를 만져 볼 유일한 자리다.**\n\n 도면에서는 트윈이 이 값을 준다 — 「닙이 40% 닫혔다」는 사실이 어디선가 온다. 저작\n 중에는 그 사실을 주는 것이 없어서, 손대지 않으면 파라미터는 기본값에 머문 채 영영\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 /** 지금 민 파라미터 값. 이름 → `range` 의 단위로 잰 수. **저장되지 않는다.** */\n @state() private giving: Record<string, number> = {}\n\n render() {\n const clips = this.source?.animations ?? []\n const parameters = this.source?.parameters ?? []\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 + parameters.length > 0 ? this.renderDrivers(clips, parameters) : ''}`}\n `\n }\n\n /**\n * 움직임 하나에 손잡이 하나.\n *\n * 두 갈래를 **눈금이 다른 채로** 낸다. clip 은 배속이라 0 이 멈춤이고 0~3 이 뜻이\n * 있으며, 파라미터는 물리량이라 그 `range` 가 곧 눈금이고 단위가 따라 붙는다. 하나의\n * 0~1 로 뭉뚱그리면 「1」이 무슨 뜻인지 매번 헷갈리고, 1200mm 를 넣어 보려던 저작자가\n * 넣을 자리를 못 찾는다.\n */\n private renderDrivers(\n clips: NonNullable<FigureSource['animations']>,\n parameters: NonNullable<FigureSource['parameters']>\n ) {\n return html`\n <div clips>\n ${parameters.map(parameter => {\n const range = parameter.range\n const value = this.giving[parameter.name] ?? parameter.default ?? range?.min ?? 0\n const step = range ? Math.max((range.max - range.min) / 100, 0.01) : 1\n\n return html`\n <label>\n <span>${parameter.label ?? parameter.name} <span drive>${range?.unit ?? ''}</span></span>\n <input\n type=\"range\"\n min=${range?.min ?? 0}\n max=${range?.max ?? 1}\n step=${step}\n .value=${String(value)}\n @input=${(e: Event) => this.give(parameter.name, Number((e.target as HTMLInputElement).value))}\n />\n <output>${Math.round(value)}</output>\n </label>\n `\n })}\n ${clips.map(clip => {\n const value = this.driving[clip.name] ?? 1\n\n return html`\n <label>\n <span>${clip.name} <span drive>배속</span></span>\n <input\n type=\"range\"\n min=\"0\"\n max=\"3\"\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 this.tellInstances('clips', this.driving)\n }\n\n /**\n * 파라미터 값을 준다 — **`range` 의 단위 그대로다.**\n *\n * 0~1 로 바꿔서 주지 않는다. 그 환산은 런타임의 일이고, 여기서 한 번 더 하면 두 곳이\n * 갈릴 수 있다. 저작자가 화면에서 보는 1200 이 씬에 그대로 간다.\n */\n private give(name: string, value: number): void {\n this.giving = { ...this.giving, [name]: value }\n this.tellInstances('parameters', this.giving)\n }\n\n private tellInstances(key: 'clips' | 'parameters', values: Record<string, number>): void {\n for (const component of this.scene?.root?.components ?? []) {\n component.set?.(key, { ...values })\n }\n }\n\n /**\n * 보기 설정이 바뀌면 **다시 세우지 않고 씬에 넣는다** — 두 개만 빼고.\n *\n * 조명·환경·바탕은 씬이 스스로 반응한다(`three-capability` 의 `onchange`). 슬라이더를\n * 끄는 동안 다시 세우면 카메라가 매 프레임 처음으로 돌아간다.\n *\n * 좌표 기준과 천장 높이는 다르다. 씬이 그 둘로 인스턴스 높이를 정하는데, 그 계산은 세울\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 /* 씬이 이 둘로 인스턴스 높이를 정한다 — 서명에 안 넣으면 다시 세워도 그대로 선다. */\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\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 }\n })\n\n const draft = {\n width: Math.round(cell.x * COLUMNS),\n height: Math.round(cell.z * rows),\n /*\n 판의 높이도 가장 큰 인스턴스를 담아야 한다. 안 주면 카메라가 평면만 보고 자리를\n 잡아서, 키가 큰 모델은 밑동만 보인다 — 시그널타워를 2 배로 놓으면 2200 인데 판이\n 그것을 모르면 받침만 화면에 든다.\n\n 어디에 올려 놓을지는 **씬이 정하므로**(`FigureRealObject.effectiveZPos`), 여기서는\n 어느 경우에도 담기는 경계만 잡는다. 바닥 기반은 가장 큰 것의 키까지, 천정 기반은\n 천장까지다. 배치를 높이로 옮기는 계산을 여기서 다시 쓰면 그것이 두 벌째가 된다.\n */\n depth: Math.round(Math.max(view.ceilingHeight, base.y * biggest) * 1.1)\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 (`ceilingHeight`) 편집 캔버스에 새면 안 되는 것(`placement`)이라서다. 둘 다 아래에\n 씬의 낱말로 따로 싣는다.\n */\n ...sceneLook(view),\n placement: view.placement,\n /*\n 부착면의 높이. 씬이 이 자리를 갖고(`real-object.ts` 의 `sceneHeights`), 천정 기반\n 도형이 여기서 제 높이를 얻는다. 도면에서는 현장이 이 값을 준다.\n */\n heights: { floor: 0, ceiling: view.ceilingHeight },\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"]}
@@ -156,17 +156,17 @@ let FigureSettings = class FigureSettings extends localize(i18next)(LitElement)
156
156
  const view = this.view ?? DEFAULT_VIEW;
157
157
  return html `
158
158
  <!--
159
- 놓이는 보드.
159
+ 놓이는 도면.
160
160
 
161
161
  이 묶음이 맨 위인 것은, 나머지가 「어떻게 보이나」인데 이것은 「무엇으로 보고
162
- 있나」이기 때문이다. 미리보기가 보드에 놓인 모습을 말하는 자리이므로, 어떤 보드를
162
+ 있나」이기 때문이다. 미리보기가 도면에 놓인 모습을 말하는 자리이므로, 어떤 도면을
163
163
  가정하는지가 먼저 정해져야 그 다음이 뜻을 갖는다.
164
164
 
165
165
  도형 탭의 「배치 기준」과 짝이지 같은 것이 아니다. 저쪽은 이 도형이 어느 면에
166
- 붙는가(저장된다), 이쪽은 그 면을 가진 보드가 어느 쪽을 기준으로 재는가(저장 안 된다).
166
+ 붙는가(저장된다), 이쪽은 그 면을 가진 도면이 어느 쪽을 기준으로 재는가(저장 안 된다).
167
167
  -->
168
168
  <section>
169
- <h3>${i18next.t('figure.label.assumed-board')}</h3>
169
+ <h3>${i18next.t('figure.label.assumed-drawing')}</h3>
170
170
  <p quiet>${i18next.t('figure.text.view-settings-are-not-saved')}</p>
171
171
 
172
172
  <label field>
@@ -1 +1 @@
1
- {"version":3,"file":"figure-settings.js","sourceRoot":"","sources":["../../client/modeller/figure-settings.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { ScrollbarStyles } from '@operato/styles'\n\nimport { DEFAULT_VIEW, ENVIRONMENTS, SCENE_REFERENCES } from './figure-view.js'\nimport type { EnvironmentName, ViewSettings } from './figure-view.js'\n\n/**\n * 저작 화면의 **보기 설정**.\n *\n * ## 자산이 아니라 작업대다\n *\n * 여기서 고치는 것은 저장되지 않는다. 조명을 밝게 하고 바닥을 켠 것은 지금 보기 편한\n * 상태일 뿐이다. 그래서 「저장 안 됨」을 화면에 적어 둔다 — 안 적으면 저작자가 조명을\n * 맞춰 두고 그것이 자산에 딸려 간다고 여긴다.\n *\n * ## 값을 들고 있지 않는다\n *\n * 이 패널은 받은 것을 그리고 바뀐 것을 알린다. 정본은 모델러 페이지가 갖는다 — 속성\n * 패널과 같은 방식이다. 여기서 들고 있으면 페이지가 다시 그릴 때 두 벌이 갈린다.\n */\n@customElement('figure-settings')\nexport class FigureSettings 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-y: auto;\n background-color: var(--md-sys-color-surface-container-lowest);\n color: var(--md-sys-color-on-surface);\n font: var(--label-font, inherit);\n font-size: 0.76rem;\n }\n\n section {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n\n /* 속성 패널의 묶음 제목과 같은 규격이다 — 두 탭이 다른 눈금을 쓰면 따로 논다 */\n h3 {\n margin: 0 0 2px 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 p[quiet] {\n margin: 0;\n font-size: 0.72rem;\n line-height: 1.5;\n color: var(--md-sys-color-on-surface-variant);\n }\n\n label[row] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-medium, 8px);\n cursor: pointer;\n }\n label[row] span {\n display: flex;\n flex-direction: column;\n }\n label[row] em {\n font-style: normal;\n font-size: 0.7rem;\n color: var(--md-sys-color-on-surface-variant);\n }\n\n input[type='checkbox'] {\n flex: none;\n width: 16px;\n height: 16px;\n margin: 0;\n accent-color: var(--md-sys-color-primary);\n cursor: pointer;\n }\n\n label[field] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n label[field] > span {\n color: var(--md-sys-color-on-surface-variant);\n }\n\n select,\n input[type='number'],\n input[type='color'] {\n width: 100%;\n padding: 5px 6px;\n font: var(--input-field-font, inherit);\n font-size: 0.76rem;\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 4px;\n }\n input[type='color'] {\n height: 30px;\n padding: 2px;\n cursor: pointer;\n }\n\n /* 값이 옆에 붙어야 끌면서 얼마인지 안다 */\n div[slider] {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n }\n input[type='range'] {\n flex: 1;\n accent-color: var(--md-sys-color-primary);\n cursor: pointer;\n }\n div[slider] output {\n flex: none;\n width: 2.4em;\n text-align: right;\n font: var(--mono-font, ui-monospace, monospace);\n font-size: 0.72rem;\n color: var(--md-sys-color-on-surface-variant);\n }\n\n button[reset] {\n align-self: flex-start;\n padding: 5px 12px;\n font: var(--label-font, inherit);\n font-size: 0.74rem;\n color: var(--md-sys-color-on-surface-variant);\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 4px;\n cursor: pointer;\n }\n button[reset]:hover {\n background-color: var(--md-sys-color-surface-container);\n }\n `\n ]\n\n @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW\n\n render() {\n const view = this.view ?? DEFAULT_VIEW\n\n return html`\n <!--\n 놓이는 보드.\n\n 이 묶음이 맨 위인 것은, 나머지가 「어떻게 보이나」인데 이것은 「무엇으로 보고\n 있나」이기 때문이다. 미리보기가 보드에 놓인 모습을 말하는 자리이므로, 어떤 보드를\n 가정하는지가 먼저 정해져야 그 다음이 뜻을 갖는다.\n\n 도형 탭의 「배치 기준」과 짝이지 같은 것이 아니다. 저쪽은 이 도형이 어느 면에\n 붙는가(저장된다), 이쪽은 그 면을 가진 보드가 어느 쪽을 기준으로 재는가(저장 안 된다).\n -->\n <section>\n <h3>${i18next.t('figure.label.assumed-board')}</h3>\n <p quiet>${i18next.t('figure.text.view-settings-are-not-saved')}</p>\n\n <label field>\n <span>${i18next.t('figure.label.scene-reference')}</span>\n <select\n .value=${view.placement}\n @change=${(e: Event) =>\n this.put({ placement: (e.target as HTMLSelectElement).value as ViewSettings['placement'] })}\n >\n ${SCENE_REFERENCES.map(\n name => html`<option value=${name} ?selected=${view.placement === name}>\n ${i18next.t(`figure.label.scene-reference-${name}`)}\n </option>`\n )}\n </select>\n </label>\n <p quiet>${i18next.t('figure.text.scene-reference-explained')}</p>\n\n <label field>\n <span>${i18next.t('figure.label.ceiling-height')}</span>\n <input\n type=\"number\"\n min=\"0\"\n step=\"100\"\n .value=${String(view.ceilingHeight)}\n @change=${(e: Event) => this.put({ ceilingHeight: Number((e.target as HTMLInputElement).value) || 0 })}\n />\n </label>\n <p quiet>${i18next.t('figure.text.ceiling-height-explained')}</p>\n </section>\n\n <section>\n <h3>${i18next.t('figure.label.guides')}</h3>\n\n ${this.toggle('grid', view.grid, i18next.t('figure.label.coordinate-ground'), i18next.t('figure.text.coordinate-ground-explained'))}\n ${this.toggle('gridLabels', view.gridLabels, i18next.t('figure.label.axis-labels'))}\n ${this.toggle('floor', view.floor, i18next.t('figure.label.scene-floor'), i18next.t('figure.text.floor-explained'))}\n </section>\n\n <section>\n <h3>${i18next.t('figure.label.environment')}</h3>\n\n <label field>\n <span>${i18next.t('figure.label.preset')}</span>\n <select\n .value=${view.environment}\n @change=${(e: Event) => this.put({ environment: (e.target as HTMLSelectElement).value as EnvironmentName })}\n >\n ${ENVIRONMENTS.map(\n name => html`<option value=${name} ?selected=${view.environment === name}>\n ${i18next.t(`figure.label.environment-${name}`)}\n </option>`\n )}\n </select>\n </label>\n <p quiet>${i18next.t('figure.text.environment-explained')}</p>\n\n <label field>\n <span>${i18next.t('figure.label.sky-color')}</span>\n <input\n type=\"color\"\n .value=${view.skyColor}\n @change=${(e: Event) => this.put({ skyColor: (e.target as HTMLInputElement).value })}\n />\n </label>\n </section>\n\n <section>\n <h3>${i18next.t('figure.label.key-light')}</h3>\n\n ${this.slider('dirLightIntensity', view.dirLightIntensity, i18next.t('figure.label.intensity'), 0, 5, 0.1)}\n ${this.toggle(\n 'dirLightFollowCamera',\n view.dirLightFollowCamera,\n i18next.t('figure.label.follow-camera'),\n i18next.t('figure.text.follow-camera-explained')\n )}\n ${this.toggle('dirShadowEnabled', view.dirShadowEnabled, i18next.t('figure.label.cast-shadow'))}\n ${view.dirLightIntensity === 0 && view.hemiIntensity === 0\n ? html`<p quiet>${i18next.t('figure.text.all-lights-off')}</p>`\n : ''}\n </section>\n\n <section>\n <h3>${i18next.t('figure.label.hemisphere-light')}</h3>\n\n ${this.slider('hemiIntensity', view.hemiIntensity, i18next.t('figure.label.intensity'), 0, 8, 0.1)}\n <p quiet>${i18next.t('figure.text.hemisphere-light-explained')}</p>\n </section>\n\n <section>\n <button reset @click=${() => this.reset()}>${i18next.t('figure.button.reset-view')}</button>\n </section>\n `\n }\n\n private toggle(key: keyof ViewSettings, on: boolean, label: string, hint?: string) {\n return html`\n <label row>\n <span>\n ${label}${hint ? html`<em>${hint}</em>` : ''}\n </span>\n <input\n type=\"checkbox\"\n .checked=${on}\n @change=${(e: Event) => this.put({ [key]: (e.target as HTMLInputElement).checked } as Partial<ViewSettings>)}\n />\n </label>\n `\n }\n\n private slider(key: keyof ViewSettings, value: number, label: string, min: number, max: number, step: number) {\n return html`\n <label field>\n <span>${label}</span>\n <div slider>\n <input\n type=\"range\"\n min=${min}\n max=${max}\n step=${step}\n .value=${String(value)}\n @input=${(e: Event) =>\n this.put({ [key]: Number((e.target as HTMLInputElement).value) } as Partial<ViewSettings>)}\n />\n <output>${value.toFixed(1)}</output>\n </div>\n </label>\n `\n }\n\n /** 처음 값으로 되돌린다. 만지다 길을 잃었을 때 돌아올 자리가 있어야 한다. */\n private reset() {\n this.tell({ ...DEFAULT_VIEW })\n }\n\n private put(patch: Partial<ViewSettings>) {\n this.tell({ ...(this.view ?? DEFAULT_VIEW), ...patch })\n }\n\n /**\n * 바뀐 것을 알린다.\n *\n * **알리기 전에 내 것도 바꿔 둔다.** Lit 은 속성을 다음 렌더에 넣으므로, 같은 프레임에\n * 두 번 고치면 두 번째가 `this.view` 의 옛 값을 읽어 첫 번째를 덮는다. 실제로 그랬다 —\n * 축 가이드를 끄고 바닥판을 켰더니 축 가이드가 도로 켜져 있었고, 화면의 체크 상자와\n * 씬의 상태가 서로 다른 말을 했다.\n *\n * 정본은 여전히 모델러 페이지가 갖는다. 여기 값은 다음 렌더에 같은 것으로 덮인다.\n */\n private tell(view: ViewSettings) {\n this.view = view\n this.dispatchEvent(new CustomEvent('update-view', { detail: { view }, bubbles: true, composed: true }))\n }\n}\n"]}
1
+ {"version":3,"file":"figure-settings.js","sourceRoot":"","sources":["../../client/modeller/figure-settings.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { ScrollbarStyles } from '@operato/styles'\n\nimport { DEFAULT_VIEW, ENVIRONMENTS, SCENE_REFERENCES } from './figure-view.js'\nimport type { EnvironmentName, ViewSettings } from './figure-view.js'\n\n/**\n * 저작 화면의 **보기 설정**.\n *\n * ## 자산이 아니라 작업대다\n *\n * 여기서 고치는 것은 저장되지 않는다. 조명을 밝게 하고 바닥을 켠 것은 지금 보기 편한\n * 상태일 뿐이다. 그래서 「저장 안 됨」을 화면에 적어 둔다 — 안 적으면 저작자가 조명을\n * 맞춰 두고 그것이 자산에 딸려 간다고 여긴다.\n *\n * ## 값을 들고 있지 않는다\n *\n * 이 패널은 받은 것을 그리고 바뀐 것을 알린다. 정본은 모델러 페이지가 갖는다 — 속성\n * 패널과 같은 방식이다. 여기서 들고 있으면 페이지가 다시 그릴 때 두 벌이 갈린다.\n */\n@customElement('figure-settings')\nexport class FigureSettings 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-y: auto;\n background-color: var(--md-sys-color-surface-container-lowest);\n color: var(--md-sys-color-on-surface);\n font: var(--label-font, inherit);\n font-size: 0.76rem;\n }\n\n section {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n\n /* 속성 패널의 묶음 제목과 같은 규격이다 — 두 탭이 다른 눈금을 쓰면 따로 논다 */\n h3 {\n margin: 0 0 2px 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 p[quiet] {\n margin: 0;\n font-size: 0.72rem;\n line-height: 1.5;\n color: var(--md-sys-color-on-surface-variant);\n }\n\n label[row] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--spacing-medium, 8px);\n cursor: pointer;\n }\n label[row] span {\n display: flex;\n flex-direction: column;\n }\n label[row] em {\n font-style: normal;\n font-size: 0.7rem;\n color: var(--md-sys-color-on-surface-variant);\n }\n\n input[type='checkbox'] {\n flex: none;\n width: 16px;\n height: 16px;\n margin: 0;\n accent-color: var(--md-sys-color-primary);\n cursor: pointer;\n }\n\n label[field] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n label[field] > span {\n color: var(--md-sys-color-on-surface-variant);\n }\n\n select,\n input[type='number'],\n input[type='color'] {\n width: 100%;\n padding: 5px 6px;\n font: var(--input-field-font, inherit);\n font-size: 0.76rem;\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 4px;\n }\n input[type='color'] {\n height: 30px;\n padding: 2px;\n cursor: pointer;\n }\n\n /* 값이 옆에 붙어야 끌면서 얼마인지 안다 */\n div[slider] {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n }\n input[type='range'] {\n flex: 1;\n accent-color: var(--md-sys-color-primary);\n cursor: pointer;\n }\n div[slider] output {\n flex: none;\n width: 2.4em;\n text-align: right;\n font: var(--mono-font, ui-monospace, monospace);\n font-size: 0.72rem;\n color: var(--md-sys-color-on-surface-variant);\n }\n\n button[reset] {\n align-self: flex-start;\n padding: 5px 12px;\n font: var(--label-font, inherit);\n font-size: 0.74rem;\n color: var(--md-sys-color-on-surface-variant);\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 4px;\n cursor: pointer;\n }\n button[reset]:hover {\n background-color: var(--md-sys-color-surface-container);\n }\n `\n ]\n\n @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW\n\n render() {\n const view = this.view ?? DEFAULT_VIEW\n\n return html`\n <!--\n 놓이는 도면.\n\n 이 묶음이 맨 위인 것은, 나머지가 「어떻게 보이나」인데 이것은 「무엇으로 보고\n 있나」이기 때문이다. 미리보기가 도면에 놓인 모습을 말하는 자리이므로, 어떤 도면을\n 가정하는지가 먼저 정해져야 그 다음이 뜻을 갖는다.\n\n 도형 탭의 「배치 기준」과 짝이지 같은 것이 아니다. 저쪽은 이 도형이 어느 면에\n 붙는가(저장된다), 이쪽은 그 면을 가진 도면이 어느 쪽을 기준으로 재는가(저장 안 된다).\n -->\n <section>\n <h3>${i18next.t('figure.label.assumed-drawing')}</h3>\n <p quiet>${i18next.t('figure.text.view-settings-are-not-saved')}</p>\n\n <label field>\n <span>${i18next.t('figure.label.scene-reference')}</span>\n <select\n .value=${view.placement}\n @change=${(e: Event) =>\n this.put({ placement: (e.target as HTMLSelectElement).value as ViewSettings['placement'] })}\n >\n ${SCENE_REFERENCES.map(\n name => html`<option value=${name} ?selected=${view.placement === name}>\n ${i18next.t(`figure.label.scene-reference-${name}`)}\n </option>`\n )}\n </select>\n </label>\n <p quiet>${i18next.t('figure.text.scene-reference-explained')}</p>\n\n <label field>\n <span>${i18next.t('figure.label.ceiling-height')}</span>\n <input\n type=\"number\"\n min=\"0\"\n step=\"100\"\n .value=${String(view.ceilingHeight)}\n @change=${(e: Event) => this.put({ ceilingHeight: Number((e.target as HTMLInputElement).value) || 0 })}\n />\n </label>\n <p quiet>${i18next.t('figure.text.ceiling-height-explained')}</p>\n </section>\n\n <section>\n <h3>${i18next.t('figure.label.guides')}</h3>\n\n ${this.toggle('grid', view.grid, i18next.t('figure.label.coordinate-ground'), i18next.t('figure.text.coordinate-ground-explained'))}\n ${this.toggle('gridLabels', view.gridLabels, i18next.t('figure.label.axis-labels'))}\n ${this.toggle('floor', view.floor, i18next.t('figure.label.scene-floor'), i18next.t('figure.text.floor-explained'))}\n </section>\n\n <section>\n <h3>${i18next.t('figure.label.environment')}</h3>\n\n <label field>\n <span>${i18next.t('figure.label.preset')}</span>\n <select\n .value=${view.environment}\n @change=${(e: Event) => this.put({ environment: (e.target as HTMLSelectElement).value as EnvironmentName })}\n >\n ${ENVIRONMENTS.map(\n name => html`<option value=${name} ?selected=${view.environment === name}>\n ${i18next.t(`figure.label.environment-${name}`)}\n </option>`\n )}\n </select>\n </label>\n <p quiet>${i18next.t('figure.text.environment-explained')}</p>\n\n <label field>\n <span>${i18next.t('figure.label.sky-color')}</span>\n <input\n type=\"color\"\n .value=${view.skyColor}\n @change=${(e: Event) => this.put({ skyColor: (e.target as HTMLInputElement).value })}\n />\n </label>\n </section>\n\n <section>\n <h3>${i18next.t('figure.label.key-light')}</h3>\n\n ${this.slider('dirLightIntensity', view.dirLightIntensity, i18next.t('figure.label.intensity'), 0, 5, 0.1)}\n ${this.toggle(\n 'dirLightFollowCamera',\n view.dirLightFollowCamera,\n i18next.t('figure.label.follow-camera'),\n i18next.t('figure.text.follow-camera-explained')\n )}\n ${this.toggle('dirShadowEnabled', view.dirShadowEnabled, i18next.t('figure.label.cast-shadow'))}\n ${view.dirLightIntensity === 0 && view.hemiIntensity === 0\n ? html`<p quiet>${i18next.t('figure.text.all-lights-off')}</p>`\n : ''}\n </section>\n\n <section>\n <h3>${i18next.t('figure.label.hemisphere-light')}</h3>\n\n ${this.slider('hemiIntensity', view.hemiIntensity, i18next.t('figure.label.intensity'), 0, 8, 0.1)}\n <p quiet>${i18next.t('figure.text.hemisphere-light-explained')}</p>\n </section>\n\n <section>\n <button reset @click=${() => this.reset()}>${i18next.t('figure.button.reset-view')}</button>\n </section>\n `\n }\n\n private toggle(key: keyof ViewSettings, on: boolean, label: string, hint?: string) {\n return html`\n <label row>\n <span>\n ${label}${hint ? html`<em>${hint}</em>` : ''}\n </span>\n <input\n type=\"checkbox\"\n .checked=${on}\n @change=${(e: Event) => this.put({ [key]: (e.target as HTMLInputElement).checked } as Partial<ViewSettings>)}\n />\n </label>\n `\n }\n\n private slider(key: keyof ViewSettings, value: number, label: string, min: number, max: number, step: number) {\n return html`\n <label field>\n <span>${label}</span>\n <div slider>\n <input\n type=\"range\"\n min=${min}\n max=${max}\n step=${step}\n .value=${String(value)}\n @input=${(e: Event) =>\n this.put({ [key]: Number((e.target as HTMLInputElement).value) } as Partial<ViewSettings>)}\n />\n <output>${value.toFixed(1)}</output>\n </div>\n </label>\n `\n }\n\n /** 처음 값으로 되돌린다. 만지다 길을 잃었을 때 돌아올 자리가 있어야 한다. */\n private reset() {\n this.tell({ ...DEFAULT_VIEW })\n }\n\n private put(patch: Partial<ViewSettings>) {\n this.tell({ ...(this.view ?? DEFAULT_VIEW), ...patch })\n }\n\n /**\n * 바뀐 것을 알린다.\n *\n * **알리기 전에 내 것도 바꿔 둔다.** Lit 은 속성을 다음 렌더에 넣으므로, 같은 프레임에\n * 두 번 고치면 두 번째가 `this.view` 의 옛 값을 읽어 첫 번째를 덮는다. 실제로 그랬다 —\n * 축 가이드를 끄고 바닥판을 켰더니 축 가이드가 도로 켜져 있었고, 화면의 체크 상자와\n * 씬의 상태가 서로 다른 말을 했다.\n *\n * 정본은 여전히 모델러 페이지가 갖는다. 여기 값은 다음 렌더에 같은 것으로 덮인다.\n */\n private tell(view: ViewSettings) {\n this.view = view\n this.dispatchEvent(new CustomEvent('update-view', { detail: { view }, bubbles: true, composed: true }))\n }\n}\n"]}
@@ -16,7 +16,7 @@ import './figure-canvas.js';
16
16
  *
17
17
  * ## 왜 서버가 아니라 브라우저인가
18
18
  *
19
- * 보드는 저장할 때 서버가 찍는데, 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는 것이 자주
19
+ * 도면은 저장할 때 서버가 찍는데, 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는 것이 자주
20
20
  * 실패한다. 도형을 그릴 GPU 는 이미 보는 사람 손에 있다.
21
21
  */
22
22
  /** 카드는 4:3 이다. */
@@ -1 +1 @@
1
- {"version":3,"file":"figure-thumbnail.js","sourceRoot":"","sources":["../../client/modeller/figure-thumbnail.ts"],"names":[],"mappings":"AAAA,OAAO,oBAAoB,CAAA;AAI3B;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,kBAAkB;AAClB,MAAM,KAAK,GAAG,GAAG,CAAA;AACjB,MAAM,MAAM,GAAG,GAAG,CAAA;AASlB;;;;;;;;;GASG;AACH,SAAS,MAAM;IACb,MAAM,MAAM,GAAG,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,CACzC,qBAAqB,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CACnF,CAAA;IAED,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAC3C,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;AACxD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,QAAQ,GAAG,IAAI,CAAA;AAErB,wDAAwD;AACxD,SAAS,MAAM,CAAI,IAAgB,EAAE,EAAU;IAC7C,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;AAC9D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAA8B,EAC9B,KAAkB;IAElB,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAC5B,OAAO,SAAS,CAAA;IAClB,CAAC;IAED;;;MAGE;IACF,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;IAC1C,IAAI,CAAC,YAAY,CAAC,wBAAwB,EAAE,EAAE,CAAC,CAAA;IAC/C,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,mDAAmD,KAAK,eAAe,MAAM,2BAA2B,CAAA;IAC7H,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;IAE/B,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,eAAe,CAAY,CAAA;IAClE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAA;IACvB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;IACrB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;IACrB,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,4CAA4C,CAAA;IAEpE,IAAI,CAAC;QACH,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;QAEzB;;;;UAIE;QACF,MAAM,IAAI,GAAG,MAAM,MAAM,CACvB,CAAC,KAAK,IAAI,EAAE;YACV,MAAM,MAAM,EAAE,CAAA;YACd,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAC7E,CAAC,CAAC,EAAE,EACJ,QAAQ,CACT,CAAA;QAED,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,IAAI,CAAC,eAAe,QAAQ,6BAA6B,CAAC,CAAA;QACpE,CAAC;QAED,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,IAAI,CAAC,2BAA4B,CAAW,CAAC,OAAO,EAAE,CAAC,CAAA;QAC/D,OAAO,SAAS,CAAA;IAClB,CAAC;YAAS,CAAC;QACT,IAAI,CAAC,MAAM,EAAE,CAAA;IACf,CAAC;AACH,CAAC","sourcesContent":["import './figure-canvas.js'\n\nimport type { FigureDraft, PartModel } from './figure-source.js'\n\n/**\n * 카드에 쓸 그림을 찍는다 — **숨은 자리에 저작 캔버스를 하나 더 세워서.**\n *\n * ## 왜 보이는 캔버스를 찍지 않나\n *\n * 저작면의 캔버스는 저작자가 돌려 둔 각도와 확대로 서 있고, 칸의 비율도 창 크기와 접힌 패널에 따라\n * 다르다. 그것을 찍으면 카드마다 프레임이 달라지고, 방금 확대해 보던 나사 하나가 그 자산의 얼굴이\n * 된다. 목록은 견주는 자리이므로 그림은 **정해진 크기와 정해진 카메라**로 찍는다.\n *\n * ## 왜 씬을 여기서 세우지 않고 부품을 세우나\n *\n * 씬 값(환경 · 하늘 · 조명 · 픽셀 비율 · 좌표 바탕)은 `figure-canvas` 가 든다. 여기서 다시 적으면\n * 두 벌이 되고, 그러면 **카드에 보이는 그림과 저작면에서 보던 것이 아무 오류 없이 갈린다.** 그래서\n * 같은 부품을 `viewOnly` 로 하나 더 세워 그 부품이 자기 씬을 세우게 하고, 찍은 뒤 버린다.\n *\n * ## 왜 서버가 아니라 브라우저인가\n *\n * 보드는 저장할 때 서버가 찍는데, 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는 것이 자주\n * 실패한다. 도형을 그릴 GPU 는 이미 보는 사람 손에 있다.\n */\n\n/** 카드는 4:3 이다. */\nconst WIDTH = 480\nconst HEIGHT = 360\n\ninterface Shooter extends HTMLElement {\n draft?: FigureDraft\n parts?: PartModel[]\n viewOnly?: boolean\n snapshot?: (width?: number, height?: number) => Promise<string | undefined>\n}\n\n/**\n * 씬이 서고 카메라가 자리를 잡을 틈 — 프레임 두 장 뒤에 한 번 더 쉰다. 씬에 그 신호가 없다.\n *\n * **rAF 만 기다리면 안 된다.** 탭이 뒤로 가거나 창이 가려지면 브라우저가 rAF 를 멈추고, 그러면\n * 이 약속이 영원히 안 풀린다. 그 위에 저장이 얹혀 있어서 저장이 통째로 멈췄다 — 단추는 잠긴 채로,\n * 아무 말도 없이. [확인함] 2026-09-10 포커스 없는 탭에서 `saving: true` 로 고정.\n *\n * 그래서 시계와 함께 경주시킨다. 늦게 깨어난 쪽은 버려지고, 화면이 안 그려지는 상황이면 아래\n * `snapshot` 이 빈 것을 돌려주거나 시간이 다 된다 — 어느 쪽이든 **저장은 계속 간다.**\n */\nfunction settle(): Promise<void> {\n const frames = new Promise<void>(resolve =>\n requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 150)))\n )\n\n return Promise.race([frames, delay(600)])\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\n/**\n * 그림을 기다리는 시간의 한도.\n *\n * 3D 를 세워 한 장 찍는 데 걸리는 시간은 형상과 기계에 따라 다르지만, 이 한도의 목적은 빠르기가\n * 아니라 **저장이 멈추지 않는 것**이다. 넘으면 그림 없이 저장한다 — 정본이 그림보다 중하고,\n * 그림은 다음에 저작면을 열 때 다시 채운다(`figure-modeller-page` 의 fillThumbnail).\n */\nconst PATIENCE = 5000\n\n/** 시간이 다 되면 없는 것으로 답한다. 던지지 않는다 — 그림이 없는 것은 실패가 아니다. */\nfunction within<T>(work: Promise<T>, ms: number): Promise<T | undefined> {\n return Promise.race([work, delay(ms).then(() => undefined)])\n}\n\nexport async function captureThumbnail(\n draft: FigureDraft | undefined,\n parts: PartModel[]\n): Promise<string | undefined> {\n if (!draft || !parts.length) {\n return undefined\n }\n\n /*\n 화면 밖에 둔다. `display: none` 이면 캔버스가 크기를 못 얻어 아무것도 그리지 못한다.\n WebGL context 는 브라우저가 8~16개에서 끊으므로 찍은 뒤 반드시 버린다(아래 finally).\n */\n const host = document.createElement('div')\n host.setAttribute('figure-thumbnail-stage', '')\n host.style.cssText = `position: fixed; left: -10000px; top: 0; width: ${WIDTH}px; height: ${HEIGHT}px; pointer-events: none;`\n document.body.appendChild(host)\n\n const shooter = document.createElement('figure-canvas') as Shooter\n shooter.viewOnly = true\n shooter.draft = draft\n shooter.parts = parts\n shooter.style.cssText = 'display: block; width: 100%; height: 100%;'\n\n try {\n host.appendChild(shooter)\n\n /*\n **한도 안에 못 찍으면 그림 없이 간다.** 이 함수를 부르는 자리가 저장이고, 저장은 그림을\n 기다리다 멈춰서는 안 된다. 못 찍은 것은 조용히 넘기지 않고 콘솔에 남긴다 — 그림이 계속\n 안 생기면 그 줄이 단서가 된다.\n */\n const shot = await within(\n (async () => {\n await settle()\n return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined\n })(),\n PATIENCE\n )\n\n if (!shot) {\n console.warn(`[thumbnail] ${PATIENCE}ms 안에 못 찍었습니다 — 그림 없이 저장합니다`)\n }\n\n return shot\n } catch (e) {\n console.warn(`[thumbnail] 세우지 못했습니다 — ${(e as Error).message}`)\n return undefined\n } finally {\n host.remove()\n }\n}\n"]}
1
+ {"version":3,"file":"figure-thumbnail.js","sourceRoot":"","sources":["../../client/modeller/figure-thumbnail.ts"],"names":[],"mappings":"AAAA,OAAO,oBAAoB,CAAA;AAI3B;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,kBAAkB;AAClB,MAAM,KAAK,GAAG,GAAG,CAAA;AACjB,MAAM,MAAM,GAAG,GAAG,CAAA;AASlB;;;;;;;;;GASG;AACH,SAAS,MAAM;IACb,MAAM,MAAM,GAAG,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,CACzC,qBAAqB,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CACnF,CAAA;IAED,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAC3C,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;AACxD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,QAAQ,GAAG,IAAI,CAAA;AAErB,wDAAwD;AACxD,SAAS,MAAM,CAAI,IAAgB,EAAE,EAAU;IAC7C,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;AAC9D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAA8B,EAC9B,KAAkB;IAElB,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAC5B,OAAO,SAAS,CAAA;IAClB,CAAC;IAED;;;MAGE;IACF,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;IAC1C,IAAI,CAAC,YAAY,CAAC,wBAAwB,EAAE,EAAE,CAAC,CAAA;IAC/C,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,mDAAmD,KAAK,eAAe,MAAM,2BAA2B,CAAA;IAC7H,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;IAE/B,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,eAAe,CAAY,CAAA;IAClE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAA;IACvB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;IACrB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;IACrB,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,4CAA4C,CAAA;IAEpE,IAAI,CAAC;QACH,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;QAEzB;;;;UAIE;QACF,MAAM,IAAI,GAAG,MAAM,MAAM,CACvB,CAAC,KAAK,IAAI,EAAE;YACV,MAAM,MAAM,EAAE,CAAA;YACd,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAC7E,CAAC,CAAC,EAAE,EACJ,QAAQ,CACT,CAAA;QAED,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,IAAI,CAAC,eAAe,QAAQ,6BAA6B,CAAC,CAAA;QACpE,CAAC;QAED,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,IAAI,CAAC,2BAA4B,CAAW,CAAC,OAAO,EAAE,CAAC,CAAA;QAC/D,OAAO,SAAS,CAAA;IAClB,CAAC;YAAS,CAAC;QACT,IAAI,CAAC,MAAM,EAAE,CAAA;IACf,CAAC;AACH,CAAC","sourcesContent":["import './figure-canvas.js'\n\nimport type { FigureDraft, PartModel } from './figure-source.js'\n\n/**\n * 카드에 쓸 그림을 찍는다 — **숨은 자리에 저작 캔버스를 하나 더 세워서.**\n *\n * ## 왜 보이는 캔버스를 찍지 않나\n *\n * 저작면의 캔버스는 저작자가 돌려 둔 각도와 확대로 서 있고, 칸의 비율도 창 크기와 접힌 패널에 따라\n * 다르다. 그것을 찍으면 카드마다 프레임이 달라지고, 방금 확대해 보던 나사 하나가 그 자산의 얼굴이\n * 된다. 목록은 견주는 자리이므로 그림은 **정해진 크기와 정해진 카메라**로 찍는다.\n *\n * ## 왜 씬을 여기서 세우지 않고 부품을 세우나\n *\n * 씬 값(환경 · 하늘 · 조명 · 픽셀 비율 · 좌표 바탕)은 `figure-canvas` 가 든다. 여기서 다시 적으면\n * 두 벌이 되고, 그러면 **카드에 보이는 그림과 저작면에서 보던 것이 아무 오류 없이 갈린다.** 그래서\n * 같은 부품을 `viewOnly` 로 하나 더 세워 그 부품이 자기 씬을 세우게 하고, 찍은 뒤 버린다.\n *\n * ## 왜 서버가 아니라 브라우저인가\n *\n * 도면은 저장할 때 서버가 찍는데, 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는 것이 자주\n * 실패한다. 도형을 그릴 GPU 는 이미 보는 사람 손에 있다.\n */\n\n/** 카드는 4:3 이다. */\nconst WIDTH = 480\nconst HEIGHT = 360\n\ninterface Shooter extends HTMLElement {\n draft?: FigureDraft\n parts?: PartModel[]\n viewOnly?: boolean\n snapshot?: (width?: number, height?: number) => Promise<string | undefined>\n}\n\n/**\n * 씬이 서고 카메라가 자리를 잡을 틈 — 프레임 두 장 뒤에 한 번 더 쉰다. 씬에 그 신호가 없다.\n *\n * **rAF 만 기다리면 안 된다.** 탭이 뒤로 가거나 창이 가려지면 브라우저가 rAF 를 멈추고, 그러면\n * 이 약속이 영원히 안 풀린다. 그 위에 저장이 얹혀 있어서 저장이 통째로 멈췄다 — 단추는 잠긴 채로,\n * 아무 말도 없이. [확인함] 2026-09-10 포커스 없는 탭에서 `saving: true` 로 고정.\n *\n * 그래서 시계와 함께 경주시킨다. 늦게 깨어난 쪽은 버려지고, 화면이 안 그려지는 상황이면 아래\n * `snapshot` 이 빈 것을 돌려주거나 시간이 다 된다 — 어느 쪽이든 **저장은 계속 간다.**\n */\nfunction settle(): Promise<void> {\n const frames = new Promise<void>(resolve =>\n requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 150)))\n )\n\n return Promise.race([frames, delay(600)])\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\n/**\n * 그림을 기다리는 시간의 한도.\n *\n * 3D 를 세워 한 장 찍는 데 걸리는 시간은 형상과 기계에 따라 다르지만, 이 한도의 목적은 빠르기가\n * 아니라 **저장이 멈추지 않는 것**이다. 넘으면 그림 없이 저장한다 — 정본이 그림보다 중하고,\n * 그림은 다음에 저작면을 열 때 다시 채운다(`figure-modeller-page` 의 fillThumbnail).\n */\nconst PATIENCE = 5000\n\n/** 시간이 다 되면 없는 것으로 답한다. 던지지 않는다 — 그림이 없는 것은 실패가 아니다. */\nfunction within<T>(work: Promise<T>, ms: number): Promise<T | undefined> {\n return Promise.race([work, delay(ms).then(() => undefined)])\n}\n\nexport async function captureThumbnail(\n draft: FigureDraft | undefined,\n parts: PartModel[]\n): Promise<string | undefined> {\n if (!draft || !parts.length) {\n return undefined\n }\n\n /*\n 화면 밖에 둔다. `display: none` 이면 캔버스가 크기를 못 얻어 아무것도 그리지 못한다.\n WebGL context 는 브라우저가 8~16개에서 끊으므로 찍은 뒤 반드시 버린다(아래 finally).\n */\n const host = document.createElement('div')\n host.setAttribute('figure-thumbnail-stage', '')\n host.style.cssText = `position: fixed; left: -10000px; top: 0; width: ${WIDTH}px; height: ${HEIGHT}px; pointer-events: none;`\n document.body.appendChild(host)\n\n const shooter = document.createElement('figure-canvas') as Shooter\n shooter.viewOnly = true\n shooter.draft = draft\n shooter.parts = parts\n shooter.style.cssText = 'display: block; width: 100%; height: 100%;'\n\n try {\n host.appendChild(shooter)\n\n /*\n **한도 안에 못 찍으면 그림 없이 간다.** 이 함수를 부르는 자리가 저장이고, 저장은 그림을\n 기다리다 멈춰서는 안 된다. 못 찍은 것은 조용히 넘기지 않고 콘솔에 남긴다 — 그림이 계속\n 안 생기면 그 줄이 단서가 된다.\n */\n const shot = await within(\n (async () => {\n await settle()\n return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined\n })(),\n PATIENCE\n )\n\n if (!shot) {\n console.warn(`[thumbnail] ${PATIENCE}ms 안에 못 찍었습니다 — 그림 없이 저장합니다`)\n }\n\n return shot\n } catch (e) {\n console.warn(`[thumbnail] 세우지 못했습니다 — ${(e as Error).message}`)\n return undefined\n } finally {\n host.remove()\n }\n}\n"]}
@@ -20,15 +20,15 @@
20
20
  */
21
21
  export interface ViewSettings {
22
22
  /**
23
- * 어떤 보드에 놓인 것으로 보나 — 씬 전체의 좌표 모드.
23
+ * 어떤 도면에 놓인 것으로 보나 — 씬 전체의 좌표 모드.
24
24
  *
25
25
  * **도형의 `placement` 와 다른 것이다.** 도형의 것은 「이 종류는 바닥에 서나, 천장에
26
- * 매달리나」이고, 이것은 「지금 보고 있는 보드가 어느 쪽을 기준으로 재나」다. 바닥
27
- * 기준 보드에 천정 기반 OHT 를 놓으면 보드는 안 뒤집히고 OHT 가 천장 높이에 매달린다.
26
+ * 매달리나」이고, 이것은 「지금 보고 있는 도면이 어느 쪽을 기준으로 재나」다. 바닥
27
+ * 기준 도면에 천정 기반 OHT 를 놓으면 도면은 안 뒤집히고 OHT 가 천장 높이에 매달린다.
28
28
  *
29
29
  * 한동안 도형의 `placement` 를 이 자리에 흘려 넣었다. 모델러에 도형이 하나뿐이라
30
- * 그럴듯해 보였지만, 그러면 도형의 성질이 씬 전체를 뒤집는다 — 보드에서는 성립할 수가
31
- * 없고(보드에는 도형이 여럿이다), 저작 중에는 배치를 고를 때마다 저작자가 적어 둔
30
+ * 그럴듯해 보였지만, 그러면 도형의 성질이 씬 전체를 뒤집는다 — 도면에서는 성립할 수가
31
+ * 없고(도면에는 도형이 여럿이다), 저작 중에는 배치를 고를 때마다 저작자가 적어 둔
32
32
  * 높이가 전부 다른 숫자로 보였다.
33
33
  *
34
34
  * 씬의 낱말 그대로다: `floor` 바닥 기준 · `space` 중심 기준(바닥 없음) · `inverted`
@@ -39,7 +39,7 @@ export interface ViewSettings {
39
39
  /**
40
40
  * 천장 높이 (mm). 천정 기반 도형을 미리보기에서 어디에 매달 것인가.
41
41
  *
42
- * 도형은 제가 놓일 방의 높이를 모른다. 보드는 알지만 미리보기에는 보드가 없으므로,
42
+ * 도형은 제가 놓일 방의 높이를 모른다. 도면은 알지만 미리보기에는 도면이 없으므로,
43
43
  * 저작자가 가정할 값을 여기서 정한다.
44
44
  */
45
45
  ceilingHeight: number;
@@ -1 +1 @@
1
- {"version":3,"file":"figure-view.js","sourceRoot":"","sources":["../../client/modeller/figure-view.ts"],"names":[],"mappings":"AAiEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,QAAQ;IACR,WAAW;IACX,SAAS;IACT,QAAQ;IACR,MAAM;IACN,OAAO;IACP,QAAQ;IACR,OAAO;CACC,CAAA;AAGV;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,SAAS,CAAC,IAAkB;IAC1C,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAA;IAClD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAU,CAAA;AAEvE,qCAAqC;AACrC,MAAM,CAAC,MAAM,SAAS,GAAG,SAAS,CAAA;AAElC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAiB;IACxC,SAAS,EAAE,OAAO;IAClB,iDAAiD;IACjD,aAAa,EAAE,IAAI;IACnB,IAAI,EAAE,IAAI;IACV,UAAU,EAAE,IAAI;IAChB,KAAK,EAAE,KAAK;IACZ,WAAW,EAAE,QAAQ;IACrB,QAAQ,EAAE,SAAS;IACnB,iBAAiB,EAAE,GAAG;IACtB,oBAAoB,EAAE,IAAI;IAC1B,gBAAgB,EAAE,IAAI;IACtB,aAAa,EAAE,GAAG;CACnB,CAAA","sourcesContent":["/**\n * 저작 화면의 **보기 설정**.\n *\n * ## 저장되지 않는다\n *\n * 이것은 자산이 아니라 **작업대**다. 조명을 밝게 하고 바닥을 켠 것은 그 사람이 지금\n * 보기 편한 상태일 뿐, 그 자산의 성질이 아니다. 그래서 `FigureSource` 에 안 들어간다 —\n * `toFigureSource` 는 판에서 `width` · `height` · `depth` 와 몇 가지만 집어 가고 나머지는\n * 버린다.\n *\n * 다른 사람이 같은 자산을 열면 자기 설정으로 본다. 그것이 맞다.\n *\n * ## 씬이 아는 이름을 그대로 쓴다\n *\n * 여기 이름은 전부 `model-layer` 의 상태 이름이다. 옮겨 적는 표를 두지 않는다 — 표를\n * 두면 씬이 새 설정을 받아도 이쪽이 모른다.\n *\n * 값이 바뀌면 씬이 스스로 반영한다(`three-capability` 의 `onchange`). 다만 `grid` 는\n * 한동안 3D 를 세울 때만 읽혔다 — things-scene 10.1.5 에서 고쳤다.\n */\nexport interface ViewSettings {\n /**\n * 어떤 보드에 놓인 것으로 보나 — 씬 전체의 좌표 모드.\n *\n * **도형의 `placement` 와 다른 것이다.** 도형의 것은 「이 종류는 바닥에 서나, 천장에\n * 매달리나」이고, 이것은 「지금 보고 있는 보드가 어느 쪽을 기준으로 재나」다. 바닥\n * 기준 보드에 천정 기반 OHT 를 놓으면 보드는 안 뒤집히고 OHT 가 천장 높이에 매달린다.\n *\n * 한동안 도형의 `placement` 를 이 자리에 흘려 넣었다. 모델러에 도형이 하나뿐이라\n * 그럴듯해 보였지만, 그러면 도형의 성질이 씬 전체를 뒤집는다 — 보드에서는 성립할 수가\n * 없고(보드에는 도형이 여럿이다), 저작 중에는 배치를 고를 때마다 저작자가 적어 둔\n * 높이가 전부 다른 숫자로 보였다.\n *\n * 씬의 낱말 그대로다: `floor` 바닥 기준 · `space` 중심 기준(바닥 없음) · `inverted`\n * 천장 기준. **미리보기에만 먹인다** — 편집 캔버스의 `zPos` 는 저작자가 직접 적는\n * 숫자라 그 뜻이 보기 설정에 따라 바뀌면 안 된다.\n */\n placement: 'floor' | 'space' | 'inverted'\n /**\n * 천장 높이 (mm). 천정 기반 도형을 미리보기에서 어디에 매달 것인가.\n *\n * 도형은 제가 놓일 방의 높이를 모른다. 보드는 알지만 미리보기에는 보드가 없으므로,\n * 저작자가 가정할 값을 여기서 정한다.\n */\n ceilingHeight: number\n /** 좌표 바탕(축 가이드). 원점과 눈금을 보인다. */\n grid: boolean\n /** 축 이름표(X · Y · Z). 눈금만 두고 글자를 끌 수 있다. */\n gridLabels: boolean\n /** 판의 바닥면. 좌표 바탕과 겹치므로 기본은 끈다. */\n floor: boolean\n /** Environment — 반사(IBL). 금속 재질이 무엇을 비출지 정한다. */\n environment: EnvironmentName\n /** Sky Color — 배경색. */\n skyColor: string\n /** Key Light Intensity. */\n dirLightIntensity: number\n /** Key Light — Follow Camera. 끄면 그림자 방향이 고정된다. */\n dirLightFollowCamera: boolean\n /** Key Light — Cast Shadow. */\n dirShadowEnabled: boolean\n /** Hemisphere Light Intensity — 그늘진 면을 들어올린다. */\n hemiIntensity: number\n}\n\n/**\n * 환경 프리셋 — **`scene-manager` 의 `SkyPreset` 과 같은 여덟이다.**\n *\n * 한동안 다섯만 적었다. 씬은 여덟을 다 받는데 이쪽이 셋을 몰라서 고를 수 없었다.\n * operato-board 의 3D 씬 속성 패널도 같은 여덟을 보인다.\n */\nexport const ENVIRONMENTS = [\n 'studio',\n 'warehouse',\n 'factory',\n 'office',\n 'home',\n 'sunny',\n 'cloudy',\n 'rainy'\n] as const\nexport type EnvironmentName = (typeof ENVIRONMENTS)[number]\n\n/**\n * 씬에 그대로 넣는 보기 설정 — **좌표 기준과 천장 높이는 빼고.**\n *\n * 나머지는 전부 `model-layer` 의 상태 이름이라 그대로 넣으면 씬이 알아서 반응한다.\n * 그런데 `placement` 는 편집 캔버스에 들어가면 안 되고(거기는 늘 상자 기준이다),\n * `ceilingHeight` 는 씬이 모르는 이름이다.\n *\n * **rest spread 로 넘기지 않으려고 이 함수가 있다.** 캔버스가 한동안\n * `const { grid, ..., ...light } = view` 로 나머지를 통째로 넘겼는데, 그러면 설정을\n * 하나 더할 때마다 그것이 두 화면에 **말없이** 실린다. 새로 더한 좌표 기준이 바로\n * 그 길로 캔버스에 들어갈 뻔했다.\n */\nexport function sceneLook(view: ViewSettings): Record<string, unknown> {\n const { placement, ceilingHeight, ...look } = view\n return look\n}\n\n/**\n * 씬의 좌표 기준 — **씬의 낱말 그대로 셋이다.**\n *\n * 형식의 `floor` · `center` · `ceiling` 과 짝이 맞지만 철자가 다르다. 옮겨 적는 표를 두지\n * 않는 것이 이 파일의 규칙이므로 씬 쪽을 쓴다. 화면에는 둘 다 보인다 — 「천장 기준\n * (inverted)」.\n */\nexport const SCENE_REFERENCES = ['floor', 'space', 'inverted'] as const\n\n/** 배경색 기본값. 회색이라 부품 색이 제 색으로 보인다. */\nexport const WORKBENCH = '#7c7c7c'\n\n/**\n * 처음 값.\n *\n * **지금 저작 화면이 실제로 쓰던 값 그대로다.** 설정을 만들면서 화면이 달라지면\n * 무엇이 설정 때문이고 무엇이 원래 그랬는지 갈리지 않는다.\n */\nexport const DEFAULT_VIEW: ViewSettings = {\n placement: 'floor',\n /* 공장 천장의 흔한 높이. 반도체 fab 의 OHT 레일이 대체로 이 언저리다. */\n ceilingHeight: 4000,\n grid: true,\n gridLabels: true,\n floor: false,\n environment: 'studio',\n skyColor: WORKBENCH,\n dirLightIntensity: 2.0,\n dirLightFollowCamera: true,\n dirShadowEnabled: true,\n hemiIntensity: 4.0\n}\n"]}
1
+ {"version":3,"file":"figure-view.js","sourceRoot":"","sources":["../../client/modeller/figure-view.ts"],"names":[],"mappings":"AAiEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,QAAQ;IACR,WAAW;IACX,SAAS;IACT,QAAQ;IACR,MAAM;IACN,OAAO;IACP,QAAQ;IACR,OAAO;CACC,CAAA;AAGV;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,SAAS,CAAC,IAAkB;IAC1C,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAA;IAClD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAU,CAAA;AAEvE,qCAAqC;AACrC,MAAM,CAAC,MAAM,SAAS,GAAG,SAAS,CAAA;AAElC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAiB;IACxC,SAAS,EAAE,OAAO;IAClB,iDAAiD;IACjD,aAAa,EAAE,IAAI;IACnB,IAAI,EAAE,IAAI;IACV,UAAU,EAAE,IAAI;IAChB,KAAK,EAAE,KAAK;IACZ,WAAW,EAAE,QAAQ;IACrB,QAAQ,EAAE,SAAS;IACnB,iBAAiB,EAAE,GAAG;IACtB,oBAAoB,EAAE,IAAI;IAC1B,gBAAgB,EAAE,IAAI;IACtB,aAAa,EAAE,GAAG;CACnB,CAAA","sourcesContent":["/**\n * 저작 화면의 **보기 설정**.\n *\n * ## 저장되지 않는다\n *\n * 이것은 자산이 아니라 **작업대**다. 조명을 밝게 하고 바닥을 켠 것은 그 사람이 지금\n * 보기 편한 상태일 뿐, 그 자산의 성질이 아니다. 그래서 `FigureSource` 에 안 들어간다 —\n * `toFigureSource` 는 판에서 `width` · `height` · `depth` 와 몇 가지만 집어 가고 나머지는\n * 버린다.\n *\n * 다른 사람이 같은 자산을 열면 자기 설정으로 본다. 그것이 맞다.\n *\n * ## 씬이 아는 이름을 그대로 쓴다\n *\n * 여기 이름은 전부 `model-layer` 의 상태 이름이다. 옮겨 적는 표를 두지 않는다 — 표를\n * 두면 씬이 새 설정을 받아도 이쪽이 모른다.\n *\n * 값이 바뀌면 씬이 스스로 반영한다(`three-capability` 의 `onchange`). 다만 `grid` 는\n * 한동안 3D 를 세울 때만 읽혔다 — things-scene 10.1.5 에서 고쳤다.\n */\nexport interface ViewSettings {\n /**\n * 어떤 도면에 놓인 것으로 보나 — 씬 전체의 좌표 모드.\n *\n * **도형의 `placement` 와 다른 것이다.** 도형의 것은 「이 종류는 바닥에 서나, 천장에\n * 매달리나」이고, 이것은 「지금 보고 있는 도면이 어느 쪽을 기준으로 재나」다. 바닥\n * 기준 도면에 천정 기반 OHT 를 놓으면 도면은 안 뒤집히고 OHT 가 천장 높이에 매달린다.\n *\n * 한동안 도형의 `placement` 를 이 자리에 흘려 넣었다. 모델러에 도형이 하나뿐이라\n * 그럴듯해 보였지만, 그러면 도형의 성질이 씬 전체를 뒤집는다 — 도면에서는 성립할 수가\n * 없고(도면에는 도형이 여럿이다), 저작 중에는 배치를 고를 때마다 저작자가 적어 둔\n * 높이가 전부 다른 숫자로 보였다.\n *\n * 씬의 낱말 그대로다: `floor` 바닥 기준 · `space` 중심 기준(바닥 없음) · `inverted`\n * 천장 기준. **미리보기에만 먹인다** — 편집 캔버스의 `zPos` 는 저작자가 직접 적는\n * 숫자라 그 뜻이 보기 설정에 따라 바뀌면 안 된다.\n */\n placement: 'floor' | 'space' | 'inverted'\n /**\n * 천장 높이 (mm). 천정 기반 도형을 미리보기에서 어디에 매달 것인가.\n *\n * 도형은 제가 놓일 방의 높이를 모른다. 도면은 알지만 미리보기에는 도면이 없으므로,\n * 저작자가 가정할 값을 여기서 정한다.\n */\n ceilingHeight: number\n /** 좌표 바탕(축 가이드). 원점과 눈금을 보인다. */\n grid: boolean\n /** 축 이름표(X · Y · Z). 눈금만 두고 글자를 끌 수 있다. */\n gridLabels: boolean\n /** 판의 바닥면. 좌표 바탕과 겹치므로 기본은 끈다. */\n floor: boolean\n /** Environment — 반사(IBL). 금속 재질이 무엇을 비출지 정한다. */\n environment: EnvironmentName\n /** Sky Color — 배경색. */\n skyColor: string\n /** Key Light Intensity. */\n dirLightIntensity: number\n /** Key Light — Follow Camera. 끄면 그림자 방향이 고정된다. */\n dirLightFollowCamera: boolean\n /** Key Light — Cast Shadow. */\n dirShadowEnabled: boolean\n /** Hemisphere Light Intensity — 그늘진 면을 들어올린다. */\n hemiIntensity: number\n}\n\n/**\n * 환경 프리셋 — **`scene-manager` 의 `SkyPreset` 과 같은 여덟이다.**\n *\n * 한동안 다섯만 적었다. 씬은 여덟을 다 받는데 이쪽이 셋을 몰라서 고를 수 없었다.\n * operato-board 의 3D 씬 속성 패널도 같은 여덟을 보인다.\n */\nexport const ENVIRONMENTS = [\n 'studio',\n 'warehouse',\n 'factory',\n 'office',\n 'home',\n 'sunny',\n 'cloudy',\n 'rainy'\n] as const\nexport type EnvironmentName = (typeof ENVIRONMENTS)[number]\n\n/**\n * 씬에 그대로 넣는 보기 설정 — **좌표 기준과 천장 높이는 빼고.**\n *\n * 나머지는 전부 `model-layer` 의 상태 이름이라 그대로 넣으면 씬이 알아서 반응한다.\n * 그런데 `placement` 는 편집 캔버스에 들어가면 안 되고(거기는 늘 상자 기준이다),\n * `ceilingHeight` 는 씬이 모르는 이름이다.\n *\n * **rest spread 로 넘기지 않으려고 이 함수가 있다.** 캔버스가 한동안\n * `const { grid, ..., ...light } = view` 로 나머지를 통째로 넘겼는데, 그러면 설정을\n * 하나 더할 때마다 그것이 두 화면에 **말없이** 실린다. 새로 더한 좌표 기준이 바로\n * 그 길로 캔버스에 들어갈 뻔했다.\n */\nexport function sceneLook(view: ViewSettings): Record<string, unknown> {\n const { placement, ceilingHeight, ...look } = view\n return look\n}\n\n/**\n * 씬의 좌표 기준 — **씬의 낱말 그대로 셋이다.**\n *\n * 형식의 `floor` · `center` · `ceiling` 과 짝이 맞지만 철자가 다르다. 옮겨 적는 표를 두지\n * 않는 것이 이 파일의 규칙이므로 씬 쪽을 쓴다. 화면에는 둘 다 보인다 — 「천장 기준\n * (inverted)」.\n */\nexport const SCENE_REFERENCES = ['floor', 'space', 'inverted'] as const\n\n/** 배경색 기본값. 회색이라 부품 색이 제 색으로 보인다. */\nexport const WORKBENCH = '#7c7c7c'\n\n/**\n * 처음 값.\n *\n * **지금 저작 화면이 실제로 쓰던 값 그대로다.** 설정을 만들면서 화면이 달라지면\n * 무엇이 설정 때문이고 무엇이 원래 그랬는지 갈리지 않는다.\n */\nexport const DEFAULT_VIEW: ViewSettings = {\n placement: 'floor',\n /* 공장 천장의 흔한 높이. 반도체 fab 의 OHT 레일이 대체로 이 언저리다. */\n ceilingHeight: 4000,\n grid: true,\n gridLabels: true,\n floor: false,\n environment: 'studio',\n skyColor: WORKBENCH,\n dirLightIntensity: 2.0,\n dirLightFollowCamera: true,\n dirShadowEnabled: true,\n hemiIntensity: 4.0\n}\n"]}
@@ -101,7 +101,7 @@ export function updatePart(parts, index, part) {
101
101
  /**
102
102
  * 정밀 배치 도구 — 좌표를 손으로 계산하지 않게 한다.
103
103
  *
104
- * 보드 편집기는 자산을 **배치**하는 도구라 대략적인 위치로도 충분하다. 여기는 자산을
104
+ * 도면 편집기는 자산을 **배치**하는 도구라 대략적인 위치로도 충분하다. 여기는 자산을
105
105
  * **만드는** 도구라 다르다. 모터를 몸통 정중앙에 놓거나, 다리를 네 귀퉁이에 대칭으로
106
106
  * 놓거나, 부품을 바닥에 정확히 붙이는 작업은 손으로 계산하면 늘 조금씩 어긋난다.
107
107
  */
@@ -1 +1 @@
1
- {"version":3,"file":"part-edits.js","sourceRoot":"","sources":["../../client/modeller/part-edits.ts"],"names":[],"mappings":"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","sourcesContent":["import { EXTRUDE_PRIMITIVES, SEGMENTED_PRIMITIVES, SEGMENT_PRESETS, PART_LIMIT } from '@hatiolab/figure-model'\nimport type { DetailLevel, PrimitiveKind } from '@hatiolab/figure-model'\n\nimport { AXIS_KEYS, draftExtent } from './figure-source.js'\nimport type { FigureDraft, PartModel, PlaneAxis } from './figure-source.js'\n\n/**\n * 부품 편집 규칙. **씬 모델의 용어로** 다룬다.\n *\n * 편집 중에 기준이 되는 데이터는 씬 모델이다 — 기준 상자 위에 부품 컴포넌트가 놓인 상태.\n * 그래서 편집도 씬의 용어(`left`·`top`·`zPos`·`width`·`height`·`depth`)로 한다.\n * FigureSource 의 좌표계로 바꾸는 변환은 `figure-source.ts` 한 곳에서만 한다.\n *\n * 사용자가 보는 축(X·Y·Z)이 씬의 어느 속성인지도 `figure-source.ts` 의 `AXIS_KEYS` 가 기준이다.\n * 여기서 다시 정의하지 않는다.\n *\n * 모든 함수가 **새 배열·새 객체를 반환한다** — 원본을 수정하지 않는다. Lit 이 변경을\n * 감지하려면 참조가 달라져야 하고, 실행 취소를 붙일 때도 이전 상태가 남아 있어야 한다.\n */\n\n/** 중복되지 않는 이름을 만든다. 부품 이름은 저장되는 식별자라 중복되면 검증에서 걸린다. */\nfunction freeName(taken: Set<string>, base: string): string {\n if (!taken.has(base)) return base\n for (let n = 2; ; n++) {\n const candidate = `${base}-${n}`\n if (!taken.has(candidate)) return candidate\n }\n}\n\n/** 이 초안에 부품을 몇 개까지 넣을 수 있는지. 디테일 등급이 결정한다. */\nexport function partLimit(draft: FigureDraft): number {\n return PART_LIMIT[(draft.detailLevel ?? 'M') as DetailLevel]\n}\n\n/**\n * 새 부품 하나를 만든다.\n *\n * 기준 상자의 3분의 1 크기로, **바닥 가운데**에 놓는다. 추가하자마자 화면에 보여야 한다 —\n * 크기 0 으로 놓으면 사용자에게는 아무 일도 일어나지 않은 것으로 보인다.\n */\nexport function newPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind): PartModel {\n const taken = new Set(parts.map(part => part.name))\n\n const width = Math.max(1, Math.round(draft.width / 3))\n const height = Math.max(1, Math.round(draft.height / 3))\n const depth = Math.max(1, Math.round(draft.depth / 3))\n\n const part: PartModel = {\n type: 'figure-part',\n name: freeName(taken, primitive),\n primitive,\n left: Math.round((draft.width - width) / 2),\n top: Math.round((draft.height - height) / 2),\n width,\n height,\n // 바닥에 붙인다. `zPos` 는 부피의 밑면이라 0 이 곧 상자 바닥이다.\n zPos: 0,\n depth,\n // 팔레트 토큰은 기존 부품의 것을 물려받는다 — 토큰이 늘어날 때마다 재질 그룹이 하나씩 는다.\n token: parts[0]?.token ?? 'palette.primary'\n }\n\n if (SEGMENTED_PRIMITIVES.includes(primitive)) {\n part.segments = SEGMENT_PRESETS[1]\n }\n if (EXTRUDE_PRIMITIVES.includes(primitive)) {\n part.round = 0\n }\n if (primitive === 'polygon') {\n // polygon 은 점이 3개 이상이어야 한다. 없으면 검증에서 막히므로 삼각형으로 시작한다.\n // 단면의 점은 부품 중심을 기준으로 적는다.\n part.path = [\n { x: -Math.round(width / 2), y: -Math.round(height / 2) },\n { x: Math.round(width / 2), y: -Math.round(height / 2) },\n { x: 0, y: Math.round(height / 2) }\n ]\n }\n\n return part\n}\n\nexport function addPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind): PartModel[] {\n return [...parts, newPart(draft, parts, primitive)]\n}\n\nexport function removePart(parts: PartModel[], index: number): PartModel[] {\n return parts.filter((_, at) => at !== index)\n}\n\n/** 부품을 복제한다. 이름만 새로 붙인다 — 중복되면 검증에서 걸린다. */\nexport function duplicatePart(parts: PartModel[], index: number): PartModel[] {\n const origin = parts[index]\n if (!origin) return parts\n\n const copy: PartModel = JSON.parse(JSON.stringify(origin))\n copy.name = freeName(new Set(parts.map(part => part.name)), `${origin.name}-copy`)\n\n const next = [...parts]\n next.splice(index + 1, 0, copy)\n return next\n}\n\n/** 목록에서 순서를 바꾼다. 범위를 벗어나면 그대로 둔다. */\nexport function movePart(parts: PartModel[], index: number, to: number): PartModel[] {\n if (to < 0 || to >= parts.length || index === to) return parts\n\n const next = [...parts]\n const [moved] = next.splice(index, 1)\n next.splice(to, 0, moved!)\n return next\n}\n\nexport function updatePart(parts: PartModel[], index: number, part: PartModel): PartModel[] {\n return parts.map((old, at) => (at === index ? part : old))\n}\n\n/**\n * 정밀 배치 도구 — 좌표를 손으로 계산하지 않게 한다.\n *\n * 보드 편집기는 자산을 **배치**하는 도구라 대략적인 위치로도 충분하다. 여기는 자산을\n * **만드는** 도구라 다르다. 모터를 몸통 정중앙에 놓거나, 다리를 네 귀퉁이에 대칭으로\n * 놓거나, 부품을 바닥에 정확히 붙이는 작업은 손으로 계산하면 늘 조금씩 어긋난다.\n */\n\nfunction put(parts: PartModel[], index: number, fields: Partial<PartModel>): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n return updatePart(parts, index, { ...part, ...fields })\n}\n\n/** 위치의 한 축만 바꾼다. 어느 속성인지는 `AXIS_KEYS` 가 결정한다. */\nfunction putAt(parts: PartModel[], index: number, axis: PlaneAxis, value: number): PartModel[] {\n return put(parts, index, { [AXIS_KEYS[axis].at]: value } as Partial<PartModel>)\n}\n\n/** 부품을 기준 상자의 해당 축 가운데로 옮긴다. */\nexport function centerPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n return putAt(parts, index, axis, Math.round((draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]) / 2))\n}\n\n/** 부품을 상자 바닥(또는 해당 축의 시작)에 붙인다. */\nexport function alignPartToStart(parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n return putAt(parts, index, axis, 0)\n}\n\n/** 부품을 상자의 끝에 붙인다. */\nexport function alignPartToEnd(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n return putAt(parts, index, axis, Math.round(draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]))\n}\n\n/**\n * 부품을 축 기준으로 대칭 복제한다.\n *\n * 다리 넷, 팔 둘 같은 형상을 이것 없이 만들면 고생한다. 이름은 **어느 쪽인지 알 수\n * 있게** 붙인다 — `leg` 를 x 축으로 대칭하면 `leg-x`.\n */\nexport function mirrorPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part || !canMirror(draft, parts, index, axis)) return parts\n\n const copy = JSON.parse(JSON.stringify(part)) as PartModel\n copy.name = freeName(new Set(parts.map(p => p.name)), `${part.name}-${axis}`)\n\n const next = [...parts]\n next.splice(index + 1, 0, copy)\n return putAt(next, index + 1, axis, mirroredAt(draft, part, axis))\n}\n\n/** 대칭했을 때의 위치. 상자의 해당 축 안에서 뒤집는다. */\nfunction mirroredAt(draft: FigureDraft, part: PartModel, axis: PlaneAxis): number {\n const { at, size } = AXIS_KEYS[axis]\n return Math.round(draftExtent(draft, axis) - part[at] - part[size])\n}\n\n/**\n * 대칭이 의미가 있는지.\n *\n * 이미 그 축의 가운데에 있는 부품을 대칭하면 **같은 자리에 사본이 생긴다.** 화면에는\n * 아무 변화가 없어 보이는데 부품 수만 늘고, 겹친 면에서 z-fighting 이 일어난다.\n * 만들어 놓고 알리는 대신 미리 막는다.\n */\nexport function canMirror(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): boolean {\n const part = parts[index]\n if (!part) return false\n\n return mirroredAt(draft, part, axis) !== Math.round(part[AXIS_KEYS[axis].at])\n}\n\n/**\n * 부품의 위치와 크기를 격자에 맞춘다.\n *\n * 격자 간격은 상자 크기에서 계산한다 — 고정 값이면 작은 형상에서는 너무 크고 큰\n * 형상에서는 너무 잘다. 3D 바닥 격자와 같은 계산이라 화면에서 보이는 것과 일치한다.\n */\nexport function snapPart(draft: FigureDraft, parts: PartModel[], index: number): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n const step = gridStep(draft)\n const round = (value: number) => Math.round(value / step) * step\n\n return put(parts, index, {\n left: round(part.left),\n top: round(part.top),\n zPos: round(part.zPos),\n // 크기는 0 이 되면 안 된다 — 부품이 사라진 것처럼 보인다\n width: Math.max(step, round(part.width)),\n height: Math.max(step, round(part.height)),\n depth: Math.max(step, round(part.depth))\n })\n}\n\n/**\n * 격자 간격. 3D 바닥에 그려지는 격자와 같은 값이다.\n *\n * **세 축을 다 본다.** 평면만 보면 키가 큰 모델에서 격자가 지나치게 잘아진다. 시그널타워\n * (바닥 160 x 160, 높이 1100)는 평면 기준으로 간격이 20 이 나와서, 옆에서 보면 높이가\n * 격자 55 칸이 된다. 격자는 크기를 가늠하는 자이므로 모델 전체에 대해 읽혀야 한다.\n */\nexport function gridStep(draft: FigureDraft): number {\n return niceStep(Math.max(draft.width, draft.height, draft.depth))\n}\n\n/**\n * 주어진 길이에 어울리는 **읽기 좋은** 격자 간격. 다섯 칸 정도가 되게 잡는다.\n *\n * 그냥 나누면 27·33 같은 값이 나온다. 그 격자에 맞추면 좌표가 54·81 이 되어 읽기\n * 어렵다. 1·2·5 배수로 올림한다 — 자와 차트 눈금이 쓰는 방식이다.\n *\n * 격자가 성겨야 형상이 눈에 먼저 들어온다. 열 칸으로 잡았더니 격자가 촘촘해 배경이\n * 먼저 보였다.\n */\nexport function niceStep(span: number): number {\n const raw = Math.max(span, 1) / 5\n const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))\n const n = raw / magnitude\n\n const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10\n return Math.max(1, multiple * magnitude)\n}\n"]}
1
+ {"version":3,"file":"part-edits.js","sourceRoot":"","sources":["../../client/modeller/part-edits.ts"],"names":[],"mappings":"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","sourcesContent":["import { EXTRUDE_PRIMITIVES, SEGMENTED_PRIMITIVES, SEGMENT_PRESETS, PART_LIMIT } from '@hatiolab/figure-model'\nimport type { DetailLevel, PrimitiveKind } from '@hatiolab/figure-model'\n\nimport { AXIS_KEYS, draftExtent } from './figure-source.js'\nimport type { FigureDraft, PartModel, PlaneAxis } from './figure-source.js'\n\n/**\n * 부품 편집 규칙. **씬 모델의 용어로** 다룬다.\n *\n * 편집 중에 기준이 되는 데이터는 씬 모델이다 — 기준 상자 위에 부품 컴포넌트가 놓인 상태.\n * 그래서 편집도 씬의 용어(`left`·`top`·`zPos`·`width`·`height`·`depth`)로 한다.\n * FigureSource 의 좌표계로 바꾸는 변환은 `figure-source.ts` 한 곳에서만 한다.\n *\n * 사용자가 보는 축(X·Y·Z)이 씬의 어느 속성인지도 `figure-source.ts` 의 `AXIS_KEYS` 가 기준이다.\n * 여기서 다시 정의하지 않는다.\n *\n * 모든 함수가 **새 배열·새 객체를 반환한다** — 원본을 수정하지 않는다. Lit 이 변경을\n * 감지하려면 참조가 달라져야 하고, 실행 취소를 붙일 때도 이전 상태가 남아 있어야 한다.\n */\n\n/** 중복되지 않는 이름을 만든다. 부품 이름은 저장되는 식별자라 중복되면 검증에서 걸린다. */\nfunction freeName(taken: Set<string>, base: string): string {\n if (!taken.has(base)) return base\n for (let n = 2; ; n++) {\n const candidate = `${base}-${n}`\n if (!taken.has(candidate)) return candidate\n }\n}\n\n/** 이 초안에 부품을 몇 개까지 넣을 수 있는지. 디테일 등급이 결정한다. */\nexport function partLimit(draft: FigureDraft): number {\n return PART_LIMIT[(draft.detailLevel ?? 'M') as DetailLevel]\n}\n\n/**\n * 새 부품 하나를 만든다.\n *\n * 기준 상자의 3분의 1 크기로, **바닥 가운데**에 놓는다. 추가하자마자 화면에 보여야 한다 —\n * 크기 0 으로 놓으면 사용자에게는 아무 일도 일어나지 않은 것으로 보인다.\n */\nexport function newPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind): PartModel {\n const taken = new Set(parts.map(part => part.name))\n\n const width = Math.max(1, Math.round(draft.width / 3))\n const height = Math.max(1, Math.round(draft.height / 3))\n const depth = Math.max(1, Math.round(draft.depth / 3))\n\n const part: PartModel = {\n type: 'figure-part',\n name: freeName(taken, primitive),\n primitive,\n left: Math.round((draft.width - width) / 2),\n top: Math.round((draft.height - height) / 2),\n width,\n height,\n // 바닥에 붙인다. `zPos` 는 부피의 밑면이라 0 이 곧 상자 바닥이다.\n zPos: 0,\n depth,\n // 팔레트 토큰은 기존 부품의 것을 물려받는다 — 토큰이 늘어날 때마다 재질 그룹이 하나씩 는다.\n token: parts[0]?.token ?? 'palette.primary'\n }\n\n if (SEGMENTED_PRIMITIVES.includes(primitive)) {\n part.segments = SEGMENT_PRESETS[1]\n }\n if (EXTRUDE_PRIMITIVES.includes(primitive)) {\n part.round = 0\n }\n if (primitive === 'polygon') {\n // polygon 은 점이 3개 이상이어야 한다. 없으면 검증에서 막히므로 삼각형으로 시작한다.\n // 단면의 점은 부품 중심을 기준으로 적는다.\n part.path = [\n { x: -Math.round(width / 2), y: -Math.round(height / 2) },\n { x: Math.round(width / 2), y: -Math.round(height / 2) },\n { x: 0, y: Math.round(height / 2) }\n ]\n }\n\n return part\n}\n\nexport function addPart(draft: FigureDraft, parts: PartModel[], primitive: PrimitiveKind): PartModel[] {\n return [...parts, newPart(draft, parts, primitive)]\n}\n\nexport function removePart(parts: PartModel[], index: number): PartModel[] {\n return parts.filter((_, at) => at !== index)\n}\n\n/** 부품을 복제한다. 이름만 새로 붙인다 — 중복되면 검증에서 걸린다. */\nexport function duplicatePart(parts: PartModel[], index: number): PartModel[] {\n const origin = parts[index]\n if (!origin) return parts\n\n const copy: PartModel = JSON.parse(JSON.stringify(origin))\n copy.name = freeName(new Set(parts.map(part => part.name)), `${origin.name}-copy`)\n\n const next = [...parts]\n next.splice(index + 1, 0, copy)\n return next\n}\n\n/** 목록에서 순서를 바꾼다. 범위를 벗어나면 그대로 둔다. */\nexport function movePart(parts: PartModel[], index: number, to: number): PartModel[] {\n if (to < 0 || to >= parts.length || index === to) return parts\n\n const next = [...parts]\n const [moved] = next.splice(index, 1)\n next.splice(to, 0, moved!)\n return next\n}\n\nexport function updatePart(parts: PartModel[], index: number, part: PartModel): PartModel[] {\n return parts.map((old, at) => (at === index ? part : old))\n}\n\n/**\n * 정밀 배치 도구 — 좌표를 손으로 계산하지 않게 한다.\n *\n * 도면 편집기는 자산을 **배치**하는 도구라 대략적인 위치로도 충분하다. 여기는 자산을\n * **만드는** 도구라 다르다. 모터를 몸통 정중앙에 놓거나, 다리를 네 귀퉁이에 대칭으로\n * 놓거나, 부품을 바닥에 정확히 붙이는 작업은 손으로 계산하면 늘 조금씩 어긋난다.\n */\n\nfunction put(parts: PartModel[], index: number, fields: Partial<PartModel>): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n return updatePart(parts, index, { ...part, ...fields })\n}\n\n/** 위치의 한 축만 바꾼다. 어느 속성인지는 `AXIS_KEYS` 가 결정한다. */\nfunction putAt(parts: PartModel[], index: number, axis: PlaneAxis, value: number): PartModel[] {\n return put(parts, index, { [AXIS_KEYS[axis].at]: value } as Partial<PartModel>)\n}\n\n/** 부품을 기준 상자의 해당 축 가운데로 옮긴다. */\nexport function centerPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n return putAt(parts, index, axis, Math.round((draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]) / 2))\n}\n\n/** 부품을 상자 바닥(또는 해당 축의 시작)에 붙인다. */\nexport function alignPartToStart(parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n return putAt(parts, index, axis, 0)\n}\n\n/** 부품을 상자의 끝에 붙인다. */\nexport function alignPartToEnd(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n return putAt(parts, index, axis, Math.round(draftExtent(draft, axis) - part[AXIS_KEYS[axis].size]))\n}\n\n/**\n * 부품을 축 기준으로 대칭 복제한다.\n *\n * 다리 넷, 팔 둘 같은 형상을 이것 없이 만들면 고생한다. 이름은 **어느 쪽인지 알 수\n * 있게** 붙인다 — `leg` 를 x 축으로 대칭하면 `leg-x`.\n */\nexport function mirrorPart(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): PartModel[] {\n const part = parts[index]\n if (!part || !canMirror(draft, parts, index, axis)) return parts\n\n const copy = JSON.parse(JSON.stringify(part)) as PartModel\n copy.name = freeName(new Set(parts.map(p => p.name)), `${part.name}-${axis}`)\n\n const next = [...parts]\n next.splice(index + 1, 0, copy)\n return putAt(next, index + 1, axis, mirroredAt(draft, part, axis))\n}\n\n/** 대칭했을 때의 위치. 상자의 해당 축 안에서 뒤집는다. */\nfunction mirroredAt(draft: FigureDraft, part: PartModel, axis: PlaneAxis): number {\n const { at, size } = AXIS_KEYS[axis]\n return Math.round(draftExtent(draft, axis) - part[at] - part[size])\n}\n\n/**\n * 대칭이 의미가 있는지.\n *\n * 이미 그 축의 가운데에 있는 부품을 대칭하면 **같은 자리에 사본이 생긴다.** 화면에는\n * 아무 변화가 없어 보이는데 부품 수만 늘고, 겹친 면에서 z-fighting 이 일어난다.\n * 만들어 놓고 알리는 대신 미리 막는다.\n */\nexport function canMirror(draft: FigureDraft, parts: PartModel[], index: number, axis: PlaneAxis): boolean {\n const part = parts[index]\n if (!part) return false\n\n return mirroredAt(draft, part, axis) !== Math.round(part[AXIS_KEYS[axis].at])\n}\n\n/**\n * 부품의 위치와 크기를 격자에 맞춘다.\n *\n * 격자 간격은 상자 크기에서 계산한다 — 고정 값이면 작은 형상에서는 너무 크고 큰\n * 형상에서는 너무 잘다. 3D 바닥 격자와 같은 계산이라 화면에서 보이는 것과 일치한다.\n */\nexport function snapPart(draft: FigureDraft, parts: PartModel[], index: number): PartModel[] {\n const part = parts[index]\n if (!part) return parts\n\n const step = gridStep(draft)\n const round = (value: number) => Math.round(value / step) * step\n\n return put(parts, index, {\n left: round(part.left),\n top: round(part.top),\n zPos: round(part.zPos),\n // 크기는 0 이 되면 안 된다 — 부품이 사라진 것처럼 보인다\n width: Math.max(step, round(part.width)),\n height: Math.max(step, round(part.height)),\n depth: Math.max(step, round(part.depth))\n })\n}\n\n/**\n * 격자 간격. 3D 바닥에 그려지는 격자와 같은 값이다.\n *\n * **세 축을 다 본다.** 평면만 보면 키가 큰 모델에서 격자가 지나치게 잘아진다. 시그널타워\n * (바닥 160 x 160, 높이 1100)는 평면 기준으로 간격이 20 이 나와서, 옆에서 보면 높이가\n * 격자 55 칸이 된다. 격자는 크기를 가늠하는 자이므로 모델 전체에 대해 읽혀야 한다.\n */\nexport function gridStep(draft: FigureDraft): number {\n return niceStep(Math.max(draft.width, draft.height, draft.depth))\n}\n\n/**\n * 주어진 길이에 어울리는 **읽기 좋은** 격자 간격. 다섯 칸 정도가 되게 잡는다.\n *\n * 그냥 나누면 27·33 같은 값이 나온다. 그 격자에 맞추면 좌표가 54·81 이 되어 읽기\n * 어렵다. 1·2·5 배수로 올림한다 — 자와 차트 눈금이 쓰는 방식이다.\n *\n * 격자가 성겨야 형상이 눈에 먼저 들어온다. 열 칸으로 잡았더니 격자가 촘촘해 배경이\n * 먼저 보였다.\n */\nexport function niceStep(span: number): number {\n const raw = Math.max(span, 1) / 5\n const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))\n const n = raw / magnitude\n\n const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10\n return Math.max(1, multiple * magnitude)\n}\n"]}
@@ -18,7 +18,7 @@ import type { FigureCost } from '@hatiolab/figure-model';
18
18
  * 삼각형 ≤ 50 만
19
19
  *
20
20
  * 그 문서가 스스로 이렇게 적어 두었다 — **「측정 없이 정한 목표다. 저작도구가 서면
21
- * 실제 보드로 재고 조정한다」**(미결 D-20). 그래서 화면에도 근거를 함께 띄운다.
21
+ * 실제 도면으로 재고 조정한다」**(미결 D-20). 그래서 화면에도 근거를 함께 띄운다.
22
22
  * 숫자만 크게 내걸면 재 본 값처럼 읽힌다.
23
23
  */
24
24
  /**
@@ -17,7 +17,7 @@
17
17
  * 삼각형 ≤ 50 만
18
18
  *
19
19
  * 그 문서가 스스로 이렇게 적어 두었다 — **「측정 없이 정한 목표다. 저작도구가 서면
20
- * 실제 보드로 재고 조정한다」**(미결 D-20). 그래서 화면에도 근거를 함께 띄운다.
20
+ * 실제 도면으로 재고 조정한다」**(미결 D-20). 그래서 화면에도 근거를 함께 띄운다.
21
21
  * 숫자만 크게 내걸면 재 본 값처럼 읽힌다.
22
22
  */
23
23
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"scene-budget.js","sourceRoot":"","sources":["../../client/modeller/scene-budget.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAA;AAEnC,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,gDAAgD;IAChD,SAAS,EAAE,IAAI;IACf,iBAAiB;IACjB,SAAS,EAAE,OAAO;CACV,CAAA;AAgBV;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,IAAgB;IACzC,OAAO,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;AAClD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,SAAiB;IAC5D,2CAA2C;IAC3C,IAAI,MAAM,IAAI,CAAC,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;QAClC,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,0DAA0D;IAC1D,MAAM,KAAK,GAAG,MAAM,GAAG,kBAAkB,CAAA;IAEzC,MAAM,WAAW,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IACrF,MAAM,WAAW,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IAE7F,OAAO,WAAW,IAAI,WAAW;QAC/B,CAAC,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,SAAS,EAAE;QAC5F,CAAC,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,CAAC,SAAS,EAAE,CAAA;AACpG,CAAC","sourcesContent":["import type { FigureCost } from '@hatiolab/figure-model'\n\n/**\n * 한 도면이 감당하는 양 — 「몇 개까지 편한가」의 분모.\n *\n * ## 이 값은 figure-model 것이어야 한다\n *\n * figure-model 의 `LIMITS` 는 전부 **Figure 하나짜리** 한도다(묶음 3 · 애니메이션 2 …).\n * 「몇 개를 놓을 수 있나」는 한 도면이 감당하는 양을 알아야 나오는데, 그 값은 아직\n * 어디에도 선언돼 있지 않다. 여기 두는 것은 임시다 — figure-model 이 선언하면 이\n * 파일을 지운다. 두 벌이 되면 갈린다.\n *\n * ## 지어낸 수가 아니다\n *\n * `docs/design/component-authoring-tool.md` §4.15 「목표 수치」에 적힌 값이다.\n *\n * 한 도면 자산 500 개\n * draw call ≤ 2500\n * 삼각형 ≤ 50 만\n *\n * 그 문서가 스스로 이렇게 적어 두었다 — **「측정 없이 정한 목표다. 저작도구가 서면\n * 실제 보드로 재고 조정한다」**(미결 D-20). 그래서 화면에도 근거를 함께 띄운다.\n * 숫자만 크게 내걸면 재 본 값처럼 읽힌다.\n */\n/**\n * 그림자 패스가 draw call 을 두 배로 만든다.\n *\n * 실측이다(2026-09-05, Apple M5). 자산 500~5,500 어디서나 실측이 figure-model 예측의\n * **정확히 2.00 배**로 나왔고, 그림자 맵을 끄면 예측과 같아졌다. 그림자 맵은 장면을\n * 한 번 더 그리기 때문이다. figure-model 의 `at.drawCalls` 는 색 패스만 센다.\n *\n * ## 등급 S 도 그림자를 진다\n *\n * 설계 문서 §4.15 는 「그림자는 등급 M 이상만」이라고 정했다. 그래서 처음에는 등급이\n * S 면 이 곱을 빼려 했다. **그런데 등급 S 로 세워 재 보니 draw call 이 똑같이 두 배로\n * 나왔다** — 그 규칙은 문서에만 있고 제품에는 없다.\n *\n * 그래서 등급을 보지 않고 언제나 곱한다. 없는 규칙을 믿고 셈하면 화면이 「S 로 하면\n * 두 배 놓을 수 있다」고 거짓말을 한다. things-scene 이 그 규칙을 구현하면 그때\n * 등급을 보면 된다 — 그쪽에 알릴 것.\n */\nexport const SHADOW_PASS_FACTOR = 2\n\nexport const SCENE_BUDGET = {\n /** 한 도면의 draw call. 로우폴리에서 먼저 차는 쪽이 대개 이것이다. */\n drawCalls: 2500,\n /** 한 도면의 삼각형. */\n triangles: 500_000\n} as const\n\n/** 무엇이 먼저 차나. */\nexport type BindingBudget = 'drawCalls' | 'triangles'\n\nexport interface FigureCapacity {\n /** 이 형상을 몇 개까지 놓을 수 있나. */\n instances: number\n /** 먼저 차는 예산. **여기부터 고치면 된다.** */\n bound: BindingBudget\n /** 인스턴스 하나가 무는 값. */\n per: number\n /** 그 예산의 한 도면 총량. */\n budget: number\n}\n\n/**\n * 몇 개까지 놓을 수 있나.\n *\n * 두 예산 중 **먼저 차는 쪽**이 답이다. 평균이나 곱을 내지 않는다 — 하나만 꽉 차도\n * 그것이 병목이고, 저작자가 고쳐야 할 곳은 그 하나다.\n *\n * 셈은 여기서 하지만 재료는 전부 figure-model 이 낸 것이다. `cost.groups` 가 인스턴스\n * 하나당 draw call 이고 `cost.triangles` 가 인스턴스 하나의 삼각형 수다.\n */\nexport function capacityOf(cost: FigureCost): FigureCapacity | undefined {\n return capacityFrom(cost.groups, cost.triangles)\n}\n\n/**\n * 저장된 값만으로 셈한다.\n *\n * 목록은 정본을 받아 오지 않는다 — 카드 수십 장마다 컴파일하면 목록이 무거워진다.\n * 대신 서버가 저장해 둔 `groups` · `triangles` 를 쓴다. **목록과 저작면이 같은 함수를\n * 쓰는 것**이 요점이다. 카드가 제 나름대로 셈하면 같은 형상이 두 화면에서 다른 개수로\n * 나온다.\n */\nexport function capacityFrom(groups: number, triangles: number): FigureCapacity | undefined {\n // 부품이 없으면 「무한히 놓을 수 있다」가 아니라 **답할 것이 없다**.\n if (groups <= 0 && triangles <= 0) {\n return undefined\n }\n\n // 화면이 실제로 내는 draw call. 그림자 패스가 장면을 한 번 더 그린다 — 등급과 무관하게.\n const calls = groups * SHADOW_PASS_FACTOR\n\n const byDrawCalls = calls > 0 ? Math.floor(SCENE_BUDGET.drawCalls / calls) : Infinity\n const byTriangles = triangles > 0 ? Math.floor(SCENE_BUDGET.triangles / triangles) : Infinity\n\n return byDrawCalls <= byTriangles\n ? { instances: byDrawCalls, bound: 'drawCalls', per: calls, budget: SCENE_BUDGET.drawCalls }\n : { instances: byTriangles, bound: 'triangles', per: triangles, budget: SCENE_BUDGET.triangles }\n}\n\n"]}
1
+ {"version":3,"file":"scene-budget.js","sourceRoot":"","sources":["../../client/modeller/scene-budget.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAA;AAEnC,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,gDAAgD;IAChD,SAAS,EAAE,IAAI;IACf,iBAAiB;IACjB,SAAS,EAAE,OAAO;CACV,CAAA;AAgBV;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,IAAgB;IACzC,OAAO,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;AAClD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,SAAiB;IAC5D,2CAA2C;IAC3C,IAAI,MAAM,IAAI,CAAC,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;QAClC,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,0DAA0D;IAC1D,MAAM,KAAK,GAAG,MAAM,GAAG,kBAAkB,CAAA;IAEzC,MAAM,WAAW,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IACrF,MAAM,WAAW,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IAE7F,OAAO,WAAW,IAAI,WAAW;QAC/B,CAAC,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,SAAS,EAAE;QAC5F,CAAC,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,CAAC,SAAS,EAAE,CAAA;AACpG,CAAC","sourcesContent":["import type { FigureCost } from '@hatiolab/figure-model'\n\n/**\n * 한 도면이 감당하는 양 — 「몇 개까지 편한가」의 분모.\n *\n * ## 이 값은 figure-model 것이어야 한다\n *\n * figure-model 의 `LIMITS` 는 전부 **Figure 하나짜리** 한도다(묶음 3 · 애니메이션 2 …).\n * 「몇 개를 놓을 수 있나」는 한 도면이 감당하는 양을 알아야 나오는데, 그 값은 아직\n * 어디에도 선언돼 있지 않다. 여기 두는 것은 임시다 — figure-model 이 선언하면 이\n * 파일을 지운다. 두 벌이 되면 갈린다.\n *\n * ## 지어낸 수가 아니다\n *\n * `docs/design/component-authoring-tool.md` §4.15 「목표 수치」에 적힌 값이다.\n *\n * 한 도면 자산 500 개\n * draw call ≤ 2500\n * 삼각형 ≤ 50 만\n *\n * 그 문서가 스스로 이렇게 적어 두었다 — **「측정 없이 정한 목표다. 저작도구가 서면\n * 실제 도면으로 재고 조정한다」**(미결 D-20). 그래서 화면에도 근거를 함께 띄운다.\n * 숫자만 크게 내걸면 재 본 값처럼 읽힌다.\n */\n/**\n * 그림자 패스가 draw call 을 두 배로 만든다.\n *\n * 실측이다(2026-09-05, Apple M5). 자산 500~5,500 어디서나 실측이 figure-model 예측의\n * **정확히 2.00 배**로 나왔고, 그림자 맵을 끄면 예측과 같아졌다. 그림자 맵은 장면을\n * 한 번 더 그리기 때문이다. figure-model 의 `at.drawCalls` 는 색 패스만 센다.\n *\n * ## 등급 S 도 그림자를 진다\n *\n * 설계 문서 §4.15 는 「그림자는 등급 M 이상만」이라고 정했다. 그래서 처음에는 등급이\n * S 면 이 곱을 빼려 했다. **그런데 등급 S 로 세워 재 보니 draw call 이 똑같이 두 배로\n * 나왔다** — 그 규칙은 문서에만 있고 제품에는 없다.\n *\n * 그래서 등급을 보지 않고 언제나 곱한다. 없는 규칙을 믿고 셈하면 화면이 「S 로 하면\n * 두 배 놓을 수 있다」고 거짓말을 한다. things-scene 이 그 규칙을 구현하면 그때\n * 등급을 보면 된다 — 그쪽에 알릴 것.\n */\nexport const SHADOW_PASS_FACTOR = 2\n\nexport const SCENE_BUDGET = {\n /** 한 도면의 draw call. 로우폴리에서 먼저 차는 쪽이 대개 이것이다. */\n drawCalls: 2500,\n /** 한 도면의 삼각형. */\n triangles: 500_000\n} as const\n\n/** 무엇이 먼저 차나. */\nexport type BindingBudget = 'drawCalls' | 'triangles'\n\nexport interface FigureCapacity {\n /** 이 형상을 몇 개까지 놓을 수 있나. */\n instances: number\n /** 먼저 차는 예산. **여기부터 고치면 된다.** */\n bound: BindingBudget\n /** 인스턴스 하나가 무는 값. */\n per: number\n /** 그 예산의 한 도면 총량. */\n budget: number\n}\n\n/**\n * 몇 개까지 놓을 수 있나.\n *\n * 두 예산 중 **먼저 차는 쪽**이 답이다. 평균이나 곱을 내지 않는다 — 하나만 꽉 차도\n * 그것이 병목이고, 저작자가 고쳐야 할 곳은 그 하나다.\n *\n * 셈은 여기서 하지만 재료는 전부 figure-model 이 낸 것이다. `cost.groups` 가 인스턴스\n * 하나당 draw call 이고 `cost.triangles` 가 인스턴스 하나의 삼각형 수다.\n */\nexport function capacityOf(cost: FigureCost): FigureCapacity | undefined {\n return capacityFrom(cost.groups, cost.triangles)\n}\n\n/**\n * 저장된 값만으로 셈한다.\n *\n * 목록은 정본을 받아 오지 않는다 — 카드 수십 장마다 컴파일하면 목록이 무거워진다.\n * 대신 서버가 저장해 둔 `groups` · `triangles` 를 쓴다. **목록과 저작면이 같은 함수를\n * 쓰는 것**이 요점이다. 카드가 제 나름대로 셈하면 같은 형상이 두 화면에서 다른 개수로\n * 나온다.\n */\nexport function capacityFrom(groups: number, triangles: number): FigureCapacity | undefined {\n // 부품이 없으면 「무한히 놓을 수 있다」가 아니라 **답할 것이 없다**.\n if (groups <= 0 && triangles <= 0) {\n return undefined\n }\n\n // 화면이 실제로 내는 draw call. 그림자 패스가 장면을 한 번 더 그린다 — 등급과 무관하게.\n const calls = groups * SHADOW_PASS_FACTOR\n\n const byDrawCalls = calls > 0 ? Math.floor(SCENE_BUDGET.drawCalls / calls) : Infinity\n const byTriangles = triangles > 0 ? Math.floor(SCENE_BUDGET.triangles / triangles) : Infinity\n\n return byDrawCalls <= byTriangles\n ? { instances: byDrawCalls, bound: 'drawCalls', per: calls, budget: SCENE_BUDGET.drawCalls }\n : { instances: byTriangles, bound: 'triangles', per: triangles, budget: SCENE_BUDGET.triangles }\n}\n\n"]}
@@ -290,7 +290,7 @@ let FigureListPage = class FigureListPage extends FigureListPageBase {
290
290
  },
291
291
  {
292
292
  /*
293
- 타입 이름은 **저장되는 식별자**다. 표시 이름과 다르게 보이게 두어야 보드가 무엇으로
293
+ 타입 이름은 **저장되는 식별자**다. 표시 이름과 다르게 보이게 두어야 도면이 무엇으로
294
294
  이 형상을 찾는지 헷갈리지 않는다.
295
295
  */
296
296
  type: 'string',