@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.
- package/client/modeller/figure-animations.ts +12 -12
- package/client/modeller/figure-canvas.ts +32 -48
- package/client/modeller/figure-capabilities.ts +10 -10
- package/client/modeller/figure-inspector.ts +39 -28
- package/client/modeller/figure-parts.ts +1 -1
- package/client/modeller/figure-preview.ts +122 -25
- package/client/modeller/figure-report.ts +24 -6
- package/client/modeller/figure-settings.ts +45 -2
- package/client/modeller/figure-side.ts +6 -6
- package/client/modeller/figure-source.ts +84 -38
- package/client/modeller/figure-thumbnail.ts +5 -5
- package/client/modeller/figure-view.ts +53 -0
- package/client/modeller/part-edits.ts +36 -36
- package/client/modeller/proposal.ts +25 -4
- package/client/pages/figure-modeller-page.ts +142 -98
- package/dist-client/modeller/figure-animations.d.ts +4 -4
- package/dist-client/modeller/figure-animations.js +10 -10
- package/dist-client/modeller/figure-animations.js.map +1 -1
- package/dist-client/modeller/figure-canvas.d.ts +7 -7
- package/dist-client/modeller/figure-canvas.js +27 -40
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-capabilities.d.ts +2 -2
- package/dist-client/modeller/figure-capabilities.js +9 -9
- package/dist-client/modeller/figure-capabilities.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +16 -5
- package/dist-client/modeller/figure-inspector.js +38 -27
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-parts.d.ts +1 -1
- package/dist-client/modeller/figure-parts.js +1 -1
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +39 -0
- package/dist-client/modeller/figure-preview.js +112 -23
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-report.d.ts +9 -0
- package/dist-client/modeller/figure-report.js +23 -5
- package/dist-client/modeller/figure-report.js.map +1 -1
- package/dist-client/modeller/figure-settings.js +42 -2
- package/dist-client/modeller/figure-settings.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +2 -2
- package/dist-client/modeller/figure-side.js +5 -5
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +38 -23
- package/dist-client/modeller/figure-source.js +40 -21
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/modeller/figure-thumbnail.d.ts +2 -2
- package/dist-client/modeller/figure-thumbnail.js +3 -3
- package/dist-client/modeller/figure-thumbnail.js.map +1 -1
- package/dist-client/modeller/figure-view.d.ts +45 -0
- package/dist-client/modeller/figure-view.js +27 -0
- package/dist-client/modeller/figure-view.js.map +1 -1
- package/dist-client/modeller/part-edits.d.ts +16 -16
- package/dist-client/modeller/part-edits.js +35 -35
- package/dist-client/modeller/part-edits.js.map +1 -1
- package/dist-client/modeller/proposal.d.ts +1 -1
- package/dist-client/modeller/proposal.js +15 -3
- package/dist-client/modeller/proposal.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +44 -9
- package/dist-client/pages/figure-modeller-page.js +134 -92
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/test/ai-proposal-contract.test.ts +393 -12
- package/test/expect.ts +55 -0
- package/test/figure-source.test.ts +562 -0
- package/test/i18n-prefix-guard.test.ts +1 -1
- package/{client/modeller → test}/part-edits.test.ts +50 -45
- package/translations/en.json +31 -28
- package/translations/ja.json +16 -21
- package/translations/ko.json +41 -38
- package/translations/ms.json +16 -21
- package/translations/zh.json +16 -21
- package/client/modeller/figure-ask.ts +0 -265
- package/client/modeller/figure-source.test.ts +0 -352
- package/dist-client/modeller/figure-ask.d.ts +0 -40
- package/dist-client/modeller/figure-ask.js +0 -282
- package/dist-client/modeller/figure-ask.js.map +0 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
2
|
import '@material/web/icon/icon.js';
|
|
3
3
|
import '../modeller/figure-side.js';
|
|
4
|
-
import '../modeller/figure-ask.js';
|
|
5
4
|
import '../modeller/figure-canvas.js';
|
|
6
5
|
import { captureThumbnail } from '../modeller/figure-thumbnail.js';
|
|
7
6
|
import '../modeller/figure-preview.js';
|
|
@@ -66,7 +65,7 @@ function rememberPanes(panes) {
|
|
|
66
65
|
*/
|
|
67
66
|
function startingModel(type) {
|
|
68
67
|
return {
|
|
69
|
-
|
|
68
|
+
draft: { width: 300, height: 300, depth: 300, figureType: type, detailLevel: 'M' },
|
|
70
69
|
parts: []
|
|
71
70
|
};
|
|
72
71
|
}
|
|
@@ -84,7 +83,7 @@ function startingModel(type) {
|
|
|
84
83
|
*
|
|
85
84
|
* ## 편집 데이터는 씬 모델 하나뿐이다
|
|
86
85
|
*
|
|
87
|
-
* 이 페이지가 갖는 것은
|
|
86
|
+
* 이 페이지가 갖는 것은 초안 하나와 부품 목록 — 미리보기에 그대로 세워지는 씬 모델이다.
|
|
88
87
|
* 저장 형식(`FigureSource`)으로 바꾸는 것은 **경계에서만** 한다: 저장할 때, 판단할 때,
|
|
89
88
|
* AI 후보와 비교할 때. 그 변환은 `figure-source.ts` 한 곳에서만 일어난다.
|
|
90
89
|
*
|
|
@@ -147,32 +146,31 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
147
146
|
detail: { figureId: this.figure?.id, source: this.folded }
|
|
148
147
|
}));
|
|
149
148
|
};
|
|
150
|
-
|
|
149
|
+
/*
|
|
150
|
+
`detail.staged` is the dock's answer to "did anything actually happen to my candidate". The
|
|
151
|
+
listener is synchronous, so the dock reads it right after dispatching and only then tells the
|
|
152
|
+
chat card to say so. It used to be called `accepted`, which claimed more than it knew — the
|
|
153
|
+
page had put the candidate somewhere, not the person had agreed to it.
|
|
154
|
+
*/
|
|
155
|
+
this.receiveDockProposal = (event) => {
|
|
151
156
|
const detail = event.detail;
|
|
152
157
|
if (!detail)
|
|
153
158
|
return;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
// ignore
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
const source = detail?.source || (detail?.proposal?.source ? (typeof detail.proposal.source === 'string' ? JSON.parse(detail.proposal.source) : detail.proposal.source) : undefined);
|
|
159
|
+
const source = detail?.source ||
|
|
160
|
+
(detail?.proposal?.source
|
|
161
|
+
? typeof detail.proposal.source === 'string'
|
|
162
|
+
? JSON.parse(detail.proposal.source)
|
|
163
|
+
: detail.proposal.source
|
|
164
|
+
: undefined);
|
|
166
165
|
if (source) {
|
|
167
|
-
this.
|
|
168
|
-
detail.accepted = true;
|
|
166
|
+
detail.staged = this.openProposalForReview(source, detail.proposal);
|
|
169
167
|
}
|
|
170
168
|
};
|
|
171
169
|
this.onFigureAiStage = (event) => {
|
|
172
170
|
const detail = event.detail;
|
|
173
171
|
const source = detail?.source || (detail?.proposal?.source ? (typeof detail.proposal.source === 'string' ? JSON.parse(detail.proposal.source) : detail.proposal.source) : undefined);
|
|
174
172
|
if (source) {
|
|
175
|
-
this.
|
|
173
|
+
this.openProposalForReview(source, detail.proposal);
|
|
176
174
|
}
|
|
177
175
|
};
|
|
178
176
|
this.onSaveRequest = () => {
|
|
@@ -702,14 +700,14 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
702
700
|
}
|
|
703
701
|
`; }
|
|
704
702
|
willUpdate(changed) {
|
|
705
|
-
if (changed.has('
|
|
706
|
-
this.folded = this.
|
|
703
|
+
if (changed.has('draft') || changed.has('parts')) {
|
|
704
|
+
this.folded = this.draft ? toFigureSource(this.draft, this.parts) : undefined;
|
|
707
705
|
}
|
|
708
706
|
}
|
|
709
707
|
/**
|
|
710
708
|
* 페이지 밖 AI 도크에도 **지금 손에 든 정본**을 알려 준다.
|
|
711
709
|
*
|
|
712
|
-
*
|
|
710
|
+
* asidebar 도크는 주소의 id만
|
|
713
711
|
* 알았으므로 DB에 저장된 옛 판을 읽었다. 저장 전 변경을 두고 "왜 발행이 안 되나" 또는
|
|
714
712
|
* "이 부품을 줄여 달라"고 물으면 대화가 엉뚱한 판을 보게 되는 결함이었다.
|
|
715
713
|
*
|
|
@@ -721,42 +719,79 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
721
719
|
if (changed.has('figure') && changed.get('figure')?.id !== this.figure?.id) {
|
|
722
720
|
this.proposalFeedback = [];
|
|
723
721
|
}
|
|
724
|
-
if (changed.has('
|
|
722
|
+
if (changed.has('draft') || changed.has('parts') || changed.has('figure')) {
|
|
725
723
|
window.dispatchEvent(new CustomEvent('figure-ai-context', {
|
|
726
724
|
detail: { figureId: this.figure?.id, source: this.folded }
|
|
727
725
|
}));
|
|
728
726
|
}
|
|
729
727
|
}
|
|
730
|
-
|
|
728
|
+
/**
|
|
729
|
+
* A candidate from the dock opens the review lane. It does not become the draft.
|
|
730
|
+
*
|
|
731
|
+
* This used to write `draft` and `parts` straight onto the working draft and then set
|
|
732
|
+
* `proposalSession = undefined` — the dock's candidate overwrote what the author had open and
|
|
733
|
+
* erased the lane built to review it. There was no moment where the person said yes, and a
|
|
734
|
+
* draft has no version history to go back to, so the previous work was simply gone.
|
|
735
|
+
*
|
|
736
|
+
* `ai-client-base`'s analysis contract states this as a literal type: `approvalRequired: true`,
|
|
737
|
+
* and of `proposal`, "It is never an instruction to mutate canonical state." The in-page
|
|
738
|
+
* The in-page entrance honoured this through `openAiProposal` before it was removed, and the dock
|
|
739
|
+
* entrance is now the same door. Applying is what the take button does, from the lane, per change.
|
|
740
|
+
*
|
|
741
|
+
* ## Why the overwrite was there — so it does not come back
|
|
742
|
+
*
|
|
743
|
+
* The dock opened the review lane originally. Before `c42a0b63df` this handler read:
|
|
744
|
+
*
|
|
745
|
+
* if (!this.active || (detail?.figureId ?? '') !== (this.figure?.id ?? '')) return
|
|
746
|
+
* if (detail?.draftType && detail.draftType !== this.folded?.type) return
|
|
747
|
+
* if (detail?.baseSource && detail.baseSource !== JSON.stringify(this.folded)) return
|
|
748
|
+
* this.receive(detail.proposal)
|
|
749
|
+
*
|
|
750
|
+
* Three guards, each a silent `return`, and the last compared a serialized source byte for byte.
|
|
751
|
+
* Any difference that changed nothing at all dropped the candidate with no message, so the dock
|
|
752
|
+
* would report a candidate and the screen would show none.
|
|
753
|
+
*
|
|
754
|
+
* The `ox-assistant-chat` migration fixed that by replacing the path rather than the guards, and
|
|
755
|
+
* the review lane went out with them — `applySourceDirectly` wrote straight to the draft, and
|
|
756
|
+
* cleared `proposalSession` on the way past. **Nobody decided that reviews were unwanted here.**
|
|
757
|
+
* It was the cost of loosening a guard by deleting what stood around it.
|
|
758
|
+
*
|
|
759
|
+
* So the guards are gone and stay gone, and the lane is back. If a candidate ever seems not to
|
|
760
|
+
* arrive, the thing to look for is a silent `return` on this path — not the lane.
|
|
761
|
+
*
|
|
762
|
+
* Returns whether the lane opened, so the dock can tell the chat whether to say it staged.
|
|
763
|
+
*/
|
|
764
|
+
openProposalForReview(rawSource, proposal) {
|
|
731
765
|
const source = typeof rawSource === 'string' ? JSON.parse(rawSource) : rawSource;
|
|
732
766
|
if (!source || !Array.isArray(source.parts))
|
|
733
|
-
return;
|
|
734
|
-
this.recordHistory();
|
|
767
|
+
return false;
|
|
735
768
|
try {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
769
|
+
/*
|
|
770
|
+
The dock sends a FigureProposal whose metrics sit at the top level; the staged-on-navigate
|
|
771
|
+
path carries the raw tool result, which nests them under score/cost. Read both rather than
|
|
772
|
+
making one of the two entrances reshape its payload.
|
|
773
|
+
*/
|
|
774
|
+
this.proposalSession = proposalSessions.openAiProposal(this.folded, {
|
|
775
|
+
source: JSON.stringify(source),
|
|
776
|
+
grade: proposal?.grade ?? proposal?.score?.grade,
|
|
777
|
+
triangles: proposal?.triangles ?? proposal?.cost?.triangles,
|
|
778
|
+
groups: proposal?.groups ?? proposal?.cost?.groups,
|
|
779
|
+
attempts: proposal?.attempts,
|
|
780
|
+
quality: proposal?.quality
|
|
781
|
+
});
|
|
782
|
+
this.mode = 'edit';
|
|
749
783
|
this.requestUpdate();
|
|
750
|
-
|
|
784
|
+
return true;
|
|
751
785
|
}
|
|
752
786
|
catch (e) {
|
|
753
|
-
console.error('[FigureModeller] Failed to
|
|
787
|
+
console.error('[FigureModeller] Failed to open the AI candidate for review:', e);
|
|
788
|
+
return false;
|
|
754
789
|
}
|
|
755
790
|
}
|
|
756
791
|
connectedCallback() {
|
|
757
792
|
super.connectedCallback();
|
|
758
793
|
window.addEventListener('figure-ai-context-request', this.announceAiContext);
|
|
759
|
-
window.addEventListener('figure-ai-review', this.
|
|
794
|
+
window.addEventListener('figure-ai-review', this.receiveDockProposal);
|
|
760
795
|
window.addEventListener('figure-ai-stage', this.onFigureAiStage);
|
|
761
796
|
window.addEventListener('figure-save-request', this.onSaveRequest);
|
|
762
797
|
window.addEventListener('keydown', this.handleKeyDown);
|
|
@@ -766,7 +801,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
766
801
|
window.removeEventListener('figure-save-request', this.onSaveRequest);
|
|
767
802
|
window.removeEventListener('figure-ai-stage', this.onFigureAiStage);
|
|
768
803
|
window.removeEventListener('figure-ai-context-request', this.announceAiContext);
|
|
769
|
-
window.removeEventListener('figure-ai-review', this.
|
|
804
|
+
window.removeEventListener('figure-ai-review', this.receiveDockProposal);
|
|
770
805
|
window.dispatchEvent(new CustomEvent('figure-ai-context', { detail: undefined }));
|
|
771
806
|
super.disconnectedCallback();
|
|
772
807
|
}
|
|
@@ -811,13 +846,13 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
811
846
|
@toggle-part-hidden=${(e) => this.toggleHidden(e.detail.index)}
|
|
812
847
|
@set-all-parts-hidden=${(e) => this.setAllHidden(e.detail.hidden)}
|
|
813
848
|
@change-detail-level=${(e) => this.changeDetailLevel(e.detail.level)}
|
|
814
|
-
@update-
|
|
849
|
+
@update-draft=${(e) => this.updateDraft(e.detail.draft)}
|
|
815
850
|
@part-tool=${(e) => this.runTool(e.detail.index, e.detail.tool, e.detail.axis)}
|
|
816
851
|
@pick-part=${(e) => this.pickPart(e.detail.name)}
|
|
817
852
|
>
|
|
818
853
|
<figure-parts
|
|
819
854
|
.parts=${this.parts}
|
|
820
|
-
.level=${(this.
|
|
855
|
+
.level=${(this.draft?.detailLevel ?? 'M')}
|
|
821
856
|
.selected=${this.selected}
|
|
822
857
|
></figure-parts>
|
|
823
858
|
|
|
@@ -831,7 +866,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
831
866
|
<h4>${i18next.t('figure.label.now')}</h4>
|
|
832
867
|
<figure-canvas
|
|
833
868
|
.figureId=${this.figure?.id || this.figure?.type}
|
|
834
|
-
.
|
|
869
|
+
.draft=${this.draft}
|
|
835
870
|
.parts=${this.parts}
|
|
836
871
|
.picked=${this.pickedName}
|
|
837
872
|
.view=${this.view}
|
|
@@ -854,10 +889,10 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
854
889
|
</div>
|
|
855
890
|
`
|
|
856
891
|
: this.mode === 'preview'
|
|
857
|
-
? html `<figure-preview .source=${source}></figure-preview>`
|
|
892
|
+
? html `<figure-preview .source=${source} .view=${this.view}></figure-preview>`
|
|
858
893
|
: html `<figure-canvas
|
|
859
894
|
.figureId=${this.figure?.id || this.figure?.type}
|
|
860
|
-
.
|
|
895
|
+
.draft=${this.draft}
|
|
861
896
|
.parts=${this.parts}
|
|
862
897
|
.picked=${this.pickedName}
|
|
863
898
|
.view=${this.view}
|
|
@@ -873,7 +908,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
873
908
|
.figureState=${this.figure?.state}
|
|
874
909
|
.figureVersion=${this.figure?.version ?? 0}
|
|
875
910
|
.dirty=${this.dirty}
|
|
876
|
-
.
|
|
911
|
+
.draft=${this.draft}
|
|
877
912
|
.parts=${this.parts}
|
|
878
913
|
.source=${source}
|
|
879
914
|
.selected=${this.selected}
|
|
@@ -991,7 +1026,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
991
1026
|
renderProposed() {
|
|
992
1027
|
if (!this.proposalSession)
|
|
993
1028
|
return '';
|
|
994
|
-
return html `<figure-preview .source=${this.proposalSession.source}></figure-preview>`;
|
|
1029
|
+
return html `<figure-preview .source=${this.proposalSession.source} .view=${this.view}></figure-preview>`;
|
|
995
1030
|
}
|
|
996
1031
|
/**
|
|
997
1032
|
* 무엇을 받을지 고른다.
|
|
@@ -1072,14 +1107,21 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1072
1107
|
}
|
|
1073
1108
|
/** 변경 하나. 무엇을 하겠다는 것인지 부호로 먼저 말한다. */
|
|
1074
1109
|
renderChange(change) {
|
|
1110
|
+
/*
|
|
1111
|
+
Every ChangeKind needs a row here. A kind the map does not answer draws an empty icon and a
|
|
1112
|
+
key with no name beside it, which reads as a broken row rather than a missing case — the
|
|
1113
|
+
`satisfies` makes the compiler say so instead.
|
|
1114
|
+
*/
|
|
1075
1115
|
const icon = {
|
|
1076
1116
|
added: 'add',
|
|
1077
1117
|
changed: 'edit',
|
|
1078
1118
|
removed: 'remove',
|
|
1079
1119
|
base: 'aspect_ratio',
|
|
1120
|
+
placement: 'vertical_align_bottom',
|
|
1080
1121
|
detail: 'tune',
|
|
1081
|
-
|
|
1082
|
-
|
|
1122
|
+
styleKit: 'palette',
|
|
1123
|
+
animations: 'animation',
|
|
1124
|
+
capabilities: 'hub'
|
|
1083
1125
|
}[change.kind];
|
|
1084
1126
|
const on = this.proposalSession?.picked.has(change.key);
|
|
1085
1127
|
return html `
|
|
@@ -1111,7 +1153,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1111
1153
|
/**
|
|
1112
1154
|
* 발행 게이트의 구조화된 수정안 하나를 현재 작업 사본에만 겹쳐 본다. 서버가 준 값은
|
|
1113
1155
|
* `part.anchor.axis = to`라는 데이터이지 사람이 읽을 문장을 다시 해석한 결과가 아니다.
|
|
1114
|
-
* 원본 `
|
|
1156
|
+
* 원본 `draft`/`parts`는 여기서 전혀 바꾸지 않고, 아래의 `take()`만 바꾼 사본을 적용한다.
|
|
1115
1157
|
*/
|
|
1116
1158
|
previewSizingFix(fix) {
|
|
1117
1159
|
if (!this.folded)
|
|
@@ -1144,8 +1186,8 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1144
1186
|
return;
|
|
1145
1187
|
this.recordHistory();
|
|
1146
1188
|
const taken = proposals.applyProposal(this.folded, this.proposalSession.source, this.proposalSession.picked);
|
|
1147
|
-
const {
|
|
1148
|
-
this.
|
|
1189
|
+
const { draft, parts } = fromFigureSource(taken);
|
|
1190
|
+
this.draft = draft;
|
|
1149
1191
|
this.parts = parts;
|
|
1150
1192
|
this.selected = parts.length ? 0 : -1;
|
|
1151
1193
|
this.dirty = true;
|
|
@@ -1216,7 +1258,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1216
1258
|
}
|
|
1217
1259
|
const pending = consumePendingFigureProposal();
|
|
1218
1260
|
if (pending?.source) {
|
|
1219
|
-
this.
|
|
1261
|
+
this.openProposalForReview(pending.source, pending.proposal);
|
|
1220
1262
|
}
|
|
1221
1263
|
return;
|
|
1222
1264
|
}
|
|
@@ -1236,7 +1278,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1236
1278
|
this.startNew();
|
|
1237
1279
|
const pending = consumePendingFigureProposal();
|
|
1238
1280
|
if (pending?.source) {
|
|
1239
|
-
this.
|
|
1281
|
+
this.openProposalForReview(pending.source, pending.proposal);
|
|
1240
1282
|
}
|
|
1241
1283
|
return;
|
|
1242
1284
|
}
|
|
@@ -1244,7 +1286,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1244
1286
|
this.startNew();
|
|
1245
1287
|
const pending = consumePendingFigureProposal();
|
|
1246
1288
|
if (pending?.source) {
|
|
1247
|
-
this.
|
|
1289
|
+
this.openProposalForReview(pending.source, pending.proposal);
|
|
1248
1290
|
}
|
|
1249
1291
|
return;
|
|
1250
1292
|
}
|
|
@@ -1252,13 +1294,13 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1252
1294
|
this.startNew();
|
|
1253
1295
|
const pending = consumePendingFigureProposal();
|
|
1254
1296
|
if (pending?.source) {
|
|
1255
|
-
this.
|
|
1297
|
+
this.openProposalForReview(pending.source, pending.proposal);
|
|
1256
1298
|
}
|
|
1257
1299
|
return;
|
|
1258
1300
|
}
|
|
1259
1301
|
const pending = consumePendingFigureProposal();
|
|
1260
1302
|
if (pending?.source) {
|
|
1261
|
-
this.
|
|
1303
|
+
this.openProposalForReview(pending.source, pending.proposal);
|
|
1262
1304
|
}
|
|
1263
1305
|
}
|
|
1264
1306
|
async load(id) {
|
|
@@ -1282,7 +1324,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1282
1324
|
// 서버가 준 것을 그대로 쓴다. 읽지 못하면 오류보고없이 빈 상태로 바꾸지 않는다.
|
|
1283
1325
|
// 그러면 사용자가 저장을 선택해 원본을 덮어쓴다.
|
|
1284
1326
|
const model = figure.source ? fromFigureSource(JSON.parse(figure.source)) : undefined;
|
|
1285
|
-
this.
|
|
1327
|
+
this.draft = model?.draft;
|
|
1286
1328
|
this.parts = model?.parts ?? [];
|
|
1287
1329
|
/*
|
|
1288
1330
|
**아무것도 고르지 않은 채로 연다.**
|
|
@@ -1310,9 +1352,9 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1310
1352
|
this.mode = 'edit';
|
|
1311
1353
|
// 타입 이름을 사람이 정하기 전까지는 임시 이름을 쓴다. 저장할 때 확정한다.
|
|
1312
1354
|
const draftType = `FIGURE_${Date.now().toString(36).toUpperCase()}`;
|
|
1313
|
-
const {
|
|
1355
|
+
const { draft, parts } = startingModel(draftType);
|
|
1314
1356
|
this.figure = { id: '', type: draftType, name: '' };
|
|
1315
|
-
this.
|
|
1357
|
+
this.draft = draft;
|
|
1316
1358
|
this.parts = parts;
|
|
1317
1359
|
/* 고른 것이 없다 — 부품이 없으니 속성 패널도 고를 것을 기다린다. */
|
|
1318
1360
|
this.selected = -1;
|
|
@@ -1332,10 +1374,10 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1332
1374
|
if (!type || this.figure?.id)
|
|
1333
1375
|
return;
|
|
1334
1376
|
const sanitized = type.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase();
|
|
1335
|
-
if (this.
|
|
1336
|
-
this.
|
|
1377
|
+
if (this.draft)
|
|
1378
|
+
this.draft.figureType = sanitized;
|
|
1337
1379
|
this.figure = { ...this.figure, type: sanitized };
|
|
1338
|
-
this.folded = this.
|
|
1380
|
+
this.folded = this.draft ? toFigureSource(this.draft, this.parts) : undefined;
|
|
1339
1381
|
this.dirty = true;
|
|
1340
1382
|
}
|
|
1341
1383
|
/**
|
|
@@ -1352,7 +1394,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1352
1394
|
}
|
|
1353
1395
|
snapshotState() {
|
|
1354
1396
|
return {
|
|
1355
|
-
|
|
1397
|
+
draft: this.draft ? structuredClone(this.draft) : undefined,
|
|
1356
1398
|
parts: structuredClone(this.parts),
|
|
1357
1399
|
selected: this.selected
|
|
1358
1400
|
};
|
|
@@ -1374,7 +1416,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1374
1416
|
const prev = this.undoStack[this.undoStack.length - 1];
|
|
1375
1417
|
this.undoStack = this.undoStack.slice(0, -1);
|
|
1376
1418
|
this.redoStack = [this.snapshotState(), ...this.redoStack.slice(0, 49)];
|
|
1377
|
-
this.
|
|
1419
|
+
this.draft = prev.draft ? structuredClone(prev.draft) : undefined;
|
|
1378
1420
|
this.parts = structuredClone(prev.parts);
|
|
1379
1421
|
this.selected = this.parts.length ? Math.max(-1, Math.min(prev.selected, this.parts.length - 1)) : -1;
|
|
1380
1422
|
this.dirty = true;
|
|
@@ -1386,7 +1428,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1386
1428
|
const next = this.redoStack[0];
|
|
1387
1429
|
this.redoStack = this.redoStack.slice(1);
|
|
1388
1430
|
this.undoStack = [...this.undoStack.slice(-49), this.snapshotState()];
|
|
1389
|
-
this.
|
|
1431
|
+
this.draft = next.draft ? structuredClone(next.draft) : undefined;
|
|
1390
1432
|
this.parts = structuredClone(next.parts);
|
|
1391
1433
|
this.selected = this.parts.length ? Math.max(-1, Math.min(next.selected, this.parts.length - 1)) : -1;
|
|
1392
1434
|
this.dirty = true;
|
|
@@ -1405,10 +1447,10 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1405
1447
|
this.dirty = true;
|
|
1406
1448
|
}
|
|
1407
1449
|
addPart(primitive) {
|
|
1408
|
-
if (!this.
|
|
1450
|
+
if (!this.draft)
|
|
1409
1451
|
return;
|
|
1410
1452
|
// 추가한 것을 바로 수정할 수 있게 선택 상태로 둔다.
|
|
1411
|
-
this.put(edits.addPart(this.
|
|
1453
|
+
this.put(edits.addPart(this.draft, this.parts, primitive), this.parts.length);
|
|
1412
1454
|
}
|
|
1413
1455
|
removePart(index) {
|
|
1414
1456
|
this.put(edits.removePart(this.parts, index), Math.max(0, index - 1));
|
|
@@ -1428,26 +1470,26 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1428
1470
|
* 부품 목록은 건드리지 않는다. `put` 은 부품을 함께 받으므로 여기서 쓰면 능력 하나를
|
|
1429
1471
|
* 고칠 때마다 부품 목록이 새 배열이 되고, 그리는 쪽이 전부 다시 그린다.
|
|
1430
1472
|
*/
|
|
1431
|
-
|
|
1432
|
-
this.recordHistory('
|
|
1433
|
-
this.
|
|
1473
|
+
updateDraft(draft) {
|
|
1474
|
+
this.recordHistory('draft');
|
|
1475
|
+
this.draft = draft;
|
|
1434
1476
|
this.dirty = true;
|
|
1435
1477
|
}
|
|
1436
1478
|
/**
|
|
1437
1479
|
* 디테일 등급을 바꾼다.
|
|
1438
1480
|
*
|
|
1439
1481
|
* 이 값은 부품 수 한도를 정하고 성숙도의 한 축이 된다. 부품보다 위에 있는 값이라
|
|
1440
|
-
*
|
|
1482
|
+
* 초안에 담는다.
|
|
1441
1483
|
*
|
|
1442
1484
|
* 한도를 내려서 이미 넣은 부품 수보다 작아질 수 있다. 그때 부품을 지우지 않는다.
|
|
1443
1485
|
* 검증이 위반으로 알리고, 성숙도가 등급에 안 맞는다고 말한다. 저작자가 무엇을 뺄지
|
|
1444
1486
|
* 정할 일이지 도구가 정할 일이 아니다.
|
|
1445
1487
|
*/
|
|
1446
1488
|
changeDetailLevel(level) {
|
|
1447
|
-
if (!this.
|
|
1489
|
+
if (!this.draft)
|
|
1448
1490
|
return;
|
|
1449
1491
|
this.recordHistory('detailLevel');
|
|
1450
|
-
this.
|
|
1492
|
+
this.draft = { ...this.draft, detailLevel: level };
|
|
1451
1493
|
this.dirty = true;
|
|
1452
1494
|
}
|
|
1453
1495
|
/**
|
|
@@ -1491,18 +1533,18 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1491
1533
|
* 규칙은 `part-edits` 가 갖는다. 여기서는 어느 것을 부를지만 정한다.
|
|
1492
1534
|
*/
|
|
1493
1535
|
runTool(index, tool, axis) {
|
|
1494
|
-
const
|
|
1495
|
-
if (!
|
|
1536
|
+
const draft = this.draft;
|
|
1537
|
+
if (!draft)
|
|
1496
1538
|
return;
|
|
1497
1539
|
if (tool === 'center')
|
|
1498
|
-
this.put(edits.centerPart(
|
|
1540
|
+
this.put(edits.centerPart(draft, this.parts, index, axis));
|
|
1499
1541
|
else if (tool === 'floor')
|
|
1500
1542
|
this.put(edits.alignPartToStart(this.parts, index, axis));
|
|
1501
1543
|
else if (tool === 'snap')
|
|
1502
|
-
this.put(edits.snapPart(
|
|
1544
|
+
this.put(edits.snapPart(draft, this.parts, index));
|
|
1503
1545
|
// 대칭 복제는 부품이 하나 늘어난다 — 새로 생긴 것을 선택 상태로 둔다
|
|
1504
1546
|
else if (tool === 'mirror')
|
|
1505
|
-
this.put(edits.mirrorPart(
|
|
1547
|
+
this.put(edits.mirrorPart(draft, this.parts, index, axis), index + 1);
|
|
1506
1548
|
}
|
|
1507
1549
|
/**
|
|
1508
1550
|
* 캔버스에서 부품을 선택했다.
|
|
@@ -1552,7 +1594,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1552
1594
|
}
|
|
1553
1595
|
async fillThumbnail(id) {
|
|
1554
1596
|
await this.updateComplete;
|
|
1555
|
-
const thumbnail = await captureThumbnail(this.
|
|
1597
|
+
const thumbnail = await captureThumbnail(this.draft, this.parts);
|
|
1556
1598
|
if (!thumbnail || this.figure?.id !== id) {
|
|
1557
1599
|
return;
|
|
1558
1600
|
}
|
|
@@ -1572,7 +1614,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1572
1614
|
}
|
|
1573
1615
|
if (!this.figure.id) {
|
|
1574
1616
|
// 신규 저장: 타입 코드와 표시 이름을 명확히 팝업으로 확인/입력받음
|
|
1575
|
-
let defaultType = this.
|
|
1617
|
+
let defaultType = this.draft?.figureType || this.figure.type || '';
|
|
1576
1618
|
if (!defaultType || defaultType.startsWith('FIGURE_')) {
|
|
1577
1619
|
defaultType = 'FIGURE';
|
|
1578
1620
|
}
|
|
@@ -1592,7 +1634,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1592
1634
|
this.saveModalError = '';
|
|
1593
1635
|
try {
|
|
1594
1636
|
if (!this.figure.id) {
|
|
1595
|
-
const typeToSave = (this.saveModalType || this.
|
|
1637
|
+
const typeToSave = (this.saveModalType || this.draft?.figureType || this.figure.type || 'FIGURE').trim().toUpperCase();
|
|
1596
1638
|
const nameToSave = (this.saveModalName || this.figure.name || typeToSave).trim();
|
|
1597
1639
|
if (!typeToSave) {
|
|
1598
1640
|
throw new Error('타입 이름을 입력하세요.');
|
|
@@ -1601,12 +1643,12 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1601
1643
|
if (taken.includes(typeToSave)) {
|
|
1602
1644
|
throw new Error(i18next.t('figure.text.type-name-already-taken', { type: typeToSave }));
|
|
1603
1645
|
}
|
|
1604
|
-
if (this.
|
|
1605
|
-
this.
|
|
1646
|
+
if (this.draft)
|
|
1647
|
+
this.draft.figureType = typeToSave;
|
|
1606
1648
|
this.figure = { ...this.figure, type: typeToSave, name: nameToSave };
|
|
1607
1649
|
this.folded = { ...this.folded, type: typeToSave };
|
|
1608
1650
|
const sourceJson = JSON.stringify(this.folded);
|
|
1609
|
-
const thumbnail = await captureThumbnail(this.
|
|
1651
|
+
const thumbnail = await captureThumbnail(this.draft, this.parts);
|
|
1610
1652
|
const result = await createFigure({
|
|
1611
1653
|
type: typeToSave,
|
|
1612
1654
|
name: nameToSave,
|
|
@@ -1625,11 +1667,11 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1625
1667
|
// 기존 모델 업데이트: folded.type이 figure.type과 일치하도록 보장
|
|
1626
1668
|
if (this.figure.type && this.folded.type !== this.figure.type) {
|
|
1627
1669
|
this.folded = { ...this.folded, type: this.figure.type };
|
|
1628
|
-
if (this.
|
|
1629
|
-
this.
|
|
1670
|
+
if (this.draft)
|
|
1671
|
+
this.draft.figureType = this.figure.type;
|
|
1630
1672
|
}
|
|
1631
1673
|
const sourceJson = JSON.stringify(this.folded);
|
|
1632
|
-
const thumbnail = await captureThumbnail(this.
|
|
1674
|
+
const thumbnail = await captureThumbnail(this.draft, this.parts);
|
|
1633
1675
|
const result = await updateFigure(this.figure.id, {
|
|
1634
1676
|
name: this.figure.name,
|
|
1635
1677
|
category: this.figure.category,
|
|
@@ -1700,7 +1742,7 @@ __decorate([
|
|
|
1700
1742
|
__decorate([
|
|
1701
1743
|
state(),
|
|
1702
1744
|
__metadata("design:type", Object)
|
|
1703
|
-
], FigureModellerPage.prototype, "
|
|
1745
|
+
], FigureModellerPage.prototype, "draft", void 0);
|
|
1704
1746
|
__decorate([
|
|
1705
1747
|
state(),
|
|
1706
1748
|
__metadata("design:type", Array)
|