@things-factory/figure-ui 10.1.15 → 10.1.17

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 +12 -12
  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 +122 -25
  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 +84 -38
  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 +25 -4
  15. package/client/pages/figure-modeller-page.ts +142 -98
  16. package/dist-client/modeller/figure-animations.d.ts +4 -4
  17. package/dist-client/modeller/figure-animations.js +10 -10
  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 +39 -0
  32. package/dist-client/modeller/figure-preview.js +112 -23
  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 +38 -23
  43. package/dist-client/modeller/figure-source.js +40 -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 +15 -3
  56. package/dist-client/modeller/proposal.js.map +1 -1
  57. package/dist-client/pages/figure-modeller-page.d.ts +44 -9
  58. package/dist-client/pages/figure-modeller-page.js +134 -92
  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 +4 -4
  64. package/test/ai-proposal-contract.test.ts +393 -12
  65. package/test/expect.ts +55 -0
  66. package/test/figure-source.test.ts +562 -0
  67. package/test/i18n-prefix-guard.test.ts +1 -1
  68. package/{client/modeller → test}/part-edits.test.ts +50 -45
  69. package/translations/en.json +31 -28
  70. package/translations/ja.json +16 -21
  71. package/translations/ko.json +41 -38
  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
@@ -8,7 +8,7 @@ import { ScrollbarStyles } from '@operato/styles'
8
8
  import { AXES, CHANNEL_PATHS, CLIP_DRIVES, INTERPOLATIONS, LIMITS } from '@hatiolab/figure-model'
9
9
  import type { ChannelPath, Vec3 } from '@hatiolab/figure-model'
10
10
 
11
- import type { BoardModel, PartModel } from './figure-source.js'
11
+ import type { FigureDraft, PartModel } from './figure-source.js'
12
12
 
13
13
  /**
14
14
  * 이 도형이 **어떻게 움직이는가.**
@@ -35,7 +35,7 @@ import type { BoardModel, PartModel } from './figure-source.js'
35
35
  * 받는다. 이 선택이 화면에서 가장 설명이 필요한 자리다.
36
36
  */
37
37
 
38
- type Clip = NonNullable<BoardModel['animations']>[number]
38
+ type Clip = NonNullable<FigureDraft['animations']>[number]
39
39
  type Channel = Clip['channels'][number]
40
40
  type Keyframe = Channel['keys'][number]
41
41
 
@@ -286,7 +286,7 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
286
286
  `
287
287
  ]
288
288
 
289
- @property({ type: Object }) board?: BoardModel
289
+ @property({ type: Object }) draft?: FigureDraft
290
290
  @property({ type: Array }) parts: PartModel[] = []
291
291
 
292
292
  /** 펼쳐 둔 clip. 이름이 아니라 자리로 잡는다 — 이름은 편집 중에 바뀐다. */
@@ -294,7 +294,7 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
294
294
  @state() private recipeTarget = ''
295
295
 
296
296
  private get clips(): Clip[] {
297
- return this.board?.animations ?? []
297
+ return this.draft?.animations ?? []
298
298
  }
299
299
 
300
300
  /** 부품 이름 — 채널이 이것을 가리킨다. */
@@ -315,7 +315,7 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
315
315
  }
316
316
 
317
317
  render(): TemplateResult {
318
- if (!this.board) return html``
318
+ if (!this.draft) return html``
319
319
 
320
320
  return html`
321
321
  <section>
@@ -626,19 +626,19 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
626
626
  // ─── 고치기 ───────────────────────────────────────────────────────────────
627
627
 
628
628
  /**
629
- * 보드에 실어 올려보낸다.
629
+ * 초안에 실어 올려보낸다.
630
630
  *
631
- * `animations` 는 도형 전체의 것이므로 부품 편집(`update-part`)이 아니라 `update-board` 로
631
+ * `animations` 는 도형 전체의 것이므로 부품 편집(`update-part`)이 아니라 `update-draft` 로
632
632
  * 간다. 빈 배열은 칸을 지운다 — clip 을 다 지운 도형은 `animations: []` 가 아니라 없는 것이다.
633
633
  */
