@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
@@ -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 }
@@ -719,23 +718,26 @@ export class FigureModellerPage extends FigureModellerPageBase {
719
718
  }))
720
719
  }
721
720
 
722
- private acceptDockProposal = (event: Event) => {
721
+ /*
722
+ `detail.staged` is the dock's answer to "did anything actually happen to my candidate". The
723
+ listener is synchronous, so the dock reads it right after dispatching and only then tells the
724
+ chat card to say so. It used to be called `accepted`, which claimed more than it knew — the
725
+ page had put the candidate somewhere, not the person had agreed to it.
726
+ */
727
+ private receiveDockProposal = (event: Event) => {
723
728
  const detail = (event as CustomEvent).detail
724
729
  if (!detail) return
725
- if (detail?.baseSource && detail.baseSource !== JSON.stringify(this.folded)) {
726
- try {
727
- const parsedBase = typeof detail.baseSource === 'string' ? JSON.parse(detail.baseSource) : detail.baseSource
728
- if (parsedBase?.type && this.folded?.type && parsedBase.type !== this.folded.type) {
729
- // type 불일치 시에만 차단하고 그 외 미세 차이는 수용
730
- }
731
- } catch {
732
- // ignore
733
- }
734
- }
735
- const source = detail?.source || (detail?.proposal?.source ? (typeof detail.proposal.source === 'string' ? JSON.parse(detail.proposal.source) : detail.proposal.source) : undefined)
730
+
731
+ const source =
732
+ detail?.source ||
733
+ (detail?.proposal?.source
734
+ ? typeof detail.proposal.source === 'string'
735
+ ? JSON.parse(detail.proposal.source)
736
+ : detail.proposal.source
737
+ : undefined)
738
+
736
739
  if (source) {
737
- this.applySourceDirectly(source, detail.proposal)
738
- detail.accepted = true
740
+ detail.staged = this.openProposalForReview(source, detail.proposal)
739
741
  }
740
742
  }
741
743
 
@@ -743,36 +745,71 @@ export class FigureModellerPage extends FigureModellerPageBase {
743
745
  const detail = (event as CustomEvent).detail
744
746
  const source = detail?.source || (detail?.proposal?.source ? (typeof detail.proposal.source === 'string' ? JSON.parse(detail.proposal.source) : detail.proposal.source) : undefined)
745
747
  if (source) {
746
- this.applySourceDirectly(source, detail.proposal)
748
+ this.openProposalForReview(source, detail.proposal)
747
749
  }
748
750
  }
749
751
 
750
- private applySourceDirectly(rawSource: any, proposal?: any) {
752
+ /**
753
+ * A candidate from the dock opens the review lane. It does not become the draft.
754
+ *
755
+ * This used to write `draft` and `parts` straight onto the working draft and then set
756
+ * `proposalSession = undefined` — the dock's candidate overwrote what the author had open and
757
+ * erased the lane built to review it. There was no moment where the person said yes, and a
758
+ * draft has no version history to go back to, so the previous work was simply gone.
759
+ *
760
+ * `ai-client-base`'s analysis contract states this as a literal type: `approvalRequired: true`,
761
+ * and of `proposal`, "It is never an instruction to mutate canonical state." The in-page
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.
764
+ *
765
+ * ## Why the overwrite was there — so it does not come back
766
+ *
767
+ * The dock opened the review lane originally. Before `c42a0b63df` this handler read:
768
+ *
769
+ * if (!this.active || (detail?.figureId ?? '') !== (this.figure?.id ?? '')) return
770
+ * if (detail?.draftType && detail.draftType !== this.folded?.type) return
771
+ * if (detail?.baseSource && detail.baseSource !== JSON.stringify(this.folded)) return
772
+ * this.receive(detail.proposal)
773
+ *
774
+ * Three guards, each a silent `return`, and the last compared a serialized source byte for byte.
775
+ * Any difference that changed nothing at all dropped the candidate with no message, so the dock
776
+ * would report a candidate and the screen would show none.
777
+ *
778
+ * The `ox-assistant-chat` migration fixed that by replacing the path rather than the guards, and
779
+ * the review lane went out with them — `applySourceDirectly` wrote straight to the draft, and
780
+ * cleared `proposalSession` on the way past. **Nobody decided that reviews were unwanted here.**
781
+ * It was the cost of loosening a guard by deleting what stood around it.
782
+ *
783
+ * So the guards are gone and stay gone, and the lane is back. If a candidate ever seems not to
784
+ * arrive, the thing to look for is a silent `return` on this path — not the lane.
785
+ *
786
+ * Returns whether the lane opened, so the dock can tell the chat whether to say it staged.
787
+ */
788
+ private openProposalForReview(rawSource: any, proposal?: any): boolean {
751
789
  const source = typeof rawSource === 'string' ? JSON.parse(rawSource) : rawSource
752
- if (!source || !Array.isArray(source.parts)) return
790
+ if (!source || !Array.isArray(source.parts)) return false
753
791
 
754
- this.recordHistory()
755
792
  try {
756
- const { board, parts } = fromFigureSource(source)
757
- this.board = board
758
- this.parts = parts
759
- this.dirty = true
760
- this.selected = parts.length ? 0 : -1
761
-
762
- if (source.type) {
763
- this.figure = { ...(this.figure || {}), type: source.type } as Figure
764
- if (this.board) {
765
- this.board = { ...this.board, figureType: source.type }
766
- }
767
- }
768
-
769
- this.proposalSession = undefined
770
- this.sceneRevision++
771
-
793
+ /*
794
+ The dock sends a FigureProposal whose metrics sit at the top level; the staged-on-navigate
795
+ path carries the raw tool result, which nests them under score/cost. Read both rather than
796
+ making one of the two entrances reshape its payload.
797
+ */
798
+ this.proposalSession = proposalSessions.openAiProposal(this.folded, {
799
+ source: JSON.stringify(source),
800
+ grade: proposal?.grade ?? proposal?.score?.grade,
801
+ triangles: proposal?.triangles ?? proposal?.cost?.triangles,
802
+ groups: proposal?.groups ?? proposal?.cost?.groups,
803
+ attempts: proposal?.attempts,
804
+ quality: proposal?.quality
805
+ } as FigureProposal)
806
+
807
+ this.mode = 'edit'
772
808
  this.requestUpdate()
773
- document.dispatchEvent(new CustomEvent('notify', { detail: { message: `${source.type || 'Figure'} 모델이 3D 뷰어에 반영되었습니다.` } }))
809
+ return true
774
810
  } catch (e) {
775
- console.error('[FigureModeller] Failed to apply AI proposal source:', e)
811
+ console.error('[FigureModeller] Failed to open the AI candidate for review:', e)
812
+ return false
776
813
  }
777
814
  }
778
815
 
@@ -781,7 +818,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
781
818
  connectedCallback() {
782
819
  super.connectedCallback()
783
820
  window.addEventListener('figure-ai-context-request', this.announceAiContext)
784
- window.addEventListener('figure-ai-review', this.acceptDockProposal)
821
+ window.addEventListener('figure-ai-review', this.receiveDockProposal)
785
822
  window.addEventListener('figure-ai-stage', this.onFigureAiStage)
786
823
  window.addEventListener('figure-save-request', this.onSaveRequest)
787
824
  window.addEventListener('keydown', this.handleKeyDown)
@@ -792,7 +829,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
792
829
  window.removeEventListener('figure-save-request', this.onSaveRequest)
793
830
  window.removeEventListener('figure-ai-stage', this.onFigureAiStage)
794
831
  window.removeEventListener('figure-ai-context-request', this.announceAiContext)
795
- window.removeEventListener('figure-ai-review', this.acceptDockProposal)
832
+ window.removeEventListener('figure-ai-review', this.receiveDockProposal)
796
833
  window.dispatchEvent(new CustomEvent('figure-ai-context', { detail: undefined }))
797
834
  super.disconnectedCallback()
798
835
  }
@@ -861,13 +898,13 @@ export class FigureModellerPage extends FigureModellerPageBase {
861
898
  @toggle-part-hidden=${(e: CustomEvent) => this.toggleHidden(e.detail.index)}
862
899
  @set-all-parts-hidden=${(e: CustomEvent) => this.setAllHidden(e.detail.hidden)}
863
900
  @change-detail-level=${(e: CustomEvent) => this.changeDetailLevel(e.detail.level)}
864
- @update-board=${(e: CustomEvent) => this.updateBoard(e.detail.board)}
901
+ @update-draft=${(e: CustomEvent) => this.updateDraft(e.detail.draft)}
865
902
  @part-tool=${(e: CustomEvent) => this.runTool(e.detail.index, e.detail.tool, e.detail.axis)}
866
903
  @pick-part=${(e: CustomEvent) => this.pickPart(e.detail.name)}
867
904
  >
868
905
  <figure-parts
869
906
  .parts=${this.parts}
870
- .level=${(this.board?.detailLevel ?? 'M') as DetailLevel}
907
+ .level=${(this.draft?.detailLevel ?? 'M') as DetailLevel}
871
908
  .selected=${this.selected}
872
909
  ></figure-parts>
873
910
 
@@ -881,7 +918,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
881
918
  <h4>${i18next.t('figure.label.now')}</h4>
882
919
  <figure-canvas
883
920
  .figureId=${this.figure?.id || this.figure?.type}
884
- .board=${this.board}
921
+ .draft=${this.draft}
885
922
  .parts=${this.parts}
886
923
  .picked=${this.pickedName}
887
924
  .view=${this.view}
@@ -904,10 +941,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
904
941
  </div>
905
942
  `
906
943
  : this.mode === 'preview'
907
- ? html`<figure-preview .source=${source}></figure-preview>`
944
+ ? html`<figure-preview .source=${source} .view=${this.view}></figure-preview>`
908
945
  : html`<figure-canvas
909
946
  .figureId=${this.figure?.id || this.figure?.type}
910
- .board=${this.board}
947
+ .draft=${this.draft}
911
948
  .parts=${this.parts}
912
949
  .picked=${this.pickedName}
913
950
  .view=${this.view}
@@ -923,7 +960,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
923
960
  .figureState=${this.figure?.state}
924
961
  .figureVersion=${this.figure?.version ?? 0}
925
962
  .dirty=${this.dirty}
926
- .board=${this.board}
963
+ .draft=${this.draft}
927
964
  .parts=${this.parts}
928
965
  .source=${source}
929
966
  .selected=${this.selected}
@@ -1047,7 +1084,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1047
1084
  private renderProposed() {
1048
1085
  if (!this.proposalSession) return ''
1049
1086
 
1050
- return html`<figure-preview .source=${this.proposalSession.source}></figure-preview>`
1087
+ return html`<figure-preview .source=${this.proposalSession.source} .view=${this.view}></figure-preview>`
1051
1088
  }
1052
1089
 
1053
1090
  /**
@@ -1133,15 +1170,22 @@ export class FigureModellerPage extends FigureModellerPageBase {
1133
1170
 
1134
1171
  /** 변경 하나. 무엇을 하겠다는 것인지 부호로 먼저 말한다. */
1135
1172
  private renderChange(change: proposals.ProposalChange) {
1136
- 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 = ({
1137
1179
  added: 'add',
1138
1180
  changed: 'edit',
1139
1181
  removed: 'remove',
1140
1182
  base: 'aspect_ratio',
1183
+ placement: 'vertical_align_bottom',
1141
1184
  detail: 'tune',
1142
- anchor: 'my_location',
1143
- styleKit: 'palette'
1144
- }[change.kind]
1185
+ styleKit: 'palette',
1186
+ animations: 'animation',
1187
+ capabilities: 'hub'
1188
+ } satisfies Record<proposals.ChangeKind, string>)[change.kind]
1145
1189
 
1146
1190
  const on = this.proposalSession?.picked.has(change.key)
1147
1191
 
@@ -1176,7 +1220,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1176
1220
  /**
1177
1221
  * 발행 게이트의 구조화된 수정안 하나를 현재 작업 사본에만 겹쳐 본다. 서버가 준 값은
1178
1222
  * `part.anchor.axis = to`라는 데이터이지 사람이 읽을 문장을 다시 해석한 결과가 아니다.
1179
- * 원본 `board`/`parts`는 여기서 전혀 바꾸지 않고, 아래의 `take()`만 바꾼 사본을 적용한다.
1223
+ * 원본 `draft`/`parts`는 여기서 전혀 바꾸지 않고, 아래의 `take()`만 바꾼 사본을 적용한다.
1180
1224
  */
1181
1225
  private previewSizingFix(fix: FigureGateFix) {
1182
1226
  if (!this.folded) return
@@ -1207,9 +1251,9 @@ export class FigureModellerPage extends FigureModellerPageBase {
1207
1251
 
1208
1252
  this.recordHistory()
1209
1253
  const taken = proposals.applyProposal(this.folded, this.proposalSession.source, this.proposalSession.picked)
1210
- const { board, parts } = fromFigureSource(taken)
1254
+ const { draft, parts } = fromFigureSource(taken)
1211
1255
 
1212
- this.board = board
1256
+ this.draft = draft
1213
1257
  this.parts = parts
1214
1258
  this.selected = parts.length ? 0 : -1
1215
1259
  this.dirty = true
@@ -1286,7 +1330,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1286
1330
  }
1287
1331
  const pending = consumePendingFigureProposal()
1288
1332
  if (pending?.source) {
1289
- this.applySourceDirectly(pending.source, pending.proposal)
1333
+ this.openProposalForReview(pending.source, pending.proposal)
1290
1334
  }
1291
1335
  return
1292
1336
  }
@@ -1307,7 +1351,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1307
1351
  this.startNew()
1308
1352
  const pending = consumePendingFigureProposal()
1309
1353
  if (pending?.source) {
1310
- this.applySourceDirectly(pending.source, pending.proposal)
1354
+ this.openProposalForReview(pending.source, pending.proposal)
1311
1355
  }
1312
1356
  return
1313
1357
  }
@@ -1316,7 +1360,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1316
1360
  this.startNew()
1317
1361
  const pending = consumePendingFigureProposal()
1318
1362
  if (pending?.source) {
1319
- this.applySourceDirectly(pending.source, pending.proposal)
1363
+ this.openProposalForReview(pending.source, pending.proposal)
1320
1364
  }
1321
1365
  return
1322
1366
  }
@@ -1325,14 +1369,14 @@ export class FigureModellerPage extends FigureModellerPageBase {
1325
1369
  this.startNew()
1326
1370
  const pending = consumePendingFigureProposal()
1327
1371
  if (pending?.source) {
1328
- this.applySourceDirectly(pending.source, pending.proposal)
1372
+ this.openProposalForReview(pending.source, pending.proposal)
1329
1373
  }
1330
1374
  return
1331
1375
  }
1332
1376
 
1333
1377
  const pending = consumePendingFigureProposal()
1334
1378
  if (pending?.source) {
1335
- this.applySourceDirectly(pending.source, pending.proposal)
1379
+ this.openProposalForReview(pending.source, pending.proposal)
1336
1380
  }
1337
1381
  }
