@things-factory/figure-ui 10.1.16 → 10.1.18

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 (78) hide show
  1. package/client/modeller/figure-animations.ts +331 -124
  2. package/client/modeller/figure-canvas.ts +32 -48
  3. package/client/modeller/figure-capabilities.ts +10 -10
  4. package/client/modeller/figure-inspector.ts +39 -28
  5. package/client/modeller/figure-parts.ts +1 -1
  6. package/client/modeller/figure-preview.ts +151 -41
  7. package/client/modeller/figure-report.ts +24 -6
  8. package/client/modeller/figure-settings.ts +45 -2
  9. package/client/modeller/figure-side.ts +6 -6
  10. package/client/modeller/figure-source.ts +62 -32
  11. package/client/modeller/figure-thumbnail.ts +5 -5
  12. package/client/modeller/figure-view.ts +53 -0
  13. package/client/modeller/part-edits.ts +36 -36
  14. package/client/modeller/proposal.ts +27 -4
  15. package/client/pages/figure-modeller-page.ts +65 -58
  16. package/dist-client/modeller/figure-animations.d.ts +37 -8
  17. package/dist-client/modeller/figure-animations.js +293 -122
  18. package/dist-client/modeller/figure-animations.js.map +1 -1
  19. package/dist-client/modeller/figure-canvas.d.ts +7 -7
  20. package/dist-client/modeller/figure-canvas.js +27 -40
  21. package/dist-client/modeller/figure-canvas.js.map +1 -1
  22. package/dist-client/modeller/figure-capabilities.d.ts +2 -2
  23. package/dist-client/modeller/figure-capabilities.js +9 -9
  24. package/dist-client/modeller/figure-capabilities.js.map +1 -1
  25. package/dist-client/modeller/figure-inspector.d.ts +16 -5
  26. package/dist-client/modeller/figure-inspector.js +38 -27
  27. package/dist-client/modeller/figure-inspector.js.map +1 -1
  28. package/dist-client/modeller/figure-parts.d.ts +1 -1
  29. package/dist-client/modeller/figure-parts.js +1 -1
  30. package/dist-client/modeller/figure-parts.js.map +1 -1
  31. package/dist-client/modeller/figure-preview.d.ts +56 -6
  32. package/dist-client/modeller/figure-preview.js +132 -39
  33. package/dist-client/modeller/figure-preview.js.map +1 -1
  34. package/dist-client/modeller/figure-report.d.ts +9 -0
  35. package/dist-client/modeller/figure-report.js +23 -5
  36. package/dist-client/modeller/figure-report.js.map +1 -1
  37. package/dist-client/modeller/figure-settings.js +42 -2
  38. package/dist-client/modeller/figure-settings.js.map +1 -1
  39. package/dist-client/modeller/figure-side.d.ts +2 -2
  40. package/dist-client/modeller/figure-side.js +5 -5
  41. package/dist-client/modeller/figure-side.js.map +1 -1
  42. package/dist-client/modeller/figure-source.d.ts +25 -15
  43. package/dist-client/modeller/figure-source.js +42 -21
  44. package/dist-client/modeller/figure-source.js.map +1 -1
  45. package/dist-client/modeller/figure-thumbnail.d.ts +2 -2
  46. package/dist-client/modeller/figure-thumbnail.js +3 -3
  47. package/dist-client/modeller/figure-thumbnail.js.map +1 -1
  48. package/dist-client/modeller/figure-view.d.ts +45 -0
  49. package/dist-client/modeller/figure-view.js +27 -0
  50. package/dist-client/modeller/figure-view.js.map +1 -1
  51. package/dist-client/modeller/part-edits.d.ts +16 -16
  52. package/dist-client/modeller/part-edits.js +35 -35
  53. package/dist-client/modeller/part-edits.js.map +1 -1
  54. package/dist-client/modeller/proposal.d.ts +1 -1
  55. package/dist-client/modeller/proposal.js +16 -3
  56. package/dist-client/modeller/proposal.js.map +1 -1
  57. package/dist-client/pages/figure-modeller-page.d.ts +9 -10
  58. package/dist-client/pages/figure-modeller-page.js +61 -54
  59. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  60. package/dist-client/route.d.ts +1 -1
  61. package/dist-client/tsconfig.tsbuildinfo +1 -1
  62. package/dist-server/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +5 -5
  64. package/test/ai-proposal-contract.test.ts +433 -12
  65. package/test/expect.ts +55 -0
  66. package/test/figure-source.test.ts +572 -0
  67. package/test/i18n-prefix-guard.test.ts +9 -12
  68. package/{client/modeller → test}/part-edits.test.ts +50 -45
  69. package/translations/en.json +46 -35
  70. package/translations/ja.json +16 -21
  71. package/translations/ko.json +56 -45
  72. package/translations/ms.json +16 -21
  73. package/translations/zh.json +16 -21
  74. package/client/modeller/figure-ask.ts +0 -265
  75. package/client/modeller/figure-source.test.ts +0 -352
  76. package/dist-client/modeller/figure-ask.d.ts +0 -40
  77. package/dist-client/modeller/figure-ask.js +0 -282
  78. package/dist-client/modeller/figure-ask.js.map +0 -1
