@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
@@ -1,6 +1,5 @@
1
1
  import '@material/web/icon/icon.js'
2
2
  import '../modeller/figure-side.js'
3
- import '../modeller/figure-ask.js'
4
3
  import '../modeller/figure-canvas.js'
5
4
  import { captureThumbnail } from '../modeller/figure-thumbnail.js'
6
5
  import '../modeller/figure-preview.js'
@@ -23,7 +22,7 @@ import * as proposals from '../modeller/proposal.js'
23
22
  import * as proposalSessions from '../modeller/proposal-session.js'
24
23
  import type { ProposalSession } from '../modeller/proposal-session.js'
25
24
  import { toFigureSource, fromFigureSource } from '../modeller/figure-source.js'
26
- import type { BoardModel, PartModel, PlaneAxis } from '../modeller/figure-source.js'
25
+ import type { FigureDraft, PartModel, PlaneAxis } from '../modeller/figure-source.js'
27
26
  import { DEFAULT_VIEW } from '../modeller/figure-view.js'
28
27
  import type { ViewSettings } from '../modeller/figure-view.js'
29
28
  import { createFigure, fetchFigure, fetchFigureTypeNames, updateFigure } from '../graphql/index.js'
@@ -86,9 +85,9 @@ function rememberPanes(panes: Panes): void {
86
85
  * 빈 화면이 가르치는 자리다: 부품 목록이 「위에서 도형을 골라 시작하세요」를 이미 말하고 팔레트가
87
86
  * 바로 그 위에 있다(`figure-parts`).
88
87
  */
89
- function startingModel(type: string): { board: BoardModel; parts: PartModel[] } {
88
+ function startingModel(type: string): { draft: FigureDraft; parts: PartModel[] } {
90
89
  return {
91
- board: { width: 300, height: 300, depth: 300, figureType: type, detailLevel: 'M' },
90
+ draft: { width: 300, height: 300, depth: 300, figureType: type, detailLevel: 'M' },
92
91
  parts: []
93
92
  }
94
93
  }
@@ -107,7 +106,7 @@ function startingModel(type: string): { board: BoardModel; parts: PartModel[] }
107
106
  *
108
107
  * ## 편집 데이터는 씬 모델 하나뿐이다
109
108
  *
110
- * 이 페이지가 갖는 것은 보드 하나와 부품 목록 — 미리보기에 그대로 세워지는 씬 모델이다.
109
+ * 이 페이지가 갖는 것은 초안 하나와 부품 목록 — 미리보기에 그대로 세워지는 씬 모델이다.
111
110
  * 저장 형식(`FigureSource`)으로 바꾸는 것은 **경계에서만** 한다: 저장할 때, 판단할 때,
112
111
  * AI 후보와 비교할 때. 그 변환은 `figure-source.ts` 한 곳에서만 일어난다.
113
112
  *
@@ -623,7 +622,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
623
622
  /** 편집 중인 Figure. 새로 만드는 중이면 `id` 가 없다. */
624
623
  @state() private figure?: Figure
625
624
  /** 편집 데이터 — 미리보기에 그대로 세워지는 씬 모델이다. */
626
- @state() private board?: BoardModel
625
+ @state() private draft?: FigureDraft
627
626
  @state() private parts: PartModel[] = []
628
627
  @state() private selected = -1
629
628
  /*
@@ -668,8 +667,8 @@ export class FigureModellerPage extends FigureModellerPageBase {
668
667
  /** 인라인 요청창과 AI 도크가 함께 쓰는 현재 Figure의 제한된 세션 피드백. */
669
668
  @state() private proposalFeedback: ProposalFeedback[] = []
670
669
 
671
- @state() private undoStack: { board?: BoardModel; parts: PartModel[]; selected: number }[] = []
672
- @state() private redoStack: { board?: BoardModel; parts: PartModel[]; selected: number }[] = []
670
+ @state() private undoStack: { draft?: FigureDraft; parts: PartModel[]; selected: number }[] = []
671
+ @state() private redoStack: { draft?: FigureDraft; parts: PartModel[]; selected: number }[] = []
673
672
  @state() private sceneRevision = 0
674
673
  private lastCoalesceKey?: string
675
674
  private lastHistoryTime = 0
@@ -683,15 +682,15 @@ export class FigureModellerPage extends FigureModellerPageBase {
683
682
  private folded?: FigureSource
684
683
 
685
684
  willUpdate(changed: Map<string, unknown>) {
686
- if (changed.has('board') || changed.has('parts')) {
687
- this.folded = this.board ? toFigureSource(this.board, this.parts) : undefined
685
+ if (changed.has('draft') || changed.has('parts')) {
686
+ this.folded = this.draft ? toFigureSource(this.draft, this.parts) : undefined
688
687
  }
689
688
  }
690
689
 
691
690
  /**
692
691
  * 페이지 밖 AI 도크에도 **지금 손에 든 정본**을 알려 준다.
693
692
  *
694
- * 모델러 안의 `figure-ask` 는 이미 `folded`를 직접 받는다. 반면 asidebar 도크는 주소의 id만
693
+ * asidebar 도크는 주소의 id만
695
694
  * 알았으므로 DB에 저장된 옛 판을 읽었다. 저장 전 변경을 두고 "왜 발행이 안 되나" 또는
696
695
  * "이 부품을 줄여 달라"고 물으면 대화가 엉뚱한 판을 보게 되는 결함이었다.
697
696
  *
@@ -703,7 +702,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
703
702
  if (changed.has('figure') && (changed.get('figure') as Figure | undefined)?.id !== this.figure?.id) {
704
703
  this.proposalFeedback = []
705
704
  }
706
- if (changed.has('board') || changed.has('parts') || changed.has('figure')) {
705
+ if (changed.has('draft') || changed.has('parts') || changed.has('figure')) {
707
706
  window.dispatchEvent(
708
707
  new CustomEvent('figure-ai-context', {
709
708
  detail: { figureId: this.figure?.id, source: this.folded }
@@ -753,15 +752,15 @@ export class FigureModellerPage extends FigureModellerPageBase {
753
752
  /**
754
753
  * A candidate from the dock opens the review lane. It does not become the draft.
755
754
  *
756
- * This used to write `board` and `parts` straight onto the working draft and then set
755
+ * This used to write `draft` and `parts` straight onto the working draft and then set
757
756
  * `proposalSession = undefined` — the dock's candidate overwrote what the author had open and
758
757
  * erased the lane built to review it. There was no moment where the person said yes, and a
759
758
  * draft has no version history to go back to, so the previous work was simply gone.
760
759
  *
761
760
  * `ai-client-base`'s analysis contract states this as a literal type: `approvalRequired: true`,
762
761
  * and of `proposal`, "It is never an instruction to mutate canonical state." The in-page
763
- * `figure-ask` entrance already honoured it through `openAiProposal`; the dock entrance is now
764
- * the same door. Applying is what the take button does, from the lane, per change.
762
+ * The in-page entrance honoured this through `openAiProposal` before it was removed, and the dock
763
+ * entrance is now the same door. Applying is what the take button does, from the lane, per change.
765
764
  *
766
765
  * ## Why the overwrite was there — so it does not come back
767
766
  *
@@ -899,13 +898,13 @@ export class FigureModellerPage extends FigureModellerPageBase {
899
898
  @toggle-part-hidden=${(e: CustomEvent) => this.toggleHidden(e.detail.index)}
900
899
  @set-all-parts-hidden=${(e: CustomEvent) => this.setAllHidden(e.detail.hidden)}
901
900
  @change-detail-level=${(e: CustomEvent) => this.changeDetailLevel(e.detail.level)}
902
- @update-board=${(e: CustomEvent) => this.updateBoard(e.detail.board)}
901
+ @update-draft=${(e: CustomEvent) => this.updateDraft(e.detail.draft)}
903
902
  @part-tool=${(e: CustomEvent) => this.runTool(e.detail.index, e.detail.tool, e.detail.axis)}
904
903
  @pick-part=${(e: CustomEvent) => this.pickPart(e.detail.name)}
905
904
  >
906
905
  <figure-parts
907
906
  .parts=${this.parts}
908
- .level=${(this.board?.detailLevel ?? 'M') as DetailLevel}
907
+ .level=${(this.draft?.detailLevel ?? 'M') as DetailLevel}
909
908
  .selected=${this.selected}
910
909
  ></figure-parts>
911
910
 
@@ -919,7 +918,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
919
918
  <h4>${i18next.t('figure.label.now')}</h4>
920
919
  <figure-canvas
921
920
  .figureId=${this.figure?.id || this.figure?.type}
922
- .board=${this.board}
921
+ .draft=${this.draft}
923
922
  .parts=${this.parts}
924
923
  .picked=${this.pickedName}
925
924
  .view=${this.view}
@@ -942,10 +941,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
942
941
  </div>
943
942
  `
944
943
  : this.mode === 'preview'
945
- ? html`<figure-preview .source=${source}></figure-preview>`
944
+ ? html`<figure-preview .source=${source} .view=${this.view}></figure-preview>`
946
945
  : html`<figure-canvas
947
946
  .figureId=${this.figure?.id || this.figure?.type}
948
- .board=${this.board}
947
+ .draft=${this.draft}
949
948
  .parts=${this.parts}
950
949
  .picked=${this.pickedName}
951
950
  .view=${this.view}
@@ -961,7 +960,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
961
960
  .figureState=${this.figure?.state}
962
961
  .figureVersion=${this.figure?.version ?? 0}
963
962
  .dirty=${this.dirty}
964
- .board=${this.board}
963
+ .draft=${this.draft}
965
964
  .parts=${this.parts}
966
965
  .source=${source}
967
966
  .selected=${this.selected}
@@ -1085,7 +1084,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1085
1084
  private renderProposed() {
1086
1085
  if (!this.proposalSession) return ''
1087
1086
 
1088
- return html`<figure-preview .source=${this.proposalSession.source}></figure-preview>`
1087
+ return html`<figure-preview .source=${this.proposalSession.source} .view=${this.view}></figure-preview>`
1089
1088
  }
1090
1089
 
1091
1090
  /**
@@ -1171,15 +1170,23 @@ export class FigureModellerPage extends FigureModellerPageBase {
1171
1170
 
1172
1171
  /** 변경 하나. 무엇을 하겠다는 것인지 부호로 먼저 말한다. */
1173
1172
  private renderChange(change: proposals.ProposalChange) {
1174
- const icon = {
1173
+ /*
1174
+ Every ChangeKind needs a row here. A kind the map does not answer draws an empty icon and a
1175
+ key with no name beside it, which reads as a broken row rather than a missing case — the
1176
+ `satisfies` makes the compiler say so instead.
1177
+ */
1178
+ const icon = ({
1175
1179
  added: 'add',
1176
1180
  changed: 'edit',
1177
1181
  removed: 'remove',
1178
1182
  base: 'aspect_ratio',
1183
+ placement: 'vertical_align_bottom',
1179
1184
  detail: 'tune',
1180
- anchor: 'my_location',
1181
- styleKit: 'palette'
1182
- }[change.kind]
1185
+ styleKit: 'palette',
1186
+ animations: 'animation',
1187
+ parameters: 'tune',
1188
+ capabilities: 'hub'
1189
+ } satisfies Record<proposals.ChangeKind, string>)[change.kind]
1183
1190
 
1184
1191
  const on = this.proposalSession?.picked.has(change.key)
1185
1192
 
@@ -1214,7 +1221,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1214
1221
  /**
1215
1222
  * 발행 게이트의 구조화된 수정안 하나를 현재 작업 사본에만 겹쳐 본다. 서버가 준 값은
1216
1223
  * `part.anchor.axis = to`라는 데이터이지 사람이 읽을 문장을 다시 해석한 결과가 아니다.
1217
- * 원본 `board`/`parts`는 여기서 전혀 바꾸지 않고, 아래의 `take()`만 바꾼 사본을 적용한다.
1224
+ * 원본 `draft`/`parts`는 여기서 전혀 바꾸지 않고, 아래의 `take()`만 바꾼 사본을 적용한다.
1218
1225
  */
1219
1226
  private previewSizingFix(fix: FigureGateFix) {
1220
1227
  if (!this.folded) return
@@ -1245,9 +1252,9 @@ export class FigureModellerPage extends FigureModellerPageBase {
1245
1252
 
1246
1253
  this.recordHistory()
1247
1254
  const taken = proposals.applyProposal(this.folded, this.proposalSession.source, this.proposalSession.picked)
1248
- const { board, parts } = fromFigureSource(taken)
1255
+ const { draft, parts } = fromFigureSource(taken)
1249
1256
 
1250
- this.board = board
1257
+ this.draft = draft
1251
1258
  this.parts = parts
1252
1259
  this.selected = parts.length ? 0 : -1
1253
1260
  this.dirty = true
@@ -1398,7 +1405,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1398
1405
  // 서버가 준 것을 그대로 쓴다. 읽지 못하면 오류보고없이 빈 상태로 바꾸지 않는다.
1399
1406
  // 그러면 사용자가 저장을 선택해 원본을 덮어쓴다.
1400
1407
  const model = figure.source ? fromFigureSource(JSON.parse(figure.source) as FigureSource) : undefined
1401
- this.board = model?.board
1408
+ this.draft = model?.draft
1402
1409
  this.parts = model?.parts ?? []
1403
1410
 
1404
1411
  /*
@@ -1428,10 +1435,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
1428
1435
  this.mode = 'edit'
1429
1436
  // 타입 이름을 사람이 정하기 전까지는 임시 이름을 쓴다. 저장할 때 확정한다.
1430
1437
  const draftType = `FIGURE_${Date.now().toString(36).toUpperCase()}`
1431
- const { board, parts } = startingModel(draftType)
1438
+ const { draft, parts } = startingModel(draftType)
1432
1439
 
1433
1440
  this.figure = { id: '', type: draftType, name: '' } as Figure
1434
- this.board = board
1441
+ this.draft = draft
1435
1442
  this.parts = parts
1436
1443
  /* 고른 것이 없다 — 부품이 없으니 속성 패널도 고를 것을 기다린다. */
1437
1444
  this.selected = -1
@@ -1452,9 +1459,9 @@ export class FigureModellerPage extends FigureModellerPageBase {
1452
1459
  private typeChanged(type: string) {
1453
1460
  if (!type || this.figure?.id) return
1454
1461
  const sanitized = type.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()
1455
- if (this.board) this.board.figureType = sanitized
1462
+ if (this.draft) this.draft.figureType = sanitized
1456
1463
  this.figure = { ...this.figure, type: sanitized } as Figure
1457
- this.folded = this.board ? toFigureSource(this.board, this.parts) : undefined
1464
+ this.folded = this.draft ? toFigureSource(this.draft, this.parts) : undefined
1458
1465
  this.dirty = true
1459
1466
  }
1460
1467
 
@@ -1471,9 +1478,9 @@ export class FigureModellerPage extends FigureModellerPageBase {
1471
1478
  this.dirty = true
1472
1479
  }
1473
1480
 
1474
- private snapshotState(): { board?: BoardModel; parts: PartModel[]; selected: number } {
1481
+ private snapshotState(): { draft?: FigureDraft; parts: PartModel[]; selected: number } {
1475
1482
  return {
1476
- board: this.board ? structuredClone(this.board) : undefined,
1483
+ draft: this.draft ? structuredClone(this.draft) : undefined,
1477
1484
  parts: structuredClone(this.parts),
1478
1485
  selected: this.selected
1479
1486
  }
@@ -1499,7 +1506,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1499
1506
  this.undoStack = this.undoStack.slice(0, -1)
1500
1507
  this.redoStack = [this.snapshotState(), ...this.redoStack.slice(0, 49)]
1501
1508
 
1502
- this.board = prev.board ? structuredClone(prev.board) : undefined
1509
+ this.draft = prev.draft ? structuredClone(prev.draft) : undefined
1503
1510
  this.parts = structuredClone(prev.parts)
1504
1511
  this.selected = this.parts.length ? Math.max(-1, Math.min(prev.selected, this.parts.length - 1)) : -1
1505
1512
  this.dirty = true
@@ -1512,7 +1519,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1512
1519
  this.redoStack = this.redoStack.slice(1)
1513
1520
  this.undoStack = [...this.undoStack.slice(-49), this.snapshotState()]
1514
1521
 
1515
- this.board = next.board ? structuredClone(next.board) : undefined
1522
+ this.draft = next.draft ? structuredClone(next.draft) : undefined
1516
1523
  this.parts = structuredClone(next.parts)
1517
1524
  this.selected = this.parts.length ? Math.max(-1, Math.min(next.selected, this.parts.length - 1)) : -1
1518
1525
  this.dirty = true
@@ -1533,9 +1540,9 @@ export class FigureModellerPage extends FigureModellerPageBase {
1533
1540
  }
1534
1541
 
1535
1542
  private addPart(primitive: PrimitiveKind) {
1536
- if (!this.board) return
1543
+ if (!this.draft) return
1537
1544
  // 추가한 것을 바로 수정할 수 있게 선택 상태로 둔다.
1538
- this.put(edits.addPart(this.board, this.parts, primitive), this.parts.length)
1545
+ this.put(edits.addPart(this.draft, this.parts, primitive), this.parts.length)
1539
1546
  }
1540
1547
 
1541
1548
  private removePart(index: number) {
@@ -1560,9 +1567,9 @@ export class FigureModellerPage extends FigureModellerPageBase {
1560
1567
  * 부품 목록은 건드리지 않는다. `put` 은 부품을 함께 받으므로 여기서 쓰면 능력 하나를
1561
1568
  * 고칠 때마다 부품 목록이 새 배열이 되고, 그리는 쪽이 전부 다시 그린다.
1562
1569
  */
1563
- private updateBoard(board: BoardModel) {
1564
- this.recordHistory('board')
1565
- this.board = board
1570
+ private updateDraft(draft: FigureDraft) {
1571
+ this.recordHistory('draft')
1572
+ this.draft = draft
1566
1573
  this.dirty = true
1567
1574
  }
1568
1575
 
@@ -1570,17 +1577,17 @@ export class FigureModellerPage extends FigureModellerPageBase {
1570
1577
  * 디테일 등급을 바꾼다.
1571
1578
  *
1572
1579
  * 이 값은 부품 수 한도를 정하고 성숙도의 한 축이 된다. 부품보다 위에 있는 값이라
1573
- * 보드에 담는다.
1580
+ * 초안에 담는다.
1574
1581
  *
1575
1582
  * 한도를 내려서 이미 넣은 부품 수보다 작아질 수 있다. 그때 부품을 지우지 않는다.
1576
1583
  * 검증이 위반으로 알리고, 성숙도가 등급에 안 맞는다고 말한다. 저작자가 무엇을 뺄지
1577
1584
  * 정할 일이지 도구가 정할 일이 아니다.
1578
1585
  */
1579
1586
  private changeDetailLevel(level: DetailLevel) {
1580
- if (!this.board) return
1587
+ if (!this.draft) return
1581
1588
 
1582
1589
  this.recordHistory('detailLevel')
1583
- this.board = { ...this.board, detailLevel: level }
1590
+ this.draft = { ...this.draft, detailLevel: level }
1584
1591
  this.dirty = true
1585
1592
  }
1586
1593
 
@@ -1627,14 +1634,14 @@ export class FigureModellerPage extends FigureModellerPageBase {
1627
1634
  * 규칙은 `part-edits` 가 갖는다. 여기서는 어느 것을 부를지만 정한다.
1628
1635
  */
1629
1636
  private runTool(index: number, tool: string, axis: PlaneAxis) {
1630
- const board = this.board
1631
- if (!board) return
1637
+ const draft = this.draft
1638
+ if (!draft) return
1632
1639
 
1633
- if (tool === 'center') this.put(edits.centerPart(board, this.parts, index, axis))
1640
+ if (tool === 'center') this.put(edits.centerPart(draft, this.parts, index, axis))
1634
1641
  else if (tool === 'floor') this.put(edits.alignPartToStart(this.parts, index, axis))
1635
- else if (tool === 'snap') this.put(edits.snapPart(board, this.parts, index))
1642
+ else if (tool === 'snap') this.put(edits.snapPart(draft, this.parts, index))
1636
1643
  // 대칭 복제는 부품이 하나 늘어난다 — 새로 생긴 것을 선택 상태로 둔다
1637
- else if (tool === 'mirror') this.put(edits.mirrorPart(board, this.parts, index, axis), index + 1)
1644
+ else if (tool === 'mirror') this.put(edits.mirrorPart(draft, this.parts, index, axis), index + 1)
1638
1645
  }
1639
1646
 
1640
1647
  /**
@@ -1689,7 +1696,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1689
1696
  private async fillThumbnail(id: string): Promise<void> {
1690
1697
  await this.updateComplete
1691
1698
 
1692
- const thumbnail = await captureThumbnail(this.board, this.parts)
1699
+ const thumbnail = await captureThumbnail(this.draft, this.parts)
1693
1700
  if (!thumbnail || this.figure?.id !== id) {
1694
1701
  return
1695
1702
  }
@@ -1711,7 +1718,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1711
1718
 
1712
1719
  if (!this.figure.id) {
1713
1720
  // 신규 저장: 타입 코드와 표시 이름을 명확히 팝업으로 확인/입력받음
1714
- let defaultType = this.board?.figureType || this.figure.type || ''
1721
+ let defaultType = this.draft?.figureType || this.figure.type || ''
1715
1722
  if (!defaultType || defaultType.startsWith('FIGURE_')) {
1716
1723
  defaultType = 'FIGURE'
1717
1724
  }
@@ -1734,7 +1741,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1734
1741
 
1735
1742
  try {
1736
1743
  if (!this.figure.id) {
1737
- const typeToSave = (this.saveModalType || this.board?.figureType || this.figure.type || 'FIGURE').trim().toUpperCase()
1744
+ const typeToSave = (this.saveModalType || this.draft?.figureType || this.figure.type || 'FIGURE').trim().toUpperCase()
1738
1745
  const nameToSave = (this.saveModalName || this.figure.name || typeToSave).trim()
1739
1746
 
1740
1747
  if (!typeToSave) {
@@ -1746,11 +1753,11 @@ export class FigureModellerPage extends FigureModellerPageBase {
1746
1753
  throw new Error(i18next.t('figure.text.type-name-already-taken', { type: typeToSave }))
1747
1754
  }
1748
1755
 
1749
- if (this.board) this.board.figureType = typeToSave
1756
+ if (this.draft) this.draft.figureType = typeToSave
1750
1757
  this.figure = { ...this.figure, type: typeToSave, name: nameToSave }
1751
1758
  this.folded = { ...this.folded, type: typeToSave }
1752
1759
  const sourceJson = JSON.stringify(this.folded)
1753
- const thumbnail = await captureThumbnail(this.board, this.parts)
1760
+ const thumbnail = await captureThumbnail(this.draft, this.parts)
1754
1761
 
1755
1762
  const result = await createFigure({
1756
1763
  type: typeToSave,
@@ -1772,10 +1779,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
1772
1779
  // 기존 모델 업데이트: folded.type이 figure.type과 일치하도록 보장
1773
1780
  if (this.figure.type && this.folded.type !== this.figure.type) {
1774
1781
  this.folded = { ...this.folded, type: this.figure.type }
1775
- if (this.board) this.board.figureType = this.figure.type
1782
+ if (this.draft) this.draft.figureType = this.figure.type
1776
1783
  }
1777
1784
  const sourceJson = JSON.stringify(this.folded)
1778
- const thumbnail = await captureThumbnail(this.board, this.parts)
1785
+ const thumbnail = await captureThumbnail(this.draft, this.parts)
1779
1786
 
1780
1787
  const result = await updateFigure(this.figure.id, {
1781
1788
  name: this.figure.name,
@@ -1,14 +1,22 @@
1
1
  import { LitElement, type TemplateResult } from 'lit';
2
- import type { BoardModel, PartModel } from './figure-source.js';
2
+ import type { FigureDraft, PartModel } from './figure-source.js';
3
3
  declare const FigureAnimations_base: (new (...args: any[]) => LitElement) & typeof LitElement;
4
4
  export declare class FigureAnimations extends FigureAnimations_base {
5
5
  static styles: import("lit").CSSResult[];
6
- board?: BoardModel;
6
+ draft?: FigureDraft;
7
7
  parts: PartModel[];
8
- /** 펼쳐 둔 clip. 이름이 아니라 자리로 잡는다 — 이름은 편집 중에 바뀐다. */
8
+ /**
9
+ * 펼쳐 둔 것. 이름이 아니라 **목록과 자리**로 잡는다 — 이름은 편집 중에 바뀐다.
10
+ *
11
+ * 목록이 둘이므로 자리 하나로는 모자란다. `clip:0` 과 `parameter:0` 은 다른 것이다.
12
+ */
9
13
  private open;
10
14
  private recipeTarget;
11
15
  private get clips();
16
+ private get parameters();
17
+ /** 두 목록의 이름을 한자리에서 본다 — 인스턴스가 이름 하나로 값을 준다. */
18
+ private get allNames();
19
+ private channelsOf;
12
20
  /** 부품 이름 — 채널이 이것을 가리킨다. */
13
21
  private get partNames();
14
22
  /**
@@ -26,8 +34,17 @@ export declare class FigureAnimations extends FigureAnimations_base {
26
34
  * 그래서 무엇을 만들 수 있는지 예를 들고, 그것이 어떻게 움직이는지(사실이 들어온다)를 적는다.
27
35
  */
28
36
  private renderRecipes;
29
- private renderClips;
37
+ /** 이름이 다른 것과 겹치나. 두 목록을 함께 본다 — 인스턴스는 이름 하나로 값을 준다. */
38
+ private taken;
30
39
  private renderClip;
40
+ /**
41
+ * 파라미터 하나 — **이름·범위·자세.**
42
+ *
43
+ * 범위가 화면에 있어야 하는 이유는 그것이 **인스턴스와 주고받는 말**이기 때문이다.
44
+ * 곡선은 0~1 이고 인스턴스는 mm 로 준다. 그 사이를 런타임이 맵하는데, 무엇부터 무엇
45
+ * 까지인지 저작자가 정하지 않으면 값을 줄 수가 없다.
46
+ */
47
+ private renderParameter;
31
48
  private renderChannels;
32
49
  /**
33
50
  * 채널 하나 — 어느 부품의 어느 속성을 움직이나.
@@ -62,20 +79,32 @@ export declare class FigureAnimations extends FigureAnimations_base {
62
79
  */
63
80
  private renderBudget;
64
81
  /**
65
- * 보드에 실어 올려보낸다.
82
+ * 초안에 실어 올려보낸다.
66
83
  *
67
- * `animations` 는 도형 전체의 것이므로 부품 편집(`update-part`)이 아니라 `update-board` 로
84
+ * `animations` 는 도형 전체의 것이므로 부품 편집(`update-part`)이 아니라 `update-draft` 로
68
85
  * 간다. 빈 배열은 칸을 지운다 — clip 을 다 지운 도형은 `animations: []` 가 아니라 없는 것이다.
69
86
  */
70
87
  private tell;
71
88
  private patchClip;
89
+ private patchParameter;
90
+ /** 채널 묶음을 통째로 얹는다. **어느 목록이든 같은 자리에 닿는다.** */
91
+ private setChannels;
72
92
  private patchChannel;
73
93
  private addClip;
74
- /** Recipe creates a valid, editable two-pose clip; it never saves or publishes the Figure. */
94
+ private addParameter;
95
+ /**
96
+ * Recipe creates a valid, editable two-pose motion; it never saves or publishes the Figure.
97
+ *
98
+ * 어느 목록으로 가는지는 레시피가 안다 — 도는 롤러는 애니메이션이고 열리는 문은
99
+ * 파라미터다. 저작자에게 그 갈래를 먼저 고르게 하지 않는 이유가 이것이다.
100
+ */
75
101
  private addRecipe;
76
102
  private renameClip;
77
- private setDrive;
103
+ private renameParameter;
104
+ private setRange;
105
+ private setDefault;
78
106
  private removeClip;
107
+ private removeParameter;
79
108
  private addChannel;
80
109
  private setChannel;
81
110
  /**