1338
1382
 
@@ -1360,7 +1404,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1360
1404
  // 서버가 준 것을 그대로 쓴다. 읽지 못하면 오류보고없이 빈 상태로 바꾸지 않는다.
1361
1405
  // 그러면 사용자가 저장을 선택해 원본을 덮어쓴다.
1362
1406
  const model = figure.source ? fromFigureSource(JSON.parse(figure.source) as FigureSource) : undefined
1363
- this.board = model?.board
1407
+ this.draft = model?.draft
1364
1408
  this.parts = model?.parts ?? []
1365
1409
 
1366
1410
  /*
@@ -1390,10 +1434,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
1390
1434
  this.mode = 'edit'
1391
1435
  // 타입 이름을 사람이 정하기 전까지는 임시 이름을 쓴다. 저장할 때 확정한다.
1392
1436
  const draftType = `FIGURE_${Date.now().toString(36).toUpperCase()}`
1393
- const { board, parts } = startingModel(draftType)
1437
+ const { draft, parts } = startingModel(draftType)
1394
1438
 
1395
1439
  this.figure = { id: '', type: draftType, name: '' } as Figure
1396
- this.board = board
1440
+ this.draft = draft
1397
1441
  this.parts = parts
1398
1442
  /* 고른 것이 없다 — 부품이 없으니 속성 패널도 고를 것을 기다린다. */
