@things-factory/figure-ui 10.1.27 → 10.1.29

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 (91) hide show
  1. package/client/graphql/index.ts +23 -0
  2. package/client/modeller/angle-dial.ts +70 -0
  3. package/client/modeller/base-box.ts +104 -0
  4. package/client/modeller/figure-animations.ts +90 -17
  5. package/client/modeller/figure-canvas.ts +24 -6
  6. package/client/modeller/figure-inspector.ts +169 -5
  7. package/client/modeller/figure-preview.ts +110 -3
  8. package/client/modeller/figure-report.ts +20 -32
  9. package/client/modeller/figure-side.ts +10 -1
  10. package/client/modeller/figure-source.ts +8 -0
  11. package/client/modeller/joint-edits.ts +66 -5
  12. package/client/modeller/joint-gizmo.ts +180 -0
  13. package/client/modeller/part-edits.ts +75 -9
  14. package/client/modeller/pose-defaults.ts +67 -0
  15. package/client/modeller/proposal.ts +9 -3
  16. package/client/modeller/scene-budget.ts +31 -40
  17. package/client/pages/figure-list-page.ts +4 -9
  18. package/client/pages/figure-modeller-page.ts +144 -43
  19. package/client/types.ts +2 -0
  20. package/dist-client/graphql/index.d.ts +7 -0
  21. package/dist-client/graphql/index.js +21 -0
  22. package/dist-client/graphql/index.js.map +1 -1
  23. package/dist-client/modeller/angle-dial.d.ts +32 -0
  24. package/dist-client/modeller/angle-dial.js +41 -0
  25. package/dist-client/modeller/angle-dial.js.map +1 -0
  26. package/dist-client/modeller/base-box.d.ts +47 -0
  27. package/dist-client/modeller/base-box.js +52 -0
  28. package/dist-client/modeller/base-box.js.map +1 -0
  29. package/dist-client/modeller/figure-animations.d.ts +19 -4
  30. package/dist-client/modeller/figure-animations.js +86 -17
  31. package/dist-client/modeller/figure-animations.js.map +1 -1
  32. package/dist-client/modeller/figure-canvas.d.ts +9 -0
  33. package/dist-client/modeller/figure-canvas.js +20 -6
  34. package/dist-client/modeller/figure-canvas.js.map +1 -1
  35. package/dist-client/modeller/figure-inspector.d.ts +37 -0
  36. package/dist-client/modeller/figure-inspector.js +159 -5
  37. package/dist-client/modeller/figure-inspector.js.map +1 -1
  38. package/dist-client/modeller/figure-preview.d.ts +26 -0
  39. package/dist-client/modeller/figure-preview.js +99 -3
  40. package/dist-client/modeller/figure-preview.js.map +1 -1
  41. package/dist-client/modeller/figure-report.d.ts +11 -6
  42. package/dist-client/modeller/figure-report.js +20 -35
  43. package/dist-client/modeller/figure-report.js.map +1 -1
  44. package/dist-client/modeller/figure-side.d.ts +5 -1
  45. package/dist-client/modeller/figure-side.js +13 -0
  46. package/dist-client/modeller/figure-side.js.map +1 -1
  47. package/dist-client/modeller/figure-source.d.ts +7 -0
  48. package/dist-client/modeller/figure-source.js.map +1 -1
  49. package/dist-client/modeller/joint-edits.d.ts +23 -3
  50. package/dist-client/modeller/joint-edits.js +53 -5
  51. package/dist-client/modeller/joint-edits.js.map +1 -1
  52. package/dist-client/modeller/joint-gizmo.d.ts +34 -0
  53. package/dist-client/modeller/joint-gizmo.js +158 -0
  54. package/dist-client/modeller/joint-gizmo.js.map +1 -0
  55. package/dist-client/modeller/part-edits.d.ts +29 -4
  56. package/dist-client/modeller/part-edits.js +61 -9
  57. package/dist-client/modeller/part-edits.js.map +1 -1
  58. package/dist-client/modeller/pose-defaults.d.ts +9 -0
  59. package/dist-client/modeller/pose-defaults.js +35 -0
  60. package/dist-client/modeller/pose-defaults.js.map +1 -0
  61. package/dist-client/modeller/proposal.d.ts +1 -1
  62. package/dist-client/modeller/proposal.js +8 -3
  63. package/dist-client/modeller/proposal.js.map +1 -1
  64. package/dist-client/modeller/scene-budget.d.ts +31 -34
  65. package/dist-client/modeller/scene-budget.js +28 -37
  66. package/dist-client/modeller/scene-budget.js.map +1 -1
  67. package/dist-client/pages/figure-list-page.js +4 -9
  68. package/dist-client/pages/figure-list-page.js.map +1 -1
  69. package/dist-client/pages/figure-modeller-page.d.ts +50 -0
  70. package/dist-client/pages/figure-modeller-page.js +141 -42
  71. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  72. package/dist-client/tsconfig.tsbuildinfo +1 -1
  73. package/dist-client/types.d.ts +2 -0
  74. package/dist-client/types.js.map +1 -1
  75. package/package.json +5 -5
  76. package/test/ai-proposal-contract.test.ts +16 -11
  77. package/test/angle-dial.test.ts +78 -0
  78. package/test/base-box.test.ts +165 -0
  79. package/test/expect.ts +2 -0
  80. package/test/i18n-prefix-guard.test.ts +17 -0
  81. package/test/joint-edits.test.ts +37 -2
  82. package/test/joint-gizmo.test.ts +174 -0
  83. package/test/joint-needle.test.ts +162 -0
  84. package/test/part-edits.test.ts +100 -0
  85. package/test/pose-defaults.test.ts +71 -0
  86. package/test/scene-budget.test.ts +82 -0
  87. package/translations/en.json +16 -9
  88. package/translations/ja.json +16 -9
  89. package/translations/ko.json +16 -9
  90. package/translations/ms.json +16 -9
  91. package/translations/zh.json +16 -9
