@things-factory/figure-ui 10.1.17 → 10.1.19

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.
@@ -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 * 바닥 기반은 땅에 서고, 천정 기반은 천장에서 제 키만큼 내려온 자리에 매달리고,\n * 중심 기반은 그 사이에 뜬다. `operato-scene` 의 archetype 이 같은 말을 한다\n * (`archetypes.ts`: ceiling — *zPos derived from ceiling minus depth*).\n */\nfunction bottomOf(placement: FigureSource['placement'], height: number, ceiling: number): number {\n switch (placement) {\n case 'ceiling':\n return ceiling - height\n case 'center':\n return (ceiling - height) / 2\n case 'floor':\n default:\n return 0\n }\n}\n\n/**\n * 밑면 높이를 씬이 받는 `zPos` 로 옮긴다.\n *\n * 씬은 부품 중심을 `zPos + geometricOffsetY` 에 놓고, 그 오프셋이 좌표 기준마다 다르다\n * (`real-object.ts`). 그래서 같은 밑면을 말하는 `zPos` 가 기준마다 다른 수다 — 바닥\n * 기준이면 밑면 그대로, 중심 기준이면 중심, 천장 기준이면 윗면.\n *\n * **여기가 유일하게 좌표 기준을 아는 자리다.** 저작한 값에는 이것이 안 들어간다.\n */\nfunction zPosOf(bottom: number, height: number, reference: ViewSettings['placement']): number {\n switch (reference) {\n case 'space':\n return bottom + height / 2\n case 'inverted':\n return bottom + height\n case 'floor':\n default:\n return bottom\n }\n}\n\ninterface SceneHandle {\n dispose?: () => void\n resize?: () => void\n fit?: (type?: string) => void\n root?: {\n invalidate?: () => void\n fitCameraToModel?: (margin?: number) => void\n /** 보기 설정을 넣는 자리. 씬이 스스로 반응한다. */\n set?: (patch: Record<string, unknown>) => void\n /** 세워 둔 인스턴스들. clip 을 몰 때 전부에 같은 값을 준다. */\n components?: { set?: (property: string, value: unknown) => void }[]\n }\n}\n\n@customElement('figure-preview')\nexport class FigurePreview extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n position: relative;\n display: flex;\n overflow: hidden;\n background-color: ${unsafeCSS(WORKBENCH)};\n font: var(--label-font, inherit);\n font-size: 0.76rem;\n }\n\n div[stage] {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n }\n\n /* 어느 것이 몇 배인지 — 나란히 놓아도 표가 없으면 읽을 수 없다 */\n div[legend] {\n position: absolute;\n top: 8px;\n left: 8px;\n display: flex;\n gap: 6px;\n padding: 4px 8px;\n color: rgba(255, 255, 255, 0.9);\n background-color: rgba(28, 28, 30, 0.6);\n border-radius: 6px;\n font-size: 0.72rem;\n }\n\n /*\n Clip 조작. **저작자가 제 애니메이션을 만져 볼 유일한 자리다.**\n\n 보드에서는 트윈이 clip 을 몬다 — 「닙이 40% 열렸다」는 사실이 어디선가 온다. 저작\n 중에는 그 사실을 주는 것이 없어서, 손대지 않으면 hold 인 clip 은 0 에 머문 채\n 영영 안 움직인다. 포크가 안 올라가는 것을 저작자가 「안 되는구나」로 읽게 된다.\n\n **미리보기 것이지 자산의 것이 아니다.** 여기서 민 값은 저장되지 않는다.\n */\n div[clips] {\n position: absolute;\n right: 8px;\n bottom: 8px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n color: rgba(255, 255, 255, 0.92);\n background-color: rgba(28, 28, 30, 0.72);\n border-radius: 8px;\n font-size: 0.72rem;\n }\n div[clips] label {\n display: grid;\n grid-template-columns: auto 96px 34px;\n align-items: center;\n gap: 8px;\n }\n div[clips] span[drive] {\n opacity: 0.6;\n font-size: 0.66rem;\n }\n div[clips] input {\n width: 96px;\n accent-color: var(--md-sys-color-primary);\n }\n div[clips] output {\n font-variant-numeric: tabular-nums;\n text-align: right;\n opacity: 0.85;\n }\n\n /* 세울 수 없는 상태를 빈 화면으로 두지 않는다 — 왜 안 보이는지 말한다 */\n div[cannot] {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--spacing-large, 12px);\n text-align: center;\n line-height: 1.6;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: ${unsafeCSS(WORKBENCH)};\n }\n `\n\n /** 저작 중인 것. 저장 형식으로 받는다 — 보드가 소비하는 것이 이 형식이다. */\n @property({ type: Object }) source?: FigureSource\n\n /**\n * 보기 설정. 저장되지 않는다(`figure-view.ts`).\n *\n * 한동안 이 화면만 조명과 환경을 **하드코딩**했다. 저작자가 환경 탭에서 무엇을 바꿔도\n * 편집 캔버스만 달라지고 여기는 그대로였다 — 두 화면이 같은 것을 다르게 보여 주었다.\n */\n @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW\n\n @query('div[stage]') private stage!: HTMLDivElement\n\n /** 세울 수 없는 사유. 없으면 세운 것이다. */\n @state() private cannot = ''\n\n private scene?: SceneHandle\n /** 지금 세워 둔 것의 값. 같으면 다시 세우지 않는다. */\n private standing?: string\n private watcher?: ResizeObserver\n\n /** 지금 민 clip 값. clip 이름 → 수. **저장되지 않는다.** */\n @state() private driving: Record<string, number> = {}\n\n render() {\n const clips = this.source?.animations ?? []\n\n return html`\n <div stage></div>\n ${this.cannot\n ? html`<div cannot>${this.cannot}</div>`\n : html`<div legend>\n ${PREVIEW_WAYS.map(way => html`<span>${way.name}</span>`)}\n </div>\n ${clips.length > 0 ? this.renderClips(clips) : ''}`}\n `\n }\n\n /**\n * Clip 하나에 손잡이 하나.\n *\n * 수의 뜻이 `drive` 에 따라 다르므로 그것을 이름 옆에 적는다. `loop` 는 배속이라 0 이\n * 멈춤이고, `hold` 는 0~1 위치라 0 이 처음이다. 둘을 같은 눈금으로 두면 「1」이 무슨\n * 뜻인지 매번 헷갈린다.\n */\n private renderClips(clips: NonNullable<FigureSource['animations']>) {\n return html`\n <div clips>\n ${clips.map(clip => {\n const loop = (clip.drive ?? 'loop') === 'loop'\n const value = this.driving[clip.name] ?? (loop ? 1 : 0)\n\n return html`\n <label>\n <span>${clip.name} <span drive>${loop ? '배속' : '위치'}</span></span>\n <input\n type=\"range\"\n min=\"0\"\n max=${loop ? 3 : 1}\n step=\"0.05\"\n .value=${String(value)}\n @input=${(e: Event) => this.drive(clip.name, Number((e.target as HTMLInputElement).value))}\n />\n <output>${value.toFixed(2)}</output>\n </label>\n `\n })}\n </div>\n `\n }\n\n /**\n * 세워 둔 인스턴스 전부에 같은 값을 준다.\n *\n * 하나만 몰면 세 배율이 서로 다른 자세로 서서, 크기 반응을 보려고 나란히 놓은 뜻이\n * 사라진다.\n */\n private drive(name: string, value: number): void {\n this.driving = { ...this.driving, [name]: value }\n\n for (const component of this.scene?.root?.components ?? []) {\n component.set?.('clips', { ...this.driving })\n }\n }\n\n /**\n * 보기 설정이 바뀌면 **다시 세우지 않고 씬에 넣는다** — 두 개만 빼고.\n *\n * 조명·환경·바탕은 씬이 스스로 반응한다(`three-capability` 의 `onchange`). 슬라이더를\n * 끄는 동안 다시 세우면 카메라가 매 프레임 처음으로 돌아간다.\n *\n * 좌표 기준과 천장 높이는 다르다. 그 둘은 컴포넌트의 `zPos` 를 만들 때 쓰였으므로,\n * 이미 세워 둔 것에 넣어 봐야 자리가 안 바뀐다. 그것만 다시 세운다.\n */\n willUpdate(changed: Map<string, unknown>) {\n if (!this.hasUpdated) return\n\n if (changed.has('source')) {\n this.standUp()\n return\n }\n\n if (!changed.has('view')) return\n\n const before = changed.get('view') as ViewSettings | undefined\n const now = this.view ?? DEFAULT_VIEW\n if (before?.placement !== now.placement || before?.ceilingHeight !== now.ceilingHeight) {\n this.standUp()\n return\n }\n\n this.scene?.root?.set?.(sceneLook(now))\n }\n\n firstUpdated() {\n this.standUp()\n this.watcher = new ResizeObserver(() => this.scene?.resize?.())\n this.watcher.observe(this)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.watcher?.disconnect()\n this.watcher = undefined\n this.teardown()\n }\n\n /**\n * 저작한 것을 컴파일해 세운다.\n *\n * **형식을 어겼으면 세우지 않는다.** 반쯤 세운 것을 보여 주면 무엇이 잘못됐는지 알 수\n * 없다. 판정 패널이 이미 사유를 말하고 있으므로 여기서는 세울 수 없다고만 한다.\n */\n private standUp() {\n const source = this.source\n if (!source || source.parts.length === 0) {\n this.cannot = i18next.t('figure.text.preview-needs-a-part')\n this.teardown()\n return\n }\n\n const view = this.view ?? DEFAULT_VIEW\n /* 좌표 기준과 천장 높이가 컴포넌트의 `zPos` 를 만든다 — 서명에 안 넣으면 다시 세워도 그대로 선다. */\n const signature = JSON.stringify([source, view.placement, view.ceilingHeight])\n if (this.scene && this.standing === signature) return\n\n const { errors } = validate(source)\n if (errors.length > 0) {\n this.cannot = i18next.t('figure.text.preview-blocked-by-errors')\n this.teardown()\n return\n }\n\n this.teardown()\n // 표본이 바뀌면 민 값도 처음으로 돌린다 — 다른 자산의 clip 이름이 남으면 안 된다.\n this.driving = {}\n\n try {\n registerFigure(compile(source) as never)\n\n /*\n 보드가 하는 일을 그대로 한다. 컴포넌트에는 타입 이름과 자리·크기만 적는다 —\n 청사진도 원본도 보드에 들어가지 않는다.\n\n 가장 큰 것이 들어갈 만큼 판을 잡고, 세로로 줄을 나눠 나란히 놓는다.\n */\n const { base } = source\n const biggest = PREVIEW_FACTOR\n /** 이 조합에서 축별 배율. 조합에 든 축만 키운다. */\n const growth = (way: (typeof PREVIEW_WAYS)[number]) => ({\n x: way.axes.includes('x') ? PREVIEW_FACTOR : 1,\n y: way.axes.includes('y') ? PREVIEW_FACTOR : 1,\n z: way.axes.includes('z') ? PREVIEW_FACTOR : 1\n })\n // 여백을 넉넉히 두면 화면에서 형상이 작아진다. 가장 큰 것이 겨우 들어갈 만큼만 잡는다.\n /** 네 칸씩 두 줄. */\n const COLUMNS = 4\n const cell = { x: base.x * biggest * 1.25, z: base.z * biggest * 1.25 }\n const rows = Math.ceil(PREVIEW_WAYS.length / COLUMNS)\n /*\n 씬의 낱말이 형식과 다르다 — `width` 가 형식의 x, `height` 가 z, `depth` 가 y 다.\n 옮기는 일을 여기 한 줄에 모아 둔다.\n\n `depth` 를 늘 적는 이유가 있다. 안 적으면 평면 두 축의 기하평균으로 잡혀서,\n 「y 만 키운다」가 x·z 를 안 건드리는데도 높이가 안 오른다.\n\n 높이는 **배치 기준이 정한다.** 바닥 기반은 땅에서 올라오고, 천정 기반은 천장에\n 붙어 내려온다. 여덟을 나란히 세우는 자리라 이것이 눈에 바로 보인다 — 바닥 기반이면\n 밑면이, 천정 기반이면 윗면이 한 줄이 된다.\n */\n const components = PREVIEW_WAYS.map((way, at) => {\n const grow = growth(way)\n const column = at % COLUMNS\n const row = Math.floor(at / COLUMNS)\n const width = Math.round(base.x * grow.x)\n const height = Math.round(base.z * grow.z)\n const tall = Math.round(base.y * grow.y)\n const bottom = bottomOf(source.placement, tall, view.ceilingHeight)\n\n return {\n type: source.type,\n id: `at-${way.name}`,\n // 칸 가운데에 놓는다 — 크기가 저마다 달라 왼쪽에 맞추면 줄이 안 읽힌다.\n left: Math.round(column * cell.x + (cell.x - width) / 2),\n top: Math.round(row * cell.z + (cell.z - height) / 2),\n width,\n height,\n depth: tall,\n zPos: Math.round(zPosOf(bottom, tall, view.placement))\n }\n })\n\n const draft = {\n width: Math.round(cell.x * COLUMNS),\n height: Math.round(cell.z * rows),\n /*\n 판의 높이도 가장 큰 인스턴스를 담아야 한다.\n\n 안 주면 카메라가 평면만 보고 자리를 잡아서, 키가 큰 모델은 밑동만 보인다.\n 시그널타워를 2 배로 놓으면 2200 인데 판이 그것을 모르면 받침만 화면에 든다.\n\n 가장 높은 **윗면**으로 잡는다. 천정 기반은 천장 높이에 매달리므로 제 키보다\n 훨씬 위에 있다 — 키만 보고 잡으면 천장에 걸린 것이 화면 밖으로 나간다.\n */\n depth: Math.round(\n Math.max(...components.map(c => bottomOf(source.placement, c.depth, view.ceilingHeight) + c.depth)) * 1.1\n )\n }\n\n this.scene = create({\n target: this.stage,\n model: {\n type: 'model-layer',\n width: draft.width,\n height: draft.height,\n depth: draft.depth,\n threed: true,\n sky: 'color',\n /*\n 보기 설정을 그대로 싣는다.\n\n 여기만 조명·환경·바탕을 하드코딩하고 있었다. 저작자가 환경 탭에서 조명을\n 올려도 편집 캔버스만 밝아지고 이 화면은 그대로여서, 같은 것을 두 화면이\n 다르게 보여 주었다.\n\n `sceneLook` 이 좌표 기준과 천장 높이를 빼고 준다 — 그 둘은 씬에 그대로 넣을\n 값이 아니라 위에서 `zPos` 를 정하는 데 쓴다. 좌표 기준만 여기 따로 싣는다.\n */\n ...sceneLook(view),\n placement: view.placement,\n pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),\n components\n },\n // 보기만 하는 자리다. 집거나 끌 수 없어야 한다.\n mode: 0\n }) as SceneHandle\n\n this.standing = signature\n this.scene.resize?.()\n this.scene.fit?.('ratio')\n this.scene.root?.fitCameraToModel?.()\n this.cannot = ''\n } catch (e) {\n // 원인을 지어내지 않는다 — 던진 말을 그대로 보여 준다.\n this.cannot = (e as Error).message\n }\n }\n\n private teardown() {\n this.scene?.dispose?.()\n this.scene = undefined\n this.standing = undefined\n }\n}\n"]}
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"]}
@@ -155,7 +155,7 @@ let FigureSide = class FigureSide extends localize(i18next)(LitElement) {
155
155
  * 0 이면 아무것도 안 붙인다 — 「0」을 붙이면 없는 것이 있는 것처럼 자리를 차지한다.
156
156
  */
157
157
  get clipCount() {
158
- const n = this.draft?.animations?.length ?? 0;
158
+ const n = (this.draft?.animations?.length ?? 0) + (this.draft?.parameters?.length ?? 0);
159
159
  return n === 0 ? undefined : n;
160
160
  }
161
161
  render() {
@@ -1 +1 @@
1
- {"version":3,"file":"figure-side.js","sourceRoot":"","sources":["../../client/modeller/figure-side.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport './figure-inspector.js'\nimport './figure-capabilities.js'\nimport './figure-animations.js'\nimport './figure-report.js'\nimport './figure-settings.js'\nimport './figure-history-panel.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport type { FigureSource } from '@hatiolab/figure-model'\n\nimport type { FigureDraft, PartModel } from './figure-source.js'\nimport { DEFAULT_VIEW } from './figure-view.js'\nimport type { ViewSettings } from './figure-view.js'\n\ntype Tab = 'properties' | 'capability' | 'animation' | 'verdict' | 'view' | 'history'\n\n/**\n * 저작면 오른쪽 칸 — 속성과 판정을 탭으로 겹쳐 둔다.\n *\n * ## 왜 위아래로 나누지 않나\n *\n * 둘 다 세로로 긴 화면이다. 위아래로 나누면 양쪽 다 잘려서, 속성은 재질까지 못\n * 내려가고 판정은 무게까지 못 내려간다. 탭으로 겹치면 각자 칸 전체를 쓴다.\n *\n * ## 그래도 개수는 늘 보인다\n *\n * 성숙도의 축 하나 —「몇 개까지 편한가」— 는 부품을 고칠 때마다 움직이고, 그것이\n * 보여야 저작자가 고칠 수 있다. 탭 뒤로 숨으면 저장하고 나서야 알게 된다. 그래서 그\n * 숫자만 **탭 이름표에 남긴다** — 성숙도를 덮어 두고 속성을 만져도 따라 움직이는\n * 것이 보인다.\n *\n * 판정은 화면에서 감춰도 **DOM 에는 남는다.** 떼었다 붙이면 다시 세느라 값이 잠깐\n * 비고, 펼쳐 둔 추이 그림 같은 상태도 날아간다.\n */\n@customElement('figure-side')\nexport class FigureSide extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n display: flex;\n flex-direction: column;\n min-height: 0;\n overflow: hidden;\n background-color: var(--md-sys-color-surface-container-lowest);\n border-left: 1px solid var(--md-sys-color-outline-variant);\n color: var(--md-sys-color-on-surface);\n font: var(--label-font, inherit);\n /*\n 이 패널의 본문 크기를 여기서 한 번 정한다.\n\n 전에는 규칙마다 따로 적었다. 그러면 빠뜨린 규칙이 본문 기본 크기(1rem)를\n 물려받아 그 줄만 제목처럼 커진다. 오류가 아니라서 화면을 띄워 봐야 안다.\n 이 파일에서만 두 번 났다.\n\n form 요소(input · select · button)는 글꼴을 물려받지 않으므로 각자 적어야 한다.\n 그것 말고는 여기 값을 따른다.\n */\n font-size: 0.76rem;\n }\n\n div[tabs] {\n display: flex;\n flex: none;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n button[tab] {\n flex: 1;\n /*\n **아이콘 하나가 한 칸을 쓴다.**\n\n 전에는 이름을 적었는데, 넷이 좁은 칸을 나눠 쓰면서 접혀 「성숙 / 도」가 되었다. 접히면\n 그 탭만 두 줄이 되어 높이가 서로 달라진다. 다섯이 되면서 더는 줄여 짜 넣을 수 없어\n 아이콘으로 바꿨고, 이름은 title 과 aria-label 로 남는다.\n\n 배지는 여전히 자리를 차지하지 않고 **아이콘 위에 겹친다**(아래 span[badge]). 개수는\n 곁들이는 것이다.\n */\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n white-space: nowrap;\n padding: 7px 6px;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background: none;\n border: none;\n /* 고른 탭을 밑줄로 말한다 — 칸을 색으로 채우면 아래 내용과 경계가 흐려진다 */\n border-bottom: 2px solid transparent;\n cursor: pointer;\n }\n button[tab]:hover:not([on]) {\n background-color: var(--md-sys-color-surface-container);\n }\n button[tab][on] {\n font-weight: 700;\n color: var(--md-sys-color-primary);\n border-bottom-color: var(--md-sys-color-primary);\n }\n\n /*\n 판정을 덮어 두어도 개수는 여기 남는다.\n\n 자리를 먹지 않게 오른쪽 위에 겹쳐 둔다 — 탭 이름을 밀지 않으므로 이름이 접히지 않는다.\n 숫자가 커져도(1,200 같은 것) 이름 위로 넘칠 뿐 줄을 바꾸지 않는다.\n */\n button[tab] md-icon {\n font-size: 20px;\n width: 20px;\n height: 20px;\n }\n span[badge] {\n position: absolute;\n /* 이름 위에 겹치되 위로 붙지 않게 — 탭 가운데 글자와 눈높이를 조금 맞춘다 */\n top: 5px;\n right: 2px;\n padding: 0 5px;\n font-size: 0.62rem;\n font-weight: 700;\n line-height: 1.5;\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border-radius: 999em;\n pointer-events: none;\n }\n\n figure-inspector,\n figure-animations,\n figure-report,\n figure-settings {\n flex: 1;\n min-height: 0;\n border-left: none;\n }\n /* 감추되 떼지는 않는다 */\n [off] {\n display: none;\n }\n `\n\n /*\n 두 형태가 이 패널에서 갈린다.\n\n 속성은 **수정하는** 곳이라 편집 중인 씬 모델을 받는다. 판정은 **저장될 것을 측정하는**\n 곳이라 변환된 FigureSource 를 받는다. 변환은 모델러 페이지가 한 번만 해서 내려보낸다.\n */\n @property({ type: String }) figureName = ''\n /** 카탈로그 분류·태그. 속성 탭이 보여 주고 바꾼다. */\n @property({ type: String }) figureCategory?: string\n @property({ type: Array }) figureTags?: string[]\n /** 이력 탭이 발행하고 되돌리는 데 필요한 것들. 정본은 저작면이 든다. */\n @property({ type: String }) figureId?: string\n @property({ type: String }) figureState?: 'draft' | 'released'\n @property({ type: Number }) figureVersion = 0\n @property({ type: Boolean }) dirty = false\n\n @property({ type: Object }) draft?: FigureDraft\n @property({ type: Array }) parts: PartModel[] = []\n @property({ type: Object }) source?: FigureSource\n @property({ type: Number }) selected = -1\n /** 저작 화면의 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다. */\n @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW\n\n @state() private tab: Tab = 'properties'\n /** 판정이 알려 준 개수. 탭 이름표에 붙는다. */\n @state() private instances?: number\n\n /**\n * 이 도형이 든 clip 수. 탭 아이콘에 붙는다.\n *\n * 아이콘만으로는 「여기에 무언가 있다」를 알 수 없다. 대부분의 도형은 움직이지 않으므로\n * 이 탭은 늘 비어 있을 것이고, 그러면 움직이는 도형을 열었을 때 그 사실이 눈에 걸려야 한다.\n * 0 이면 아무것도 안 붙인다 — 「0」을 붙이면 없는 것이 있는 것처럼 자리를 차지한다.\n */\n private get clipCount(): number | undefined {\n const n = this.draft?.animations?.length ?? 0\n return n === 0 ? undefined : n\n }\n\n render() {\n return html`\n <div tabs>\n ${this.renderTab('properties', 'tune', i18next.t('figure.label.properties'))}\n ${this.renderTab('capability', 'conveyor_belt', i18next.t('figure.label.tab-capability'))}\n ${this.renderTab('animation', 'animation', i18next.t('figure.label.animation'), this.clipCount)}\n ${this.renderTab('verdict', 'insights', i18next.t('figure.label.maturity'), this.instances)}\n ${this.renderTab('view', 'wb_sunny', i18next.t('figure.label.tab-environment'))}\n ${this.renderTab('history', 'history', i18next.t('figure.label.history'))}\n </div>\n\n <figure-inspector\n ?off=${this.tab !== 'properties'}\n .figureId=${this.figureId}\n .figureName=${this.figureName}\n .figureCategory=${this.figureCategory}\n .figureTags=${this.figureTags}\n .draft=${this.draft}\n .parts=${this.parts}\n .selected=${this.selected}\n ></figure-inspector>\n\n <figure-capabilities\n ?off=${this.tab !== 'capability'}\n .draft=${this.draft}\n .parts=${this.parts}\n ></figure-capabilities>\n\n <figure-animations\n ?off=${this.tab !== 'animation'}\n .draft=${this.draft}\n .parts=${this.parts}\n ></figure-animations>\n\n <figure-report\n ?off=${this.tab !== 'verdict'}\n .source=${this.source}\n @verdict-changed=${(e: CustomEvent) => (this.instances = e.detail.instances)}\n ></figure-report>\n\n <figure-settings ?off=${this.tab !== 'view'} .view=${this.view}></figure-settings>\n\n <!--\n 이력은 감춰도 DOM 에 남긴다 — 다른 탭들과 같은 규칙이다. 떼었다 붙이면 판본을 다시 부른다.\n -->\n <figure-history-panel\n ?off=${this.tab !== 'history'}\n .figureId=${this.figureId}\n .figureState=${this.figureState}\n .figureVersion=${this.figureVersion}\n .dirty=${this.dirty}\n ></figure-history-panel>\n `\n }\n\n /**\n * 탭 하나 — **아이콘이 이름을 대신한다.**\n *\n * 탭이 다섯이 되면서 이름이 좁은 칸을 나눠 쓰다 접혔다. 앞으로 더 늘어날 자리이므로 이름을\n * 줄여 짜 넣는 대신 아이콘으로 바꾼다.\n *\n * **이름을 버리지는 않는다.** 아이콘만으로는 무엇인지 모르는 사람이 있고, 특히 「능력」처럼\n * 이 도구에만 있는 개념이 그렇다. 그래서 이름은 `title` 로 남아 마우스를 올리면 나오고,\n * `aria-label` 로 읽는 기계에도 그대로 간다.\n */\n private renderTab(tab: Tab, icon: string, label: string, badge?: number) {\n return html`\n <button\n tab\n ?on=${this.tab === tab}\n title=${label}\n aria-label=${label}\n @click=${() => (this.tab = tab)}\n >\n <md-icon>${icon}</md-icon>${badge == null ? '' : html`<span badge>${badge.toLocaleString()}</span>`}\n </button>\n `\n }\n}\n"]}
1
+ {"version":3,"file":"figure-side.js","sourceRoot":"","sources":["../../client/modeller/figure-side.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport './figure-inspector.js'\nimport './figure-capabilities.js'\nimport './figure-animations.js'\nimport './figure-report.js'\nimport './figure-settings.js'\nimport './figure-history-panel.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport type { FigureSource } from '@hatiolab/figure-model'\n\nimport type { FigureDraft, PartModel } from './figure-source.js'\nimport { DEFAULT_VIEW } from './figure-view.js'\nimport type { ViewSettings } from './figure-view.js'\n\ntype Tab = 'properties' | 'capability' | 'animation' | 'verdict' | 'view' | 'history'\n\n/**\n * 저작면 오른쪽 칸 — 속성과 판정을 탭으로 겹쳐 둔다.\n *\n * ## 왜 위아래로 나누지 않나\n *\n * 둘 다 세로로 긴 화면이다. 위아래로 나누면 양쪽 다 잘려서, 속성은 재질까지 못\n * 내려가고 판정은 무게까지 못 내려간다. 탭으로 겹치면 각자 칸 전체를 쓴다.\n *\n * ## 그래도 개수는 늘 보인다\n *\n * 성숙도의 축 하나 —「몇 개까지 편한가」— 는 부품을 고칠 때마다 움직이고, 그것이\n * 보여야 저작자가 고칠 수 있다. 탭 뒤로 숨으면 저장하고 나서야 알게 된다. 그래서 그\n * 숫자만 **탭 이름표에 남긴다** — 성숙도를 덮어 두고 속성을 만져도 따라 움직이는\n * 것이 보인다.\n *\n * 판정은 화면에서 감춰도 **DOM 에는 남는다.** 떼었다 붙이면 다시 세느라 값이 잠깐\n * 비고, 펼쳐 둔 추이 그림 같은 상태도 날아간다.\n */\n@customElement('figure-side')\nexport class FigureSide extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n display: flex;\n flex-direction: column;\n min-height: 0;\n overflow: hidden;\n background-color: var(--md-sys-color-surface-container-lowest);\n border-left: 1px solid var(--md-sys-color-outline-variant);\n color: var(--md-sys-color-on-surface);\n font: var(--label-font, inherit);\n /*\n 이 패널의 본문 크기를 여기서 한 번 정한다.\n\n 전에는 규칙마다 따로 적었다. 그러면 빠뜨린 규칙이 본문 기본 크기(1rem)를\n 물려받아 그 줄만 제목처럼 커진다. 오류가 아니라서 화면을 띄워 봐야 안다.\n 이 파일에서만 두 번 났다.\n\n form 요소(input · select · button)는 글꼴을 물려받지 않으므로 각자 적어야 한다.\n 그것 말고는 여기 값을 따른다.\n */\n font-size: 0.76rem;\n }\n\n div[tabs] {\n display: flex;\n flex: none;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n button[tab] {\n flex: 1;\n /*\n **아이콘 하나가 한 칸을 쓴다.**\n\n 전에는 이름을 적었는데, 넷이 좁은 칸을 나눠 쓰면서 접혀 「성숙 / 도」가 되었다. 접히면\n 그 탭만 두 줄이 되어 높이가 서로 달라진다. 다섯이 되면서 더는 줄여 짜 넣을 수 없어\n 아이콘으로 바꿨고, 이름은 title 과 aria-label 로 남는다.\n\n 배지는 여전히 자리를 차지하지 않고 **아이콘 위에 겹친다**(아래 span[badge]). 개수는\n 곁들이는 것이다.\n */\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n white-space: nowrap;\n padding: 7px 6px;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background: none;\n border: none;\n /* 고른 탭을 밑줄로 말한다 — 칸을 색으로 채우면 아래 내용과 경계가 흐려진다 */\n border-bottom: 2px solid transparent;\n cursor: pointer;\n }\n button[tab]:hover:not([on]) {\n background-color: var(--md-sys-color-surface-container);\n }\n button[tab][on] {\n font-weight: 700;\n color: var(--md-sys-color-primary);\n border-bottom-color: var(--md-sys-color-primary);\n }\n\n /*\n 판정을 덮어 두어도 개수는 여기 남는다.\n\n 자리를 먹지 않게 오른쪽 위에 겹쳐 둔다 — 탭 이름을 밀지 않으므로 이름이 접히지 않는다.\n 숫자가 커져도(1,200 같은 것) 이름 위로 넘칠 뿐 줄을 바꾸지 않는다.\n */\n button[tab] md-icon {\n font-size: 20px;\n width: 20px;\n height: 20px;\n }\n span[badge] {\n position: absolute;\n /* 이름 위에 겹치되 위로 붙지 않게 — 탭 가운데 글자와 눈높이를 조금 맞춘다 */\n top: 5px;\n right: 2px;\n padding: 0 5px;\n font-size: 0.62rem;\n font-weight: 700;\n line-height: 1.5;\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border-radius: 999em;\n pointer-events: none;\n }\n\n figure-inspector,\n figure-animations,\n figure-report,\n figure-settings {\n flex: 1;\n min-height: 0;\n border-left: none;\n }\n /* 감추되 떼지는 않는다 */\n [off] {\n display: none;\n }\n `\n\n /*\n 두 형태가 이 패널에서 갈린다.\n\n 속성은 **수정하는** 곳이라 편집 중인 씬 모델을 받는다. 판정은 **저장될 것을 측정하는**\n 곳이라 변환된 FigureSource 를 받는다. 변환은 모델러 페이지가 한 번만 해서 내려보낸다.\n */\n @property({ type: String }) figureName = ''\n /** 카탈로그 분류·태그. 속성 탭이 보여 주고 바꾼다. */\n @property({ type: String }) figureCategory?: string\n @property({ type: Array }) figureTags?: string[]\n /** 이력 탭이 발행하고 되돌리는 데 필요한 것들. 정본은 저작면이 든다. */\n @property({ type: String }) figureId?: string\n @property({ type: String }) figureState?: 'draft' | 'released'\n @property({ type: Number }) figureVersion = 0\n @property({ type: Boolean }) dirty = false\n\n @property({ type: Object }) draft?: FigureDraft\n @property({ type: Array }) parts: PartModel[] = []\n @property({ type: Object }) source?: FigureSource\n @property({ type: Number }) selected = -1\n /** 저작 화면의 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다. */\n @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW\n\n @state() private tab: Tab = 'properties'\n /** 판정이 알려 준 개수. 탭 이름표에 붙는다. */\n @state() private instances?: number\n\n /**\n * 이 도형이 든 clip 수. 탭 아이콘에 붙는다.\n *\n * 아이콘만으로는 「여기에 무언가 있다」를 알 수 없다. 대부분의 도형은 움직이지 않으므로\n * 이 탭은 늘 비어 있을 것이고, 그러면 움직이는 도형을 열었을 때 그 사실이 눈에 걸려야 한다.\n * 0 이면 아무것도 안 붙인다 — 「0」을 붙이면 없는 것이 있는 것처럼 자리를 차지한다.\n */\n private get clipCount(): number | undefined {\n const n = (this.draft?.animations?.length ?? 0) + (this.draft?.parameters?.length ?? 0)\n return n === 0 ? undefined : n\n }\n\n render() {\n return html`\n <div tabs>\n ${this.renderTab('properties', 'tune', i18next.t('figure.label.properties'))}\n ${this.renderTab('capability', 'conveyor_belt', i18next.t('figure.label.tab-capability'))}\n ${this.renderTab('animation', 'animation', i18next.t('figure.label.animation'), this.clipCount)}\n ${this.renderTab('verdict', 'insights', i18next.t('figure.label.maturity'), this.instances)}\n ${this.renderTab('view', 'wb_sunny', i18next.t('figure.label.tab-environment'))}\n ${this.renderTab('history', 'history', i18next.t('figure.label.history'))}\n </div>\n\n <figure-inspector\n ?off=${this.tab !== 'properties'}\n .figureId=${this.figureId}\n .figureName=${this.figureName}\n .figureCategory=${this.figureCategory}\n .figureTags=${this.figureTags}\n .draft=${this.draft}\n .parts=${this.parts}\n .selected=${this.selected}\n ></figure-inspector>\n\n <figure-capabilities\n ?off=${this.tab !== 'capability'}\n .draft=${this.draft}\n .parts=${this.parts}\n ></figure-capabilities>\n\n <figure-animations\n ?off=${this.tab !== 'animation'}\n .draft=${this.draft}\n .parts=${this.parts}\n ></figure-animations>\n\n <figure-report\n ?off=${this.tab !== 'verdict'}\n .source=${this.source}\n @verdict-changed=${(e: CustomEvent) => (this.instances = e.detail.instances)}\n ></figure-report>\n\n <figure-settings ?off=${this.tab !== 'view'} .view=${this.view}></figure-settings>\n\n <!--\n 이력은 감춰도 DOM 에 남긴다 — 다른 탭들과 같은 규칙이다. 떼었다 붙이면 판본을 다시 부른다.\n -->\n <figure-history-panel\n ?off=${this.tab !== 'history'}\n .figureId=${this.figureId}\n .figureState=${this.figureState}\n .figureVersion=${this.figureVersion}\n .dirty=${this.dirty}\n ></figure-history-panel>\n `\n }\n\n /**\n * 탭 하나 — **아이콘이 이름을 대신한다.**\n *\n * 탭이 다섯이 되면서 이름이 좁은 칸을 나눠 쓰다 접혔다. 앞으로 더 늘어날 자리이므로 이름을\n * 줄여 짜 넣는 대신 아이콘으로 바꾼다.\n *\n * **이름을 버리지는 않는다.** 아이콘만으로는 무엇인지 모르는 사람이 있고, 특히 「능력」처럼\n * 이 도구에만 있는 개념이 그렇다. 그래서 이름은 `title` 로 남아 마우스를 올리면 나오고,\n * `aria-label` 로 읽는 기계에도 그대로 간다.\n */\n private renderTab(tab: Tab, icon: string, label: string, badge?: number) {\n return html`\n <button\n tab\n ?on=${this.tab === tab}\n title=${label}\n aria-label=${label}\n @click=${() => (this.tab = tab)}\n >\n <md-icon>${icon}</md-icon>${badge == null ? '' : html`<span badge>${badge.toLocaleString()}</span>`}\n </button>\n `\n }\n}\n"]}
@@ -94,6 +94,14 @@ export interface FigureDraft {
94
94
  * 저장하는 것만으로 **조용히 지워지기** 때문이다.
95
95
  */
96
96
  animations?: FigureSource['animations'];
97
+ /**
98
+ * 값이 만드는 자세 — **애니메이션과 다른 칸이다.**
99
+ *
100
+ * 저것은 시각이 흘러 움직이는 것(도는 롤러)이고 이것은 인스턴스가 준 값이 그대로 자세가
101
+ * 되는 것(1200mm 내려온 호이스트)이다. 한동안 애니메이션 안에 `drive: 'hold'` 로 숨어
102
+ * 있었고, 그래서 「길이값을 변수로 노출하라」는 요청에 아무도 이것을 못 찾았다(ADR-0051).
103
+ */
104
+ parameters?: FigureSource['parameters'];
97
105
  /**
98
106
  * 이 도형이 지는 능력. 씬의 capability mixin 이름이다.
99
107
  *
@@ -76,6 +76,7 @@ export function toFigureSource(draft, parts) {
76
76
  styleKit: draft.styleKit,
77
77
  parts: parts.map(part => partTo(part, half)),
78
78
  animations: draft.animations,
79
+ parameters: draft.parameters,
79
80
  capabilities: draft.capabilities
80
81
  });
81
82
  }
@@ -138,6 +139,7 @@ export function fromFigureSource(source) {
138
139
  detailLevel: source.detailLevel,
139
140
  styleKit: source.styleKit,
140
141
  animations: source.animations,
142
+ parameters: source.parameters,
141
143
  capabilities: source.capabilities
142
144
  });
143
145
  return { draft, parts: source.parts.map(part => partFrom(part, half)) };
@@ -1 +1 @@
1
- {"version":3,"file":"figure-source.js","sourceRoot":"","sources":["../../client/modeller/figure-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { FIGURE_SOURCE_VERSION, type DetailLevel, type FigurePart, type FigurePlacement, type FigureSource, type Vec3 } from '@hatiolab/figure-model'\n\n/**\n * Scene model <-> `FigureSource`.\n *\n * ## Why this is the only place that converts\n *\n * While authoring, the source of truth is the **scene model** -- part\n * components sitting on a draft. `FigureSource` is produced from it when the\n * asset is saved or scored.\n *\n * The two use different vocabularies. The scene inherited 2D canvas terms\n * (`width`/`height` in plan, `depth` for height, `left`/`top`/`zPos` for\n * position); the format uses the axes 3D tools use (`x` right, `y` up, `z`\n * front, position at the part's **centre**).\n *\n * **Convert in one place.** Two places drift, and this format has already had\n * one incident caused by axes disagreeing.\n *\n * ## It was called `fold`, and that was wrong\n *\n * `fold` is an established term -- it reduces a structure to a summary, and\n * this repo already uses it correctly (`foldKpi`, `foldProbeResults`). Nothing\n * is reduced here: the same information comes out in a different shape, and it\n * goes back unchanged. A reader who learned `fold` from `foldKpi` reads this\n * one as reducing something, and the user did exactly that.\n *\n * ## The mapping\n *\n * The draft is the base box; its centre is the format's origin.\n *\n * format scene\n * ──────────────── ──────────────────────────────\n * base.x draft width\n * base.y draft depth <- height\n * base.z draft height\n *\n * size.x width\n * size.y depth\n * size.z height\n *\n * position.x left + width/2 - draft width/2\n * position.y zPos + depth/2 - draft depth/2 <- zPos is the underside\n * position.z top + height/2 - draft height/2\n *\n * rotation.x rotationX (scene radians, format degrees)\n * rotation.y -rotation\n * rotation.z rotationY\n *\n * The rotation names are crossed because of the scene: `rotationY` becomes\n * three's z (`real-object.ts`, `get rotation()`). That stays inside this table.\n *\n * ## Fields nothing reads yet still travel\n *\n * `materialSlot`, `keepRound`, `sizing`, `anchor`, `repeat` and `label` exist in the format\n * and the editor ignores them. They still have to survive a round trip. When a\n * source proposed by AI is opened, edited and written back, a field that\n * disappears **without an error** is one the author cannot know about.\n *\n * **Five have gone that way here**: `ports`, `animations`, `anchor`,\n * `shape.hollow` and `keepRound`. The last one the test caught first, which\n * is the guard below actually earning its keep. The third showed up as a conveyor motor that stayed on the\n * floor no matter what the sample said; the fourth as a hollowed tray that\n * came out a solid block.\n *\n * All four passed the round-trip test, because the fixture it uses did not\n * carry the field. So the test now reads the **field list the format declares**\n * rather than trusting the fixture.\n *\n * `shape` is unpacked into `round`, `path` and `hollow` rather than carried\n * whole, because the editor holds those as separate state. Every field added\n * under `shape` costs a line here, and forgetting that line was the fourth\n * of the four.\n */\n/** 판 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface FigureDraft {\n /** 형식 버전. */\n version?: number\n /*\n The three sizes keep the scene component's names, and the scene assigns its axes differently\n from the format: here `depth` is the vertical one. Naming the axis alone (\"the y axis\") does\n not say which of the two conventions is meant, and this interface sits on the boundary between\n them — so each field names the `FigureSource` field it becomes. `toFigureSource` and\n `fromFigureSource` below are the only places that may disagree with these three lines.\n */\n /** 가로 너비 (mm) → `base.x`. */\n width: number\n /** 앞뒤 깊이 (mm) → `base.z`. 저작자가 위에서 내려다볼 때의 세로다. */\n height: number\n /** 높이 (mm) → `base.y`. 서 있는 방향이다. */\n depth: number\n /** 컴포넌트 타입 이름. **저장되는 식별자다.** */\n figureType?: string\n /** 3D 배치 기준면 ('floor' | 'ceiling' | 'center'). */\n placement?: FigurePlacement\n detailLevel?: DetailLevel\n styleKit?: string\n /**\n * 움직임. **부품 바깥에 있다** — clip 하나가 부품 여럿을 함께 움직이기 때문이다.\n *\n * 편집면이 아직 이것을 만지지 않는다. 그래도 여기 실어 두는 것은, 안 실으면 열었다\n * 저장하는 것만으로 **조용히 지워지기** 때문이다.\n */\n animations?: FigureSource['animations']\n /**\n * 이 도형이 지는 능력. 씬의 capability mixin 이름이다.\n *\n * **부품이 아니라 도형 전체의 것이다** — 「이 컨베이어는 흐름의 마디다」는 부품 하나가\n * 아니라 도형이 지는 성질이다. 부품 쪽 `capability` 는 그 능력을 **어느 부품이 받쳐\n * 주나**를 말하는 다른 칸이다.\n */\n capabilities?: FigureSource['capabilities']\n}\n\n/** 부품 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface PartModel {\n type: 'figure-part'\n /** 부품 이름. **저장되는 식별자다.** 씬은 이 값을 검색에도 쓴다. */\n name: string\n primitive: FigurePart['primitive']\n\n left: number\n top: number\n width: number\n height: number\n /** 부피의 **밑면** 높이. 씬의 'floor' 배치에서 그렇게 정해져 있다. */\n zPos: number\n depth: number\n\n /** 라디안. 씬이 그렇게 쓴다. */\n rotationX?: number\n rotation?: number\n rotationY?: number\n\n segments?: number\n round?: number\n path?: { x: number; y: number }[]\n /** 속을 판 단면. 편집면이 아직 안 만지지만 잃어버리면 통이 통짜 상자가 된다. */\n hollow?: NonNullable<FigurePart['shape']>['hollow']\n\n /**\n * 편집 중에만 쓰는 상태 — **저장 형식으로 넘어가지 않는다.**\n *\n * 씬이 이미 갖고 있는 속성이다. 편집 모드에서는 `faint`(반투명 + 색을 버리고 회색으로\n * 수렴)로 보이고, 뷰 모드에서는 아예 안 그려진다. 판단은 씬의 `modeling-faint` 한 곳에\n * 있고 2D 캔버스·3D 재질·DOM 세 채널이 같이 쓴다.\n *\n * 숨김은 \"지금 이 뒤를 만지려고\" 하는 것이지 그 부품의 성질이 아니다. 그래서\n * `toFigureSource` 가 싣지 않고, 다시 열면 전부 보이는 상태로 돌아온다.\n */\n hidden?: boolean\n\n /** 색 토큰 이름 (`palette.primary` 등). */\n token?: string\n /*\n `FigurePart['material']` is required in the format, so these index into it directly. The\n `NonNullable` on `shape` above is not the same case — `shape` is optional there.\n */\n preset?: FigurePart['material']['preset']\n flatShading?: boolean\n transparent?: boolean\n /**\n * 자체발광. 없으면 안 빛난다.\n *\n * 바깥의 `token` 이 **꺼진 모습**이고 이 안의 `token` 이 **켜진 모습**이다 — 꺼진\n * 전구는 빨갛지 않고 어두운 플라스틱이다.\n */\n emissive?: FigurePart['material']['emissive']\n surface?: FigurePart['material']['surface']\n\n /* 아직 아무 일도 안 하지만 잃어버리지 않는다. */\n materialSlot?: FigurePart['materialSlot']\n /** 단면을 원으로 지킬까. 곡면만 뜻이 있고 기본은 지키는 것이다. */\n keepRound?: FigurePart['keepRound']\n sizing?: FigurePart['sizing']\n /** 축마다 어느 면을 붙잡나. `sizing` 이 정한 것을 덮어쓴다. */\n anchor?: FigurePart['anchor']\n repeat?: FigurePart['repeat']\n label?: FigurePart['label']\n /**\n * 이 부품이 능력의 어느 자리인가 — 물건이 앉는 자리(slot)이거나 드나드는 문(port)이다.\n *\n * 이 칸이 있는 부품은 **그려지지 않는다.** 저작면에서는 어디인지 보여야 하니 그리지만,\n * 청사진에서는 `groups` 가 아니라 `anchors` 로 간다.\n */\n capability?: FigurePart['capability']\n}\n\nconst RAD_TO_DEG = 180 / Math.PI\nconst DEG_TO_RAD = Math.PI / 180\n\n/** 값이 있는 것만 남긴다. 없는 필드를 `undefined` 로 두면 접었다 편 것이 원본과 달라진다. */\nfunction withoutEmpty<T extends object>(value: T): T {\n const out = {} as Record<string, unknown>\n for (const [key, item] of Object.entries(value)) {\n if (item !== undefined) out[key] = item\n }\n return out as T\n}\n\n/** 소수점 넷째 자리까지. 라디안↔도를 오가며 생기는 꼬리를 자른다. */\nexport function round4(value: number): number {\n return Math.round(value * 1e4) / 1e4\n}\n\n/**\n * 판의 절반. 형식의 원점이 판 중심이라 이 값이 늘 함께 다닌다.\n *\n * 축 이름은 **형식의 것**이다 — x 가로 · y 높이 · z 세로.\n */\nexport function halfOf(draft: FigureDraft): Vec3 {\n return { x: draft.width / 2, y: draft.depth / 2, z: draft.height / 2 }\n}\n\n/*\n * ── The placement is not in these numbers ────────────────────────────────────\n *\n * `placement` says which reference surface a figure of this kind attaches to — a conveyor stands\n * on the floor, an OHT hangs from the ceiling. That is a fact about the figure *type*, and the\n * scene acts on it when an instance is placed: `floor` puts the box on the ground, `ceiling` at\n * the ceiling height less the figure's own height. Nothing about the authored shape changes.\n *\n * So the conversion below does not read it, and the editing canvas does not either. The box is\n * always `0 … base.y` there and `zPos` is always the part's underside. Two figures with identical\n * parts and different placements have identical `parts` arrays, which is what the format says.\n *\n * This used to be wired the other way: the canvas fed the figure's placement to the model layer's\n * `state.placement`, which is a scene-wide coordinate mode ('floor' | 'space' | 'inverted') that\n * flips what `zPos` names for everything in that layer. It stood in for the archetype because the\n * modeller holds exactly one figure — and it cost a whole day of defects, because every number the\n * author typed then meant a different face depending on a dropdown. That coordinate mode is now\n * where it belongs: a workbench setting in `figure-view.ts`, applied to the preview alone.\n */\n\n/**\n * 저작자가 보는 축이 씬의 어느 용어인가.\n *\n * 씬은 2D 캔버스에서 온 용어를 쓰므로 축 이름이 그대로 없다. 이 표가 **유일한** 대응이다 —\n * 편집 규칙이 저마다 자기 표를 들면 한 곳을 고칠 때 다른 곳이 남는다.\n */\nexport const AXIS_KEYS = {\n x: { at: 'left', size: 'width' },\n y: { at: 'zPos', size: 'depth' },\n z: { at: 'top', size: 'height' }\n} as const\n\nexport type PlaneAxis = keyof typeof AXIS_KEYS\n\n/** 판이 그 축으로 얼마나 넓은가. */\nexport function draftExtent(draft: FigureDraft, axis: PlaneAxis): number {\n return axis === 'x' ? draft.width : axis === 'y' ? draft.depth : draft.height\n}\n\n/** 부품 하나를 저장 형식으로 변환한다. 저작 화면이 저작자의 어휘로 보일 때 쓴다. */\nexport function partToFigure(part: PartModel, draft: FigureDraft): FigurePart {\n return partTo(part, halfOf(draft))\n}\n\n/** 부품 하나를 씬 용어로 되돌린다. */\nexport function partFromFigure(part: FigurePart, draft: FigureDraft): PartModel {\n return partFrom(part, halfOf(draft))\n}\n\n/** 씬 모델을 저장 형식으로 변환한다. */\nexport function toFigureSource(draft: FigureDraft, parts: PartModel[]): FigureSource {\n const half = halfOf(draft)\n\n return withoutEmpty({\n version: draft.version ?? FIGURE_SOURCE_VERSION,\n type: draft.figureType ?? '',\n base: { x: draft.width, y: draft.depth, z: draft.height },\n placement: draft.placement,\n detailLevel: draft.detailLevel,\n styleKit: draft.styleKit,\n parts: parts.map(part => partTo(part, half)),\n animations: draft.animations,\n capabilities: draft.capabilities\n }) as FigureSource\n}\n\nfunction partTo(part: PartModel, half: Vec3): FigurePart {\n const rotation = withoutEmpty({\n x: part.rotationX ? round4(part.rotationX * RAD_TO_DEG) : undefined,\n y: part.rotation ? round4(-part.rotation * RAD_TO_DEG) : undefined,\n z: part.rotationY ? round4(part.rotationY * RAD_TO_DEG) : undefined\n })\n\n const shape = withoutEmpty({ round: part.round, path: part.path, hollow: part.hollow })\n /*\n 재질이 하나도 없으면 **칸 자체를 안 적는다.**\n\n 빈 객체를 적으면 형식이 「재질은 있는데 무엇으로 칠할지 모른다」로 읽어\n `material-unresolvable` 을 낸다. 그리지 않는 자리(`capability`)에는 재질이 없는 것이\n 정상이고, 그 부품이 조용히 오류를 달고 다니게 된다.\n */\n const material = withoutEmpty({\n token: part.token,\n preset: part.preset,\n flatShading: part.flatShading,\n transparent: part.transparent,\n emissive: part.emissive,\n surface: part.surface\n })\n\n return withoutEmpty({\n name: part.name,\n primitive: part.primitive,\n transform: withoutEmpty({\n position: {\n x: round4(part.left + part.width / 2 - half.x),\n y: round4(part.zPos + part.depth / 2 - half.y),\n z: round4(part.top + part.height / 2 - half.z)\n },\n size: { x: part.width, y: part.depth, z: part.height },\n rotation: Object.keys(rotation).length > 0 ? rotation : undefined\n }),\n segments: part.segments,\n shape: Object.keys(shape).length > 0 ? shape : undefined,\n material: Object.keys(material).length > 0 ? material : undefined,\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability\n }) as FigurePart\n}\n\n/** 형식을 씬 모델로 편다. */\nexport function fromFigureSource(source: FigureSource): { draft: FigureDraft; parts: PartModel[] } {\n const half: Vec3 = { x: source.base.x / 2, y: source.base.y / 2, z: source.base.z / 2 }\n\n const draft = withoutEmpty({\n version: source.version,\n width: source.base.x,\n height: source.base.z,\n depth: source.base.y,\n figureType: source.type,\n placement: source.placement,\n detailLevel: source.detailLevel,\n styleKit: source.styleKit,\n animations: source.animations,\n capabilities: source.capabilities\n }) as FigureDraft\n\n return { draft, parts: source.parts.map(part => partFrom(part, half)) }\n}\n\nfunction partFrom(part: FigurePart, half: Vec3): PartModel {\n const { position, size, rotation } = part.transform\n const material = part.material ?? {}\n\n return withoutEmpty({\n type: 'figure-part',\n name: part.name,\n primitive: part.primitive,\n\n left: round4(half.x + position.x - size.x / 2),\n top: round4(half.z + position.z - size.z / 2),\n width: size.x,\n height: size.z,\n zPos: round4(half.y + position.y - size.y / 2),\n depth: size.y,\n\n /*\n 라디안은 자르지 않는다.\n\n 소수 넷째에서 잘랐더니 도로 되돌릴 때 57 배로 커져 45 도가 45.0001 이 됐다.\n 자르는 것은 **사람이 읽는 쪽**(도)에서만 한다.\n */\n rotationX: rotation?.x ? rotation.x * DEG_TO_RAD : undefined,\n rotation: rotation?.y ? -rotation.y * DEG_TO_RAD : undefined,\n rotationY: rotation?.z ? rotation.z * DEG_TO_RAD : undefined,\n\n segments: part.segments,\n round: part.shape?.round,\n path: part.shape?.path,\n hollow: part.shape?.hollow,\n\n token: material.token,\n preset: material.preset,\n flatShading: material.flatShading,\n transparent: material.transparent,\n emissive: material.emissive,\n surface: material.surface,\n\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability\n }) as PartModel\n}\n"]}
1
+ {"version":3,"file":"figure-source.js","sourceRoot":"","sources":["../../client/modeller/figure-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { FIGURE_SOURCE_VERSION, type DetailLevel, type FigurePart, type FigurePlacement, type FigureSource, type Vec3 } from '@hatiolab/figure-model'\n\n/**\n * Scene model <-> `FigureSource`.\n *\n * ## Why this is the only place that converts\n *\n * While authoring, the source of truth is the **scene model** -- part\n * components sitting on a draft. `FigureSource` is produced from it when the\n * asset is saved or scored.\n *\n * The two use different vocabularies. The scene inherited 2D canvas terms\n * (`width`/`height` in plan, `depth` for height, `left`/`top`/`zPos` for\n * position); the format uses the axes 3D tools use (`x` right, `y` up, `z`\n * front, position at the part's **centre**).\n *\n * **Convert in one place.** Two places drift, and this format has already had\n * one incident caused by axes disagreeing.\n *\n * ## It was called `fold`, and that was wrong\n *\n * `fold` is an established term -- it reduces a structure to a summary, and\n * this repo already uses it correctly (`foldKpi`, `foldProbeResults`). Nothing\n * is reduced here: the same information comes out in a different shape, and it\n * goes back unchanged. A reader who learned `fold` from `foldKpi` reads this\n * one as reducing something, and the user did exactly that.\n *\n * ## The mapping\n *\n * The draft is the base box; its centre is the format's origin.\n *\n * format scene\n * ──────────────── ──────────────────────────────\n * base.x draft width\n * base.y draft depth <- height\n * base.z draft height\n *\n * size.x width\n * size.y depth\n * size.z height\n *\n * position.x left + width/2 - draft width/2\n * position.y zPos + depth/2 - draft depth/2 <- zPos is the underside\n * position.z top + height/2 - draft height/2\n *\n * rotation.x rotationX (scene radians, format degrees)\n * rotation.y -rotation\n * rotation.z rotationY\n *\n * The rotation names are crossed because of the scene: `rotationY` becomes\n * three's z (`real-object.ts`, `get rotation()`). That stays inside this table.\n *\n * ## Fields nothing reads yet still travel\n *\n * `materialSlot`, `keepRound`, `sizing`, `anchor`, `repeat` and `label` exist in the format\n * and the editor ignores them. They still have to survive a round trip. When a\n * source proposed by AI is opened, edited and written back, a field that\n * disappears **without an error** is one the author cannot know about.\n *\n * **Five have gone that way here**: `ports`, `animations`, `anchor`,\n * `shape.hollow` and `keepRound`. The last one the test caught first, which\n * is the guard below actually earning its keep. The third showed up as a conveyor motor that stayed on the\n * floor no matter what the sample said; the fourth as a hollowed tray that\n * came out a solid block.\n *\n * All four passed the round-trip test, because the fixture it uses did not\n * carry the field. So the test now reads the **field list the format declares**\n * rather than trusting the fixture.\n *\n * `shape` is unpacked into `round`, `path` and `hollow` rather than carried\n * whole, because the editor holds those as separate state. Every field added\n * under `shape` costs a line here, and forgetting that line was the fourth\n * of the four.\n */\n/** 판 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface FigureDraft {\n /** 형식 버전. */\n version?: number\n /*\n The three sizes keep the scene component's names, and the scene assigns its axes differently\n from the format: here `depth` is the vertical one. Naming the axis alone (\"the y axis\") does\n not say which of the two conventions is meant, and this interface sits on the boundary between\n them — so each field names the `FigureSource` field it becomes. `toFigureSource` and\n `fromFigureSource` below are the only places that may disagree with these three lines.\n */\n /** 가로 너비 (mm) → `base.x`. */\n width: number\n /** 앞뒤 깊이 (mm) → `base.z`. 저작자가 위에서 내려다볼 때의 세로다. */\n height: number\n /** 높이 (mm) → `base.y`. 서 있는 방향이다. */\n depth: number\n /** 컴포넌트 타입 이름. **저장되는 식별자다.** */\n figureType?: string\n /** 3D 배치 기준면 ('floor' | 'ceiling' | 'center'). */\n placement?: FigurePlacement\n detailLevel?: DetailLevel\n styleKit?: string\n /**\n * 움직임. **부품 바깥에 있다** — clip 하나가 부품 여럿을 함께 움직이기 때문이다.\n *\n * 편집면이 아직 이것을 만지지 않는다. 그래도 여기 실어 두는 것은, 안 실으면 열었다\n * 저장하는 것만으로 **조용히 지워지기** 때문이다.\n */\n animations?: FigureSource['animations']\n /**\n * 값이 만드는 자세 — **애니메이션과 다른 칸이다.**\n *\n * 저것은 시각이 흘러 움직이는 것(도는 롤러)이고 이것은 인스턴스가 준 값이 그대로 자세가\n * 되는 것(1200mm 내려온 호이스트)이다. 한동안 애니메이션 안에 `drive: 'hold'` 로 숨어\n * 있었고, 그래서 「길이값을 변수로 노출하라」는 요청에 아무도 이것을 못 찾았다(ADR-0051).\n */\n parameters?: FigureSource['parameters']\n /**\n * 이 도형이 지는 능력. 씬의 capability mixin 이름이다.\n *\n * **부품이 아니라 도형 전체의 것이다** — 「이 컨베이어는 흐름의 마디다」는 부품 하나가\n * 아니라 도형이 지는 성질이다. 부품 쪽 `capability` 는 그 능력을 **어느 부품이 받쳐\n * 주나**를 말하는 다른 칸이다.\n */\n capabilities?: FigureSource['capabilities']\n}\n\n/** 부품 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface PartModel {\n type: 'figure-part'\n /** 부품 이름. **저장되는 식별자다.** 씬은 이 값을 검색에도 쓴다. */\n name: string\n primitive: FigurePart['primitive']\n\n left: number\n top: number\n width: number\n height: number\n /** 부피의 **밑면** 높이. 씬의 'floor' 배치에서 그렇게 정해져 있다. */\n zPos: number\n depth: number\n\n /** 라디안. 씬이 그렇게 쓴다. */\n rotationX?: number\n rotation?: number\n rotationY?: number\n\n segments?: number\n round?: number\n path?: { x: number; y: number }[]\n /** 속을 판 단면. 편집면이 아직 안 만지지만 잃어버리면 통이 통짜 상자가 된다. */\n hollow?: NonNullable<FigurePart['shape']>['hollow']\n\n /**\n * 편집 중에만 쓰는 상태 — **저장 형식으로 넘어가지 않는다.**\n *\n * 씬이 이미 갖고 있는 속성이다. 편집 모드에서는 `faint`(반투명 + 색을 버리고 회색으로\n * 수렴)로 보이고, 뷰 모드에서는 아예 안 그려진다. 판단은 씬의 `modeling-faint` 한 곳에\n * 있고 2D 캔버스·3D 재질·DOM 세 채널이 같이 쓴다.\n *\n * 숨김은 \"지금 이 뒤를 만지려고\" 하는 것이지 그 부품의 성질이 아니다. 그래서\n * `toFigureSource` 가 싣지 않고, 다시 열면 전부 보이는 상태로 돌아온다.\n */\n hidden?: boolean\n\n /** 색 토큰 이름 (`palette.primary` 등). */\n token?: string\n /*\n `FigurePart['material']` is required in the format, so these index into it directly. The\n `NonNullable` on `shape` above is not the same case — `shape` is optional there.\n */\n preset?: FigurePart['material']['preset']\n flatShading?: boolean\n transparent?: boolean\n /**\n * 자체발광. 없으면 안 빛난다.\n *\n * 바깥의 `token` 이 **꺼진 모습**이고 이 안의 `token` 이 **켜진 모습**이다 — 꺼진\n * 전구는 빨갛지 않고 어두운 플라스틱이다.\n */\n emissive?: FigurePart['material']['emissive']\n surface?: FigurePart['material']['surface']\n\n /* 아직 아무 일도 안 하지만 잃어버리지 않는다. */\n materialSlot?: FigurePart['materialSlot']\n /** 단면을 원으로 지킬까. 곡면만 뜻이 있고 기본은 지키는 것이다. */\n keepRound?: FigurePart['keepRound']\n sizing?: FigurePart['sizing']\n /** 축마다 어느 면을 붙잡나. `sizing` 이 정한 것을 덮어쓴다. */\n anchor?: FigurePart['anchor']\n repeat?: FigurePart['repeat']\n label?: FigurePart['label']\n /**\n * 이 부품이 능력의 어느 자리인가 — 물건이 앉는 자리(slot)이거나 드나드는 문(port)이다.\n *\n * 이 칸이 있는 부품은 **그려지지 않는다.** 저작면에서는 어디인지 보여야 하니 그리지만,\n * 청사진에서는 `groups` 가 아니라 `anchors` 로 간다.\n */\n capability?: FigurePart['capability']\n}\n\nconst RAD_TO_DEG = 180 / Math.PI\nconst DEG_TO_RAD = Math.PI / 180\n\n/** 값이 있는 것만 남긴다. 없는 필드를 `undefined` 로 두면 접었다 편 것이 원본과 달라진다. */\nfunction withoutEmpty<T extends object>(value: T): T {\n const out = {} as Record<string, unknown>\n for (const [key, item] of Object.entries(value)) {\n if (item !== undefined) out[key] = item\n }\n return out as T\n}\n\n/** 소수점 넷째 자리까지. 라디안↔도를 오가며 생기는 꼬리를 자른다. */\nexport function round4(value: number): number {\n return Math.round(value * 1e4) / 1e4\n}\n\n/**\n * 판의 절반. 형식의 원점이 판 중심이라 이 값이 늘 함께 다닌다.\n *\n * 축 이름은 **형식의 것**이다 — x 가로 · y 높이 · z 세로.\n */\nexport function halfOf(draft: FigureDraft): Vec3 {\n return { x: draft.width / 2, y: draft.depth / 2, z: draft.height / 2 }\n}\n\n/*\n * ── The placement is not in these numbers ────────────────────────────────────\n *\n * `placement` says which reference surface a figure of this kind attaches to — a conveyor stands\n * on the floor, an OHT hangs from the ceiling. That is a fact about the figure *type*, and the\n * scene acts on it when an instance is placed: `floor` puts the box on the ground, `ceiling` at\n * the ceiling height less the figure's own height. Nothing about the authored shape changes.\n *\n * So the conversion below does not read it, and the editing canvas does not either. The box is\n * always `0 … base.y` there and `zPos` is always the part's underside. Two figures with identical\n * parts and different placements have identical `parts` arrays, which is what the format says.\n *\n * This used to be wired the other way: the canvas fed the figure's placement to the model layer's\n * `state.placement`, which is a scene-wide coordinate mode ('floor' | 'space' | 'inverted') that\n * flips what `zPos` names for everything in that layer. It stood in for the archetype because the\n * modeller holds exactly one figure — and it cost a whole day of defects, because every number the\n * author typed then meant a different face depending on a dropdown. That coordinate mode is now\n * where it belongs: a workbench setting in `figure-view.ts`, applied to the preview alone.\n */\n\n/**\n * 저작자가 보는 축이 씬의 어느 용어인가.\n *\n * 씬은 2D 캔버스에서 온 용어를 쓰므로 축 이름이 그대로 없다. 이 표가 **유일한** 대응이다 —\n * 편집 규칙이 저마다 자기 표를 들면 한 곳을 고칠 때 다른 곳이 남는다.\n */\nexport const AXIS_KEYS = {\n x: { at: 'left', size: 'width' },\n y: { at: 'zPos', size: 'depth' },\n z: { at: 'top', size: 'height' }\n} as const\n\nexport type PlaneAxis = keyof typeof AXIS_KEYS\n\n/** 판이 그 축으로 얼마나 넓은가. */\nexport function draftExtent(draft: FigureDraft, axis: PlaneAxis): number {\n return axis === 'x' ? draft.width : axis === 'y' ? draft.depth : draft.height\n}\n\n/** 부품 하나를 저장 형식으로 변환한다. 저작 화면이 저작자의 어휘로 보일 때 쓴다. */\nexport function partToFigure(part: PartModel, draft: FigureDraft): FigurePart {\n return partTo(part, halfOf(draft))\n}\n\n/** 부품 하나를 씬 용어로 되돌린다. */\nexport function partFromFigure(part: FigurePart, draft: FigureDraft): PartModel {\n return partFrom(part, halfOf(draft))\n}\n\n/** 씬 모델을 저장 형식으로 변환한다. */\nexport function toFigureSource(draft: FigureDraft, parts: PartModel[]): FigureSource {\n const half = halfOf(draft)\n\n return withoutEmpty({\n version: draft.version ?? FIGURE_SOURCE_VERSION,\n type: draft.figureType ?? '',\n base: { x: draft.width, y: draft.depth, z: draft.height },\n placement: draft.placement,\n detailLevel: draft.detailLevel,\n styleKit: draft.styleKit,\n parts: parts.map(part => partTo(part, half)),\n animations: draft.animations,\n parameters: draft.parameters,\n capabilities: draft.capabilities\n }) as FigureSource\n}\n\nfunction partTo(part: PartModel, half: Vec3): FigurePart {\n const rotation = withoutEmpty({\n x: part.rotationX ? round4(part.rotationX * RAD_TO_DEG) : undefined,\n y: part.rotation ? round4(-part.rotation * RAD_TO_DEG) : undefined,\n z: part.rotationY ? round4(part.rotationY * RAD_TO_DEG) : undefined\n })\n\n const shape = withoutEmpty({ round: part.round, path: part.path, hollow: part.hollow })\n /*\n 재질이 하나도 없으면 **칸 자체를 안 적는다.**\n\n 빈 객체를 적으면 형식이 「재질은 있는데 무엇으로 칠할지 모른다」로 읽어\n `material-unresolvable` 을 낸다. 그리지 않는 자리(`capability`)에는 재질이 없는 것이\n 정상이고, 그 부품이 조용히 오류를 달고 다니게 된다.\n */\n const material = withoutEmpty({\n token: part.token,\n preset: part.preset,\n flatShading: part.flatShading,\n transparent: part.transparent,\n emissive: part.emissive,\n surface: part.surface\n })\n\n return withoutEmpty({\n name: part.name,\n primitive: part.primitive,\n transform: withoutEmpty({\n position: {\n x: round4(part.left + part.width / 2 - half.x),\n y: round4(part.zPos + part.depth / 2 - half.y),\n z: round4(part.top + part.height / 2 - half.z)\n },\n size: { x: part.width, y: part.depth, z: part.height },\n rotation: Object.keys(rotation).length > 0 ? rotation : undefined\n }),\n segments: part.segments,\n shape: Object.keys(shape).length > 0 ? shape : undefined,\n material: Object.keys(material).length > 0 ? material : undefined,\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability\n }) as FigurePart\n}\n\n/** 형식을 씬 모델로 편다. */\nexport function fromFigureSource(source: FigureSource): { draft: FigureDraft; parts: PartModel[] } {\n const half: Vec3 = { x: source.base.x / 2, y: source.base.y / 2, z: source.base.z / 2 }\n\n const draft = withoutEmpty({\n version: source.version,\n width: source.base.x,\n height: source.base.z,\n depth: source.base.y,\n figureType: source.type,\n placement: source.placement,\n detailLevel: source.detailLevel,\n styleKit: source.styleKit,\n animations: source.animations,\n parameters: source.parameters,\n capabilities: source.capabilities\n }) as FigureDraft\n\n return { draft, parts: source.parts.map(part => partFrom(part, half)) }\n}\n\nfunction partFrom(part: FigurePart, half: Vec3): PartModel {\n const { position, size, rotation } = part.transform\n const material = part.material ?? {}\n\n return withoutEmpty({\n type: 'figure-part',\n name: part.name,\n primitive: part.primitive,\n\n left: round4(half.x + position.x - size.x / 2),\n top: round4(half.z + position.z - size.z / 2),\n width: size.x,\n height: size.z,\n zPos: round4(half.y + position.y - size.y / 2),\n depth: size.y,\n\n /*\n 라디안은 자르지 않는다.\n\n 소수 넷째에서 잘랐더니 도로 되돌릴 때 57 배로 커져 45 도가 45.0001 이 됐다.\n 자르는 것은 **사람이 읽는 쪽**(도)에서만 한다.\n */\n rotationX: rotation?.x ? rotation.x * DEG_TO_RAD : undefined,\n rotation: rotation?.y ? -rotation.y * DEG_TO_RAD : undefined,\n rotationY: rotation?.z ? rotation.z * DEG_TO_RAD : undefined,\n\n segments: part.segments,\n round: part.shape?.round,\n path: part.shape?.path,\n hollow: part.shape?.hollow,\n\n token: material.token,\n preset: material.preset,\n flatShading: material.flatShading,\n transparent: material.transparent,\n emissive: material.emissive,\n surface: material.surface,\n\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability\n }) as PartModel\n}\n"]}
@@ -19,7 +19,7 @@ import type { FigurePart, FigureSource } from '@hatiolab/figure-model';
19
19
  * 기준 치수와 디테일 등급도 목록에 넣는다. 부품만 골라 받으면 새 부품이 옛 기준