634
634
  private tell(animations: Clip[]): void {
635
- if (!this.board) return
635
+ if (!this.draft) return
636
636
 
637
- const board: BoardModel = { ...this.board }
638
- if (animations.length === 0) delete board.animations
639
- else board.animations = animations
637
+ const draft: FigureDraft = { ...this.draft }
638
+ if (animations.length === 0) delete draft.animations
639
+ else draft.animations = animations
640
640
 
641
- this.dispatchEvent(new CustomEvent('update-board', { detail: { board }, bubbles: true, composed: true }))
641
+ this.dispatchEvent(new CustomEvent('update-draft', { detail: { draft }, bubbles: true, composed: true }))
642
642
  }
643
643
 
644
644
  private patchClip(at: number, patch: Partial<Clip>): void {
@@ -5,22 +5,9 @@ import { i18next, localize } from '@operato/i18n'
5
5
  import { create, Model } from '@hatiolab/things-scene'
6
6
 
7
7
  import { gridStep } from './part-edits.js'
8
- import { DEFAULT_VIEW, WORKBENCH } from './figure-view.js'
8
+ import { DEFAULT_VIEW, sceneLook, WORKBENCH } from './figure-view.js'
9
9
  import type { ViewSettings } from './figure-view.js'
10
- import type { BoardModel, PartModel } from './figure-source.js'
11
- import type { FigurePlacement } from '@hatiolab/figure-model'
12
-
13
- function toScenePlacement(placement?: FigurePlacement): 'floor' | 'inverted' | 'space' {
14
- switch (placement) {
15
- case 'ceiling':
16
- return 'inverted'
17
- case 'center':
18
- return 'space'
19
- case 'floor':
20
- default:
21
- return 'floor'
22
- }
23
- }
10
+ import type { FigureDraft, PartModel } from './figure-source.js'
24
11
 
25
12
  /**
26
13
  * 판의 종이 색 — 거의 흰색.
@@ -70,10 +57,10 @@ interface SceneHandle {
70
57
  * 있다. 부품은 판에 붙고, 선택 알림은 루트가 낸다. 둘을 섞으면 알림이 안 온다 —
71
58
  * 실제로 그랬다.
72
59
  */
73
- root?: SceneBoard
60
+ root?: SceneLayer
74
61
  }
75
62
 
