@things-factory/figure-ui 10.1.27 → 10.1.28

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 (75) hide show
  1. package/client/graphql/index.ts +23 -0
  2. package/client/modeller/base-box.ts +104 -0
  3. package/client/modeller/figure-animations.ts +81 -8
  4. package/client/modeller/figure-canvas.ts +18 -3
  5. package/client/modeller/figure-inspector.ts +151 -2
  6. package/client/modeller/figure-preview.ts +55 -0
  7. package/client/modeller/figure-report.ts +9 -4
  8. package/client/modeller/figure-side.ts +9 -1
  9. package/client/modeller/figure-source.ts +8 -0
  10. package/client/modeller/joint-edits.ts +66 -5
  11. package/client/modeller/joint-gizmo.ts +118 -0
  12. package/client/modeller/pose-defaults.ts +67 -0
  13. package/client/modeller/proposal.ts +9 -3
  14. package/client/modeller/scene-budget.ts +31 -40
  15. package/client/pages/figure-list-page.ts +4 -9
  16. package/client/pages/figure-modeller-page.ts +124 -41
  17. package/dist-client/graphql/index.d.ts +7 -0
  18. package/dist-client/graphql/index.js +21 -0
  19. package/dist-client/graphql/index.js.map +1 -1
  20. package/dist-client/modeller/base-box.d.ts +47 -0
  21. package/dist-client/modeller/base-box.js +52 -0
  22. package/dist-client/modeller/base-box.js.map +1 -0
  23. package/dist-client/modeller/figure-animations.d.ts +15 -0
  24. package/dist-client/modeller/figure-animations.js +77 -8
  25. package/dist-client/modeller/figure-animations.js.map +1 -1
  26. package/dist-client/modeller/figure-canvas.d.ts +9 -0
  27. package/dist-client/modeller/figure-canvas.js +14 -3
  28. package/dist-client/modeller/figure-canvas.js.map +1 -1
  29. package/dist-client/modeller/figure-inspector.d.ts +26 -0
  30. package/dist-client/modeller/figure-inspector.js +139 -2
  31. package/dist-client/modeller/figure-inspector.js.map +1 -1
  32. package/dist-client/modeller/figure-preview.d.ts +15 -0
  33. package/dist-client/modeller/figure-preview.js +46 -0
  34. package/dist-client/modeller/figure-preview.js.map +1 -1
  35. package/dist-client/modeller/figure-report.d.ts +3 -2
  36. package/dist-client/modeller/figure-report.js +9 -4
  37. package/dist-client/modeller/figure-report.js.map +1 -1
  38. package/dist-client/modeller/figure-side.d.ts +5 -1
  39. package/dist-client/modeller/figure-side.js +12 -0
  40. package/dist-client/modeller/figure-side.js.map +1 -1
  41. package/dist-client/modeller/figure-source.d.ts +7 -0
  42. package/dist-client/modeller/figure-source.js.map +1 -1
  43. package/dist-client/modeller/joint-edits.d.ts +23 -3
  44. package/dist-client/modeller/joint-edits.js +53 -5
  45. package/dist-client/modeller/joint-edits.js.map +1 -1
  46. package/dist-client/modeller/joint-gizmo.d.ts +31 -0
  47. package/dist-client/modeller/joint-gizmo.js +104 -0
  48. package/dist-client/modeller/joint-gizmo.js.map +1 -0
  49. package/dist-client/modeller/pose-defaults.d.ts +9 -0
  50. package/dist-client/modeller/pose-defaults.js +35 -0
  51. package/dist-client/modeller/pose-defaults.js.map +1 -0
  52. package/dist-client/modeller/proposal.d.ts +1 -1
  53. package/dist-client/modeller/proposal.js +8 -3
  54. package/dist-client/modeller/proposal.js.map +1 -1
  55. package/dist-client/modeller/scene-budget.d.ts +31 -34
  56. package/dist-client/modeller/scene-budget.js +28 -37
  57. package/dist-client/modeller/scene-budget.js.map +1 -1
  58. package/dist-client/pages/figure-list-page.js +4 -9
  59. package/dist-client/pages/figure-list-page.js.map +1 -1
  60. package/dist-client/pages/figure-modeller-page.d.ts +43 -0
  61. package/dist-client/pages/figure-modeller-page.js +121 -40
  62. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  63. package/dist-client/tsconfig.tsbuildinfo +1 -1
  64. package/package.json +3 -3
  65. package/test/ai-proposal-contract.test.ts +16 -11
  66. package/test/base-box.test.ts +154 -0
  67. package/test/joint-edits.test.ts +37 -2
  68. package/test/joint-gizmo.test.ts +174 -0
  69. package/test/pose-defaults.test.ts +71 -0
  70. package/test/scene-budget.test.ts +53 -0
  71. package/translations/en.json +13 -2
  72. package/translations/ja.json +13 -2
  73. package/translations/ko.json +13 -2
  74. package/translations/ms.json +13 -2
  75. package/translations/zh.json +13 -2
@@ -1,6 +1,7 @@
1
1
  import { LitElement } from 'lit';
2
2
  import type { FigureSource } from '@hatiolab/figure-model';
3
3
  import type { ViewSettings } from './figure-view.js';
4
+ import type { FigureDraft } from './figure-source.js';
4
5
  /**
5
6
  * 저작 중인 것을 **도면에 놓았을 때**의 모습.
6
7
  *
@@ -45,6 +46,13 @@ export declare class FigurePreview extends FigurePreview_base {
45
46
  static styles: import("lit").CSSResult;
46
47
  /** 저작 중인 것. 저장 형식으로 받는다 — 도면이 소비하는 것이 이 형식이다. */
47
48
  source?: FigureSource;
49
+ /**
50
+ * 저작 중인 초안. **기본값이 지금 무엇인지 알려고만 쥔다** — 고치지 않는다.
51
+ *
52
+ * `source` 로도 파라미터를 읽을 수 있지만, 후보를 미리 볼 때는 `source` 가 후보의 것이고
53
+ * 초안은 열려 있는 도형의 것이다. 「이 자세를 기본으로」는 열려 있는 도형에만 뜻이 있다.
54
+ */
55
+ draft?: FigureDraft;
48
56
  /**
49
57
  * 보기 설정. 저장되지 않는다(`figure-view.ts`).
50
58
  *
@@ -87,6 +95,13 @@ export declare class FigurePreview extends FigurePreview_base {
87
95
  * 갈릴 수 있다. 저작자가 화면에서 보는 1200 이 씬에 그대로 간다.
88
96
  */
89
97
  private give;
98
+ /**
99
+ * 세운 자세를 기본값으로 — **셈은 여기서 하지 않는다.**
100
+ *
101
+ * 초안을 쥐고 있는 것은 페이지이고, 되돌리기와 「저장 안 됨」도 거기서 매긴다. 여기서
102
+ * 초안을 만들어 보내면 그 셋이 두 곳으로 갈린다. 민 값만 올린다.
103
+ */
104
+ private poseAsDefault;
90
105
  private tellInstances;