20
20
  * 상자 밖으로 나가 **검증에 걸릴 수 있다** — 무엇을 함께 받아야 하는지 보여야 한다.
21
21
  */
22
- export type ChangeKind = 'added' | 'changed' | 'removed' | 'base' | 'placement' | 'detail' | 'styleKit' | 'animations' | 'capabilities';
22
+ export type ChangeKind = 'added' | 'changed' | 'removed' | 'base' | 'placement' | 'detail' | 'styleKit' | 'animations' | 'parameters' | 'capabilities';
23
23
  export interface ProposalChange {
24
24
  kind: ChangeKind;
25
25
  /** 부품 변경이면 부품 이름. 그 밖이면 무엇이 바뀌는지. */
@@ -14,13 +14,14 @@
14
14
  * 식별자라 후보가 바꿔 와도 받지 않고(`applyProposal` 참고), `version` 은 저작자가 고를 것이
15
15
  * 아니라 저장할 때 서버가 찍는다.
16
16
  */
17
- const FIGURE_FIELDS = ['base', 'placement', 'detailLevel', 'styleKit', 'animations', 'capabilities'];
17
+ const FIGURE_FIELDS = ['base', 'placement', 'detailLevel', 'styleKit', 'animations', 'parameters', 'capabilities'];
18
18
  const FIELD_KIND = {
19
19
  base: 'base',
20
20
  placement: 'placement',
21
21
  detailLevel: 'detail',
22
22
  styleKit: 'styleKit',
23
23
  animations: 'animations',
24
+ parameters: 'parameters',
24
25
  capabilities: 'capabilities'
25
26
  };
26
27
  /** 부품 둘이 같은가. 순서까지 같은 JSON 으로 견준다 — 필드 순서는 만드는 쪽이 정한다. */
@@ -1 +1 @@
1
- {"version":3,"file":"proposal.js","sourceRoot":"","sources":["../../client/modeller/proposal.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FigurePart, FigureSource } from '@hatiolab/figure-model'\n\n/**\n * AI 후보를 지금 것과 비교하고, 고른 것만 받아들인다.\n *\n * 비교는 **저장 형식**으로 한다. 편집 중의 데이터는 씬 모델이지만, 후보는 서버에서\n * FigureSource 로 오고 기준 치수·디테일 등급처럼 부품 밖의 값도 함께 봐야 한다.\n *\n * 부품 편집 규칙은 여기 없다 — `part-edits.ts` 가 씬 모델 용어로 갖고 있다.\n */\n\n/**\n * 후보가 무엇을 바꾸자는 것인가.\n *\n * 후보를 통째로만 받게 두면 「몸통은 좋은데 모터 자리가 틀렸다」에 답할 수 없다 —\n * 그게 가장 흔한 반응이다. 그래서 후보를 **변경 목록**으로 본다.\n *\n * 부품은 이름으로 짝짓는다. `FigurePart.name` 이 저장되는 식별자이고 바인딩이\n * 그 이름을 가리키므로, 이름이 같으면 같은 부품이다.\n *\n * 기준 치수와 디테일 등급도 목록에 넣는다. 부품만 골라 받으면 새 부품이 옛 기준\n * 상자 밖으로 나가 **검증에 걸릴 수 있다** — 무엇을 함께 받아야 하는지 보여야 한다.\n */\nexport type ChangeKind =\n | 'added'\n | 'changed'\n | 'removed'\n | 'base'\n | 'placement'\n | 'detail'\n | 'styleKit'\n | 'animations'\n | 'capabilities'\n\n/**\n * 부품 밖에서 바뀔 수 있는 것 전부.\n *\n * 여기 없는 필드는 **조용히 버려진다** — 후보가 바꿔 왔어도 목록에 안 뜨고 받아도\n * 반영되지 않는다. 형식에 필드가 늘면 여기에 더해야 한다.\n *\n * 그 일이 실제로 났다(2026-09-15). `animations` · `capabilities` · `placement` 셋이 빠져 있었다.\n * 후보가 애니메이션만 더해 오면 변경 목록이 **비어서** 「n건 반영」 단추가 잠기고, 저작자는\n * 후보를 받지도 버리지도 못한 채 멈춘다. 다른 변경과 섞여 오면 단추는 눌리는데 애니메이션만\n * 말없이 사라진다. 그리고 `anchor` 는 도형 수준 필드였다가 형식에서 없어진 것인데(2026-09-10)\n * 여기 남아 있었다 — 이 목록이 형식을 따라가지 않은 것이 양쪽으로 드러난 셈이다.\n *\n * 형식의 `FigureSource` 는 `type` · `parts` 를 빼면 이 여섯이 전부다. `type` 은 저장되는\n * 식별자라 후보가 바꿔 와도 받지 않고(`applyProposal` 참고), `version` 은 저작자가 고를 것이\n * 아니라 저장할 때 서버가 찍는다.\n */\nconst FIGURE_FIELDS = ['base', 'placement', 'detailLevel', 'styleKit', 'animations', 'capabilities'] as const\nconst FIELD_KIND: Record<(typeof FIGURE_FIELDS)[number], ChangeKind> = {\n base: 'base',\n placement: 'placement',\n detailLevel: 'detail',\n styleKit: 'styleKit',\n animations: 'animations',\n capabilities: 'capabilities'\n}\n\nexport interface ProposalChange {\n kind: ChangeKind\n /** 부품 변경이면 부품 이름. 그 밖이면 무엇이 바뀌는지. */\n key: string\n /** 부품 변경일 때 후보 쪽 부품. */\n part?: FigurePart\n}\n\n/** 부품 둘이 같은가. 순서까지 같은 JSON 으로 견준다 — 필드 순서는 만드는 쪽이 정한다. */\nfunction samePart(a: FigurePart, b: FigurePart): boolean {\n return JSON.stringify(sortKeys(a)) === JSON.stringify(sortKeys(b))\n}\n\n/** 키 순서를 맞춰 견줄 수 있게 한다. */\nfunction sortKeys(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortKeys)\n if (value && typeof value === 'object') {\n const out: Record<string, unknown> = {}\n for (const key of Object.keys(value as object).sort()) {\n out[key] = sortKeys((value as Record<string, unknown>)[key])\n }\n return out\n }\n return value\n}\n\nexport function diffProposal(current: FigureSource | undefined, proposal: FigureSource): ProposalChange[] {\n const changes: ProposalChange[] = []\n const mine = new Map((current?.parts ?? []).map(part => [part.name, part]))\n const theirs = new Map(proposal.parts.map(part => [part.name, part]))\n\n for (const field of FIGURE_FIELDS) {\n if (!current) break\n const before = JSON.stringify(sortKeys(current[field]))\n const after = JSON.stringify(sortKeys(proposal[field]))\n if (before !== after) {\n changes.push({ kind: FIELD_KIND[field], key: field })\n }\n }\n\n for (const part of proposal.parts) {\n const old = mine.get(part.name)\n if (!old) changes.push({ kind: 'added', key: part.name, part })\n else if (!samePart(old, part)) changes.push({ kind: 'changed', key: part.name, part })\n }\n for (const part of current?.parts ?? []) {\n if (!theirs.has(part.name)) changes.push({ kind: 'removed', key: part.name })\n }\n\n return changes\n}\n\n/**\n * 고른 변경만 받는다.\n *\n * **타입 이름은 지금 것을 지킨다.** 만든 뒤에는 고칠 수 없는 저장 식별자이고,\n * 배치된 인스턴스가 그 이름으로 청사진을 찾는다. 후보가 다른 이름을 지어 왔어도\n * 그것 때문에 이미 놓인 것이 빈 자리가 되면 안 된다.\n *\n * `picked` 를 주지 않으면 전부 받는다 — 그것이 보통의 길이다.\n *\n * 더해지는 부품은 **뒤에 붙인다.** 후보가 놓은 자리에 끼워 넣으면, 일부만 골랐을\n * 때 순서가 어디서 온 것인지 알 수 없게 된다.\n */\nexport function applyProposal(\n current: FigureSource | undefined,\n proposal: FigureSource,\n picked?: Set<string>\n): FigureSource {\n const takes = (change: ProposalChange) => !picked || picked.has(change.key)\n const changes = diffProposal(current, proposal)\n\n if (!current) {\n return { ...proposal }\n }\n\n const removed = new Set(changes.filter(c => c.kind === 'removed' && takes(c)).map(c => c.key))\n const replaced = new Map(\n changes.filter(c => c.kind === 'changed' && takes(c)).map(c => [c.key, c.part as FigurePart])\n )\n const added = changes.filter(c => c.kind === 'added' && takes(c)).map(c => c.part as FigurePart)\n\n const parts = [\n ...current.parts.filter(part => !removed.has(part.name)).map(part => replaced.get(part.name) ?? part),\n ...added\n ]\n\n // 고른 필드만 후보 값으로 바꾼다. 안 고른 것은 지금 값이 남는다.\n const fields: Record<string, unknown> = {}\n for (const field of FIGURE_FIELDS) {\n const change = changes.find(c => c.key === field)\n if (change && takes(change)) {\n fields[field] = proposal[field]\n }\n }\n\n return { ...current, ...fields, parts } as FigureSource\n}\n\n/** 전부 받는다. */\nexport function takeProposal(current: FigureSource | undefined, proposal: FigureSource): FigureSource {\n return applyProposal(current, proposal)\n}\n"]}
1
+ {"version":3,"file":"proposal.js","sourceRoot":"","sources":["../../client/modeller/proposal.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FigurePart, FigureSource } from '@hatiolab/figure-model'\n\n/**\n * AI 후보를 지금 것과 비교하고, 고른 것만 받아들인다.\n *\n * 비교는 **저장 형식**으로 한다. 편집 중의 데이터는 씬 모델이지만, 후보는 서버에서\n * FigureSource 로 오고 기준 치수·디테일 등급처럼 부품 밖의 값도 함께 봐야 한다.\n *\n * 부품 편집 규칙은 여기 없다 — `part-edits.ts` 가 씬 모델 용어로 갖고 있다.\n */\n\n/**\n * 후보가 무엇을 바꾸자는 것인가.\n *\n * 후보를 통째로만 받게 두면 「몸통은 좋은데 모터 자리가 틀렸다」에 답할 수 없다 —\n * 그게 가장 흔한 반응이다. 그래서 후보를 **변경 목록**으로 본다.\n *\n * 부품은 이름으로 짝짓는다. `FigurePart.name` 이 저장되는 식별자이고 바인딩이\n * 그 이름을 가리키므로, 이름이 같으면 같은 부품이다.\n *\n * 기준 치수와 디테일 등급도 목록에 넣는다. 부품만 골라 받으면 새 부품이 옛 기준\n * 상자 밖으로 나가 **검증에 걸릴 수 있다** — 무엇을 함께 받아야 하는지 보여야 한다.\n */\nexport type ChangeKind =\n | 'added'\n | 'changed'\n | 'removed'\n | 'base'\n | 'placement'\n | 'detail'\n | 'styleKit'\n | 'animations'\n | 'parameters'\n | 'capabilities'\n\n/**\n * 부품 밖에서 바뀔 수 있는 것 전부.\n *\n * 여기 없는 필드는 **조용히 버려진다** — 후보가 바꿔 왔어도 목록에 안 뜨고 받아도\n * 반영되지 않는다. 형식에 필드가 늘면 여기에 더해야 한다.\n *\n * 그 일이 실제로 났다(2026-09-15). `animations` · `capabilities` · `placement` 셋이 빠져 있었다.\n * 후보가 애니메이션만 더해 오면 변경 목록이 **비어서** 「n건 반영」 단추가 잠기고, 저작자는\n * 후보를 받지도 버리지도 못한 채 멈춘다. 다른 변경과 섞여 오면 단추는 눌리는데 애니메이션만\n * 말없이 사라진다. 그리고 `anchor` 는 도형 수준 필드였다가 형식에서 없어진 것인데(2026-09-10)\n * 여기 남아 있었다 — 이 목록이 형식을 따라가지 않은 것이 양쪽으로 드러난 셈이다.\n *\n * 형식의 `FigureSource` 는 `type` · `parts` 를 빼면 이 여섯이 전부다. `type` 은 저장되는\n * 식별자라 후보가 바꿔 와도 받지 않고(`applyProposal` 참고), `version` 은 저작자가 고를 것이\n * 아니라 저장할 때 서버가 찍는다.\n */\nconst FIGURE_FIELDS = ['base', 'placement', 'detailLevel', 'styleKit', 'animations', 'parameters', 'capabilities'] as const\nconst FIELD_KIND: Record<(typeof FIGURE_FIELDS)[number], ChangeKind> = {\n base: 'base',\n placement: 'placement',\n detailLevel: 'detail',\n styleKit: 'styleKit',\n animations: 'animations',\n parameters: 'parameters',\n capabilities: 'capabilities'\n}\n\nexport interface ProposalChange {\n kind: ChangeKind\n /** 부품 변경이면 부품 이름. 그 밖이면 무엇이 바뀌는지. */\n key: string\n /** 부품 변경일 때 후보 쪽 부품. */\n part?: FigurePart\n}\n\n/** 부품 둘이 같은가. 순서까지 같은 JSON 으로 견준다 — 필드 순서는 만드는 쪽이 정한다. */\nfunction samePart(a: FigurePart, b: FigurePart): boolean {\n return JSON.stringify(sortKeys(a)) === JSON.stringify(sortKeys(b))\n}\n\n/** 키 순서를 맞춰 견줄 수 있게 한다. */\nfunction sortKeys(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortKeys)\n if (value && typeof value === 'object') {\n const out: Record<string, unknown> = {}\n for (const key of Object.keys(value as object).sort()) {\n out[key] = sortKeys((value as Record<string, unknown>)[key])\n }\n return out\n }\n return value\n}\n\nexport function diffProposal(current: FigureSource | undefined, proposal: FigureSource): ProposalChange[] {\n const changes: ProposalChange[] = []\n const mine = new Map((current?.parts ?? []).map(part => [part.name, part]))\n const theirs = new Map(proposal.parts.map(part => [part.name, part]))\n\n for (const field of FIGURE_FIELDS) {\n if (!current) break\n const before = JSON.stringify(sortKeys(current[field]))\n const after = JSON.stringify(sortKeys(proposal[field]))\n if (before !== after) {\n changes.push({ kind: FIELD_KIND[field], key: field })\n }\n }\n\n for (const part of proposal.parts) {\n const old = mine.get(part.name)\n if (!old) changes.push({ kind: 'added', key: part.name, part })\n else if (!samePart(old, part)) changes.push({ kind: 'changed', key: part.name, part })\n }\n for (const part of current?.parts ?? []) {\n if (!theirs.has(part.name)) changes.push({ kind: 'removed', key: part.name })\n }\n\n return changes\n}\n\n/**\n * 고른 변경만 받는다.\n *\n * **타입 이름은 지금 것을 지킨다.** 만든 뒤에는 고칠 수 없는 저장 식별자이고,\n * 배치된 인스턴스가 그 이름으로 청사진을 찾는다. 후보가 다른 이름을 지어 왔어도\n * 그것 때문에 이미 놓인 것이 빈 자리가 되면 안 된다.\n *\n * `picked` 를 주지 않으면 전부 받는다 — 그것이 보통의 길이다.\n *\n * 더해지는 부품은 **뒤에 붙인다.** 후보가 놓은 자리에 끼워 넣으면, 일부만 골랐을\n * 때 순서가 어디서 온 것인지 알 수 없게 된다.\n */\nexport function applyProposal(\n current: FigureSource | undefined,\n proposal: FigureSource,\n picked?: Set<string>\n): FigureSource {\n const takes = (change: ProposalChange) => !picked || picked.has(change.key)\n const changes = diffProposal(current, proposal)\n\n if (!current) {\n return { ...proposal }\n }\n\n const removed = new Set(changes.filter(c => c.kind === 'removed' && takes(c)).map(c => c.key))\n const replaced = new Map(\n changes.filter(c => c.kind === 'changed' && takes(c)).map(c => [c.key, c.part as FigurePart])\n )\n const added = changes.filter(c => c.kind === 'added' && takes(c)).map(c => c.part as FigurePart)\n\n const parts = [\n ...current.parts.filter(part => !removed.has(part.name)).map(part => replaced.get(part.name) ?? part),\n ...added\n ]\n\n // 고른 필드만 후보 값으로 바꾼다. 안 고른 것은 지금 값이 남는다.\n const fields: Record<string, unknown> = {}\n for (const field of FIGURE_FIELDS) {\n const change = changes.find(c => c.key === field)\n if (change && takes(change)) {\n fields[field] = proposal[field]\n }\n }\n\n return { ...current, ...fields, parts } as FigureSource\n}\n\n/** 전부 받는다. */\nexport function takeProposal(current: FigureSource | undefined, proposal: FigureSource): FigureSource {\n return applyProposal(current, proposal)\n}\n"]}
@@ -1121,6 +1121,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1121
1121
  detail: 'tune',
1122
1122
  styleKit: 'palette',
1123
1123
  animations: 'animation',
1124
+ parameters: 'tune',
1124
1125
  capabilities: 'hub'
1125
1126
  }[change.kind];
1126
1127
  const on = this.proposalSession?.picked.has(change.key);