@@ -6,6 +6,9 @@ import { compile, validate } from '@hatiolab/figure-model'
6
6
  import type { FigureSource } from '@hatiolab/figure-model'
7
7
  import { create, registerFigure } from '@hatiolab/things-scene'
8
8
 
9
+ import { DEFAULT_VIEW, sceneLook, WORKBENCH } from './figure-view.js'
10
+ import type { ViewSettings } from './figure-view.js'
11
+
9
12
  /**
10
13
  * 저작 중인 것을 **보드에 놓았을 때**의 모습.
11
14
  *
@@ -29,9 +32,6 @@ import { create, registerFigure } from '@hatiolab/things-scene'
29
32
  * 뷰 모드로 세운다.
30
33
  */
31
34
 
32
- /** 배경. 편집 캔버스와 같은 값을 쓴다 — 갈리면 두 화면이 다른 도구처럼 보인다. */
33
- const WORKBENCH = '#7c7c7c'
34
-
35
35
  /** 얼마나 키워 보나. */
36
36
  export const PREVIEW_FACTOR = 2
37
37
 
@@ -57,6 +57,18 @@ export const PREVIEW_WAYS: readonly { name: string; axes: readonly ('x' | 'y' |
57
57
  { name: 'x·y·z', axes: ['x', 'y', 'z'] }
58
58
  ]
59
59
 
60
+ /*
61
+ * 높이는 여기서 안 정한다.
62
+ *
63
+ * 한동안 배치 기준을 높이로 옮기는 계산을 이 파일이 들고 있었다. 이제 씬이 한다 —
64
+ * `FigureRealObject.effectiveZPos` 가 청사진의 `placement` 와 레이어의 `heights` 로 정한다
65
+ * (ADR-0045, `operato-twin/design/04-decisions.md`). 보드가 그렇게 세우므로 「보드에 놓았을
66
+ * 때의 모습」을 말하는 이 화면도 그렇게 세워야 한다 — 여기서 `zPos` 를 적으면 **적은 값이
67
+ * 이겨서**, 씬이 무엇을 할지가 이 화면에서만 안 보인다.
68
+ *
69
+ * 규칙이 두 벌이면 갈린다. 오늘 하루가 그 이야기였다.
70
+ */
71
+
60
72
  interface SceneHandle {
61
73
  dispose?: () => void
62
74
  resize?: () => void
@@ -64,6 +76,8 @@ interface SceneHandle {
64
76
  root?: {
65
77
  invalidate?: () => void
66
78
  fitCameraToModel?: (margin?: number) => void
79
+ /** 보기 설정을 넣는 자리. 씬이 스스로 반응한다. */
80
+ set?: (patch: Record<string, unknown>) => void
67
81
  /** 세워 둔 인스턴스들. clip 을 몰 때 전부에 같은 값을 준다. */
68
82
  components?: { set?: (property: string, value: unknown) => void }[]
69
83
  }
@@ -103,11 +117,11 @@ export class FigurePreview extends localize(i18next)(LitElement) {
103
117
  }
104
118
 
105
119
  /*
106
- Clip 조작. **저작자가 제 애니메이션을 만져 볼 유일한 자리다.**
120
+ 움직임 조작. **저작자가 제 애니메이션과 파라미터를 만져 볼 유일한 자리다.**
107
121
 
108
- 보드에서는 트윈이 clip 몬다 — 「닙이 40% 열렸다」는 사실이 어디선가 온다. 저작
109
- 중에는 그 사실을 주는 것이 없어서, 손대지 않으면 hold clip 은 0 에 머문 채
110
- 영영 안 움직인다. 포크가 안 올라가는 것을 저작자가 「안 되는구나」로 읽게 된다.
122
+ 도면에서는 트윈이 값을 준다 — 「닙이 40% 닫혔다」는 사실이 어디선가 온다. 저작
123
+ 중에는 그 사실을 주는 것이 없어서, 손대지 않으면 파라미터는 기본값에 머문 채 영영
124
+ 안 움직인다. 포크가 안 올라가는 것을 저작자가 「안 되는구나」로 읽게 된다.
111
125
 
112
126
  **미리보기 것이지 자산의 것이 아니다.** 여기서 민 값은 저장되지 않는다.
113
127
  */
@@ -162,6 +176,14 @@ export class FigurePreview extends localize(i18next)(LitElement) {
162
176
  /** 저작 중인 것. 저장 형식으로 받는다 — 보드가 소비하는 것이 이 형식이다. */
163
177
  @property({ type: Object }) source?: FigureSource
164
178
 
179
+ /**
180
+ * 보기 설정. 저장되지 않는다(`figure-view.ts`).
181
+ *
182
+ * 한동안 이 화면만 조명과 환경을 **하드코딩**했다. 저작자가 환경 탭에서 무엇을 바꿔도
183
+ * 편집 캔버스만 달라지고 여기는 그대로였다 — 두 화면이 같은 것을 다르게 보여 주었다.
184
+ */
185
+ @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW
186
+
165
187
  @query('div[stage]') private stage!: HTMLDivElement
166
188
 
167
189
  /** 세울 수 없는 사유. 없으면 세운 것이다. */
@@ -172,11 +194,14 @@ export class FigurePreview extends localize(i18next)(LitElement) {
172
194
  private standing?: string
173
195
  private watcher?: ResizeObserver
174
196
 
175
- /** 지금 민 clip 값. clip 이름 → 수. **저장되지 않는다.** */
197
+ /** 지금 민 clip 배속. clip 이름 → 수. **저장되지 않는다.** */
176
198
  @state() private driving: Record<string, number> = {}
199
+ /** 지금 민 파라미터 값. 이름 → `range` 의 단위로 잰 수. **저장되지 않는다.** */
200
+ @state() private giving: Record<string, number> = {}
177
201
 
178
202
  render() {
179
203
  const clips = this.source?.animations ?? []
204
+ const parameters = this.source?.parameters ?? []
180
205
 
181
206
  return html`
182
207
  <div stage></div>
@@ -185,31 +210,54 @@ export class FigurePreview extends localize(i18next)(LitElement) {
185
210
  : html`<div legend>
186
211
  ${PREVIEW_WAYS.map(way => html`<span>${way.name}</span>`)}
187
212
  </div>
188
- ${clips.length > 0 ? this.renderClips(clips) : ''}`}
213
+ ${clips.length + parameters.length > 0 ? this.renderDrivers(clips, parameters) : ''}`}
189
214
  `
190
215
  }
191
216
 
192
217
  /**
193
- * Clip 하나에 손잡이 하나.
218
+ * 움직임 하나에 손잡이 하나.
194
219
  *
195
- * 수의 뜻이 `drive` 따라 다르므로 그것을 이름 옆에 적는다. `loop` 배속이라 0 이
196
- * 멈춤이고, `hold` 0~1 위치라 0 처음이다. 둘을 같은 눈금으로 두면 「1」이 무슨
197
- * 뜻인지 매번 헷갈린다.
220
+ * 갈래를 **눈금이 다른 채로** 낸다. clip 배속이라 0 멈춤이고 0~3 뜻이
221
+ * 있으며, 파라미터는 물리량이라 `range` 눈금이고 단위가 따라 붙는다. 하나의
222
+ * 0~1 로 뭉뚱그리면 「1」이 무슨 뜻인지 매번 헷갈리고, 1200mm 를 넣어 보려던 저작자가
223
+ * 넣을 자리를 못 찾는다.
198
224
  */
199
- private renderClips(clips: NonNullable<FigureSource['animations']>) {
225
+ private renderDrivers(
226
+ clips: NonNullable<FigureSource['animations']>,
227
+ parameters: NonNullable<FigureSource['parameters']>
228
+ ) {
200
229
  return html`
201
230
  <div clips>
231
+ ${parameters.map(parameter => {
232
+ const range = parameter.range
233
+ const value = this.giving[parameter.name] ?? parameter.default ?? range?.min ?? 0
234
+ const step = range ? Math.max((range.max - range.min) / 100, 0.01) : 1
235
+
236
+ return html`
237
+ <label>
238
+ <span>${parameter.label ?? parameter.name} <span drive>${range?.unit ?? ''}</span></span>
239
+ <input
240
+ type="range"
241
+ min=${range?.min ?? 0}
242
+ max=${range?.max ?? 1}
243
+ step=${step}
244
+ .value=${String(value)}
245
+ @input=${(e: Event) => this.give(parameter.name, Number((e.target as HTMLInputElement).value))}
246
+ />
247
+ <output>${Math.round(value)}</output>
248
+ </label>
249
+ `
250
+ })}
202
251
  ${clips.map(clip => {
203
- const loop = (clip.drive ?? 'loop') === 'loop'
204
- const value = this.driving[clip.name] ?? (loop ? 1 : 0)
252
+ const value = this.driving[clip.name] ?? 1
205
253
 
206
254
  return html`
207
255
  <label>
208
- <span>${clip.name} <span drive>${loop ? '배속' : '위치'}</span></span>
256
+ <span>${clip.name} <span drive>배속</span></span>
209
257
  <input
210
258
  type="range"
211
259
  min="0"
212
- max=${loop ? 3 : 1}
260
+ max="3"
213
261
  step="0.05"
214
262
  .value=${String(value)}
215
263
  @input=${(e: Event) => this.drive(clip.name, Number((e.target as HTMLInputElement).value))}
@@ -230,15 +278,53 @@ export class FigurePreview extends localize(i18next)(LitElement) {
230
278
  */
231
279
  private drive(name: string, value: number): void {
232
280
  this.driving = { ...this.driving, [name]: value }
281
+ this.tellInstances('clips', this.driving)
282
+ }
233
283
 
284
+ /**
285
+ * 파라미터 값을 준다 — **`range` 의 단위 그대로다.**
286
+ *
287
+ * 0~1 로 바꿔서 주지 않는다. 그 환산은 런타임의 일이고, 여기서 한 번 더 하면 두 곳이
288
+ * 갈릴 수 있다. 저작자가 화면에서 보는 1200 이 씬에 그대로 간다.
289
+ */
290
+ private give(name: string, value: number): void {
291
+ this.giving = { ...this.giving, [name]: value }
292
+ this.tellInstances('parameters', this.giving)
293
+ }
294
+
295
+ private tellInstances(key: 'clips' | 'parameters', values: Record<string, number>): void {
234
296
  for (const component of this.scene?.root?.components ?? []) {
235
- component.set?.('clips', { ...this.driving })
297
+ component.set?.(key, { ...values })
236
298
  }
237
299
  }
238
300
 
301
+ /**
302
+ * 보기 설정이 바뀌면 **다시 세우지 않고 씬에 넣는다** — 두 개만 빼고.
303
+ *
304
+ * 조명·환경·바탕은 씬이 스스로 반응한다(`three-capability` 의 `onchange`). 슬라이더를
305
+ * 끄는 동안 다시 세우면 카메라가 매 프레임 처음으로 돌아간다.
306
+ *
307
+ * 좌표 기준과 천장 높이는 다르다. 씬이 그 둘로 인스턴스 높이를 정하는데, 그 계산은 세울
308
+ * 때 한 번 돈다 — 이미 세워 둔 것에 넣어 봐야 자리가 안 바뀐다. 그것만 다시 세운다.
309
+ */
239
310
  willUpdate(changed: Map<string, unknown>) {
240
311
  if (!this.hasUpdated) return
241
- if (changed.has('source')) this.standUp()
312
+
313
+ if (changed.has('source')) {
314
+ this.standUp()
315
+ return
316
+ }
317
+
318
+ if (!changed.has('view')) return
319
+
320
+ const before = changed.get('view') as ViewSettings | undefined
321
+ const now = this.view ?? DEFAULT_VIEW
322
+ if (before?.placement !== now.placement || before?.ceilingHeight !== now.ceilingHeight) {
323
+ this.standUp()
324
+ return
325
+ }
326
+
327
+ this.scene?.root?.set?.(sceneLook(now))
242
328
  }
243
329
 
244
330
  firstUpdated() {
@@ -268,7 +354,9 @@ export class FigurePreview extends localize(i18next)(LitElement) {
268
354
  return
269
355
  }
270
356
 
271
- const signature = JSON.stringify(source)
357
+ const view = this.view ?? DEFAULT_VIEW
358
+ /* 씬이 이 둘로 인스턴스 높이를 정한다 — 서명에 안 넣으면 다시 세워도 그대로 선다. */
359
+ const signature = JSON.stringify([source, view.placement, view.ceilingHeight])
272
360
  if (this.scene && this.standing === signature) return
273
361
 
274
362
  const { errors } = validate(source)
@@ -304,24 +392,16 @@ export class FigurePreview extends localize(i18next)(LitElement) {
304
392
  const COLUMNS = 4
305
393
  const cell = { x: base.x * biggest * 1.25, z: base.z * biggest * 1.25 }
306
394
  const rows = Math.ceil(PREVIEW_WAYS.length / COLUMNS)
307
- const board = {
308
- width: Math.round(cell.x * COLUMNS),
309
- height: Math.round(cell.z * rows),
310
- /*
311
- 판의 높이도 가장 큰 인스턴스를 담아야 한다.
312
-
313
- 안 주면 카메라가 평면만 보고 자리를 잡아서, 키가 큰 모델은 밑동만 보인다.
314
- 시그널타워를 2 배로 놓으면 2200 인데 판이 그것을 모르면 받침만 화면에 든다.
315
- */
316
- depth: Math.round(base.y * biggest * 1.1)
317
- }
318
-
319
395
  /*
320
396
  씬의 낱말이 형식과 다르다 — `width` 가 형식의 x, `height` 가 z, `depth` 가 y 다.
321
397
  옮기는 일을 여기 한 줄에 모아 둔다.
322
398
 
323
399
  `depth` 를 늘 적는 이유가 있다. 안 적으면 평면 두 축의 기하평균으로 잡혀서,
324
400
  「y 만 키운다」가 x·z 를 안 건드리는데도 높이가 안 오른다.
401
+
402
+ 높이는 **배치 기준이 정한다.** 바닥 기반은 땅에서 올라오고, 천정 기반은 천장에
403
+ 붙어 내려온다. 여덟을 나란히 세우는 자리라 이것이 눈에 바로 보인다 — 바닥 기반이면
404
+ 밑면이, 천정 기반이면 윗면이 한 줄이 된다.
325
405
  */
326
406
  const components = PREVIEW_WAYS.map((way, at) => {
327
407
  const grow = growth(way)
@@ -329,6 +409,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
329
409
  const row = Math.floor(at / COLUMNS)
330
410
  const width = Math.round(base.x * grow.x)
331
411
  const height = Math.round(base.z * grow.z)
412
+ const tall = Math.round(base.y * grow.y)
332
413
 
333
414
  return {
334
415
  type: source.type,
@@ -338,23 +419,52 @@ export class FigurePreview extends localize(i18next)(LitElement) {
338
419
  top: Math.round(row * cell.z + (cell.z - height) / 2),
339
420
  width,
340
421
  height,
341
- depth: Math.round(base.y * grow.y)
422
+ depth: tall
342
423
  }
343
424
  })
344
425
 
426
+ const draft = {
427
+ width: Math.round(cell.x * COLUMNS),
428
+ height: Math.round(cell.z * rows),
429
+ /*
430
+ 판의 높이도 가장 큰 인스턴스를 담아야 한다. 안 주면 카메라가 평면만 보고 자리를
431
+ 잡아서, 키가 큰 모델은 밑동만 보인다 — 시그널타워를 2 배로 놓으면 2200 인데 판이
432
+ 그것을 모르면 받침만 화면에 든다.
433
+
434
+ 어디에 올려 놓을지는 **씬이 정하므로**(`FigureRealObject.effectiveZPos`), 여기서는
435
+ 어느 경우에도 담기는 경계만 잡는다. 바닥 기반은 가장 큰 것의 키까지, 천정 기반은
436
+ 천장까지다. 배치를 높이로 옮기는 계산을 여기서 다시 쓰면 그것이 두 벌째가 된다.
437
+ */
438
+ depth: Math.round(Math.max(view.ceilingHeight, base.y * biggest) * 1.1)
439
+ }
440
+
345
441
  this.scene = create({
346
442
  target: this.stage,
347
443
  model: {
348
444
  type: 'model-layer',
349
- width: board.width,
350
- height: board.height,
351
- depth: board.depth,
445
+ width: draft.width,
446
+ height: draft.height,
447
+ depth: draft.depth,
352
448
  threed: true,
353
- environment: 'studio',
354
- grid: true,
355
449
  sky: 'color',
356
- skyColor: WORKBENCH,
357
- floor: false,
450
+ /*
451
+ 보기 설정을 그대로 싣는다.
452
+
453
+ 여기만 조명·환경·바탕을 하드코딩하고 있었다. 저작자가 환경 탭에서 조명을
454
+ 올려도 편집 캔버스만 밝아지고 이 화면은 그대로여서, 같은 것을 두 화면이
455
+ 다르게 보여 주었다.
456
+
457
+ `sceneLook` 이 좌표 기준과 천장 높이를 빼고 준다 — 씬이 아는 이름이 아니거나
458
+ (`ceilingHeight`) 편집 캔버스에 새면 안 되는 것(`placement`)이라서다. 둘 다 아래에
459
+ 씬의 낱말로 따로 싣는다.
460
+ */
461
+ ...sceneLook(view),
462
+ placement: view.placement,
463
+ /*
464
+ 부착면의 높이. 씬이 이 자리를 갖고(`real-object.ts` 의 `sceneHeights`), 천정 기반
465
+ 도형이 여기서 제 높이를 얻는다. 보드에서는 현장이 이 값을 준다.
466
+ */
467
+ heights: { floor: 0, ceiling: view.ceilingHeight },
358
468
  pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),
359
469
  components
360
470
  },
@@ -6,7 +6,7 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js'
6
6
 
7
7
  import { i18next, localize } from '@operato/i18n'
8
8
  import { ScrollbarStyles } from '@operato/styles'
9
- import { compile, costCurveSvg, costOf, scoreOf, sizingReport, validate } from '@hatiolab/figure-model'
9
+ import { blocksRelease, compile, costCurveSvg, costOf, scoreOf, sizingReport, validate } from '@hatiolab/figure-model'
10
10
  import type { FigureCost, FigureScore, FigureSource } from '@hatiolab/figure-model'
11
11
 
12
12
  import { capacityOf } from './scene-budget.js'
@@ -439,7 +439,16 @@ export class FigureReport extends localize(i18next)(LitElement) {
439
439
  /** 추이 그림을 제 크기로 펼쳤나. */
440
440
  @state() private expanded = false
441
441
  @state() private errors: { code: string; message: string; at?: string }[] = []
442
- @state() private violations: { code: string; message: string }[] = []
442
+ /**
443
+ * 형식이 권하지 않는 것 — **그중 둘은 발행을 막는다.**
444
+ *
445
+ * 등급을 형식에서 받는다(`blocksRelease`). 화면이 제 목록을 들면 형식에 코드가 하나 늘 때
446
+ * 조용히 「알림」으로 그려지고, 저작자는 발행이 막힌 것을 **눌러 보고 나서야** 안다.
447
+ *
448
+ * `actual` 은 얼마나 넘쳤나다. 「상자를 벗어난다」만으로는 2mm 인지 250mm 인지 모르고,
449
+ * 그 둘은 고치는 품이 다르다.
450
+ */
451
+ @state() private violations: { code: string; message: string; blocking: boolean; actual?: number }[] = []
443
452
  /**
444
453
  * 크기를 바꿔 봤을 때 갈라지는가 — **발행을 막는 것**이다.
445
454
  *
@@ -864,9 +873,13 @@ export class FigureReport extends localize(i18next)(LitElement) {
864
873
  )}
865
874
  ${this.violations.map(
866
875
  v => html`
867
- <li violation>
868
- <md-icon>info</md-icon>
869
- <span>${v.message}</span>
876
+ <li ?violation=${!v.blocking}>
877
+ <md-icon>${v.blocking ? 'block' : 'info'}</md-icon>
878
+ <span>
879
+ ${v.message}${v.blocking && v.actual !== undefined
880
+ ? html` <code at>${v.actual}</code>`
881
+ : ''}
882
+ </span>
870
883
  </li>
871
884
  `
872
885
  )}
@@ -909,7 +922,12 @@ export class FigureReport extends localize(i18next)(LitElement) {
909
922
 
910
923
  const result = validate(source)
911
924
  this.errors = result.errors.map(e => ({ code: e.code, message: e.message, at: e.path }))
912
- this.violations = result.violations.map(v => ({ code: v.code, message: v.message }))
925
+ this.violations = result.violations.map(v => ({
926
+ code: v.code,
927
+ message: v.message,
928
+ blocking: blocksRelease(v.code),
929
+ actual: v.actual
930
+ }))
913
931
 
914
932
  if (this.errors.length > 0) {
915
933
  // 형식을 어겼으면 컴파일이 던진다. 셈을 비우고 오류만 보여 준다.
@@ -4,7 +4,7 @@ import { customElement, property } from 'lit/decorators.js'
4
4
  import { i18next, localize } from '@operato/i18n'
5
5
  import { ScrollbarStyles } from '@operato/styles'
6
6
 
7
- import { DEFAULT_VIEW, ENVIRONMENTS } from './figure-view.js'
7
+ import { DEFAULT_VIEW, ENVIRONMENTS, SCENE_REFERENCES } from './figure-view.js'
8
8
  import type { EnvironmentName, ViewSettings } from './figure-view.js'
9
9
 
10
10
  /**
@@ -99,6 +99,7 @@ export class FigureSettings extends localize(i18next)(LitElement) {
99
99
  }
100
100
 
101
101
  select,
102
+ input[type='number'],
102
103
  input[type='color'] {
103
104
  width: 100%;
104
105
  padding: 5px 6px;
@@ -158,10 +159,52 @@ export class FigureSettings extends localize(i18next)(LitElement) {
158
159
  const view = this.view ?? DEFAULT_VIEW
159
160
 
160
161
  return html`
162
+ <!--
163
+ 놓이는 보드.
164
+
165
+ 이 묶음이 맨 위인 것은, 나머지가 「어떻게 보이나」인데 이것은 「무엇으로 보고
166
+ 있나」이기 때문이다. 미리보기가 보드에 놓인 모습을 말하는 자리이므로, 어떤 보드를
167
+ 가정하는지가 먼저 정해져야 그 다음이 뜻을 갖는다.
168
+
169
+ 도형 탭의 「배치 기준」과 짝이지 같은 것이 아니다. 저쪽은 이 도형이 어느 면에
170
+ 붙는가(저장된다), 이쪽은 그 면을 가진 보드가 어느 쪽을 기준으로 재는가(저장 안 된다).
171
+ -->
161
172
  <section>
162
- <h3>${i18next.t('figure.label.guides')}</h3>
173
+ <h3>${i18next.t('figure.label.assumed-board')}</h3>
163
174
  <p quiet>${i18next.t('figure.text.view-settings-are-not-saved')}</p>
164
175
 
176
+ <label field>
177
+ <span>${i18next.t('figure.label.scene-reference')}</span>
178
+ <select
179
+ .value=${view.placement}
180
+ @change=${(e: Event) =>
181
+ this.put({ placement: (e.target as HTMLSelectElement).value as ViewSettings['placement'] })}
182
+ >
183
+ ${SCENE_REFERENCES.map(
184
+ name => html`<option value=${name} ?selected=${view.placement === name}>
185
+ ${i18next.t(`figure.label.scene-reference-${name}`)}
186
+ </option>`
187
+ )}
188
+ </select>
189
+ </label>
190
+ <p quiet>${i18next.t('figure.text.scene-reference-explained')}</p>
191
+
192
+ <label field>
193
+ <span>${i18next.t('figure.label.ceiling-height')}</span>
194
+ <input
195
+ type="number"
196
+ min="0"
197
+ step="100"
198
+ .value=${String(view.ceilingHeight)}
199
+ @change=${(e: Event) => this.put({ ceilingHeight: Number((e.target as HTMLInputElement).value) || 0 })}
200
+ />
201
+ </label>
202
+ <p quiet>${i18next.t('figure.text.ceiling-height-explained')}</p>
203
+ </section>
204
+
205
+ <section>
206
+ <h3>${i18next.t('figure.label.guides')}</h3>
207
+
165
208
  ${this.toggle('grid', view.grid, i18next.t('figure.label.coordinate-ground'), i18next.t('figure.text.coordinate-ground-explained'))}
166
209
  ${this.toggle('gridLabels', view.gridLabels, i18next.t('figure.label.axis-labels'))}
167
210
  ${this.toggle('floor', view.floor, i18next.t('figure.label.scene-floor'), i18next.t('figure.text.floor-explained'))}
@@ -12,7 +12,7 @@ import { customElement, property, state } from 'lit/decorators.js'
12
12
  import { i18next, localize } from '@operato/i18n'
13
13
  import type { FigureSource } from '@hatiolab/figure-model'
14
14
 
15
- import type { BoardModel, PartModel } from './figure-source.js'
15
+ import type { FigureDraft, PartModel } from './figure-source.js'
16
16
  import { DEFAULT_VIEW } from './figure-view.js'
17
17
  import type { ViewSettings } from './figure-view.js'
18
18
 
@@ -156,7 +156,7 @@ export class FigureSide extends localize(i18next)(LitElement) {
156
156
  @property({ type: Number }) figureVersion = 0
157
157
  @property({ type: Boolean }) dirty = false
158
158
 
159
- @property({ type: Object }) board?: BoardModel
159
+ @property({ type: Object }) draft?: FigureDraft
160
160
  @property({ type: Array }) parts: PartModel[] = []
161
161
  @property({ type: Object }) source?: FigureSource
162
162
  @property({ type: Number }) selected = -1
@@ -175,7 +175,7 @@ export class FigureSide extends localize(i18next)(LitElement) {
175
175
  * 0 이면 아무것도 안 붙인다 — 「0」을 붙이면 없는 것이 있는 것처럼 자리를 차지한다.
176
176
  */
177
177
  private get clipCount(): number | undefined {
178
- const n = this.board?.animations?.length ?? 0
178
+ const n = (this.draft?.animations?.length ?? 0) + (this.draft?.parameters?.length ?? 0)
179
179
  return n === 0 ? undefined : n
180
180
  }
181
181
 
@@ -196,20 +196,20 @@ export class FigureSide extends localize(i18next)(LitElement) {
196
196
  .figureName=${this.figureName}
197
197
  .figureCategory=${this.figureCategory}
198
198
  .figureTags=${this.figureTags}
199
- .board=${this.board}
199
+ .draft=${this.draft}
200
200
  .parts=${this.parts}
201
201
  .selected=${this.selected}
202
202
  ></figure-inspector>
203
203
 
204
204
  <figure-capabilities
205
205
  ?off=${this.tab !== 'capability'}
206
- .board=${this.board}
206
+ .draft=${this.draft}
207
207
  .parts=${this.parts}
208
208
  ></figure-capabilities>
209
209
 
210
210
  <figure-animations
211
211
  ?off=${this.tab !== 'animation'}
212
- .board=${this.board}
212
+ .draft=${this.draft}
213
213
  .parts=${this.parts}
214
214
  ></figure-animations>
215
215