91
106
  /**
92
107
  * 보기 설정이 바뀌면 **다시 세우지 않고 씬에 넣는다** — 두 개만 빼고.
@@ -5,6 +5,7 @@ import { i18next, localize } from '@operato/i18n';
5
5
  import { compile, validate } from '@hatiolab/figure-model';
6
6
  import { create, registerFigure } from '@hatiolab/things-scene';
7
7
  import { DEFAULT_VIEW, previewBoardDepth, sceneLook, WORKBENCH } from './figure-view.js';
8
+ import { poseChanges } from './pose-defaults.js';
8
9
  /**
9
10
  * 저작 중인 것을 **도면에 놓았을 때**의 모습.
10
11
  *
@@ -126,6 +127,24 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
126
127
  align-items: center;
127
128
  gap: 8px;
128
129
  }
130
+ /*
131
+ 막대 위에 놓는 단추. 자세를 만든 자리에 있어야 「지금 이것을 기본으로」가 읽힌다.
132
+ 앉힐 것이 있을 때만 나오므로, 늘 서 있는 칸이 아니다.
133
+ */
134
+ div[clips] button[pose] {
135
+ align-self: stretch;
136
+ padding: 4px 8px;
137
+ font: inherit;
138
+ font-size: 0.7rem;
139
+ color: rgba(255, 255, 255, 0.92);
140
+ background-color: rgba(255, 255, 255, 0.14);
141
+ border: 1px solid rgba(255, 255, 255, 0.22);
142
+ border-radius: 5px;
143
+ cursor: pointer;
144
+ }
145
+ div[clips] button[pose]:hover {
146
+ background-color: rgba(255, 255, 255, 0.24);
147
+ }
129
148
  div[clips] span[drive] {
130
149
  opacity: 0.6;
131
150
  font-size: 0.66rem;
@@ -176,8 +195,22 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
176
195
  * 넣을 자리를 못 찾는다.
177
196
  */
178
197
  renderDrivers(clips, parameters) {
198
+ /*
199
+ 지금 세운 자세를 기본값으로 앉히는 자리. **막대 옆에 둔다** — 자세를 만든 곳이 여기라,
200
+ 값을 읽어 파라미터 칸으로 옮겨 적는 왕복이 없어진다. 앉힐 것이 없으면 아예 안 나온다.
201
+ */
202
+ const changes = poseChanges(this.draft, this.giving);
179
203
  return html `
180
204
  <div clips>
205
+ ${changes.length > 0
206
+ ? html `<button
207
+ pose
208
+ title=${i18next.t('figure.text.pose-as-default-explained')}
209
+ @click=${() => this.poseAsDefault()}
210
+ >
211
+ ${i18next.t('figure.button.pose-as-default')}
212
+ </button>`
213
+ : ''}
181
214
  ${parameters.map(parameter => {
182
215
  const range = parameter.range;
183
216
  const value = this.giving[parameter.name] ?? parameter.default ?? range?.min ?? 0;
@@ -237,6 +270,15 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
237
270
  this.giving = { ...this.giving, [name]: value };
238
271
  this.tellInstances('parameters', this.giving);
239
272
  }
273
+ /**
274
+ * 세운 자세를 기본값으로 — **셈은 여기서 하지 않는다.**
275
+ *
276
+ * 초안을 쥐고 있는 것은 페이지이고, 되돌리기와 「저장 안 됨」도 거기서 매긴다. 여기서
277
+ * 초안을 만들어 보내면 그 셋이 두 곳으로 갈린다. 민 값만 올린다.
278
+ */
279
+ poseAsDefault() {
280
+ this.dispatchEvent(new CustomEvent('pose-as-default', { detail: { values: { ...this.giving } }, bubbles: true, composed: true }));
281
+ }
240
282
  tellInstances(key, values) {
241
283
  for (const component of this.scene?.root?.components ?? []) {
242
284
  component.set?.(key, { ...values });
@@ -424,6 +466,10 @@ __decorate([
424
466
  property({ type: Object }),
425
467
  __metadata("design:type", Object)
426
468
  ], FigurePreview.prototype, "source", void 0);
469
+ __decorate([
470
+ property({ type: Object }),
471
+ __metadata("design:type", Object)
472
+ ], FigurePreview.prototype, "draft", void 0);
427
473
  __decorate([
428
474
  property({ type: Object }),
429
475
  __metadata("design:type", Object)
@@ -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, previewBoardDepth, 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: 'base', 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.axes.length === 0 ? i18next.t('figure.label.preview-base') : 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>${i18next.t('figure.label.playback-speed')}</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: previewBoardDepth(source, view.ceilingHeight, biggest)\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, previewBoardDepth, sceneLook, WORKBENCH } from './figure-view.js'\nimport type { ViewSettings } from './figure-view.js'\nimport { poseChanges } from './pose-defaults.js'\nimport type { FigureDraft } from './figure-source.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: 'base', 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 /*\n 막대 위에 놓는 단추. 자세를 만든 자리에 있어야 「지금 이것을 기본으로」가 읽힌다.\n 앉힐 것이 있을 때만 나오므로, 늘 서 있는 칸이 아니다.\n */\n div[clips] button[pose] {\n align-self: stretch;\n padding: 4px 8px;\n font: inherit;\n font-size: 0.7rem;\n color: rgba(255, 255, 255, 0.92);\n background-color: rgba(255, 255, 255, 0.14);\n border: 1px solid rgba(255, 255, 255, 0.22);\n border-radius: 5px;\n cursor: pointer;\n }\n div[clips] button[pose]:hover {\n background-color: rgba(255, 255, 255, 0.24);\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 * 저작 중인 초안. **기본값이 지금 무엇인지 알려고만 쥔다** — 고치지 않는다.\n *\n * `source` 로도 파라미터를 읽을 수 있지만, 후보를 미리 볼 때는 `source` 가 후보의 것이고\n * 초안은 열려 있는 도형의 것이다. 「이 자세를 기본으로」는 열려 있는 도형에만 뜻이 있다.\n */\n @property({ type: Object }) draft?: FigureDraft\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.axes.length === 0 ? i18next.t('figure.label.preview-base') : 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 /*\n 지금 세운 자세를 기본값으로 앉히는 자리. **막대 옆에 둔다** — 자세를 만든 곳이 여기라,\n 값을 읽어 파라미터 칸으로 옮겨 적는 왕복이 없어진다. 앉힐 것이 없으면 아예 안 나온다.\n */\n const changes = poseChanges(this.draft, this.giving)\n\n return html`\n <div clips>\n ${changes.length > 0\n ? html`<button\n pose\n title=${i18next.t('figure.text.pose-as-default-explained')}\n @click=${() => this.poseAsDefault()}\n >\n ${i18next.t('figure.button.pose-as-default')}\n </button>`\n : ''}\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>${i18next.t('figure.label.playback-speed')}</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 /**\n * 세운 자세를 기본값으로 — **셈은 여기서 하지 않는다.**\n *\n * 초안을 쥐고 있는 것은 페이지이고, 되돌리기와 「저장 안 됨」도 거기서 매긴다. 여기서\n * 초안을 만들어 보내면 그 셋이 두 곳으로 갈린다. 민 값만 올린다.\n */\n private poseAsDefault(): void {\n this.dispatchEvent(\n new CustomEvent('pose-as-default', { detail: { values: { ...this.giving } }, bubbles: true, composed: true })\n )\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: previewBoardDepth(source, view.ceilingHeight, biggest)\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"]}
@@ -65,8 +65,9 @@ export declare class FigureReport extends FigureReport_base {
65
65
  * 직접 답하지 않는다 — 형상이 선언된 한도 안인지를 말할 뿐이다. 그래서 개수를
66
66
  * 머리에 올리고 등급은 옆의 badge 로 내린다.
67
67
  *
68
- * 근거를 함께 낸다. 개수의 분모는 실측이 아니라 설계 문서의 목표치이고(미결
69
- * D-20), 사실이 화면에 없으면 값처럼 읽힌다.
68
+ * 근거를 함께 낸다. 묶음으로 갈리는 개수의 분모는 **실측**이고(2026-09-18, Apple M5),
69
+ * 기계와 날짜를 함께 적는다 적지 않으면 어디서나 참인 수로 읽힌다. 삼각형 쪽은 아직 설계
70
+ * 문서의 목표치라 그렇다고 적는다.
70
71
  */
71
72
  private renderVerdict;
72
73
  /**
@@ -6,7 +6,7 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
6
6
  import { i18next, localize } from '@operato/i18n';
7
7
  import { ScrollbarStyles } from '@operato/styles';
8
8
  import { blocksRelease, compile, costCurveSvg, costOf, scoreOf, sizingReport, validate } from '@hatiolab/figure-model';
9
- import { capacityOf } from './scene-budget.js';
9
+ import { capacityOf, MEASURED } from './scene-budget.js';
10
10
  import { maturityOf } from './maturity.js';
11
11
  /**
12
12
  * 판단 패널. 모델러 화면에 **늘 떠 있다.**
@@ -488,8 +488,9 @@ let FigureReport = class FigureReport extends localize(i18next)(LitElement) {
488
488
  * 직접 답하지 않는다 — 형상이 선언된 한도 안인지를 말할 뿐이다. 그래서 개수를
489
489
  * 머리에 올리고 등급은 옆의 badge 로 내린다.
490
490
  *
491
- * 근거를 함께 낸다. 개수의 분모는 실측이 아니라 설계 문서의 목표치이고(미결
492
- * D-20), 사실이 화면에 없으면 값처럼 읽힌다.
491
+ * 근거를 함께 낸다. 묶음으로 갈리는 개수의 분모는 **실측**이고(2026-09-18, Apple M5),
492
+ * 기계와 날짜를 함께 적는다 적지 않으면 어디서나 참인 수로 읽힌다. 삼각형 쪽은 아직 설계
493
+ * 문서의 목표치라 그렇다고 적는다.
493
494
  */
494
495
  renderVerdict() {
495
496
  const score = this.score;
@@ -525,7 +526,11 @@ let FigureReport = class FigureReport extends localize(i18next)(LitElement) {
525
526
  per: capacity.per.toLocaleString(),
526
527
  budget: capacity.budget.toLocaleString()
527
528
  })}
528
- <em>${i18next.t('figure.text.budget-not-measured-yet')}</em>
529
+ <em>
530
+ ${capacity.bound === 'groups'
531
+ ? i18next.t('figure.text.budget-measured-on', { gpu: MEASURED.gpu, on: MEASURED.on })
532
+ : i18next.t('figure.text.budget-not-measured-yet')}
533
+ </em>
529
534
  </p>
530
535
  </section>
531
536
  `;
@@ -1 +1 @@
1
- {"version":3,"file":"figure-report.js","sourceRoot":"","sources":["../../client/modeller/figure-report.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing, svg } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { unsafeSVG } from 'lit/directives/unsafe-svg.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { ScrollbarStyles } from '@operato/styles'\nimport { blocksRelease, compile, costCurveSvg, costOf, scoreOf, sizingReport, validate } from '@hatiolab/figure-model'\nimport type { FigureCost, FigureScore, FigureSource } from '@hatiolab/figure-model'\n\nimport { capacityOf } from './scene-budget.js'\nimport { maturityOf } from './maturity.js'\nimport type { MaturityAxis } from './maturity.js'\nimport type { FigureCapacity } from './scene-budget.js'\n\n/**\n * 판단 패널. 모델러 화면에 **늘 떠 있다.**\n *\n * 저장을 선택해야 아는 것이 아니다. 부품을 하나 놓을 때마다 무게와 점수가 움직이고,\n * 그것이 보여야 저작자가 고칠 수 있다. 「무엇 때문에 점수가 낮은가」를 말해 주지\n * 않으면 점수는 잔소리일 뿐이다.\n *\n * ## 셈을 여기서 하지 않는다\n *\n * 검증·원가·점수는 전부 `@hatiolab/figure-model` 이 한다. 화면은 **부르고 보여 줄\n * 뿐**이다. 한도나 등급 경계를 화면이 다시 정하면 서버가 거절할 것을 화면이\n * 통과시키게 된다.\n */\n@customElement('figure-report')\nexport class FigureReport extends localize(i18next)(LitElement) {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow-y: auto;\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 section {\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n section > h3 {\n margin: 0 0 6px 0;\n font-size: var(--ui-t-micro, 11px);\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* ── 몇 개까지 편한가 ───────────────────────────────────────────\n 로우폴리의 값은 「얼마나 많이 편하게 놓을 수 있나」다. 그 답이 머리에 온다.\n 등급은 형상 자체가 한도 안인지를 말할 뿐이라 옆의 작은 badge 로 둔다. */\n div[verdict] {\n display: flex;\n align-items: baseline;\n gap: 6px;\n }\n span[capacity] {\n font-size: 2.1rem;\n font-weight: 800;\n line-height: 1;\n letter-spacing: -0.01em;\n color: var(--md-sys-color-primary);\n }\n span[unit] {\n font-size: 0.9rem;\n font-weight: 600;\n color: var(--md-sys-color-primary);\n }\n /*\n 등급 badge.\n\n 전에는 \"C 0\" 만 있었다. 글자 하나와 숫자 하나인데 어디에도 뜻이 없어서, 크게 뜬\n 개수 옆에 빨간 badge 가 붙은 것으로만 읽혔다. 0 을 100점 만점의 0 점으로 읽는 것이\n 자연스럽다. 실제 뜻은 \"선언한 한도에 남은 여유가 0\" 이다.\n\n 그래서 무엇을 측정하는지를 badge 안에 적고, 눈금은 바로 아래 줄에 늘 보이게 둔다.\n */\n span[grade] {\n display: flex;\n align-items: baseline;\n gap: 5px;\n margin-left: auto;\n padding: 3px 10px;\n font-size: 0.76rem;\n font-weight: 700;\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border-radius: 999em;\n }\n span[grade] small {\n font-size: 0.68rem;\n font-weight: 500;\n }\n /*\n C 는 한도를 지킨 것이다. 빨강은 넘긴 것(D)에만 쓴다. 지킨 것에 빨강을 쓰면\n 고칠 것이 없는데도 고쳐야 할 것처럼 보인다.\n */\n span[grade][tight] {\n color: var(--md-sys-color-on-tertiary-container, #3d2f00);\n background-color: var(--md-sys-color-tertiary-container, #ffdf9a);\n }\n /*\n Pair the container with its on-container.\n\n This used to fill with --md-sys-color-error and write with\n --md-sys-color-on-error-container. Once the palette made containers\n neutral and moved the meaning into the text colour, those two became the\n same red and the badge went blank -- a filled pill with no readable text.\n */\n span[grade][over] {\n color: var(--md-sys-color-on-error-container);\n background-color: var(--md-sys-color-error-container);\n }\n\n /* 등급 눈금. 접어 두지 않는다 — 한 번 보고 마는 값이 아니라 볼 때마다 필요하다 */\n p[scale] {\n display: flex;\n flex-wrap: wrap;\n gap: 2px 10px;\n margin: 6px 0 0 0;\n font-size: 0.7rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n p[scale] b {\n font-weight: 700;\n }\n p[scale] span[now] b {\n color: var(--md-sys-color-primary);\n }\n p[scale] span[now] {\n color: var(--md-sys-color-on-surface);\n }\n /* 이 큰 숫자가 어느 축인지 먼저 말한다 — 성숙도의 한 축이지 전부가 아니다 */\n p[lead] {\n margin: 0 0 4px 0;\n font-size: 0.74rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* 근거를 함께 낸다 — 숫자만 크게 내걸면 재 본 값처럼 읽힌다 */\n p[basis] {\n margin: 6px 0 0 0;\n font-size: 0.72rem;\n line-height: 1.55;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n p[basis] em {\n font-style: normal;\n color: var(--md-sys-color-tertiary);\n }\n span[score] {\n font-size: 0.8rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* 무엇 때문에 이 등급인가 — 한도에 가장 가까운 항목 */\n p[because] {\n margin: 8px 0 0 0;\n /* 본문 크기를 물려받으면 이 줄만 제목처럼 커진다 — 화면을 띄워 보고 알았다 */\n font-size: 0.78rem;\n line-height: 1.6;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n p[because] strong {\n color: var(--md-sys-color-on-surface);\n }\n\n /* ── 성숙도 radar ────────────────────────────────────────────────\n 여섯 축을 한 그림으로. 목록만 두면 「어느 쪽이 모자란가」가 안 보인다.\n 못 잰 축은 점을 찍지 않고 살만 흐리게 남긴다 — 0 으로 찍으면 「나쁘다」가 된다. */\n div[radar] {\n display: flex;\n justify-content: center;\n padding: 2px 0 6px 0;\n color: var(--md-sys-color-primary);\n }\n div[radar] svg {\n width: 100%;\n max-width: 240px;\n height: auto;\n }\n div[radar] polygon[web] {\n fill: none;\n stroke: var(--md-sys-color-outline-variant);\n stroke-width: 0.6;\n }\n div[radar] line[spoke] {\n stroke: var(--md-sys-color-outline-variant);\n stroke-width: 0.6;\n }\n div[radar] polygon[shape] {\n fill: color-mix(in srgb, var(--md-sys-color-primary) 22%, transparent);\n stroke: var(--md-sys-color-primary);\n stroke-width: 1.4;\n stroke-linejoin: round;\n }\n div[radar] circle[dot] {\n fill: var(--md-sys-color-primary);\n }\n div[radar] text {\n font-size: 5.2px;\n fill: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* 그림에 자리를 주지 않은 축을 글로 밝힌다 */\n p[unmeasured] {\n margin: 0 0 8px 0;\n font-size: 0.7rem;\n line-height: 1.5;\n color: var(--md-sys-color-tertiary);\n }\n\n /* ── 성숙도 축 ──────────────────────────────────────────────────\n 한 숫자로 합치지 않는다. 합치면 「무엇을 고쳐야 하나」가 사라진다. */\n ul[axes] {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 7px;\n }\n ul[axes] li {\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 2px 8px;\n font-size: 0.75rem;\n }\n ul[axes] li[blocked] {\n opacity: 0.7;\n }\n ul[axes] em {\n grid-column: 1 / -1;\n font-style: normal;\n font-size: 0.68rem;\n line-height: 1.5;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n /* 못 잰 축은 0 으로 두지 않는다 — 0 은 「나쁘다」는 뜻이 되어 버린다 */\n ul[axes] li[blocked] em {\n color: var(--md-sys-color-tertiary);\n }\n span[reading] {\n font-family: var(--mono-font, monospace);\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* ── 예산 막대 ────────────────────────────────────────────────── */\n ul[budgets] {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n ul[budgets] li {\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 2px 8px;\n font-size: 0.75rem;\n }\n div[bar] {\n grid-column: 1 / -1;\n height: 4px;\n background-color: var(--md-sys-color-surface-container-highest);\n border-radius: 2px;\n overflow: hidden;\n }\n div[bar] > i {\n display: block;\n height: 100%;\n background-color: var(--md-sys-color-primary);\n }\n div[bar] > i[tight] {\n background-color: var(--md-sys-color-error);\n }\n\n /* ── 무게 ─────────────────────────────────────────────────────── */\n dl[metrics] {\n margin: 0;\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 4px 8px;\n font-size: 0.78rem;\n }\n dl[metrics] dt {\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n dl[metrics] dd {\n margin: 0;\n text-align: right;\n font-family: var(--mono-font, monospace);\n }\n\n /* ── 추이 ──────────────────────────────────────────────────────────\n 그림은 자연 크기가 680px 다. 300px 칸에 넣으면 글자가 4px 이 되어 아무것도\n 읽을 수 없다 — 사이드바에는 숫자를 두고, 그림은 선택해서 크게 본다. */\n button[expand] {\n margin-top: 6px;\n padding: 4px 10px;\n font: var(--label-font, inherit);\n font-size: 0.74rem;\n color: var(--md-sys-color-on-surface);\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n cursor: pointer;\n }\n button[expand]:hover {\n background-color: var(--md-sys-color-surface-container);\n }\n\n div[sheet] {\n position: fixed;\n inset: 0;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--spacing-large, 12px);\n background-color: rgba(0, 0, 0, 0.32);\n }\n div[sheet] > figure {\n max-width: 100%;\n max-height: 100%;\n margin: 0;\n padding: var(--spacing-large, 12px);\n overflow: auto;\n color: var(--md-sys-color-primary);\n background-color: var(--md-sys-color-surface-container-lowest);\n /* 담는 면이 팝업이면 안쪽 모서리는 알약이다 — 시트 자체는 페이지 값을 쓴다 */\n border-radius: 6px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.28);\n }\n div[sheet] svg {\n display: block;\n /* 자연 크기보다 작게 줄이지 않는다 — 좁으면 넘겨 보게 둔다 */\n min-width: 680px;\n }\n div[sheet] figcaption {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n }\n div[sheet] button[close] {\n padding: 5px 14px;\n font: var(--label-font, inherit);\n color: var(--md-sys-color-on-surface);\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 999em;\n cursor: pointer;\n }\n\n /* ── 알림 ─────────────────────────────────────────────────────── */\n ul[findings] {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n ul[findings] li {\n display: flex;\n gap: 6px;\n line-height: 1.5;\n }\n ul[findings] md-icon {\n --md-icon-size: 16px;\n flex: none;\n color: var(--md-sys-color-error);\n }\n ul[findings] li[violation] md-icon {\n color: var(--md-sys-color-tertiary);\n }\n /* 어긴 자리. 문구에 붙어 흐르면 문장의 일부로 읽힌다 — badge 로 보이게 뗀다 */\n code[at] {\n padding: 1px 5px;\n font-family: var(--mono-font, monospace);\n font-size: 0.68rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: var(--md-sys-color-surface-container);\n border-radius: 4px;\n white-space: nowrap;\n }\n\n /*\n 발견이 없을 때의 문구.\n\n 크기를 안 적어 두었더니 본문 기본 크기를 물려받아 이 줄만 제목처럼 커졌다. 이\n 패널의 글자는 전부 0.7rem 대인데 여기만 1rem 이라 아래쪽이 무너져 보였다.\n\n 찾은 것이 있을 때의 목록(ul[findings] li)과 같은 크기로 맞춘다. 있을 때와 없을 때\n 절의 생김새가 달라질 이유가 없다.\n\n 이 파일에서 같은 실수를 두 번째로 한다. p[because] 에도 같은 주석이 있다.\n */\n p[quiet] {\n margin: 0;\n font-size: 0.76rem;\n line-height: 1.6;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n `\n ]\n\n /** 저작 중인 원본 데이터. 바뀌면 다시 측정한다. */\n @property({ type: Object }) source?: FigureSource\n\n @state() private cost?: FigureCost\n @state() private score?: FigureScore\n @state() private curve = ''\n /** 1만 개를 놓았을 때. 저작자가 실제로 궁금한 숫자다. */\n @state() private many?: FigureCost\n /** 몇 개까지 놓을 수 있나. 이 패널의 머리다. */\n @state() private capacity?: FigureCapacity\n /** 추이 그림을 제 크기로 펼쳤나. */\n @state() private expanded = false\n @state() private errors: { code: string; message: string; at?: string }[] = []\n /**\n * 형식이 권하지 않는 것 — **그중 둘은 발행을 막는다.**\n *\n * 등급을 형식에서 받는다(`blocksRelease`). 화면이 제 목록을 들면 형식에 코드가 하나 늘 때\n * 조용히 「알림」으로 그려지고, 저작자는 발행이 막힌 것을 **눌러 보고 나서야** 안다.\n *\n * `actual` 은 얼마나 넘쳤나다. 「상자를 벗어난다」만으로는 2mm 인지 250mm 인지 모르고,\n * 그 둘은 고치는 품이 다르다.\n */\n @state() private violations: { code: string; message: string; blocking: boolean; actual?: number }[] = []\n /**\n * 크기를 바꿔 봤을 때 갈라지는가 — **발행을 막는 것**이다.\n *\n * 이력 탭이 같은 판정을 서버에서 받아 발행 문을 잠그지만, 그것은 **저장된 정본**에 대한\n * 것이라 저장하기 전에는 답하지 못한다. 여기서는 저작 중인 정본을 그대로 재므로, 부품을\n * 옮기는 그 자리에서 갈라짐이 생겼는지 사라졌는지 보인다 — 저장하고 탭을 옮겨 확인하게\n * 두면 고칠 때가 이미 지나 있다.\n */\n @state() private sizing: { code: string; message: string; at?: string; blocking: boolean }[] = []\n /**\n * 아직 부품이 하나도 없나 — **저작 전이지 형식을 어긴 것이 아니다.**\n *\n * 형식은 부품 0개를 오류로 본다(`missing-parts`). 발행 관점에서는 맞다. 그런데 새로 만들기를\n * 누른 직후가 바로 그 상태이므로, 그것을 그대로 오류로 그리면 **아무것도 하지 않았는데 붉은\n * 화면**을 보게 된다. 그래서 이 자리에서 둘을 가른다.\n */\n @state() private bare = false\n\n willUpdate(changed: Map<string, unknown>) {\n if (changed.has('source')) {\n this.measure()\n }\n }\n\n render() {\n /*\n 부품이 없으면 이 자리는 **판정이 아니라 안내**다. 이미 있던 「부품을 추가하면 나옵니다」가\n 정확히 그 말이므로 그것을 쓴다 — 같은 상태에 문장을 둘 두지 않는다.\n\n 아래 셈들(머리·축·한도·추이)은 부품이 있어야 뜻이 있고, 재지 않은 값을 들고 그리면 0 으로\n 보인다. 0 은 「나쁘다」로 읽히므로 그 자리에 두지 않는다.\n */\n if (!this.source || this.bare) {\n return html`\n <section>\n <p quiet>${i18next.t('figure.text.report-appears-when-you-add-a-part')}</p>\n </section>\n `\n }\n\n // 형식을 어긴 상태에서는 셈이 뜻을 갖지 못한다 — 무엇이 잘못됐는지만 보여 준다.\n if (this.errors.length > 0) {\n return html`${this.renderFindings()}`\n }\n\n return html`\n ${this.renderVerdict()} ${this.renderAxes()} ${this.renderBudgets()} ${this.renderMetrics()} ${this.renderCurve()}\n ${this.renderFindings()} ${this.renderSheet()}\n `\n }\n\n /**\n * 머리 — 몇 개까지 편한가.\n *\n * 로우폴리의 값은 「얼마나 많이 편하게 놓을 수 있나」에 있다. 등급 B 는 그 물음에\n * 직접 답하지 않는다 — 형상이 선언된 한도 안인지를 말할 뿐이다. 그래서 개수를\n * 머리에 올리고 등급은 옆의 badge 로 내린다.\n *\n * 근거를 함께 낸다. 이 개수의 분모는 실측이 아니라 설계 문서의 목표치이고(미결\n * D-20), 그 사실이 화면에 없으면 재 본 값처럼 읽힌다.\n */\n private renderVerdict() {\n const score = this.score\n const capacity = this.capacity\n if (!score || !capacity) return nothing\n\n return html`\n <section>\n <h3>${i18next.t('figure.label.maturity')}</h3>\n <p lead>${i18next.t('figure.label.axis-scale')} — ${i18next.t('figure.text.how-many-fit')}</p>\n <div verdict>\n <span capacity>${capacity.instances.toLocaleString()}</span>\n <span unit>${i18next.t('figure.text.instances-unit')}</span>\n <span\n grade\n ?tight=${score.grade === 'C'}\n ?over=${score.grade === 'D'}\n title=${i18next.t('figure.text.grade-explained')}\n >\n ${score.grade}\n <small>${i18next.t('figure.text.headroom-percent', { percent: Math.round(score.score) })}</small>\n </span>\n </div>\n <p because>\n ${i18next.t('figure.text.grade-decided-by')}\n <strong>${i18next.t('figure.label.budget-' + score.tightest.key)}</strong>\n — ${score.tightest.used} / ${score.tightest.limit}\n (${Math.round((score.tightest.used / score.tightest.limit) * 100)}%)\n </p>\n ${this.renderGradeScale(score.grade)}\n <p basis>\n ${i18next.t('figure.text.bound-by-' + capacity.bound, {\n per: capacity.per.toLocaleString(),\n budget: capacity.budget.toLocaleString()\n })}\n <em>${i18next.t('figure.text.budget-not-measured-yet')}</em>\n </p>\n </section>\n `\n }\n\n /**\n * 등급 눈금.\n *\n * A·B·C·D 가 무엇을 뜻하는지 화면 어디에도 없었다. badge 만 보고는 좋은지 나쁜지도,\n * 무엇을 고쳐야 오르는지도 알 수 없다. 눈금을 늘 곁에 두고 지금 어디에 있는지를\n * 표시한다.\n *\n * 접어 두지 않는다. 한 번 배우고 마는 값이 아니라 볼 때마다 대조하는 값이다.\n */\n private renderGradeScale(now: string) {\n const bounds: [string, string][] = [\n ['A', '≤ 50%'],\n ['B', '≤ 75%'],\n ['C', '≤ 100%'],\n ['D', '> 100%']\n ]\n\n return html`\n <p scale>\n <span>${i18next.t('figure.text.grade-scale-lead')}</span>\n ${bounds.map(\n ([grade, bound]) => html`<span ?now=${grade === now}><b>${grade}</b> ${bound}</span>`\n )}\n </p>\n `\n }\n\n /**\n * 성숙도 — 여러 축으로 본다.\n *\n * 「몇 개까지 편한가」 하나로는 좋은 형상과 나쁜 형상이 구분되지 않는다. 부품 하나짜리\n * 상자도 그 축에서는 만점이다.\n *\n * **못 재는 축을 0 으로 두지 않는다.** 0 은 「나쁘다」는 뜻이 되는데, 사실은 아직\n * 측정할 방법이 없는 것이다. 측정하지 못한다고 알리고 이유를 붙인다. 지어낸 점수보다 그것이\n * 이 리포트를 믿을 만하게 만든다.\n */\n private renderAxes() {\n if (!this.cost || !this.score || !this.source) return nothing\n\n /*\n 규모는 목록에서 뺀다 — 머리로 올라갔다. 같은 숫자를 두 번 내면 읽는 사람이\n 둘이 다른 것인 줄 안다.\n */\n const axes = maturityOf(this.source, this.cost, this.score).filter(axis => axis.key !== 'scale')\n\n // radar 에는 여섯을 다 그린다. 규모가 어디쯤인지 함께 보여야 뜻이 있다\n const all = maturityOf(this.source, this.cost, this.score)\n\n return html`\n <section>\n <h3>${i18next.t('figure.label.by-axis')}</h3>\n <div radar>${this.renderRadar(all.filter(axis => axis.ratio !== undefined))}</div>\n ${this.renderUnmeasured(all.filter(axis => axis.ratio === undefined))}\n <ul axes>\n ${axes.map(axis => this.renderAxis(axis))}\n </ul>\n </section>\n `\n }\n\n /**\n * 성숙도 radar. **측정한 축만 그린다.**\n *\n * 측정하지 못하는 축도 spoke 를 그려 두고 도형만 건너뛰던 때가 있었다. 그러면 그 방향을 가로지르는\n * 직선이 생겨서 도형이 그쪽으로 움푹 들어간 것처럼 보인다. 0 으로 찍지 않으려고 한\n * 일인데 결과는 0 으로 찍은 것과 똑같이 읽혔다. 점선 살과 물음표로는 그 인상이\n * 지워지지 않았다.\n *\n * 그래서 아예 빼고, 못 재는 축은 그림 아래에 글로 적는다. 잴 수 없는 것은 그림에\n * 자리를 주지 않는 편이 정직하다.\n */\n private renderRadar(axes: MaturityAxis[]) {\n // 그림 안에서는 **짧은 이름**을 쓴다. 긴 이름을 그대로 쓰면 왼쪽이 잘린다 —\n // 화면을 띄워 보고 알았다.\n const cx = 60\n const cy = 46\n const r = 28\n\n const at = (index: number, ratio: number) => {\n const angle = -Math.PI / 2 + (index * 2 * Math.PI) / axes.length\n return [cx + r * ratio * Math.cos(angle), cy + r * ratio * Math.sin(angle)]\n }\n\n const points = axes\n .map((axis, index) => at(index, Math.max(0.04, Math.min(1, axis.ratio ?? 0))).map(n => n.toFixed(1)).join(','))\n .join(' ')\n\n return svg`\n <svg viewBox=\"0 0 120 96\" aria-hidden=\"true\">\n ${[0.33, 0.66, 1].map(\n ring => svg`<polygon web points=${axes.map((_, i) => at(i, ring).map(n => n.toFixed(1)).join(',')).join(' ')}></polygon>`\n )}\n ${axes.map((_, index) => {\n const [x, y] = at(index, 1)\n return svg`<line spoke x1=${cx} y1=${cy} x2=${x.toFixed(1)} y2=${y.toFixed(1)}></line>`\n })}\n <polygon shape points=${points}></polygon>\n ${axes.map((axis, index) => {\n const [x, y] = at(index, Math.max(0.04, Math.min(1, axis.ratio ?? 0)))\n return svg`<circle dot cx=${x.toFixed(1)} cy=${y.toFixed(1)} r=\"1.4\"></circle>`\n })}\n ${axes.map((axis, index) => {\n const [x, y] = at(index, 1.28)\n const anchor = x < cx - 3 ? 'end' : x > cx + 3 ? 'start' : 'middle'\n return svg`\n <text x=${x.toFixed(1)} y=${(y + 1.8).toFixed(1)} text-anchor=${anchor}>\n ${i18next.t('figure.label.axis-' + axis.key + '-short')}\n </text>\n `\n })}\n </svg>\n `\n }\n\n /**\n * 그림에 없는 축을 글로 밝힌다.\n *\n * 빼기만 하면 축이 여섯인지 다섯인지 알 수 없고, 없는 것을 없다고 말하지 않은 셈이\n * 된다. 무엇을 아직 못 재는지 한 줄로 적는다. 왜 못 재는지는 아래 목록의 그 항목에\n * 그대로 있다.\n */\n private renderUnmeasured(axes: MaturityAxis[]) {\n if (axes.length === 0) return nothing\n\n return html`\n <p unmeasured>\n ${i18next.t('figure.text.axes-not-measurable-yet', {\n axes: axes.map(axis => i18next.t('figure.label.axis-' + axis.key)).join(' · ')\n })}\n </p>\n `\n }\n\n private renderAxis(axis: MaturityAxis) {\n const name = i18next.t('figure.label.axis-' + axis.key)\n\n if (axis.blocked) {\n return html`\n <li blocked>\n <span>${name}</span>\n <span reading>${i18next.t('figure.text.not-measurable-yet')}</span>\n <em>${i18next.t(axis.blocked)}</em>\n </li>\n `\n }\n\n const ratio = Math.max(0, Math.min(1, axis.ratio ?? 0))\n return html`\n <li>\n <span>${name}</span>\n <span reading>${axis.readingKey ? i18next.t(axis.readingKey) : (axis.reading ?? '')}</span>\n <div bar><i style=\"width:${Math.round(ratio * 100)}%\"></i></div>\n ${axis.because ? html`<em>${i18next.t(axis.because)}</em>` : ''}\n </li>\n `\n }\n\n /**\n * 예산 막대.\n *\n * 숫자만 늘어놓으면 어느 것이 꽉 찼는지 눈에 안 들어온다. 한도에 가장 가까운 하나를 색으로\n * 구분한다. 그것이 점수를 정한 항목이다.\n *\n * ## `use.label` 을 쓰지 않는 이유\n *\n * `FigureBudgetUse` 에 `label` 과 `why` 가 있고 「저작자에게 그대로 보여 준다」고\n * 적혀 있다. 그런데 그 문구가 **한국어로 구워져** 있다. 이 제품은 5언어를 쓰므로\n * 그것을 그대로 실으면 다른 언어 사용자에게 한국어가 나온다.\n *\n * `budgets` 는 **배열**이다. `Object.entries` 로 돌면 키가 인덱스(0·1·2…)가 되어\n * `figure.label.budget-0` 같은 없는 키를 찾는다 — 화면을 띄워 보고서야 드러났다.\n * 배열을 그대로 돌고 `use.key` 를 쓴다.\n *\n * 그래서 **안정된 `key`** 로 이 화면의 i18n 을 찾는다. `key` 는 열거형이라 형식의\n * 일부이고, 문구는 화면의 몫이다. figure-model 이 문구를 언어 중립으로 바꾸면\n * 그때 `label` 을 쓸 수 있다.\n */\n private renderBudgets() {\n const score = this.score\n if (!score) return nothing\n\n return html`\n <section>\n <h3>${i18next.t('figure.label.budgets')}</h3>\n <ul budgets>\n ${score.budgets.map(use => {\n const ratio = Math.min(1, use.ratio)\n const tight = use.key === score.tightest.key\n return html`\n <li>\n <span>${i18next.t('figure.label.budget-' + use.key)}</span>\n <span>${use.used} / ${use.limit}</span>\n <div bar><i ?tight=${tight} style=\"width:${Math.round(ratio * 100)}%\"></i></div>\n </li>\n `\n })}\n </ul>\n </section>\n `\n }\n\n private renderMetrics() {\n const cost = this.cost\n if (!cost) return nothing\n\n return html`\n <section>\n <h3>${i18next.t('figure.label.weight')}</h3>\n <dl metrics>\n <dt>${i18next.t('figure.label.triangles')}</dt>\n <dd>${cost.triangles.toLocaleString()}</dd>\n\n <dt>${i18next.t('figure.label.triangles-at-100')}</dt>\n <dd>${cost.at.triangles.toLocaleString()}</dd>\n\n <dt>${i18next.t('figure.label.draw-calls-at-100')}</dt>\n <dd>${cost.at.drawCalls.toLocaleString()}</dd>\n\n <dt>${i18next.t('figure.label.distinct-shapes')}</dt>\n <dd>${cost.distinctShapes}</dd>\n\n <dt>${i18next.t('figure.label.distinct-materials')}</dt>\n <dd>${cost.distinctMaterials}</dd>\n </dl>\n </section>\n `\n }\n\n /**\n * 재활용 추이 — 「많이 놓으면 어떻게 되나」에 답한다.\n *\n * ## 그림을 사이드바에 밀어 넣지 않는다\n *\n * `costCurveSvg` 의 자연 크기는 680x380 이고 글자가 11px 다. 300px 칸에서\n * width:100% 로 줄이면 글자가 4px 이 되어 **아무것도 읽을 수 없다** — 화면을 띄워\n * 보고서야 드러났다. 읽을 수 없는 그림은 없는 것과 같으므로, 사이드바에는 저작자가\n * 실제로 궁금한 숫자(1만 개일 때)를 두고 그림은 선택해서 제 크기로 본다.\n */\n private renderCurve() {\n const many = this.many\n if (!many && !this.curve) return nothing\n\n return html`\n <section>\n <h3>${i18next.t('figure.label.reuse-trend')}</h3>\n ${many\n ? html`\n <dl metrics>\n <dt>${i18next.t('figure.label.triangles-at-10k')}</dt>\n <dd>${many.at.triangles.toLocaleString()}</dd>\n\n <dt>${i18next.t('figure.label.draw-calls-at-10k')}</dt>\n <dd>${many.at.drawCalls.toLocaleString()}</dd>\n </dl>\n `\n : nothing}\n ${this.curve\n ? html`\n <button expand @click=${() => (this.expanded = true)}>\n ${i18next.t('figure.button.see-the-trend-chart')}\n </button>\n `\n : nothing}\n </section>\n `\n }\n\n /** 그림을 제 크기로 보여 준다. 좁으면 줄이지 않고 넘겨 보게 둔다. */\n private renderSheet() {\n if (!this.expanded || !this.curve) return nothing\n\n return html`\n <div sheet @click=${() => (this.expanded = false)}>\n <figure @click=${(e: Event) => e.stopPropagation()}>\n ${unsafeSVG(this.curve)}\n <figcaption>\n <button close @click=${() => (this.expanded = false)}>${i18next.t('figure.button.close')}</button>\n </figcaption>\n </figure>\n </div>\n `\n }\n\n /**\n * 발견.\n *\n * 오류(막는 것)와 위반(알리는 것)을 **가려서** 보여 준다. 섞으면 저작자가 무엇을\n * 반드시 고쳐야 하는지 모른다. 아무것도 없으면 「없다」고 말한다 — 빈 자리로 두면\n * 검사가 돌았는지 알 수 없다.\n */\n private renderFindings() {\n if (this.errors.length === 0 && this.violations.length === 0 && this.sizing.length === 0) {\n return html`\n <section>\n <h3>${i18next.t('figure.label.findings')}</h3>\n <p quiet>${i18next.t('figure.text.no-findings')}</p>\n </section>\n `\n }\n\n return html`\n <section>\n <h3>${i18next.t('figure.label.findings')}</h3>\n <ul findings>\n ${this.sizing.map(\n finding => html`\n <li ?violation=${!finding.blocking}>\n <md-icon>call_split</md-icon>\n <span>${finding.message} ${finding.at ? html`<code at>${finding.at}</code>` : ''}</span>\n </li>\n `\n )}\n ${this.errors.map(\n e => html`\n <li>\n <md-icon>error</md-icon>\n <span>${e.message} ${e.at ? html`<code at>${e.at}</code>` : ''}</span>\n </li>\n `\n )}\n ${this.violations.map(\n v => html`\n <li ?violation=${!v.blocking}>\n <md-icon>${v.blocking ? 'block' : 'info'}</md-icon>\n <span>\n ${v.message}${v.blocking && v.actual !== undefined\n ? html` <code at>${v.actual}</code>`\n : ''}\n </span>\n </li>\n `\n )}\n </ul>\n </section>\n `\n }\n\n /** 원본 데이터를 측정해 결과를 상태에 담는다. 셈은 전부 figure-model 이 한다. */\n private measure() {\n const source = this.source\n if (!source) {\n this.cost = undefined\n this.score = undefined\n this.many = undefined\n this.capacity = undefined\n this.curve = ''\n this.errors = []\n this.violations = []\n this.sizing = []\n return\n }\n\n /*\n 부품이 없으면 판정을 아예 하지 않는다 — 잴 것이 없다. 「무엇을 고쳐야 하나」가 아니라\n 「무엇부터 놓나」인 상태이고, 그 안내는 부품 목록이 이미 하고 있다.\n */\n this.bare = !source.parts?.length\n if (this.bare) {\n this.cost = undefined\n this.score = undefined\n this.many = undefined\n this.capacity = undefined\n this.curve = ''\n this.errors = []\n this.violations = []\n this.sizing = []\n return\n }\n\n const result = validate(source)\n this.errors = result.errors.map(e => ({ code: e.code, message: e.message, at: e.path }))\n this.violations = result.violations.map(v => ({\n code: v.code,\n message: v.message,\n blocking: blocksRelease(v.code),\n actual: v.actual\n }))\n\n if (this.errors.length > 0) {\n // 형식을 어겼으면 컴파일이 던진다. 셈을 비우고 오류만 보여 준다.\n this.cost = undefined\n this.score = undefined\n this.many = undefined\n this.capacity = undefined\n this.curve = ''\n this.sizing = []\n this.dispatchEvent(\n new CustomEvent('verdict-changed', { detail: {}, bubbles: true, composed: true })\n )\n return\n }\n\n const blueprint = compile(source)\n\n /*\n 크기를 바꿔 보는 일곱 조합. 판정 규칙은 `figure-model` 의 `gate.ts` 가 정한 것을 그대로\n 쓴다 — 무엇이 막는 것인지 화면이 다시 정하면 서버와 다른 말을 하게 된다.\n */\n const report = sizingReport(blueprint)\n this.sizing = report.findings.map(finding => ({\n code: finding.code,\n message: finding.message,\n /* 숫자가 아니라 자리를 말한다 — 갈라져 나온 부품 이름이 고칠 자리다. */\n at: finding.clumps.map(clump => [...clump].sort().join('+')).join(' | ') || undefined,\n blocking: finding.code === 'sizing-splits'\n }))\n\n this.cost = costOf(blueprint, 100)\n // 1만 개는 figure-model 에 다시 묻는다 — 화면이 곱셈으로 짐작하지 않는다.\n this.many = costOf(blueprint, 10000)\n this.score = scoreOf(blueprint)\n this.capacity = capacityOf(this.cost)\n\n // 개수를 밖으로 알린다. 탭 이름표가 이 값을 보여 주는데, 거기서 다시 셈하면\n // 같은 형상이 두 자리에서 다르게 나올 수 있다.\n this.dispatchEvent(\n new CustomEvent('verdict-changed', {\n detail: { instances: this.capacity?.instances, grade: this.score?.grade },\n bubbles: true,\n composed: true\n })\n )\n this.curve = costCurveSvg(blueprint)\n }\n}\n"]}
1
+ {"version":3,"file":"figure-report.js","sourceRoot":"","sources":["../../client/modeller/figure-report.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing, svg } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { unsafeSVG } from 'lit/directives/unsafe-svg.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { ScrollbarStyles } from '@operato/styles'\nimport { blocksRelease, compile, costCurveSvg, costOf, scoreOf, sizingReport, validate } from '@hatiolab/figure-model'\nimport type { FigureCost, FigureScore, FigureSource } from '@hatiolab/figure-model'\n\nimport { capacityOf, MEASURED } from './scene-budget.js'\nimport { maturityOf } from './maturity.js'\nimport type { MaturityAxis } from './maturity.js'\nimport type { FigureCapacity } from './scene-budget.js'\n\n/**\n * 판단 패널. 모델러 화면에 **늘 떠 있다.**\n *\n * 저장을 선택해야 아는 것이 아니다. 부품을 하나 놓을 때마다 무게와 점수가 움직이고,\n * 그것이 보여야 저작자가 고칠 수 있다. 「무엇 때문에 점수가 낮은가」를 말해 주지\n * 않으면 점수는 잔소리일 뿐이다.\n *\n * ## 셈을 여기서 하지 않는다\n *\n * 검증·원가·점수는 전부 `@hatiolab/figure-model` 이 한다. 화면은 **부르고 보여 줄\n * 뿐**이다. 한도나 등급 경계를 화면이 다시 정하면 서버가 거절할 것을 화면이\n * 통과시키게 된다.\n */\n@customElement('figure-report')\nexport class FigureReport extends localize(i18next)(LitElement) {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow-y: auto;\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 section {\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n section > h3 {\n margin: 0 0 6px 0;\n font-size: var(--ui-t-micro, 11px);\n font-weight: 700;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* ── 몇 개까지 편한가 ───────────────────────────────────────────\n 로우폴리의 값은 「얼마나 많이 편하게 놓을 수 있나」다. 그 답이 머리에 온다.\n 등급은 형상 자체가 한도 안인지를 말할 뿐이라 옆의 작은 badge 로 둔다. */\n div[verdict] {\n display: flex;\n align-items: baseline;\n gap: 6px;\n }\n span[capacity] {\n font-size: 2.1rem;\n font-weight: 800;\n line-height: 1;\n letter-spacing: -0.01em;\n color: var(--md-sys-color-primary);\n }\n span[unit] {\n font-size: 0.9rem;\n font-weight: 600;\n color: var(--md-sys-color-primary);\n }\n /*\n 등급 badge.\n\n 전에는 \"C 0\" 만 있었다. 글자 하나와 숫자 하나인데 어디에도 뜻이 없어서, 크게 뜬\n 개수 옆에 빨간 badge 가 붙은 것으로만 읽혔다. 0 을 100점 만점의 0 점으로 읽는 것이\n 자연스럽다. 실제 뜻은 \"선언한 한도에 남은 여유가 0\" 이다.\n\n 그래서 무엇을 측정하는지를 badge 안에 적고, 눈금은 바로 아래 줄에 늘 보이게 둔다.\n */\n span[grade] {\n display: flex;\n align-items: baseline;\n gap: 5px;\n margin-left: auto;\n padding: 3px 10px;\n font-size: 0.76rem;\n font-weight: 700;\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border-radius: 999em;\n }\n span[grade] small {\n font-size: 0.68rem;\n font-weight: 500;\n }\n /*\n C 는 한도를 지킨 것이다. 빨강은 넘긴 것(D)에만 쓴다. 지킨 것에 빨강을 쓰면\n 고칠 것이 없는데도 고쳐야 할 것처럼 보인다.\n */\n span[grade][tight] {\n color: var(--md-sys-color-on-tertiary-container, #3d2f00);\n background-color: var(--md-sys-color-tertiary-container, #ffdf9a);\n }\n /*\n Pair the container with its on-container.\n\n This used to fill with --md-sys-color-error and write with\n --md-sys-color-on-error-container. Once the palette made containers\n neutral and moved the meaning into the text colour, those two became the\n same red and the badge went blank -- a filled pill with no readable text.\n */\n span[grade][over] {\n color: var(--md-sys-color-on-error-container);\n background-color: var(--md-sys-color-error-container);\n }\n\n /* 등급 눈금. 접어 두지 않는다 — 한 번 보고 마는 값이 아니라 볼 때마다 필요하다 */\n p[scale] {\n display: flex;\n flex-wrap: wrap;\n gap: 2px 10px;\n margin: 6px 0 0 0;\n font-size: 0.7rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n p[scale] b {\n font-weight: 700;\n }\n p[scale] span[now] b {\n color: var(--md-sys-color-primary);\n }\n p[scale] span[now] {\n color: var(--md-sys-color-on-surface);\n }\n /* 이 큰 숫자가 어느 축인지 먼저 말한다 — 성숙도의 한 축이지 전부가 아니다 */\n p[lead] {\n margin: 0 0 4px 0;\n font-size: 0.74rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* 근거를 함께 낸다 — 숫자만 크게 내걸면 재 본 값처럼 읽힌다 */\n p[basis] {\n margin: 6px 0 0 0;\n font-size: 0.72rem;\n line-height: 1.55;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n p[basis] em {\n font-style: normal;\n color: var(--md-sys-color-tertiary);\n }\n span[score] {\n font-size: 0.8rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* 무엇 때문에 이 등급인가 — 한도에 가장 가까운 항목 */\n p[because] {\n margin: 8px 0 0 0;\n /* 본문 크기를 물려받으면 이 줄만 제목처럼 커진다 — 화면을 띄워 보고 알았다 */\n font-size: 0.78rem;\n line-height: 1.6;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n p[because] strong {\n color: var(--md-sys-color-on-surface);\n }\n\n /* ── 성숙도 radar ────────────────────────────────────────────────\n 여섯 축을 한 그림으로. 목록만 두면 「어느 쪽이 모자란가」가 안 보인다.\n 못 잰 축은 점을 찍지 않고 살만 흐리게 남긴다 — 0 으로 찍으면 「나쁘다」가 된다. */\n div[radar] {\n display: flex;\n justify-content: center;\n padding: 2px 0 6px 0;\n color: var(--md-sys-color-primary);\n }\n div[radar] svg {\n width: 100%;\n max-width: 240px;\n height: auto;\n }\n div[radar] polygon[web] {\n fill: none;\n stroke: var(--md-sys-color-outline-variant);\n stroke-width: 0.6;\n }\n div[radar] line[spoke] {\n stroke: var(--md-sys-color-outline-variant);\n stroke-width: 0.6;\n }\n div[radar] polygon[shape] {\n fill: color-mix(in srgb, var(--md-sys-color-primary) 22%, transparent);\n stroke: var(--md-sys-color-primary);\n stroke-width: 1.4;\n stroke-linejoin: round;\n }\n div[radar] circle[dot] {\n fill: var(--md-sys-color-primary);\n }\n div[radar] text {\n font-size: 5.2px;\n fill: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* 그림에 자리를 주지 않은 축을 글로 밝힌다 */\n p[unmeasured] {\n margin: 0 0 8px 0;\n font-size: 0.7rem;\n line-height: 1.5;\n color: var(--md-sys-color-tertiary);\n }\n\n /* ── 성숙도 축 ──────────────────────────────────────────────────\n 한 숫자로 합치지 않는다. 합치면 「무엇을 고쳐야 하나」가 사라진다. */\n ul[axes] {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 7px;\n }\n ul[axes] li {\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 2px 8px;\n font-size: 0.75rem;\n }\n ul[axes] li[blocked] {\n opacity: 0.7;\n }\n ul[axes] em {\n grid-column: 1 / -1;\n font-style: normal;\n font-size: 0.68rem;\n line-height: 1.5;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n /* 못 잰 축은 0 으로 두지 않는다 — 0 은 「나쁘다」는 뜻이 되어 버린다 */\n ul[axes] li[blocked] em {\n color: var(--md-sys-color-tertiary);\n }\n span[reading] {\n font-family: var(--mono-font, monospace);\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* ── 예산 막대 ────────────────────────────────────────────────── */\n ul[budgets] {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n ul[budgets] li {\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 2px 8px;\n font-size: 0.75rem;\n }\n div[bar] {\n grid-column: 1 / -1;\n height: 4px;\n background-color: var(--md-sys-color-surface-container-highest);\n border-radius: 2px;\n overflow: hidden;\n }\n div[bar] > i {\n display: block;\n height: 100%;\n background-color: var(--md-sys-color-primary);\n }\n div[bar] > i[tight] {\n background-color: var(--md-sys-color-error);\n }\n\n /* ── 무게 ─────────────────────────────────────────────────────── */\n dl[metrics] {\n margin: 0;\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 4px 8px;\n font-size: 0.78rem;\n }\n dl[metrics] dt {\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n dl[metrics] dd {\n margin: 0;\n text-align: right;\n font-family: var(--mono-font, monospace);\n }\n\n /* ── 추이 ──────────────────────────────────────────────────────────\n 그림은 자연 크기가 680px 다. 300px 칸에 넣으면 글자가 4px 이 되어 아무것도\n 읽을 수 없다 — 사이드바에는 숫자를 두고, 그림은 선택해서 크게 본다. */\n button[expand] {\n margin-top: 6px;\n padding: 4px 10px;\n font: var(--label-font, inherit);\n font-size: 0.74rem;\n color: var(--md-sys-color-on-surface);\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n cursor: pointer;\n }\n button[expand]:hover {\n background-color: var(--md-sys-color-surface-container);\n }\n\n div[sheet] {\n position: fixed;\n inset: 0;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--spacing-large, 12px);\n background-color: rgba(0, 0, 0, 0.32);\n }\n div[sheet] > figure {\n max-width: 100%;\n max-height: 100%;\n margin: 0;\n padding: var(--spacing-large, 12px);\n overflow: auto;\n color: var(--md-sys-color-primary);\n background-color: var(--md-sys-color-surface-container-lowest);\n /* 담는 면이 팝업이면 안쪽 모서리는 알약이다 — 시트 자체는 페이지 값을 쓴다 */\n border-radius: 6px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.28);\n }\n div[sheet] svg {\n display: block;\n /* 자연 크기보다 작게 줄이지 않는다 — 좁으면 넘겨 보게 둔다 */\n min-width: 680px;\n }\n div[sheet] figcaption {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n }\n div[sheet] button[close] {\n padding: 5px 14px;\n font: var(--label-font, inherit);\n color: var(--md-sys-color-on-surface);\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 999em;\n cursor: pointer;\n }\n\n /* ── 알림 ─────────────────────────────────────────────────────── */\n ul[findings] {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n ul[findings] li {\n display: flex;\n gap: 6px;\n line-height: 1.5;\n }\n ul[findings] md-icon {\n --md-icon-size: 16px;\n flex: none;\n color: var(--md-sys-color-error);\n }\n ul[findings] li[violation] md-icon {\n color: var(--md-sys-color-tertiary);\n }\n /* 어긴 자리. 문구에 붙어 흐르면 문장의 일부로 읽힌다 — badge 로 보이게 뗀다 */\n code[at] {\n padding: 1px 5px;\n font-family: var(--mono-font, monospace);\n font-size: 0.68rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: var(--md-sys-color-surface-container);\n border-radius: 4px;\n white-space: nowrap;\n }\n\n /*\n 발견이 없을 때의 문구.\n\n 크기를 안 적어 두었더니 본문 기본 크기를 물려받아 이 줄만 제목처럼 커졌다. 이\n 패널의 글자는 전부 0.7rem 대인데 여기만 1rem 이라 아래쪽이 무너져 보였다.\n\n 찾은 것이 있을 때의 목록(ul[findings] li)과 같은 크기로 맞춘다. 있을 때와 없을 때\n 절의 생김새가 달라질 이유가 없다.\n\n 이 파일에서 같은 실수를 두 번째로 한다. p[because] 에도 같은 주석이 있다.\n */\n p[quiet] {\n margin: 0;\n font-size: 0.76rem;\n line-height: 1.6;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n `\n ]\n\n /** 저작 중인 원본 데이터. 바뀌면 다시 측정한다. */\n @property({ type: Object }) source?: FigureSource\n\n @state() private cost?: FigureCost\n @state() private score?: FigureScore\n @state() private curve = ''\n /** 1만 개를 놓았을 때. 저작자가 실제로 궁금한 숫자다. */\n @state() private many?: FigureCost\n /** 몇 개까지 놓을 수 있나. 이 패널의 머리다. */\n @state() private capacity?: FigureCapacity\n /** 추이 그림을 제 크기로 펼쳤나. */\n @state() private expanded = false\n @state() private errors: { code: string; message: string; at?: string }[] = []\n /**\n * 형식이 권하지 않는 것 — **그중 둘은 발행을 막는다.**\n *\n * 등급을 형식에서 받는다(`blocksRelease`). 화면이 제 목록을 들면 형식에 코드가 하나 늘 때\n * 조용히 「알림」으로 그려지고, 저작자는 발행이 막힌 것을 **눌러 보고 나서야** 안다.\n *\n * `actual` 은 얼마나 넘쳤나다. 「상자를 벗어난다」만으로는 2mm 인지 250mm 인지 모르고,\n * 그 둘은 고치는 품이 다르다.\n */\n @state() private violations: { code: string; message: string; blocking: boolean; actual?: number }[] = []\n /**\n * 크기를 바꿔 봤을 때 갈라지는가 — **발행을 막는 것**이다.\n *\n * 이력 탭이 같은 판정을 서버에서 받아 발행 문을 잠그지만, 그것은 **저장된 정본**에 대한\n * 것이라 저장하기 전에는 답하지 못한다. 여기서는 저작 중인 정본을 그대로 재므로, 부품을\n * 옮기는 그 자리에서 갈라짐이 생겼는지 사라졌는지 보인다 — 저장하고 탭을 옮겨 확인하게\n * 두면 고칠 때가 이미 지나 있다.\n */\n @state() private sizing: { code: string; message: string; at?: string; blocking: boolean }[] = []\n /**\n * 아직 부품이 하나도 없나 — **저작 전이지 형식을 어긴 것이 아니다.**\n *\n * 형식은 부품 0개를 오류로 본다(`missing-parts`). 발행 관점에서는 맞다. 그런데 새로 만들기를\n * 누른 직후가 바로 그 상태이므로, 그것을 그대로 오류로 그리면 **아무것도 하지 않았는데 붉은\n * 화면**을 보게 된다. 그래서 이 자리에서 둘을 가른다.\n */\n @state() private bare = false\n\n willUpdate(changed: Map<string, unknown>) {\n if (changed.has('source')) {\n this.measure()\n }\n }\n\n render() {\n /*\n 부품이 없으면 이 자리는 **판정이 아니라 안내**다. 이미 있던 「부품을 추가하면 나옵니다」가\n 정확히 그 말이므로 그것을 쓴다 — 같은 상태에 문장을 둘 두지 않는다.\n\n 아래 셈들(머리·축·한도·추이)은 부품이 있어야 뜻이 있고, 재지 않은 값을 들고 그리면 0 으로\n 보인다. 0 은 「나쁘다」로 읽히므로 그 자리에 두지 않는다.\n */\n if (!this.source || this.bare) {\n return html`\n <section>\n <p quiet>${i18next.t('figure.text.report-appears-when-you-add-a-part')}</p>\n </section>\n `\n }\n\n // 형식을 어긴 상태에서는 셈이 뜻을 갖지 못한다 — 무엇이 잘못됐는지만 보여 준다.\n if (this.errors.length > 0) {\n return html`${this.renderFindings()}`\n }\n\n return html`\n ${this.renderVerdict()} ${this.renderAxes()} ${this.renderBudgets()} ${this.renderMetrics()} ${this.renderCurve()}\n ${this.renderFindings()} ${this.renderSheet()}\n `\n }\n\n /**\n * 머리 — 몇 개까지 편한가.\n *\n * 로우폴리의 값은 「얼마나 많이 편하게 놓을 수 있나」에 있다. 등급 B 는 그 물음에\n * 직접 답하지 않는다 — 형상이 선언된 한도 안인지를 말할 뿐이다. 그래서 개수를\n * 머리에 올리고 등급은 옆의 badge 로 내린다.\n *\n * 근거를 함께 낸다. 묶음으로 갈리는 개수의 분모는 **실측**이고(2026-09-18, Apple M5), 잰\n * 기계와 날짜를 함께 적는다 — 적지 않으면 어디서나 참인 수로 읽힌다. 삼각형 쪽은 아직 설계\n * 문서의 목표치라 그렇다고 적는다.\n */\n private renderVerdict() {\n const score = this.score\n const capacity = this.capacity\n if (!score || !capacity) return nothing\n\n return html`\n <section>\n <h3>${i18next.t('figure.label.maturity')}</h3>\n <p lead>${i18next.t('figure.label.axis-scale')} — ${i18next.t('figure.text.how-many-fit')}</p>\n <div verdict>\n <span capacity>${capacity.instances.toLocaleString()}</span>\n <span unit>${i18next.t('figure.text.instances-unit')}</span>\n <span\n grade\n ?tight=${score.grade === 'C'}\n ?over=${score.grade === 'D'}\n title=${i18next.t('figure.text.grade-explained')}\n >\n ${score.grade}\n <small>${i18next.t('figure.text.headroom-percent', { percent: Math.round(score.score) })}</small>\n </span>\n </div>\n <p because>\n ${i18next.t('figure.text.grade-decided-by')}\n <strong>${i18next.t('figure.label.budget-' + score.tightest.key)}</strong>\n — ${score.tightest.used} / ${score.tightest.limit}\n (${Math.round((score.tightest.used / score.tightest.limit) * 100)}%)\n </p>\n ${this.renderGradeScale(score.grade)}\n <p basis>\n ${i18next.t('figure.text.bound-by-' + capacity.bound, {\n per: capacity.per.toLocaleString(),\n budget: capacity.budget.toLocaleString()\n })}\n <em>\n ${capacity.bound === 'groups'\n ? i18next.t('figure.text.budget-measured-on', { gpu: MEASURED.gpu, on: MEASURED.on })\n : i18next.t('figure.text.budget-not-measured-yet')}\n </em>\n </p>\n </section>\n `\n }\n\n /**\n * 등급 눈금.\n *\n * A·B·C·D 가 무엇을 뜻하는지 화면 어디에도 없었다. badge 만 보고는 좋은지 나쁜지도,\n * 무엇을 고쳐야 오르는지도 알 수 없다. 눈금을 늘 곁에 두고 지금 어디에 있는지를\n * 표시한다.\n *\n * 접어 두지 않는다. 한 번 배우고 마는 값이 아니라 볼 때마다 대조하는 값이다.\n */\n private renderGradeScale(now: string) {\n const bounds: [string, string][] = [\n ['A', '≤ 50%'],\n ['B', '≤ 75%'],\n ['C', '≤ 100%'],\n ['D', '> 100%']\n ]\n\n return html`\n <p scale>\n <span>${i18next.t('figure.text.grade-scale-lead')}</span>\n ${bounds.map(\n ([grade, bound]) => html`<span ?now=${grade === now}><b>${grade}</b> ${bound}</span>`\n )}\n </p>\n `\n }\n\n /**\n * 성숙도 — 여러 축으로 본다.\n *\n * 「몇 개까지 편한가」 하나로는 좋은 형상과 나쁜 형상이 구분되지 않는다. 부품 하나짜리\n * 상자도 그 축에서는 만점이다.\n *\n * **못 재는 축을 0 으로 두지 않는다.** 0 은 「나쁘다」는 뜻이 되는데, 사실은 아직\n * 측정할 방법이 없는 것이다. 측정하지 못한다고 알리고 이유를 붙인다. 지어낸 점수보다 그것이\n * 이 리포트를 믿을 만하게 만든다.\n */\n private renderAxes() {\n if (!this.cost || !this.score || !this.source) return nothing\n\n /*\n 규모는 목록에서 뺀다 — 머리로 올라갔다. 같은 숫자를 두 번 내면 읽는 사람이\n 둘이 다른 것인 줄 안다.\n */\n const axes = maturityOf(this.source, this.cost, this.score).filter(axis => axis.key !== 'scale')\n\n // radar 에는 여섯을 다 그린다. 규모가 어디쯤인지 함께 보여야 뜻이 있다\n const all = maturityOf(this.source, this.cost, this.score)\n\n return html`\n <section>\n <h3>${i18next.t('figure.label.by-axis')}</h3>\n <div radar>${this.renderRadar(all.filter(axis => axis.ratio !== undefined))}</div>\n ${this.renderUnmeasured(all.filter(axis => axis.ratio === undefined))}\n <ul axes>\n ${axes.map(axis => this.renderAxis(axis))}\n </ul>\n </section>\n `\n }\n\n /**\n * 성숙도 radar. **측정한 축만 그린다.**\n *\n * 측정하지 못하는 축도 spoke 를 그려 두고 도형만 건너뛰던 때가 있었다. 그러면 그 방향을 가로지르는\n * 직선이 생겨서 도형이 그쪽으로 움푹 들어간 것처럼 보인다. 0 으로 찍지 않으려고 한\n * 일인데 결과는 0 으로 찍은 것과 똑같이 읽혔다. 점선 살과 물음표로는 그 인상이\n * 지워지지 않았다.\n *\n * 그래서 아예 빼고, 못 재는 축은 그림 아래에 글로 적는다. 잴 수 없는 것은 그림에\n * 자리를 주지 않는 편이 정직하다.\n */\n private renderRadar(axes: MaturityAxis[]) {\n // 그림 안에서는 **짧은 이름**을 쓴다. 긴 이름을 그대로 쓰면 왼쪽이 잘린다 —\n // 화면을 띄워 보고 알았다.\n const cx = 60\n const cy = 46\n const r = 28\n\n const at = (index: number, ratio: number) => {\n const angle = -Math.PI / 2 + (index * 2 * Math.PI) / axes.length\n return [cx + r * ratio * Math.cos(angle), cy + r * ratio * Math.sin(angle)]\n }\n\n const points = axes\n .map((axis, index) => at(index, Math.max(0.04, Math.min(1, axis.ratio ?? 0))).map(n => n.toFixed(1)).join(','))\n .join(' ')\n\n return svg`\n <svg viewBox=\"0 0 120 96\" aria-hidden=\"true\">\n ${[0.33, 0.66, 1].map(\n ring => svg`<polygon web points=${axes.map((_, i) => at(i, ring).map(n => n.toFixed(1)).join(',')).join(' ')}></polygon>`\n )}\n ${axes.map((_, index) => {\n const [x, y] = at(index, 1)\n return svg`<line spoke x1=${cx} y1=${cy} x2=${x.toFixed(1)} y2=${y.toFixed(1)}></line>`\n })}\n <polygon shape points=${points}></polygon>\n ${axes.map((axis, index) => {\n const [x, y] = at(index, Math.max(0.04, Math.min(1, axis.ratio ?? 0)))\n return svg`<circle dot cx=${x.toFixed(1)} cy=${y.toFixed(1)} r=\"1.4\"></circle>`\n })}\n ${axes.map((axis, index) => {\n const [x, y] = at(index, 1.28)\n const anchor = x < cx - 3 ? 'end' : x > cx + 3 ? 'start' : 'middle'\n return svg`\n <text x=${x.toFixed(1)} y=${(y + 1.8).toFixed(1)} text-anchor=${anchor}>\n ${i18next.t('figure.label.axis-' + axis.key + '-short')}\n </text>\n `\n })}\n </svg>\n `\n }\n\n /**\n * 그림에 없는 축을 글로 밝힌다.\n *\n * 빼기만 하면 축이 여섯인지 다섯인지 알 수 없고, 없는 것을 없다고 말하지 않은 셈이\n * 된다. 무엇을 아직 못 재는지 한 줄로 적는다. 왜 못 재는지는 아래 목록의 그 항목에\n * 그대로 있다.\n */\n private renderUnmeasured(axes: MaturityAxis[]) {\n if (axes.length === 0) return nothing\n\n return html`\n <p unmeasured>\n ${i18next.t('figure.text.axes-not-measurable-yet', {\n axes: axes.map(axis => i18next.t('figure.label.axis-' + axis.key)).join(' · ')\n })}\n </p>\n `\n }\n\n private renderAxis(axis: MaturityAxis) {\n const name = i18next.t('figure.label.axis-' + axis.key)\n\n if (axis.blocked) {\n return html`\n <li blocked>\n <span>${name}</span>\n <span reading>${i18next.t('figure.text.not-measurable-yet')}</span>\n <em>${i18next.t(axis.blocked)}</em>\n </li>\n `\n }\n\n const ratio = Math.max(0, Math.min(1, axis.ratio ?? 0))\n return html`\n <li>\n <span>${name}</span>\n <span reading>${axis.readingKey ? i18next.t(axis.readingKey) : (axis.reading ?? '')}</span>\n <div bar><i style=\"width:${Math.round(ratio * 100)}%\"></i></div>\n ${axis.because ? html`<em>${i18next.t(axis.because)}</em>` : ''}\n </li>\n `\n }\n\n /**\n * 예산 막대.\n *\n * 숫자만 늘어놓으면 어느 것이 꽉 찼는지 눈에 안 들어온다. 한도에 가장 가까운 하나를 색으로\n * 구분한다. 그것이 점수를 정한 항목이다.\n *\n * ## `use.label` 을 쓰지 않는 이유\n *\n * `FigureBudgetUse` 에 `label` 과 `why` 가 있고 「저작자에게 그대로 보여 준다」고\n * 적혀 있다. 그런데 그 문구가 **한국어로 구워져** 있다. 이 제품은 5언어를 쓰므로\n * 그것을 그대로 실으면 다른 언어 사용자에게 한국어가 나온다.\n *\n * `budgets` 는 **배열**이다. `Object.entries` 로 돌면 키가 인덱스(0·1·2…)가 되어\n * `figure.label.budget-0` 같은 없는 키를 찾는다 — 화면을 띄워 보고서야 드러났다.\n * 배열을 그대로 돌고 `use.key` 를 쓴다.\n *\n * 그래서 **안정된 `key`** 로 이 화면의 i18n 을 찾는다. `key` 는 열거형이라 형식의\n * 일부이고, 문구는 화면의 몫이다. figure-model 이 문구를 언어 중립으로 바꾸면\n * 그때 `label` 을 쓸 수 있다.\n */\n private renderBudgets() {\n const score = this.score\n if (!score) return nothing\n\n return html`\n <section>\n <h3>${i18next.t('figure.label.budgets')}</h3>\n <ul budgets>\n ${score.budgets.map(use => {\n const ratio = Math.min(1, use.ratio)\n const tight = use.key === score.tightest.key\n return html`\n <li>\n <span>${i18next.t('figure.label.budget-' + use.key)}</span>\n <span>${use.used} / ${use.limit}</span>\n <div bar><i ?tight=${tight} style=\"width:${Math.round(ratio * 100)}%\"></i></div>\n </li>\n `\n })}\n </ul>\n </section>\n `\n }\n\n private renderMetrics() {\n const cost = this.cost\n if (!cost) return nothing\n\n return html`\n <section>\n <h3>${i18next.t('figure.label.weight')}</h3>\n <dl metrics>\n <dt>${i18next.t('figure.label.triangles')}</dt>\n <dd>${cost.triangles.toLocaleString()}</dd>\n\n <dt>${i18next.t('figure.label.triangles-at-100')}</dt>\n <dd>${cost.at.triangles.toLocaleString()}</dd>\n\n <dt>${i18next.t('figure.label.draw-calls-at-100')}</dt>\n <dd>${cost.at.drawCalls.toLocaleString()}</dd>\n\n <dt>${i18next.t('figure.label.distinct-shapes')}</dt>\n <dd>${cost.distinctShapes}</dd>\n\n <dt>${i18next.t('figure.label.distinct-materials')}</dt>\n <dd>${cost.distinctMaterials}</dd>\n </dl>\n </section>\n `\n }\n\n /**\n * 재활용 추이 — 「많이 놓으면 어떻게 되나」에 답한다.\n *\n * ## 그림을 사이드바에 밀어 넣지 않는다\n *\n * `costCurveSvg` 의 자연 크기는 680x380 이고 글자가 11px 다. 300px 칸에서\n * width:100% 로 줄이면 글자가 4px 이 되어 **아무것도 읽을 수 없다** — 화면을 띄워\n * 보고서야 드러났다. 읽을 수 없는 그림은 없는 것과 같으므로, 사이드바에는 저작자가\n * 실제로 궁금한 숫자(1만 개일 때)를 두고 그림은 선택해서 제 크기로 본다.\n */\n private renderCurve() {\n const many = this.many\n if (!many && !this.curve) return nothing\n\n return html`\n <section>\n <h3>${i18next.t('figure.label.reuse-trend')}</h3>\n ${many\n ? html`\n <dl metrics>\n <dt>${i18next.t('figure.label.triangles-at-10k')}</dt>\n <dd>${many.at.triangles.toLocaleString()}</dd>\n\n <dt>${i18next.t('figure.label.draw-calls-at-10k')}</dt>\n <dd>${many.at.drawCalls.toLocaleString()}</dd>\n </dl>\n `\n : nothing}\n ${this.curve\n ? html`\n <button expand @click=${() => (this.expanded = true)}>\n ${i18next.t('figure.button.see-the-trend-chart')}\n </button>\n `\n : nothing}\n </section>\n `\n }\n\n /** 그림을 제 크기로 보여 준다. 좁으면 줄이지 않고 넘겨 보게 둔다. */\n private renderSheet() {\n if (!this.expanded || !this.curve) return nothing\n\n return html`\n <div sheet @click=${() => (this.expanded = false)}>\n <figure @click=${(e: Event) => e.stopPropagation()}>\n ${unsafeSVG(this.curve)}\n <figcaption>\n <button close @click=${() => (this.expanded = false)}>${i18next.t('figure.button.close')}</button>\n </figcaption>\n </figure>\n </div>\n `\n }\n\n /**\n * 발견.\n *\n * 오류(막는 것)와 위반(알리는 것)을 **가려서** 보여 준다. 섞으면 저작자가 무엇을\n * 반드시 고쳐야 하는지 모른다. 아무것도 없으면 「없다」고 말한다 — 빈 자리로 두면\n * 검사가 돌았는지 알 수 없다.\n */\n private renderFindings() {\n if (this.errors.length === 0 && this.violations.length === 0 && this.sizing.length === 0) {\n return html`\n <section>\n <h3>${i18next.t('figure.label.findings')}</h3>\n <p quiet>${i18next.t('figure.text.no-findings')}</p>\n </section>\n `\n }\n\n return html`\n <section>\n <h3>${i18next.t('figure.label.findings')}</h3>\n <ul findings>\n ${this.sizing.map(\n finding => html`\n <li ?violation=${!finding.blocking}>\n <md-icon>call_split</md-icon>\n <span>${finding.message} ${finding.at ? html`<code at>${finding.at}</code>` : ''}</span>\n </li>\n `\n )}\n ${this.errors.map(\n e => html`\n <li>\n <md-icon>error</md-icon>\n <span>${e.message} ${e.at ? html`<code at>${e.at}</code>` : ''}</span>\n </li>\n `\n )}\n ${this.violations.map(\n v => html`\n <li ?violation=${!v.blocking}>\n <md-icon>${v.blocking ? 'block' : 'info'}</md-icon>\n <span>\n ${v.message}${v.blocking && v.actual !== undefined\n ? html` <code at>${v.actual}</code>`\n : ''}\n </span>\n </li>\n `\n )}\n </ul>\n </section>\n `\n }\n\n /** 원본 데이터를 측정해 결과를 상태에 담는다. 셈은 전부 figure-model 이 한다. */\n private measure() {\n const source = this.source\n if (!source) {\n this.cost = undefined\n this.score = undefined\n this.many = undefined\n this.capacity = undefined\n this.curve = ''\n this.errors = []\n this.violations = []\n this.sizing = []\n return\n }\n\n /*\n 부품이 없으면 판정을 아예 하지 않는다 — 잴 것이 없다. 「무엇을 고쳐야 하나」가 아니라\n 「무엇부터 놓나」인 상태이고, 그 안내는 부품 목록이 이미 하고 있다.\n */\n this.bare = !source.parts?.length\n if (this.bare) {\n this.cost = undefined\n this.score = undefined\n this.many = undefined\n this.capacity = undefined\n this.curve = ''\n this.errors = []\n this.violations = []\n this.sizing = []\n return\n }\n\n const result = validate(source)\n this.errors = result.errors.map(e => ({ code: e.code, message: e.message, at: e.path }))\n this.violations = result.violations.map(v => ({\n code: v.code,\n message: v.message,\n blocking: blocksRelease(v.code),\n actual: v.actual\n }))\n\n if (this.errors.length > 0) {\n // 형식을 어겼으면 컴파일이 던진다. 셈을 비우고 오류만 보여 준다.\n this.cost = undefined\n this.score = undefined\n this.many = undefined\n this.capacity = undefined\n this.curve = ''\n this.sizing = []\n this.dispatchEvent(\n new CustomEvent('verdict-changed', { detail: {}, bubbles: true, composed: true })\n )\n return\n }\n\n const blueprint = compile(source)\n\n /*\n 크기를 바꿔 보는 일곱 조합. 판정 규칙은 `figure-model` 의 `gate.ts` 가 정한 것을 그대로\n 쓴다 — 무엇이 막는 것인지 화면이 다시 정하면 서버와 다른 말을 하게 된다.\n */\n const report = sizingReport(blueprint)\n this.sizing = report.findings.map(finding => ({\n code: finding.code,\n message: finding.message,\n /* 숫자가 아니라 자리를 말한다 — 갈라져 나온 부품 이름이 고칠 자리다. */\n at: finding.clumps.map(clump => [...clump].sort().join('+')).join(' | ') || undefined,\n blocking: finding.code === 'sizing-splits'\n }))\n\n this.cost = costOf(blueprint, 100)\n // 1만 개는 figure-model 에 다시 묻는다 — 화면이 곱셈으로 짐작하지 않는다.\n this.many = costOf(blueprint, 10000)\n this.score = scoreOf(blueprint)\n this.capacity = capacityOf(this.cost)\n\n // 개수를 밖으로 알린다. 탭 이름표가 이 값을 보여 주는데, 거기서 다시 셈하면\n // 같은 형상이 두 자리에서 다르게 나올 수 있다.\n this.dispatchEvent(\n new CustomEvent('verdict-changed', {\n detail: { instances: this.capacity?.instances, grade: this.score?.grade },\n bubbles: true,\n composed: true\n })\n )\n this.curve = costCurveSvg(blueprint)\n }\n}\n"]}
@@ -6,7 +6,7 @@ import './figure-report.js';
6
6
  import './figure-settings.js';
7
7
  import './figure-history-panel.js';
8
8
  import { LitElement } from 'lit';
9
- import type { FigureSource } from '@hatiolab/figure-model';
9
+ import type { ContractChange, FigureSource } from '@hatiolab/figure-model';
10
10
  import type { FigureDraft, PartModel } from './figure-source.js';
11
11
  import type { ViewSettings } from './figure-view.js';
12
12
  declare const FigureSide_base: (new (...args: any[]) => LitElement) & typeof LitElement;
@@ -42,6 +42,10 @@ export declare class FigureSide extends FigureSide_base {
42
42
  draft?: FigureDraft;
43
43
  parts: PartModel[];
44
44
  source?: FigureSource;
45
+ /** 이 고침이 이미 놓인 인스턴스에 무엇을 하나(ADR-0063 결정 3). 발행 전 도형에는 비어 있다. */
46
+ contract: ContractChange[];
47
+ /** 견주지 못한 까닭. 있으면 화면이 「확인하지 못했다」로 말한다. */
48
+ contractFailure?: string;
45
49
  selected: number;
46
50
  /** 저작 화면의 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다. */
47
51
  view: ViewSettings;