76
- interface SceneBoard {
63
+ interface SceneLayer {
77
64
  invalidate?: () => void
78
65
  /** 보기 설정을 넣는 자리. 씬이 스스로 반응한다. */
79
66
  set?: (patch: Record<string, unknown>) => void
@@ -101,7 +88,7 @@ interface SceneComponent {
101
88
  }
102
89
 
103
90
  /**
104
- * 편집 캔버스 — 부품들을 보드 위에 세우고, 여기서 고치게 한다.
91
+ * 편집 캔버스 — 부품들을 기준 상자 위에 세우고, 여기서 고치게 한다.
105
92
  *
106
93
  * ## 미리보기가 아니다
107
94
  *
@@ -119,10 +106,10 @@ interface SceneComponent {
119
106
  * 이제 부품 하나가 컴포넌트 하나다. 선택 · 기즈모 · 윤곽선 · 다중 선택 · 되돌리기가
120
107
  * 씬의 기본 동작으로 그냥 된다.
121
108
  *
122
- * ## 보드가 곧 기준 상자다
109
+ * ## 초안이 곧 기준 상자다
123
110
  *
124
- * 보드의 `width` · `height` · `depth` 가 `FigureSource.base` 다. 부품 좌표를 변환할 때
125
- * 보드 중심만 빼면 된다 — `figure-source.ts` 가 그 일을 한다.
111
+ * 초안의 `width` · `height` · `depth` 가 `FigureSource.base` 다. 부품 좌표를 변환할 때
112
+ * 상자 중심만 빼면 된다 — `figure-source.ts` 가 그 일을 한다.
126
113
  *
127
114
  * ## 다시 세우지 않는다
128
115
  *
@@ -194,10 +181,10 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
194
181
  *
195
182
  * 한동안 `FigureSource` 로 받아 여기서 변환했다. 그런데 모델러 페이지가 갖고 있는 것이
196
183
  * 이미 씬 모델이므로, 그러면 값을 하나 고칠 때마다 왕복 변환이 한 번씩 일어난다.
197
- * 무손실이긴 하지만 하는 일이 없다 — 보드가보드이고 부품이 곧 컴포넌트다.
184
+ * 무손실이긴 하지만 하는 일이 없다 — 초안이판이고 부품이 곧 컴포넌트다.
198
185
  */
199
186
  @property({ type: String }) figureId?: string
200
- @property({ type: Object }) board?: BoardModel
187
+ @property({ type: Object }) draft?: FigureDraft
201
188
  /** 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다(`figure-view.ts`). */
202
189
  @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW
203
190
  @property({ type: Array }) parts: PartModel[] = []
@@ -246,7 +233,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
246
233
  아무것도 보여 주지 않고, 저장할 때마다 판 번호만 올라간다 — 없는 것보다 나쁘다.
247
234
  */
248
235
  const layer = this.scene?.root as
249
- | (SceneBoard & {
236
+ | (SceneLayer & {
250
237
  snapshot3d?: (
251
238
  width?: number,
252
239
  height?: number,
@@ -290,7 +277,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
290
277
  if (!this.hasUpdated) return
291
278
 
292
279
  if (changed.has('revision')) this.standing = undefined
293
- if (changed.has('board') || changed.has('parts') || changed.has('figureId') || changed.has('revision')) this.standUp()
280
+ if (changed.has('draft') || changed.has('parts') || changed.has('figureId') || changed.has('revision')) this.standUp()
294
281
  if (changed.has('view')) this.applyView()
295
282
  if (changed.has('picked')) this.showPicked()
296
283
  }
@@ -318,8 +305,8 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
318
305
  * 숫자 하나 고칠 때마다 화면이 처음으로 돌아간다.
319
306
  */
320
307
  private standUp() {
321
- const board = this.board
322
- if (!board) {
308
+ const draft = this.draft
309
+ if (!draft) {
323
310
  this.teardown()
324
311
  this.cannot = i18next.t('figure.text.canvas-appears-once-a-part-exists')
325
312
  return
@@ -332,7 +319,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
332
319
  return
333
320
  }
334
321
 
335
- const signature = `${this.figureId ?? ''}:${board.width}x${board.height}x${board.depth}:${board.placement ?? 'floor'}`
322
+ const signature = `${this.figureId ?? ''}:${draft.width}x${draft.height}x${draft.depth}`
336
323
  if (this.scene && this.standing === signature) {
337
324
  this.syncParts()
338
325
  return
@@ -345,10 +332,9 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
345
332
  target: this.stage,
346
333
  model: {
347
334
  type: 'model-layer',
348
- width: board.width,
349
- height: board.height,
350
- depth: board.depth,
351
- placement: toScenePlacement(board.placement),
335
+ width: draft.width,
336
+ height: draft.height,
337
+ depth: draft.depth,
352
338
  threed: true,
353
339
  environment: 'studio',
354
340
  /*
@@ -363,7 +349,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
363
349
  보드는 이 값을 안 주므로 안 보인다.
364
350
  */
365
351
  ...this.sceneView(),
366
- gridStep: gridStep(board),
352
+ gridStep: gridStep(draft),
367
353
  sky: 'color',
368
354
  /*
369
355
  판의 면 — 2D 는 밝은 종이, 3D 는 없음.
@@ -439,9 +425,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
439
425
  * 이름이다. 표를 두면 씬이 새 설정을 받아도 이쪽이 모른다.
440
426
  */
441
427
  private sceneView(): Record<string, unknown> {
442
- const { grid, gridLabels, floor, environment, skyColor, ...light } = this.view ?? DEFAULT_VIEW
443
-
444
- return { grid, gridLabels, floor, environment, skyColor, ...light }
428
+ return sceneLook(this.view ?? DEFAULT_VIEW)
445
429
  }
446
430
 
447
431
  /**
@@ -452,11 +436,11 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
452
436
  * 밝기 하나 끄는 동안 화면이 계속 처음으로 돌아간다.
453
437
  */
454
438
  private applyView(): void {
455
- const board = this.scene?.root
456
- if (!board) return
439
+ const draft = this.scene?.root
440
+ if (!draft) return
457
441
 
458
- board.set?.(this.sceneView())
459
- board.invalidate?.()
442
+ draft.set?.(this.sceneView())
443
+ draft.invalidate?.()
460
444
  }
461
445
 
462
446
  /**
@@ -509,9 +493,9 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
509
493
  * 목록이 바뀌면 엉뚱한 부품을 가리킨다.
510
494
  */
511
495
  private watchScene() {
512
- const board = this.scene?.root
513
- const root = board?.root
514
- if (!board || !root?.on || this.viewOnly) return
496
+ const draft = this.scene?.root
497
+ const root = draft?.root
498
+ if (!draft || !root?.on || this.viewOnly) return
515
499
 
516
500
  root.on('selected', (after: unknown) => {
517
501
  const chosen = (after as SceneComponent[] | undefined)?.[0]
@@ -528,7 +512,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
528
512
  뒤에 붙여 부모에게 넘긴다. 첫 인자를 컴포넌트로 읽으면 아무 일도 안 일어난다 —
529
513
  실제로 그랬다.
530
514
  */
531
- board.on?.('change', (...args: unknown[]) => {
515
+ draft.on?.('change', (...args: unknown[]) => {
532
516
  const hint = args[args.length - 1] as { origin?: SceneComponent } | undefined
533
517
  const changed = hint?.origin
534
518
  const name = changed?.state?.name
@@ -544,13 +528,13 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
544
528
 
545
529
  /** 목록에서 고른 것을 캔버스의 선택에 맞춘다. */
546
530
  private showPicked() {
547
- const board = this.scene?.root
548
- const root = board?.root
549
- if (!board || !root) return
531
+ const draft = this.scene?.root
532
+ const root = draft?.root
533
+ if (!draft || !root) return
550
534
 
551
535
  const component = this.picked ? this.partsOnBoard().get(this.picked) : undefined
552
536
  root.selected = component ? [component] : []
553
- board.invalidate?.()
537
+ draft.invalidate?.()
554
538
  }
555
539
 
556
540
  private tell(what: string, detail: object) {
@@ -5,7 +5,7 @@ import { live } from 'lit/directives/live.js'
5
5
  import { i18next, localize } from '@operato/i18n'
6
6
  import { ScrollbarStyles } from '@operato/styles'
7
7
 
8
- import type { BoardModel, PartModel } from './figure-source.js'
8
+ import type { FigureDraft, PartModel } from './figure-source.js'
9
9
 
10
10
  /**
11
11
  * 이 도형이 **무엇을 하는가.**
@@ -240,12 +240,12 @@ export class FigureCapabilities extends localize(i18next)(LitElement) {
240
240
  `
241
241
  ]
242
242
 
243
- @property({ type: Object }) board?: BoardModel
243
+ @property({ type: Object }) draft?: FigureDraft
244
244
  @property({ type: Array }) parts: PartModel[] = []
245
245
 
246
246
  render() {
247
- const rung = rungOf(this.board?.capabilities)
248
- const names = this.board?.capabilities ?? []
247
+ const rung = rungOf(this.draft?.capabilities)
248
+ const names = this.draft?.capabilities ?? []
249
249
 
250
250
  return html`
251
251
  <section>
@@ -349,10 +349,10 @@ export class FigureCapabilities extends localize(i18next)(LitElement) {
349
349
  * 되고, 그 도형은 멀쩡해 보이면서 아무것도 못 받는다.
350
350
  */
351
351
  private setRung(which: Rung) {
352
- const board = this.board
353
- if (!board) return
352
+ const draft = this.draft
353
+ if (!draft) return
354
354
 
355
- this.tellBoard({ ...board, capabilities: RUNGS[which].length > 0 ? [...RUNGS[which]] : undefined })
355
+ this.tellBoard({ ...draft, capabilities: RUNGS[which].length > 0 ? [...RUNGS[which]] : undefined })
356
356
 
357
357
  if (which === 'none') {
358
358
  this.parts.forEach((part, index) => {
@@ -365,8 +365,8 @@ export class FigureCapabilities extends localize(i18next)(LitElement) {
365
365
  }
366
366
  }
367
367
 
368
- private tellBoard(board: BoardModel) {
369
- this.board = board
370
- this.dispatchEvent(new CustomEvent('update-board', { detail: { board }, bubbles: true, composed: true }))
368
+ private tellBoard(draft: FigureDraft) {
369
+ this.draft = draft
370
+ this.dispatchEvent(new CustomEvent('update-draft', { detail: { draft }, bubbles: true, composed: true }))
371
371
  }
372
372
  }
@@ -21,7 +21,7 @@ import type { AnchorRule, Axis, FigurePart, FigurePlacement, MaterialPreset, Par
21
21
  import { activePalette } from '@hatiolab/things-scene'
22
22
 
23
23
  import { partToFigure, partFromFigure } from './figure-source.js'
24
- import type { BoardModel, PartModel } from './figure-source.js'
24
+ import type { FigureDraft, PartModel } from './figure-source.js'
25
25
  import { canMirror } from './part-edits.js'
26
26
 
27
27
  /**
@@ -29,7 +29,7 @@ import { canMirror } from './part-edits.js'
29
29
  *
30
30
  * ## 여기가 사용자 용어로 보여 주는 자리다
31
31
  *
32
- * 모델러 페이지가 갖고 있는 것은 **씬 모델**이다 — 보드 위에 부품 컴포넌트가 놓인 상태.
32
+ * 모델러 페이지가 갖고 있는 것은 **씬 모델**이다 — 기준 상자 위에 부품 컴포넌트가 놓인 상태.
33
33
  * 그런데 사용자에게 보여야 하는 것은 씬 속성 이름(`left`·`top`·`zPos`)이 아니라
34
34
  * **X·Y·Z** 다. 다른 3D 도구가 모두 그렇게 부르고, 위치도 부품의 **중심**으로 읽는다.
35
35
  *
@@ -787,8 +787,8 @@ export class FigureInspector extends localize(i18next)(LitElement) {
787
787
  /** 펴 둔 도움말. 부품을 바꿔도 유지된다 — 매번 다시 펴게 하지 않는다. */
788
788
  @state() private opened = new Set<string>()
789
789
 
790
- @property({ type: Object }) board?: BoardModel
791
- /** 보드 위의 부품들. 씬 모델 그대로다. */
790
+ @property({ type: Object }) draft?: FigureDraft
791
+ /** 초안 위의 부품들. 씬 모델 그대로다. */
792
792
  @property({ type: Array }) parts: PartModel[] = []
793
793
  @property({ type: Number }) selected = -1
794
794
 
@@ -799,7 +799,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
799
799
 
800
800
  render() {
801
801
  const part = this.chosen
802
- if (!part || !this.board) {
802
+ if (!part || !this.draft) {
803
803
  return html`
804
804
  ${this.renderFigure(false)}
805
805
  <section>
@@ -810,7 +810,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
810
810
  }
811
811
 
812
812
  // 여기부터는 사용자 용어다 — X·Y·Z, 위치는 부품 중심, 회전은 도(degree).
813
- const shown = partToFigure(part, this.board)
813
+ const shown = partToFigure(part, this.draft)
814
814
 
815
815
  /*
816
816
  역할을 먼저 묻는다.
@@ -837,7 +837,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
837
837
  * 컨베이어가 길어지면 실을 자리가 늘어나는 것이 그것이고, 여기 적은 값은 안 쓰인다.
838
838
  */
839
839
  private renderRole(part: FigurePart) {
840
- if (!this.board?.capabilities?.length) return nothing
840
+ if (!this.draft?.capabilities?.length) return nothing
841
841
 
842
842
  const roles = part.capability?.roles ?? []
843
843
  const repeats = part.sizing === 'repeat' && !!part.repeat
@@ -967,7 +967,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
967
967
  <label>${i18next.t('figure.label.figure-type')}${this.figureId ? this.helpButton('figure.text.type-name-cannot-change') : nothing}</label>
968
968
  ${!this.figureId
969
969
  ? html`<input
970
- .value=${this.board?.figureType ?? ''}
970
+ .value=${this.draft?.figureType ?? ''}
971
971
  placeholder=${i18next.t('figure.text.figure-type-placeholder', { defaultValue: '도형 식별 타입 (예: OHT)' })}
972
972
  @change=${(e: Event) =>
973
973
  this.dispatchEvent(
@@ -979,7 +979,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
979
979
  )}
980
980
  />`
981
981
  : html`<code fixed title=${i18next.t('figure.text.type-name-cannot-change')}>
982
- <md-icon>lock</md-icon>${this.board?.figureType ?? ''}
982
+ <md-icon>lock</md-icon>${this.draft?.figureType ?? ''}
983
983
  </code>
984
984
  ${this.helpHint('figure.text.type-name-cannot-change')}`}
985
985
  </div>
@@ -991,16 +991,16 @@ export class FigureInspector extends localize(i18next)(LitElement) {
991
991
  <div inline-field>
992
992
  <label>${i18next.t('figure.label.figure-placement')}${this.helpButton('figure.text.placement-explained')}</label>
993
993
  <select
994
- .value=${this.board?.placement ?? 'floor'}
994
+ .value=${this.draft?.placement ?? 'floor'}
995
995
  @change=${(e: Event) => this.setPlacement((e.target as HTMLSelectElement).value as FigurePlacement)}
996
996
  >
997
- <option value="floor" ?selected=${(this.board?.placement ?? 'floor') === 'floor'}>
997
+ <option value="floor" ?selected=${(this.draft?.placement ?? 'floor') === 'floor'}>
998
998
  ${i18next.t('figure.label.placement-floor')}
999
999
  </option>
1000
- <option value="ceiling" ?selected=${this.board?.placement === 'ceiling'}>
1000
+ <option value="ceiling" ?selected=${this.draft?.placement === 'ceiling'}>
1001
1001
  ${i18next.t('figure.label.placement-ceiling')}
1002
1002
  </option>
1003
- <option value="center" ?selected=${this.board?.placement === 'center'}>
1003
+ <option value="center" ?selected=${this.draft?.placement === 'center'}>
1004
1004
  ${i18next.t('figure.label.placement-center')}
1005
1005
  </option>
1006
1006
  </select>
@@ -1034,7 +1034,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
1034
1034
  <details asset>
1035
1035
  <summary>
1036
1036
  <b>${i18next.t('figure.label.figure')}</b>
1037
- <small>${this.figureName || this.board?.figureType || ''}</small>
1037
+ <small>${this.figureName || this.draft?.figureType || ''}</small>
1038
1038
  </summary>
1039
1039
  ${fields}
1040
1040
  </details>
@@ -1075,11 +1075,22 @@ export class FigureInspector extends localize(i18next)(LitElement) {
1075
1075
  this.dispatchEvent(new CustomEvent('figure-catalog', { detail: patch, bubbles: true, composed: true }))
1076
1076
  }
1077
1077
 
1078
- /** 3D 배치 기준면을 바꾼다 ('floor' | 'ceiling' | 'center'). */
1078
+ /**
1079
+ * 3D 배치 기준면을 바꾼다 ('floor' | 'ceiling' | 'center').
1080
+ *
1081
+ * **부품은 건드리지 않는다.** 이 값은 「이 종류는 어느 면에 붙는가」를 말할 뿐이고,
1082
+ * 저작한 모양은 그것과 무관하다. 형식이 부품을 상자 중심에서 재므로, 배치가 달라도
1083
+ * `parts` 는 한 글자도 안 바뀐다.
1084
+ *
1085
+ * 한동안 여기서 `zPos` 를 다시 계산했다. 편집 캔버스가 이 값을 씬의 좌표 모드로
1086
+ * 흘려보내던 때의 일인데, 그러면 배치를 고를 때마다 저작자가 적어 둔 높이가 전부
1087
+ * 다른 숫자로 보였다 — 저장되는 것은 그대로인데. 좌표 모드는 이제 보기 설정이고
1088
+ * (`figure-view.ts`), 편집 캔버스는 늘 상자 기준이다.
1089
+ */
1079
1090
  private setPlacement(placement: FigurePlacement) {
1080
- if (!this.board) return
1081
- const board = { ...this.board, placement }
1082
- this.dispatchEvent(new CustomEvent('update-board', { detail: { board }, bubbles: true, composed: true }))
1091
+ if (!this.draft) return
1092
+ const draft = { ...this.draft, placement }
1093
+ this.dispatchEvent(new CustomEvent('update-draft', { detail: { draft }, bubbles: true, composed: true }))
1083
1094
  }
1084
1095
 
1085
1096
  private renderIdentity(part: FigurePart) {
@@ -1180,7 +1191,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
1180
1191
  </button>
1181
1192
  ${(['x', 'z'] as const).map(axis => {
1182
1193
  // 이미 가운데면 대칭이 제자리다 — 겹친 부품이 조용히 생기지 않게 막는다
1183
- const able = this.board ? canMirror(this.board, this.parts, this.selected, axis) : false
1194
+ const able = this.draft ? canMirror(this.draft, this.parts, this.selected, axis) : false
1184
1195
  return html`
1185
1196
  <button
1186
1197
  ?disabled=${!able}
@@ -1603,10 +1614,10 @@ export class FigureInspector extends localize(i18next)(LitElement) {
1603
1614
  * 이 축의 anchor 가 정하는데 화면에는 그 말이 없었다. 그래서 어느 쪽인지 함께 적는다.
1604
1615
  */
1605
1616
  private renderRepeatCount(part: FigurePart) {
1606
- const board = this.board
1607
- if (!board) return nothing
1617
+ const draft = this.draft
1618
+ if (!draft) return nothing
1608
1619
 
1609
- const base = { x: board.width, y: board.depth, z: board.height }
1620
+ const base = { x: draft.width, y: draft.depth, z: draft.height }
1610
1621
  const plan = repeatPlanOfPart(part, { x: 1, y: 1, z: 1 }, base)
1611
1622
  if (!plan) return nothing
1612
1623
 
@@ -1693,8 +1704,8 @@ export class FigureInspector extends localize(i18next)(LitElement) {
1693
1704
  }
1694
1705
 
1695
1706
  private renderAnchors(part: FigurePart) {
1696
- const base = this.board
1697
- ? { x: this.board.width, y: this.board.height, z: this.board.depth }
1707
+ const base = this.draft
1708
+ ? { x: this.draft.width, y: this.draft.height, z: this.draft.depth }
1698
1709
  : { x: 0, y: 0, z: 0 }
1699
1710
  const nameOf = (rule: string, axis: Axis) =>
1700
1711
  `${i18next.t(rule === 'min' || rule === 'max' ? `figure.label.anchor-${rule}-${axis}` : `figure.label.anchor-${rule}`)} (${rule})`
@@ -1841,7 +1852,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
1841
1852
  */
1842
1853
  private putEmissive(intensity: number) {
1843
1854
  // `chosen` 은 씬 모델이다. 형식으로 옮긴 값은 `partToFigure` 이 만든 것이라 여기서 다시 꺼낸다.
1844
- const glow = this.board && this.chosen ? partToFigure(this.chosen, this.board).material?.emissive : undefined
1855
+ const glow = this.draft && this.chosen ? partToFigure(this.chosen, this.draft).material?.emissive : undefined
1845
1856
 
1846
1857
  this.putMaterial({ emissive: intensity > 0 ? { ...glow, intensity } : undefined })
1847
1858
  }
@@ -1862,10 +1873,10 @@ export class FigureInspector extends localize(i18next)(LitElement) {
1862
1873
  */
1863
1874
  private change(edit: (part: FigurePart) => FigurePart) {
1864
1875
  const part = this.chosen
1865
- const board = this.board
1866
- if (!part || !board) return
1876
+ const draft = this.draft
1877
+ if (!part || !draft) return
1867
1878
 
1868
- this.emit(partFromFigure(edit(partToFigure(part, board)), board))
1879
+ this.emit(partFromFigure(edit(partToFigure(part, draft)), draft))
1869
1880
  }
1870
1881
 
1871
1882
  /** 정밀 도구를 시킨다. 정본은 저작면이 갖는다 — 여기서는 알리기만 한다. */
@@ -303,7 +303,7 @@ export class FigureParts extends localize(i18next)(LitElement) {
303
303
  ]
304
304
 
305
305
  /**
306
- * 보드 위의 부품들. **씬 모델 그대로 받는다.**
306
+ * 초안 위의 부품들. **씬 모델 그대로 받는다.**
307
307
  *
308
308
  * 이 패널은 이름과 도형만 읽으므로 어느 쪽 형태로 받아도 화면은 같다. 그래도 씬
309
309
  * 모델로 받는 것은 모델러 페이지가 데이터를 **한 벌만** 갖게 하기 위해서다. 여기만