@@ -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
  *
@@ -73,6 +81,17 @@ export declare class FigurePreview extends FigurePreview_base {
73
81
  * 넣을 자리를 못 찾는다.
74
82
  */
75
83
  private renderDrivers;
84
+ /**
85
+ * 각도 파라미터에만 다는 눈.
86
+ *
87
+ * 미리보기의 3D 는 여덟 대를 한 화면에 세우느라 한 대가 20 픽셀쯤이다. 관절이 돌았다는
88
+ * 표시를 그 안에 그리면 몇 픽셀에 그친다 — 부품에 단 바늘이 4 × 2 픽셀로 나왔다(잰 값).
89
+ * 각을 읽는 일은 배율에서 떼어 여기에 둔다.
90
+ *
91
+ * 각이 아닌 파라미터(밀리미터 · 퍼센트 · 초)에는 안 단다. 눈금이 도는 것이 아니라서
92
+ * 원으로 그리면 없는 뜻이 생긴다.
93
+ */
94
+ private renderDial;
76
95
  /**
77
96
  * 세워 둔 인스턴스 전부에 같은 값을 준다.
78
97
  *
@@ -87,6 +106,13 @@ export declare class FigurePreview extends FigurePreview_base {
87
106
  * 갈릴 수 있다. 저작자가 화면에서 보는 1200 이 씬에 그대로 간다.
88
107
  */
89
108
  private give;
109
+ /**
110
+ * 세운 자세를 기본값으로 — **셈은 여기서 하지 않는다.**
111
+ *
112
+ * 초안을 쥐고 있는 것은 페이지이고, 되돌리기와 「저장 안 됨」도 거기서 매긴다. 여기서
113
+ * 초안을 만들어 보내면 그 셋이 두 곳으로 갈린다. 민 값만 올린다.
114
+ */
115
+ private poseAsDefault;
90
116
  private tellInstances;
91
117
  /**
92
118
  * 보기 설정이 바뀌면 **다시 세우지 않고 씬에 넣는다** — 두 개만 빼고.
@@ -5,6 +5,9 @@ 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 { dialShape } from './angle-dial.js';
9
+ import { jointNeedles } from './joint-gizmo.js';
10
+ import { poseChanges } from './pose-defaults.js';
8
11
  /**
9
12
  * 저작 중인 것을 **도면에 놓았을 때**의 모습.
10
13
  *
@@ -122,10 +125,45 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
122
125
  }
123
126
  div[clips] label {
124
127
  display: grid;
125
- grid-template-columns: auto 96px 34px;
128
+ grid-template-columns: auto 20px 96px 34px;
126
129
  align-items: center;
127
130
  gap: 8px;
128
131
  }
132
+ /* 각도 눈. 3D 가 아무리 작게 서 있어도 여기서는 같은 크기로 읽힌다. */
133
+ div[clips] svg[dial] {
134
+ width: 20px;
135
+ height: 20px;
136
+ }
137
+ div[clips] svg[dial] path {
138
+ fill: rgba(255, 255, 255, 0.22);
139
+ }
140
+ div[clips] svg[dial] line {
141
+ stroke: var(--md-sys-color-primary, #7cc6ff);
142
+ stroke-width: 2;
143
+ stroke-linecap: round;
144
+ }
145
+ div[clips] svg[dial] circle {
146
+ fill: none;
147
+ stroke: rgba(255, 255, 255, 0.35);
148
+ }
149
+ /*
150
+ 막대 위에 놓는 단추. 자세를 만든 자리에 있어야 「지금 이것을 기본으로」가 읽힌다.
151
+ 앉힐 것이 있을 때만 나오므로, 늘 서 있는 칸이 아니다.
152
+ */
153
+ div[clips] button[pose] {
154
+ align-self: stretch;
155
+ padding: 4px 8px;
156
+ font: inherit;
157
+ font-size: 0.7rem;
158
+ color: rgba(255, 255, 255, 0.92);
159
+ background-color: rgba(255, 255, 255, 0.14);
160
+ border: 1px solid rgba(255, 255, 255, 0.22);
161
+ border-radius: 5px;
162
+ cursor: pointer;
163
+ }
164
+ div[clips] button[pose]:hover {
165
+ background-color: rgba(255, 255, 255, 0.24);
166
+ }
129
167
  div[clips] span[drive] {
130
168
  opacity: 0.6;
131
169
  font-size: 0.66rem;
@@ -176,8 +214,22 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
176
214
  * 넣을 자리를 못 찾는다.
177
215
  */
178
216
  renderDrivers(clips, parameters) {
217
+ /*
218
+ 지금 세운 자세를 기본값으로 앉히는 자리. **막대 옆에 둔다** — 자세를 만든 곳이 여기라,
219
+ 값을 읽어 파라미터 칸으로 옮겨 적는 왕복이 없어진다. 앉힐 것이 없으면 아예 안 나온다.
220
+ */
221
+ const changes = poseChanges(this.draft, this.giving);
179
222
  return html `
180
223
  <div clips>
224
+ ${changes.length > 0
225
+ ? html `<button
226
+ pose
227
+ title=${i18next.t('figure.text.pose-as-default-explained')}
228
+ @click=${() => this.poseAsDefault()}
229
+ >
230
+ ${i18next.t('figure.button.pose-as-default')}
231
+ </button>`
232
+ : ''}
181
233
  ${parameters.map(parameter => {
182
234
  const range = parameter.range;
183
235
  const value = this.giving[parameter.name] ?? parameter.default ?? range?.min ?? 0;
@@ -185,6 +237,7 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
185
237
  return html `
186
238
  <label>
187
239
  <span>${parameter.label ?? parameter.name} <span drive>${range?.unit ?? ''}</span></span>
240
+ ${this.renderDial(range, value)}
188
241
  <input
189
242
  type="range"
190
243
  min=${range?.min ?? 0}
@@ -202,6 +255,7 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
202
255
  return html `
203
256
  <label>
204
257
  <span>${clip.name} <span drive>${i18next.t('figure.label.playback-speed')}</span></span>
258
+ <span></span>
205
259
  <input
206
260
  type="range"
207
261
  min="0"
@@ -217,6 +271,29 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
217
271
  </div>
218
272
  `;
219
273
  }
274
+ /**
275
+ * 각도 파라미터에만 다는 눈.
276
+ *
277
+ * 미리보기의 3D 는 여덟 대를 한 화면에 세우느라 한 대가 20 픽셀쯤이다. 관절이 돌았다는
278
+ * 표시를 그 안에 그리면 몇 픽셀에 그친다 — 부품에 단 바늘이 4 × 2 픽셀로 나왔다(잰 값).
279
+ * 각을 읽는 일은 배율에서 떼어 여기에 둔다.
280
+ *
281
+ * 각이 아닌 파라미터(밀리미터 · 퍼센트 · 초)에는 안 단다. 눈금이 도는 것이 아니라서
282
+ * 원으로 그리면 없는 뜻이 생긴다.
283
+ */
284
+ renderDial(range, value) {
285
+ if (!range || range.unit !== 'deg')
286
+ return html `<span></span>`;
287
+ const dial = dialShape(range.min, range.max, value);
288
+ const span = dial.radius * 2 + 2;
289
+ return html `
290
+ <svg dial viewBox="0 0 ${span} ${span}" aria-hidden="true">
291
+ <path d=${dial.sector}></path>
292
+ <circle cx=${dial.centre.x} cy=${dial.centre.y} r=${dial.radius}></circle>
293
+ <line x1=${dial.centre.x} y1=${dial.centre.y} x2=${dial.needle.x} y2=${dial.needle.y}></line>
294
+ </svg>
295
+ `;
296
+ }
220
297
  /**
221
298
  * 세워 둔 인스턴스 전부에 같은 값을 준다.
222
299
  *
@@ -237,6 +314,15 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
237
314
  this.giving = { ...this.giving, [name]: value };
238
315
  this.tellInstances('parameters', this.giving);
239
316
  }
317
+ /**
318
+ * 세운 자세를 기본값으로 — **셈은 여기서 하지 않는다.**
319
+ *
320
+ * 초안을 쥐고 있는 것은 페이지이고, 되돌리기와 「저장 안 됨」도 거기서 매긴다. 여기서
321
+ * 초안을 만들어 보내면 그 셋이 두 곳으로 갈린다. 민 값만 올린다.
322
+ */
323
+ poseAsDefault() {
324
+ this.dispatchEvent(new CustomEvent('pose-as-default', { detail: { values: { ...this.giving } }, bubbles: true, composed: true }));
325
+ }
240
326
  tellInstances(key, values) {
241
327
  for (const component of this.scene?.root?.components ?? []) {
242
328
  component.set?.(key, { ...values });
@@ -293,8 +379,14 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
293
379
  return;
294
380
  }
295
381
  const view = this.view ?? DEFAULT_VIEW;
382
+ /*
383
+ 관절마다 바늘 하나를 붙여 세운다(`joint-gizmo.ts`). 대칭인 부품은 돌아도 그림이 같아서,
384
+ 바늘이 없으면 저작자가 막대를 끝까지 밀고도 관절이 도는 것을 못 본다. 여기서만 붙인다 —
385
+ 저장되는 것도 저작면에 서는 것도 원본 그대로다.
386
+ */
387
+ const shown = { ...source, parts: [...source.parts, ...jointNeedles(source)] };
296
388
  /* 씬이 이 둘로 인스턴스 높이를 정한다 — 서명에 안 넣으면 다시 세워도 그대로 선다. */
297
- const signature = JSON.stringify([source, view.placement, view.ceilingHeight]);
389
+ const signature = JSON.stringify([shown, view.placement, view.ceilingHeight]);
298
390
  if (this.scene && this.standing === signature)
299
391
  return;
300
392
  const { errors } = validate(source);
@@ -307,7 +399,7 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
307
399
  // 표본이 바뀌면 민 값도 처음으로 돌린다 — 다른 자산의 clip 이름이 남으면 안 된다.
308
400
  this.driving = {};
309
401
  try {
310
- registerFigure(compile(source));
402
+ registerFigure(compile(shown));
311
403
  /*
312
404
  도면이 하는 일을 그대로 한다. 컴포넌트에는 타입 이름과 자리·크기만 적는다 —
313
405
  청사진도 원본도 도면에 들어가지 않는다.
@@ -424,6 +516,10 @@ __decorate([
424
516
  property({ type: Object }),
425
517
  __metadata("design:type", Object)
426
518
  ], FigurePreview.prototype, "source", void 0);
519
+ __decorate([
520
+ property({ type: Object }),
521
+ __metadata("design:type", Object)
522
+ ], FigurePreview.prototype, "draft", void 0);
427
523
  __decorate([
428
524
  property({ type: Object }),
429
525
  __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 { dialShape } from './angle-dial.js'\nimport { jointNeedles } from './joint-gizmo.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 20px 96px 34px;\n align-items: center;\n gap: 8px;\n }\n /* 각도 눈. 3D 가 아무리 작게 서 있어도 여기서는 같은 크기로 읽힌다. */\n div[clips] svg[dial] {\n width: 20px;\n height: 20px;\n }\n div[clips] svg[dial] path {\n fill: rgba(255, 255, 255, 0.22);\n }\n div[clips] svg[dial] line {\n stroke: var(--md-sys-color-primary, #7cc6ff);\n stroke-width: 2;\n stroke-linecap: round;\n }\n div[clips] svg[dial] circle {\n fill: none;\n stroke: rgba(255, 255, 255, 0.35);\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 ${this.renderDial(range, value)}\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 <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 * 미리보기의 3D 는 여덟 대를 한 화면에 세우느라 한 대가 20 픽셀쯤이다. 관절이 돌았다는\n * 표시를 그 안에 그리면 몇 픽셀에 그친다 — 부품에 단 바늘이 4 × 2 픽셀로 나왔다(잰 값).\n * 각을 읽는 일은 배율에서 떼어 여기에 둔다.\n *\n * 각이 아닌 파라미터(밀리미터 · 퍼센트 · 초)에는 안 단다. 눈금이 도는 것이 아니라서\n * 원으로 그리면 없는 뜻이 생긴다.\n */\n private renderDial(range: { unit?: string; min: number; max: number } | undefined, value: number) {\n if (!range || range.unit !== 'deg') return html`<span></span>`\n\n const dial = dialShape(range.min, range.max, value)\n const span = dial.radius * 2 + 2\n\n return html`\n <svg dial viewBox=\"0 0 ${span} ${span}\" aria-hidden=\"true\">\n <path d=${dial.sector}></path>\n <circle cx=${dial.centre.x} cy=${dial.centre.y} r=${dial.radius}></circle>\n <line x1=${dial.centre.x} y1=${dial.centre.y} x2=${dial.needle.x} y2=${dial.needle.y}></line>\n </svg>\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 관절마다 바늘 하나를 붙여 세운다(`joint-gizmo.ts`). 대칭인 부품은 돌아도 그림이 같아서,\n 바늘이 없으면 저작자가 막대를 끝까지 밀고도 관절이 도는 것을 못 본다. 여기서만 붙인다 —\n 저장되는 것도 저작면에 서는 것도 원본 그대로다.\n */\n const shown: FigureSource = { ...source, parts: [...source.parts, ...jointNeedles(source)] }\n /* 씬이 이 둘로 인스턴스 높이를 정한다 — 서명에 안 넣으면 다시 세워도 그대로 선다. */\n const signature = JSON.stringify([shown, 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(shown) 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"]}
@@ -23,7 +23,6 @@ export declare class FigureReport extends FigureReport_base {
23
23
  private score?;
24
24
  private curve;
25
25
  /** 1만 개를 놓았을 때. 저작자가 실제로 궁금한 숫자다. */
26
- private many?;
27
26
  /** 몇 개까지 놓을 수 있나. 이 패널의 머리다. */
28
27
  private capacity?;
29
28
  /** 추이 그림을 제 크기로 펼쳤나. */
@@ -65,8 +64,9 @@ export declare class FigureReport extends FigureReport_base {
65
64
  * 직접 답하지 않는다 — 형상이 선언된 한도 안인지를 말할 뿐이다. 그래서 개수를
66
65
  * 머리에 올리고 등급은 옆의 badge 로 내린다.
67
66
  *
68
- * 근거를 함께 낸다. 개수의 분모는 실측이 아니라 설계 문서의 목표치이고(미결
69
- * D-20), 사실이 화면에 없으면 값처럼 읽힌다.
67
+ * 근거를 함께 낸다. 묶음으로 갈리는 개수의 분모는 **실측**이고(2026-09-18, Apple M5),
68
+ * 기계와 날짜를 함께 적는다 적지 않으면 어디서나 참인 수로 읽힌다. 삼각형 쪽은 아직 설계
69
+ * 문서의 목표치라 그렇다고 적는다.
70
70
  */
71
71
  private renderVerdict;
72
72
  /**
@@ -134,14 +134,19 @@ export declare class FigureReport extends FigureReport_base {
134
134
  private renderBudgets;
135
135
  private renderMetrics;
136
136
  /**
137
- * 재활용 추이 — 「많이 놓으면 어떻게 되나」에 답한다.
137
+ * 재활용 추이 그림.
138
+ *
139
+ * **「몇 개 놓으면 얼마」를 숫자로 적지 않는다.** 놓는 개수는 값을 치르지 않는다 —
140
+ * 한 화면에 보이는 개수 × 그 도형의 묶음 수가 치른다(실측 2026-09-18, ADR-0051 ③).
141
+ * 전에는 여기에 「100개 기준 draw call」 · 「1만개 기준」을 적었고, 그 수는 실제로 드는
142
+ * 값과 어긋났다. 답은 위의 성숙도 칸이 한다 — 잰 기계와 날짜를 달고 「한 화면에 몇 대」를
143
+ * 말한다.
138
144
  *
139
145
  * ## 그림을 사이드바에 밀어 넣지 않는다
140
146
  *
141
147
  * `costCurveSvg` 의 자연 크기는 680x380 이고 글자가 11px 다. 300px 칸에서
142
148
  * width:100% 로 줄이면 글자가 4px 이 되어 **아무것도 읽을 수 없다** — 화면을 띄워
143
- * 보고서야 드러났다. 읽을 수 없는 그림은 없는 것과 같으므로, 사이드바에는 저작자가
144
- * 실제로 궁금한 숫자(1만 개일 때)를 두고 그림은 선택해서 제 크기로 본다.
149
+ * 보고서야 드러났다. 그림은 선택해서 크기로 본다.
145
150
  */
146
151
  private renderCurve;
147
152
  /** 그림을 제 크기로 보여 준다. 좁으면 줄이지 않고 넘겨 보게 둔다. */
@@ -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
  `;
@@ -725,12 +730,6 @@ let FigureReport = class FigureReport extends localize(i18next)(LitElement) {
725
730
  <dt>${i18next.t('figure.label.triangles')}</dt>
726
731
  <dd>${cost.triangles.toLocaleString()}</dd>
727
732
 
728
- <dt>${i18next.t('figure.label.triangles-at-100')}</dt>
729
- <dd>${cost.at.triangles.toLocaleString()}</dd>
730
-
731
- <dt>${i18next.t('figure.label.draw-calls-at-100')}</dt>
732
- <dd>${cost.at.drawCalls.toLocaleString()}</dd>
733
-
734
733
  <dt>${i18next.t('figure.label.distinct-shapes')}</dt>
735
734
  <dd>${cost.distinctShapes}</dd>
736
735
 
@@ -741,33 +740,26 @@ let FigureReport = class FigureReport extends localize(i18next)(LitElement) {
741
740
  `;
742
741
  }
743
742
  /**
744
- * 재활용 추이 — 「많이 놓으면 어떻게 되나」에 답한다.
743
+ * 재활용 추이 그림.
744
+ *
745
+ * **「몇 개 놓으면 얼마」를 숫자로 적지 않는다.** 놓는 개수는 값을 치르지 않는다 —
746
+ * 한 화면에 보이는 개수 × 그 도형의 묶음 수가 치른다(실측 2026-09-18, ADR-0051 ③).
747
+ * 전에는 여기에 「100개 기준 draw call」 · 「1만개 기준」을 적었고, 그 수는 실제로 드는
748
+ * 값과 어긋났다. 답은 위의 성숙도 칸이 한다 — 잰 기계와 날짜를 달고 「한 화면에 몇 대」를
749
+ * 말한다.
745
750
  *
746
751
  * ## 그림을 사이드바에 밀어 넣지 않는다
747
752
  *
748
753
  * `costCurveSvg` 의 자연 크기는 680x380 이고 글자가 11px 다. 300px 칸에서
749
754
  * width:100% 로 줄이면 글자가 4px 이 되어 **아무것도 읽을 수 없다** — 화면을 띄워
750
- * 보고서야 드러났다. 읽을 수 없는 그림은 없는 것과 같으므로, 사이드바에는 저작자가
751
- * 실제로 궁금한 숫자(1만 개일 때)를 두고 그림은 선택해서 제 크기로 본다.
755
+ * 보고서야 드러났다. 그림은 선택해서 크기로 본다.
752
756
  */
753
757
  renderCurve() {
754
- const many = this.many;
755
- if (!many && !this.curve)
758
+ if (!this.curve)
756
759
  return nothing;
757
760
  return html `
758
761
  <section>
759
762
  <h3>${i18next.t('figure.label.reuse-trend')}</h3>
760
- ${many
761
- ? html `
762
- <dl metrics>
763
- <dt>${i18next.t('figure.label.triangles-at-10k')}</dt>
764
- <dd>${many.at.triangles.toLocaleString()}</dd>
765
-
766
- <dt>${i18next.t('figure.label.draw-calls-at-10k')}</dt>
767
- <dd>${many.at.drawCalls.toLocaleString()}</dd>
768
- </dl>
769
- `
770
- : nothing}
771
763
  ${this.curve
772
764
  ? html `
773
765
  <button expand @click=${() => (this.expanded = true)}>
@@ -845,7 +837,6 @@ let FigureReport = class FigureReport extends localize(i18next)(LitElement) {
845
837
  if (!source) {
846
838
  this.cost = undefined;
847
839
  this.score = undefined;
848
- this.many = undefined;
849
840
  this.capacity = undefined;
850
841
  this.curve = '';
851
842
  this.errors = [];
@@ -861,7 +852,6 @@ let FigureReport = class FigureReport extends localize(i18next)(LitElement) {
861
852
  if (this.bare) {
862
853
  this.cost = undefined;
863
854
  this.score = undefined;
864
- this.many = undefined;
865
855
  this.capacity = undefined;
866
856
  this.curve = '';
867
857
  this.errors = [];
@@ -881,7 +871,6 @@ let FigureReport = class FigureReport extends localize(i18next)(LitElement) {
881
871
  // 형식을 어겼으면 컴파일이 던진다. 셈을 비우고 오류만 보여 준다.
882
872
  this.cost = undefined;
883
873
  this.score = undefined;
884
- this.many = undefined;
885
874
  this.capacity = undefined;
886
875
  this.curve = '';
887
876
  this.sizing = [];
@@ -901,9 +890,9 @@ let FigureReport = class FigureReport extends localize(i18next)(LitElement) {
901
890
  at: finding.clumps.map(clump => [...clump].sort().join('+')).join(' | ') || undefined,
902
891
  blocking: finding.code === 'sizing-splits'
903
892
  }));
904
- this.cost = costOf(blueprint, 100);
893
+ /* 개수 인자는 `cost.at` 정한다 — 그 칸을 화면에서 뺐으니 기본값 그대로 둔다. */
894
+ this.cost = costOf(blueprint);
905
895
  // 1만 개는 figure-model 에 다시 묻는다 — 화면이 곱셈으로 짐작하지 않는다.
906
- this.many = costOf(blueprint, 10000);
907
896
  this.score = scoreOf(blueprint);
908
897
  this.capacity = capacityOf(this.cost);
909
898
  // 개수를 밖으로 알린다. 탭 이름표가 이 값을 보여 주는데, 거기서 다시 셈하면
@@ -932,10 +921,6 @@ __decorate([
932
921
  state(),
933
922
  __metadata("design:type", Object)
934
923
  ], FigureReport.prototype, "curve", void 0);
935
- __decorate([
936
- state(),
937
- __metadata("design:type", Object)
938
- ], FigureReport.prototype, "many", void 0);
939
924
  __decorate([
940
925
  state(),
941
926
  __metadata("design:type", Object)