1399
1443
  this.selected = -1
@@ -1414,9 +1458,9 @@ export class FigureModellerPage extends FigureModellerPageBase {
1414
1458
  private typeChanged(type: string) {
1415
1459
  if (!type || this.figure?.id) return
1416
1460
  const sanitized = type.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()
1417
- if (this.board) this.board.figureType = sanitized
1461
+ if (this.draft) this.draft.figureType = sanitized
1418
1462
  this.figure = { ...this.figure, type: sanitized } as Figure
1419
- this.folded = this.board ? toFigureSource(this.board, this.parts) : undefined
1463
+ this.folded = this.draft ? toFigureSource(this.draft, this.parts) : undefined
1420
1464
  this.dirty = true
1421
1465
  }
1422
1466
 
@@ -1433,9 +1477,9 @@ export class FigureModellerPage extends FigureModellerPageBase {
1433
1477
  this.dirty = true
1434
1478
  }
1435
1479
 
1436
- private snapshotState(): { board?: BoardModel; parts: PartModel[]; selected: number } {
1480
+ private snapshotState(): { draft?: FigureDraft; parts: PartModel[]; selected: number } {
1437
1481
  return {
1438
- board: this.board ? structuredClone(this.board) : undefined,
1482
+ draft: this.draft ? structuredClone(this.draft) : undefined,
1439
1483
  parts: structuredClone(this.parts),
1440
1484
  selected: this.selected
1441
1485
  }
@@ -1461,7 +1505,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1461
1505
  this.undoStack = this.undoStack.slice(0, -1)
1462
1506
  this.redoStack = [this.snapshotState(), ...this.redoStack.slice(0, 49)]
1463
1507
 
1464
- this.board = prev.board ? structuredClone(prev.board) : undefined
1508
+ this.draft = prev.draft ? structuredClone(prev.draft) : undefined
1465
1509
  this.parts = structuredClone(prev.parts)
1466
1510
  this.selected = this.parts.length ? Math.max(-1, Math.min(prev.selected, this.parts.length - 1)) : -1
1467
1511
  this.dirty = true
@@ -1474,7 +1518,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1474
1518
  this.redoStack = this.redoStack.slice(1)
1475
1519
  this.undoStack = [...this.undoStack.slice(-49), this.snapshotState()]
1476
1520
 
1477
- this.board = next.board ? structuredClone(next.board) : undefined
1521
+ this.draft = next.draft ? structuredClone(next.draft) : undefined
1478
1522
  this.parts = structuredClone(next.parts)
1479
1523
  this.selected = this.parts.length ? Math.max(-1, Math.min(next.selected, this.parts.length - 1)) : -1
1480
1524
  this.dirty = true
@@ -1495,9 +1539,9 @@ export class FigureModellerPage extends FigureModellerPageBase {
1495
1539
  }
1496
1540
 
1497
1541
  private addPart(primitive: PrimitiveKind) {
1498
- if (!this.board) return
1542
+ if (!this.draft) return
1499
1543
  // 추가한 것을 바로 수정할 수 있게 선택 상태로 둔다.
1500
- this.put(edits.addPart(this.board, this.parts, primitive), this.parts.length)
1544
+ this.put(edits.addPart(this.draft, this.parts, primitive), this.parts.length)
1501
1545
  }
1502
1546
 
1503
1547
  private removePart(index: number) {
@@ -1522,9 +1566,9 @@ export class FigureModellerPage extends FigureModellerPageBase {
1522
1566
  * 부품 목록은 건드리지 않는다. `put` 은 부품을 함께 받으므로 여기서 쓰면 능력 하나를
1523
1567
  * 고칠 때마다 부품 목록이 새 배열이 되고, 그리는 쪽이 전부 다시 그린다.
1524
1568
  */
1525
- private updateBoard(board: BoardModel) {
1526
- this.recordHistory('board')
1527
- this.board = board
1569
+ private updateDraft(draft: FigureDraft) {
1570
+ this.recordHistory('draft')
1571
+ this.draft = draft
1528
1572
  this.dirty = true
1529
1573
  }
1530
1574
 
@@ -1532,17 +1576,17 @@ export class FigureModellerPage extends FigureModellerPageBase {
1532
1576
  * 디테일 등급을 바꾼다.
1533
1577
  *
1534
1578
  * 이 값은 부품 수 한도를 정하고 성숙도의 한 축이 된다. 부품보다 위에 있는 값이라
1535
- * 보드에 담는다.
1579
+ * 초안에 담는다.
1536
1580
  *
1537
1581
  * 한도를 내려서 이미 넣은 부품 수보다 작아질 수 있다. 그때 부품을 지우지 않는다.
1538
1582
  * 검증이 위반으로 알리고, 성숙도가 등급에 안 맞는다고 말한다. 저작자가 무엇을 뺄지
1539
1583
  * 정할 일이지 도구가 정할 일이 아니다.
1540
1584
  */
1541
1585
  private changeDetailLevel(level: DetailLevel) {
1542
- if (!this.board) return
1586
+ if (!this.draft) return
1543
1587
 
1544
1588
  this.recordHistory('detailLevel')
1545
- this.board = { ...this.board, detailLevel: level }
1589
+ this.draft = { ...this.draft, detailLevel: level }
1546
1590
  this.dirty = true
1547
1591
  }
1548
1592
 
@@ -1589,14 +1633,14 @@ export class FigureModellerPage extends FigureModellerPageBase {
1589
1633
  * 규칙은 `part-edits` 가 갖는다. 여기서는 어느 것을 부를지만 정한다.
1590
1634
  */
1591
1635
  private runTool(index: number, tool: string, axis: PlaneAxis) {
1592
- const board = this.board
1593
- if (!board) return
1636
+ const draft = this.draft
1637
+ if (!draft) return
1594
1638
 
1595
- if (tool === 'center') this.put(edits.centerPart(board, this.parts, index, axis))
1639
+ if (tool === 'center') this.put(edits.centerPart(draft, this.parts, index, axis))
1596
1640
  else if (tool === 'floor') this.put(edits.alignPartToStart(this.parts, index, axis))
1597
- else if (tool === 'snap') this.put(edits.snapPart(board, this.parts, index))
1641
+ else if (tool === 'snap') this.put(edits.snapPart(draft, this.parts, index))
1598
1642
  // 대칭 복제는 부품이 하나 늘어난다 — 새로 생긴 것을 선택 상태로 둔다
1599
- else if (tool === 'mirror') this.put(edits.mirrorPart(board, this.parts, index, axis), index + 1)
1643
+ else if (tool === 'mirror') this.put(edits.mirrorPart(draft, this.parts, index, axis), index + 1)
1600
1644
  }
1601
1645
 
1602
1646
  /**
@@ -1651,7 +1695,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1651
1695
  private async fillThumbnail(id: string): Promise<void> {
1652
1696
  await this.updateComplete
1653
1697
 
1654
- const thumbnail = await captureThumbnail(this.board, this.parts)
1698
+ const thumbnail = await captureThumbnail(this.draft, this.parts)
1655
1699
  if (!thumbnail || this.figure?.id !== id) {
1656
1700
  return
1657
1701
  }
@@ -1673,7 +1717,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1673
1717
 
1674
1718
  if (!this.figure.id) {
1675
1719
  // 신규 저장: 타입 코드와 표시 이름을 명확히 팝업으로 확인/입력받음
1676
- let defaultType = this.board?.figureType || this.figure.type || ''
1720
+ let defaultType = this.draft?.figureType || this.figure.type || ''
1677
1721
  if (!defaultType || defaultType.startsWith('FIGURE_')) {
1678
1722
  defaultType = 'FIGURE'
1679
1723
  }
@@ -1696,7 +1740,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
1696
1740
 
1697
1741
  try {
1698
1742
  if (!this.figure.id) {
1699
- const typeToSave = (this.saveModalType || this.board?.figureType || this.figure.type || 'FIGURE').trim().toUpperCase()
1743
+ const typeToSave = (this.saveModalType || this.draft?.figureType || this.figure.type || 'FIGURE').trim().toUpperCase()
1700
1744
  const nameToSave = (this.saveModalName || this.figure.name || typeToSave).trim()
1701
1745
 
1702
1746
  if (!typeToSave) {
@@ -1708,11 +1752,11 @@ export class FigureModellerPage extends FigureModellerPageBase {
1708
1752
  throw new Error(i18next.t('figure.text.type-name-already-taken', { type: typeToSave }))
1709
1753
  }
1710
1754
 
1711
- if (this.board) this.board.figureType = typeToSave
1755
+ if (this.draft) this.draft.figureType = typeToSave
1712
1756
  this.figure = { ...this.figure, type: typeToSave, name: nameToSave }
1713
1757
  this.folded = { ...this.folded, type: typeToSave }
1714
1758
  const sourceJson = JSON.stringify(this.folded)
1715
- const thumbnail = await captureThumbnail(this.board, this.parts)
1759
+ const thumbnail = await captureThumbnail(this.draft, this.parts)
1716
1760
 
1717
1761
  const result = await createFigure({
1718
1762
  type: typeToSave,
@@ -1734,10 +1778,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
1734
1778
  // 기존 모델 업데이트: folded.type이 figure.type과 일치하도록 보장
1735
1779
  if (this.figure.type && this.folded.type !== this.figure.type) {
1736
1780
  this.folded = { ...this.folded, type: this.figure.type }
1737
- if (this.board) this.board.figureType = this.figure.type
1781
+ if (this.draft) this.draft.figureType = this.figure.type
1738
1782
  }
1739
1783
  const sourceJson = JSON.stringify(this.folded)
1740
- const thumbnail = await captureThumbnail(this.board, this.parts)
1784
+ const thumbnail = await captureThumbnail(this.draft, this.parts)
1741
1785
 
1742
1786
  const result = await updateFigure(this.figure.id, {
1743
1787
  name: this.figure.name,
@@ -1,9 +1,9 @@
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
8
  /** 펼쳐 둔 clip. 이름이 아니라 자리로 잡는다 — 이름은 편집 중에 바뀐다. */
9
9
  private open;
@@ -62,9 +62,9 @@ export declare class FigureAnimations extends FigureAnimations_base {
62
62
  */
63
63
  private renderBudget;
64
64
  /**
65
- * 보드에 실어 올려보낸다.
65
+ * 초안에 실어 올려보낸다.
66
66
  *
67
- * `animations` 는 도형 전체의 것이므로 부품 편집(`update-part`)이 아니라 `update-board` 로
67
+ * `animations` 는 도형 전체의 것이므로 부품 편집(`update-part`)이 아니라 `update-draft` 로
68
68
  * 간다. 빈 배열은 칸을 지운다 — clip 을 다 지운 도형은 `animations: []` 가 아니라 없는 것이다.
69
69
  */
70
70
  private tell;
@@ -246,7 +246,7 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
246
246
  `
247
247
  ]; }
248
248
  get clips() {
249
- return this.board?.animations ?? [];
249
+ return this.draft?.animations ?? [];
250
250
  }
251
251
  /** 부품 이름 — 채널이 이것을 가리킨다. */
252
252
  get partNames() {
@@ -266,7 +266,7 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
266
266
  return moving.size;
267
267
  }
268
268
  render() {
269
- if (!this.board)
269
+ if (!this.draft)
270
270
  return html ``;
271
271
  return html `
272
272
  <section>
@@ -550,20 +550,20 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
550
550
  }
551
551
  // ─── 고치기 ───────────────────────────────────────────────────────────────
552
552
  /**
553
- * 보드에 실어 올려보낸다.
553
+ * 초안에 실어 올려보낸다.
554
554
  *
555
- * `animations` 는 도형 전체의 것이므로 부품 편집(`update-part`)이 아니라 `update-board` 로
555
+ * `animations` 는 도형 전체의 것이므로 부품 편집(`update-part`)이 아니라 `update-draft` 로
556
556
  * 간다. 빈 배열은 칸을 지운다 — clip 을 다 지운 도형은 `animations: []` 가 아니라 없는 것이다.
557
557
  */
558
558
  tell(animations) {
559
- if (!this.board)
559
+ if (!this.draft)
560
560
  return;
561
- const board = { ...this.board };
561
+ const draft = { ...this.draft };
562
562
  if (animations.length === 0)
563
- delete board.animations;
563
+ delete draft.animations;
564
564
  else
565
- board.animations = animations;
566
- this.dispatchEvent(new CustomEvent('update-board', { detail: { board }, bubbles: true, composed: true }));
565
+ draft.animations = animations;
566
+ this.dispatchEvent(new CustomEvent('update-draft', { detail: { draft }, bubbles: true, composed: true }));
567
567
  }
568
568
  patchClip(at, patch) {
569
569
  this.tell(this.clips.map((clip, i) => (i === at ? { ...clip, ...patch } : clip)));
@@ -688,7 +688,7 @@ let FigureAnimations = class FigureAnimations extends localize(i18next)(LitEleme
688
688
  __decorate([
689
689
  property({ type: Object }),
690
690
  __metadata("design:type", Object)
691
- ], FigureAnimations.prototype, "board", void 0);
691
+ ], FigureAnimations.prototype, "draft", void 0);
692
692
  __decorate([
693
693
  property({ type: Array }),
694
694
  __metadata("design:type", Array)