@things-factory/figure-ui 10.1.10 → 10.1.14
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-ai-target.ts +11 -0
- package/client/modeller/figure-ask.ts +8 -5
- package/client/modeller/figure-canvas.ts +10 -4
- package/client/modeller/new-figure-entry.ts +4 -0
- package/client/pages/figure-modeller-page.ts +241 -120
- package/dist-client/modeller/figure-ai-target.d.ts +8 -0
- package/dist-client/modeller/figure-ai-target.js +12 -0
- package/dist-client/modeller/figure-ai-target.js.map +1 -0
- package/dist-client/modeller/figure-ask.d.ts +1 -0
- package/dist-client/modeller/figure-ask.js +12 -5
- package/dist-client/modeller/figure-ask.js.map +1 -1
- package/dist-client/modeller/figure-canvas.d.ts +1 -0
- package/dist-client/modeller/figure-canvas.js +10 -4
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/new-figure-entry.d.ts +2 -0
- package/dist-client/modeller/new-figure-entry.js +5 -0
- package/dist-client/modeller/new-figure-entry.js.map +1 -0
- package/dist-client/pages/figure-modeller-page.d.ts +14 -0
- package/dist-client/pages/figure-modeller-page.js +240 -120
- 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/package.json +3 -3
- package/test/ai-proposal-contract.test.ts +24 -3
- package/test/ai-target.test.ts +25 -0
- package/test/modeller-undo-redo.test.ts +33 -0
- package/test/new-figure-entry.test.ts +15 -0
- package/translations/en.json +2 -0
- package/translations/ja.json +2 -0
- package/translations/ko.json +2 -0
- package/translations/ms.json +2 -0
- package/translations/zh.json +2 -0
|
@@ -5,6 +5,7 @@ import '../modeller/figure-ask.js';
|
|
|
5
5
|
import '../modeller/figure-canvas.js';
|
|
6
6
|
import { captureThumbnail } from '../modeller/figure-thumbnail.js';
|
|
7
7
|
import '../modeller/figure-preview.js';
|
|
8
|
+
import { shouldStartNewFigure } from '../modeller/new-figure-entry.js';
|
|
8
9
|
import '../modeller/figure-parts.js';
|
|
9
10
|
import '../modeller/figure-inspector.js';
|
|
10
11
|
import { css, html } from 'lit';
|
|
@@ -129,6 +130,46 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
129
130
|
this.saveFailure = '';
|
|
130
131
|
/** 인라인 요청창과 AI 도크가 함께 쓰는 현재 Figure의 제한된 세션 피드백. */
|
|
131
132
|
this.proposalFeedback = [];
|
|
133
|
+
this.undoStack = [];
|
|
134
|
+
this.redoStack = [];
|
|
135
|
+
this.lastHistoryTime = 0;
|
|
136
|
+
this.announceAiContext = () => {
|
|
137
|
+
if (!this.active)
|
|
138
|
+
return;
|
|
139
|
+
window.dispatchEvent(new CustomEvent('figure-ai-context', {
|
|
140
|
+
detail: { figureId: this.figure?.id, source: this.folded }
|
|
141
|
+
}));
|
|
142
|
+
};
|
|
143
|
+
this.acceptDockProposal = (event) => {
|
|
144
|
+
const detail = event.detail;
|
|
145
|
+
if (!this.active || (detail?.figureId ?? '') !== (this.figure?.id ?? ''))
|
|
146
|
+
return;
|
|
147
|
+
if (detail?.draftType && detail.draftType !== this.folded?.type)
|
|
148
|
+
return;
|
|
149
|
+
if (detail?.baseSource && detail.baseSource !== JSON.stringify(this.folded))
|
|
150
|
+
return;
|
|
151
|
+
this.receive(detail.proposal);
|
|
152
|
+
detail.accepted = true;
|
|
153
|
+
};
|
|
154
|
+
this.handleKeyDown = (e) => {
|
|
155
|
+
if (!this.active || this.mode !== 'edit' || this.proposalSession)
|
|
156
|
+
return;
|
|
157
|
+
const target = e.composedPath()[0];
|
|
158
|
+
const tag = target?.tagName?.toLowerCase();
|
|
159
|
+
if (tag === 'input' || tag === 'textarea' || target?.isContentEditable)
|
|
160
|
+
return;
|
|
161
|
+
if ((e.metaKey || e.ctrlKey) && !e.altKey) {
|
|
162
|
+
if (e.key === 'z' && !e.shiftKey) {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
this.undo();
|
|
165
|
+
}
|
|
166
|
+
else if ((e.key === 'z' && e.shiftKey) || e.key === 'y') {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
this.redo();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
this.loadGeneration = 0;
|
|
132
173
|
}
|
|
133
174
|
static { this.styles = css `
|
|
134
175
|
:host {
|
|
@@ -201,84 +242,9 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
201
242
|
display: none;
|
|
202
243
|
}
|
|
203
244
|
|
|
204
|
-
/*
|
|
205
|
-
머리줄 — 이름 · 타입 · 미저장 표시 · 말로 시키는 줄 · 저장을 **한 줄에** 담는다.
|
|
206
|
-
|
|
207
|
-
이름은 페이지 제목에도 나온다(셸이 context.title 로 그린다). 그래도 여기 입력칸을 두는
|
|
208
|
-
이유는 **고치는 자리**가 필요해서다 — 제목은 읽는 것이고 이 칸은 바꾸는 것이다. 두 줄로
|
|
209
|
-
벌리는 대신 한 줄에 모아 캔버스에 높이를 넘긴다.
|
|
210
|
-
*/
|
|
211
|
-
div[header] {
|
|
212
|
-
display: flex;
|
|
213
|
-
align-items: center;
|
|
214
|
-
gap: var(--spacing-medium, 8px);
|
|
215
|
-
padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
|
|
216
|
-
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
217
|
-
}
|
|
218
|
-
div[header] figure-ask {
|
|
219
|
-
flex: 1;
|
|
220
|
-
min-width: 0;
|
|
221
|
-
}
|
|
222
|
-
input[name] {
|
|
223
|
-
flex: none;
|
|
224
|
-
width: 180px;
|
|
225
|
-
padding: 5px 9px;
|
|
226
|
-
font: var(--input-field-font, inherit);
|
|
227
|
-
font-size: 0.82rem;
|
|
228
|
-
color: var(--md-sys-color-on-surface);
|
|
229
|
-
background-color: var(--md-sys-color-surface-container-lowest);
|
|
230
|
-
border: 1px solid var(--md-sys-color-outline-variant);
|
|
231
|
-
border-radius: 6px;
|
|
232
|
-
}
|
|
233
|
-
/* 타입 이름은 만든 뒤 고칠 수 없다 — 그 사실이 화면에서 보여야 한다 */
|
|
234
|
-
code[type] {
|
|
235
|
-
padding: 4px 8px;
|
|
236
|
-
font-family: var(--mono-font, monospace);
|
|
237
|
-
font-size: 0.76rem;
|
|
238
|
-
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
239
|
-
background-color: var(--md-sys-color-surface-container);
|
|
240
|
-
border-radius: 6px;
|
|
241
|
-
}
|
|
242
|
-
span[dirty] {
|
|
243
|
-
font: var(--label-font, inherit);
|
|
244
|
-
font-size: 0.74rem;
|
|
245
|
-
color: var(--md-sys-color-tertiary);
|
|
246
|
-
}
|
|
247
245
|
div[spacer] {
|
|
248
246
|
flex: 1;
|
|
249
247
|
}
|
|
250
|
-
button[save] {
|
|
251
|
-
padding: 6px 16px;
|
|
252
|
-
font: var(--label-font, inherit);
|
|
253
|
-
font-size: 0.76rem;
|
|
254
|
-
font-weight: 600;
|
|
255
|
-
color: var(--md-sys-color-on-primary, #fff);
|
|
256
|
-
background-color: var(--md-sys-color-primary);
|
|
257
|
-
border: none;
|
|
258
|
-
border-radius: 6px;
|
|
259
|
-
cursor: pointer;
|
|
260
|
-
}
|
|
261
|
-
button[save][disabled] {
|
|
262
|
-
opacity: 0.4;
|
|
263
|
-
cursor: default;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/*
|
|
267
|
-
저장이 거절된 사유. 단추 **바로 왼쪽**에 둔다 — 누른 자리에서 답을 봐야 한다. 길면 줄이지만
|
|
268
|
-
전문은 title 로 남긴다(서버 문장이 길 수 있다).
|
|
269
|
-
*/
|
|
270
|
-
span[save-failed] {
|
|
271
|
-
max-width: 280px;
|
|
272
|
-
overflow: hidden;
|
|
273
|
-
text-overflow: ellipsis;
|
|
274
|
-
white-space: nowrap;
|
|
275
|
-
padding: 4px 8px;
|
|
276
|
-
font: var(--label-font, inherit);
|
|
277
|
-
font-size: 0.72rem;
|
|
278
|
-
color: var(--md-sys-color-on-error-container, var(--md-sys-color-on-surface));
|
|
279
|
-
background-color: var(--md-sys-color-error-container);
|
|
280
|
-
border-radius: 6px;
|
|
281
|
-
}
|
|
282
248
|
|
|
283
249
|
/*
|
|
284
250
|
칸을 벡터 저작도구의 관례대로 놓는다 — 왼쪽에 무엇이 있나(레이어), 오른쪽에
|
|
@@ -301,16 +267,17 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
301
267
|
}
|
|
302
268
|
|
|
303
269
|
/*
|
|
304
|
-
편집 · 미리보기
|
|
270
|
+
편집 · 미리보기 전환 및 조작 띠.
|
|
305
271
|
|
|
306
272
|
가운데 칸 위에 얇게 둔다. 무엇을 보고 있는지가 늘 보여야 하고, 두 화면이 같은
|
|
307
273
|
자리를 쓰므로 어느 쪽인지 모르면 편집이 안 먹는 것처럼 읽힌다.
|
|
308
274
|
*/
|
|
309
275
|
div[modes] {
|
|
310
276
|
display: flex;
|
|
277
|
+
align-items: center;
|
|
311
278
|
flex: none;
|
|
312
279
|
gap: 2px;
|
|
313
|
-
padding:
|
|
280
|
+
padding: 4px var(--spacing-medium, 8px);
|
|
314
281
|
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
315
282
|
}
|
|
316
283
|
div[modes] button[mode] {
|
|
@@ -331,13 +298,36 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
331
298
|
color: var(--md-sys-color-on-primary-container);
|
|
332
299
|
background-color: var(--md-sys-color-primary-container);
|
|
333
300
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
301
|
+
div[modes] div[separator] {
|
|
302
|
+
width: 1px;
|
|
303
|
+
height: 16px;
|
|
304
|
+
margin: 0 4px;
|
|
305
|
+
background-color: var(--md-sys-color-outline-variant);
|
|
306
|
+
}
|
|
307
|
+
div[modes] button[history] {
|
|
308
|
+
display: flex;
|
|
309
|
+
align-items: center;
|
|
310
|
+
justify-content: center;
|
|
311
|
+
width: 26px;
|
|
312
|
+
height: 26px;
|
|
313
|
+
padding: 0;
|
|
314
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
315
|
+
background: none;
|
|
316
|
+
border: none;
|
|
317
|
+
border-radius: 6px;
|
|
318
|
+
cursor: pointer;
|
|
319
|
+
}
|
|
320
|
+
div[modes] button[history]:hover:not([disabled]) {
|
|
321
|
+
background-color: var(--md-sys-color-surface-container);
|
|
322
|
+
color: var(--md-sys-color-on-surface);
|
|
323
|
+
}
|
|
324
|
+
div[modes] button[history][disabled] {
|
|
325
|
+
opacity: 0.3;
|
|
326
|
+
cursor: default;
|
|
327
|
+
}
|
|
328
|
+
div[modes] button[history] md-icon {
|
|
329
|
+
--md-icon-size: 18px;
|
|
330
|
+
}
|
|
341
331
|
div[modes] button[collapse] {
|
|
342
332
|
display: flex;
|
|
343
333
|
align-items: center;
|
|
@@ -355,6 +345,46 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
355
345
|
div[modes] button[collapse] md-icon {
|
|
356
346
|
--md-icon-size: 18px;
|
|
357
347
|
}
|
|
348
|
+
div[modes] span[dirty] {
|
|
349
|
+
align-self: center;
|
|
350
|
+
margin-right: 6px;
|
|
351
|
+
font: var(--label-font, inherit);
|
|
352
|
+
font-size: 0.72rem;
|
|
353
|
+
color: var(--md-sys-color-tertiary);
|
|
354
|
+
}
|
|
355
|
+
div[modes] span[save-failed] {
|
|
356
|
+
align-self: center;
|
|
357
|
+
max-width: 240px;
|
|
358
|
+
overflow: hidden;
|
|
359
|
+
text-overflow: ellipsis;
|
|
360
|
+
white-space: nowrap;
|
|
361
|
+
margin-right: 6px;
|
|
362
|
+
padding: 3px 7px;
|
|
363
|
+
font: var(--label-font, inherit);
|
|
364
|
+
font-size: 0.7rem;
|
|
365
|
+
color: var(--md-sys-color-on-error-container, var(--md-sys-color-on-surface));
|
|
366
|
+
background-color: var(--md-sys-color-error-container);
|
|
367
|
+
border-radius: 4px;
|
|
368
|
+
}
|
|
369
|
+
div[modes] button[save] {
|
|
370
|
+
padding: 4px 14px;
|
|
371
|
+
margin-right: 4px;
|
|
372
|
+
font: var(--label-font, inherit);
|
|
373
|
+
font-size: 0.74rem;
|
|
374
|
+
font-weight: 600;
|
|
375
|
+
color: var(--md-sys-color-on-primary, #fff);
|
|
376
|
+
background-color: var(--md-sys-color-primary);
|
|
377
|
+
border: none;
|
|
378
|
+
border-radius: 6px;
|
|
379
|
+
cursor: pointer;
|
|
380
|
+
}
|
|
381
|
+
div[modes] button[save]:hover:not([disabled]) {
|
|
382
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
383
|
+
}
|
|
384
|
+
div[modes] button[save][disabled] {
|
|
385
|
+
opacity: 0.35;
|
|
386
|
+
cursor: default;
|
|
387
|
+
}
|
|
358
388
|
|
|
359
389
|
/*
|
|
360
390
|
후보가 오면 가운데를 둘로 나눈다.
|
|
@@ -568,7 +598,16 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
568
598
|
}));
|
|
569
599
|
}
|
|
570
600
|
}
|
|
601
|
+
connectedCallback() {
|
|
602
|
+
super.connectedCallback();
|
|
603
|
+
window.addEventListener('figure-ai-context-request', this.announceAiContext);
|
|
604
|
+
window.addEventListener('figure-ai-review', this.acceptDockProposal);
|
|
605
|
+
window.addEventListener('keydown', this.handleKeyDown);
|
|
606
|
+
}
|
|
571
607
|
disconnectedCallback() {
|
|
608
|
+
window.removeEventListener('keydown', this.handleKeyDown);
|
|
609
|
+
window.removeEventListener('figure-ai-context-request', this.announceAiContext);
|
|
610
|
+
window.removeEventListener('figure-ai-review', this.acceptDockProposal);
|
|
572
611
|
window.dispatchEvent(new CustomEvent('figure-ai-context', { detail: undefined }));
|
|
573
612
|
super.disconnectedCallback();
|
|
574
613
|
}
|
|
@@ -591,29 +630,6 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
591
630
|
render() {
|
|
592
631
|
const source = this.folded;
|
|
593
632
|
return html `
|
|
594
|
-
<div header>
|
|
595
|
-
<!--
|
|
596
|
-
이름과 타입은 **속성 패널**에 있다(figure-inspector 의 도형 절). 여기 있던 이름 칸은
|
|
597
|
-
하네스 시절의 자리였다 — 앱에서는 셸이 페이지 제목으로 같은 이름을 그리므로 글자가 두 번
|
|
598
|
-
나오고, 캔버스가 주인공인 화면에서 그만큼 높이를 잃었다.
|
|
599
|
-
-->
|
|
600
|
-
${this.dirty ? html `<span dirty>${i18next.t('figure.text.unsaved-changes')}</span>` : ''}
|
|
601
|
-
<!--
|
|
602
|
-
말로 시키는 줄이 머리줄의 남는 폭을 쓴다. 따로 두면 화면 위가 두 줄이 되고, 그 위 줄은
|
|
603
|
-
이름을 페이지 제목과 겹쳐 적는다.
|
|
604
|
-
-->
|
|
605
|
-
<figure-ask
|
|
606
|
-
.source=${source}
|
|
607
|
-
.type=${source?.type ?? ''}
|
|
608
|
-
.feedback=${this.proposalFeedback}
|
|
609
|
-
@proposed=${(e) => this.receive(e.detail.proposal)}
|
|
610
|
-
></figure-ask>
|
|
611
|
-
${this.saveFailure ? html `<span save-failed title=${this.saveFailure}>${this.saveFailure}</span>` : ''}
|
|
612
|
-
<button save ?disabled=${!this.canSave} @click=${() => this.save()}>
|
|
613
|
-
${this.saving ? i18next.t('figure.text.saving-figure') : i18next.t('figure.button.save')}
|
|
614
|
-
</button>
|
|
615
|
-
</div>
|
|
616
|
-
|
|
617
633
|
<div
|
|
618
634
|
edits
|
|
619
635
|
?collapse-left=${this.collapsed.left}
|
|
@@ -654,6 +670,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
654
670
|
<div lane>
|
|
655
671
|
<h4>${i18next.t('figure.label.now')}</h4>
|
|
656
672
|
<figure-canvas
|
|
673
|
+
.figureId=${this.figure?.id || this.figure?.type}
|
|
657
674
|
.board=${this.board}
|
|
658
675
|
.parts=${this.parts}
|
|
659
676
|
.picked=${this.pickedName}
|
|
@@ -678,9 +695,11 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
678
695
|
: this.mode === 'preview'
|
|
679
696
|
? html `<figure-preview .source=${source}></figure-preview>`
|
|
680
697
|
: html `<figure-canvas
|
|
698
|
+
.figureId=${this.figure?.id || this.figure?.type}
|
|
681
699
|
.board=${this.board}
|
|
682
700
|
.parts=${this.parts}
|
|
683
701
|
.picked=${this.pickedName}
|
|
702
|
+
.view=${this.view}
|
|
684
703
|
></figure-canvas>`}
|
|
685
704
|
</div>
|
|
686
705
|
|
|
@@ -732,7 +751,33 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
732
751
|
`;
|
|
733
752
|
return html `<div modes>
|
|
734
753
|
${this.renderCollapse('left')}${this.proposalSession ? '' : html `${one('edit')}${one('preview')}`}
|
|
754
|
+
${this.mode === 'edit' && !this.proposalSession
|
|
755
|
+
? html `
|
|
756
|
+
<div separator></div>
|
|
757
|
+
<button
|
|
758
|
+
history
|
|
759
|
+
?disabled=${this.undoStack.length === 0}
|
|
760
|
+
title="${i18next.t('figure.button.undo')} (Ctrl+Z)"
|
|
761
|
+
@click=${() => this.undo()}
|
|
762
|
+
>
|
|
763
|
+
<md-icon>undo</md-icon>
|
|
764
|
+
</button>
|
|
765
|
+
<button
|
|
766
|
+
history
|
|
767
|
+
?disabled=${this.redoStack.length === 0}
|
|
768
|
+
title="${i18next.t('figure.button.redo')} (Ctrl+Y)"
|
|
769
|
+
@click=${() => this.redo()}
|
|
770
|
+
>
|
|
771
|
+
<md-icon>redo</md-icon>
|
|
772
|
+
</button>
|
|
773
|
+
`
|
|
774
|
+
: ''}
|
|
735
775
|
<div spacer></div>
|
|
776
|
+
${this.saveFailure ? html `<span save-failed title=${this.saveFailure}>${this.saveFailure}</span>` : ''}
|
|
777
|
+
${this.dirty ? html `<span dirty>${i18next.t('figure.text.unsaved-changes')}</span>` : ''}
|
|
778
|
+
<button save ?disabled=${!this.canSave} @click=${() => this.save()}>
|
|
779
|
+
${this.saving ? i18next.t('figure.text.saving-figure') : i18next.t('figure.button.save')}
|
|
780
|
+
</button>
|
|
736
781
|
${this.renderCollapse('right')}
|
|
737
782
|
</div>`;
|
|
738
783
|
}
|
|
@@ -791,6 +836,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
791
836
|
renderDecision() {
|
|
792
837
|
if (!this.proposalSession)
|
|
793
838
|
return '';
|
|
839
|
+
const stale = this.proposalBaseSource !== undefined && this.proposalBaseSource !== JSON.stringify(this.folded);
|
|
794
840
|
const changes = proposals.diffProposal(this.folded, this.proposalSession.source);
|
|
795
841
|
const picked = proposals.applyProposal(this.folded, this.proposalSession.source, this.proposalSession.picked);
|
|
796
842
|
const { errors, violations } = validate(picked);
|
|
@@ -799,7 +845,8 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
799
845
|
<span>${this.proposalSession.title || i18next.t('figure.text.assistant-proposed-a-figure')}</span>
|
|
800
846
|
<div spacer></div>
|
|
801
847
|
<button drop @click=${() => this.discard()}>${i18next.t('figure.button.discard')}</button>
|
|
802
|
-
|
|
848
|
+
${stale ? html `<span role="alert">원본이 변경되었습니다. 후보를 취소하고 AI 지원에서 다시 요청하세요.</span>` : ''}
|
|
849
|
+
<button take ?disabled=${stale || errors.length > 0 || this.proposalSession.picked.size === 0} @click=${() => this.take()}>
|
|
803
850
|
${i18next.t('figure.button.take-n-changes', { n: this.proposalSession.picked.size })}
|
|
804
851
|
</button>
|
|
805
852
|
</div>
|
|
@@ -884,6 +931,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
884
931
|
* 무엇이 자기 것이고 무엇이 기계 것인지 모르게 된다.
|
|
885
932
|
*/
|
|
886
933
|
receive(proposal) {
|
|
934
|
+
this.proposalBaseSource = JSON.stringify(this.folded);
|
|
887
935
|
this.proposalSession = proposalSessions.openAiProposal(this.folded, proposal);
|
|
888
936
|
}
|
|
889
937
|
/**
|
|
@@ -894,6 +942,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
894
942
|
previewSizingFix(fix) {
|
|
895
943
|
if (!this.folded)
|
|
896
944
|
return;
|
|
945
|
+
this.proposalBaseSource = JSON.stringify(this.folded);
|
|
897
946
|
this.proposalSession = proposalSessions.openSizingFix(this.folded, fix);
|
|
898
947
|
}
|
|
899
948
|
/**
|
|
@@ -917,6 +966,9 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
917
966
|
take() {
|
|
918
967
|
if (!this.proposalSession)
|
|
919
968
|
return;
|
|
969
|
+
if (this.proposalBaseSource !== undefined && this.proposalBaseSource !== JSON.stringify(this.folded))
|
|
970
|
+
return;
|
|
971
|
+
this.recordHistory();
|
|
920
972
|
const taken = proposals.applyProposal(this.folded, this.proposalSession.source, this.proposalSession.picked);
|
|
921
973
|
const { board, parts } = fromFigureSource(taken);
|
|
922
974
|
this.board = board;
|
|
@@ -951,6 +1003,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
951
1003
|
if (reportFeedback)
|
|
952
1004
|
this.reportProposalFeedback('discarded');
|
|
953
1005
|
this.proposalSession = undefined;
|
|
1006
|
+
this.proposalBaseSource = undefined;
|
|
954
1007
|
}
|
|
955
1008
|
/**
|
|
956
1009
|
* 저장할 수 있나.
|
|
@@ -975,11 +1028,12 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
975
1028
|
}
|
|
976
1029
|
async pageUpdated(changes, lifecycle) {
|
|
977
1030
|
if (!this.active) {
|
|
1031
|
+
++this.loadGeneration;
|
|
978
1032
|
return;
|
|
979
1033
|
}
|
|
980
1034
|
const id = lifecycle?.resourceId;
|
|
981
1035
|
if (id) {
|
|
982
|
-
if (id !== this.figure?.id) {
|
|
1036
|
+
if (changes.active || id !== this.figure?.id) {
|
|
983
1037
|
await this.load(id);
|
|
984
1038
|
}
|
|
985
1039
|
return;
|
|
@@ -1001,19 +1055,21 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1001
1055
|
return;
|
|
1002
1056
|
}
|
|
1003
1057
|
if (this.figure.id) {
|
|
1004
|
-
/*
|
|
1005
|
-
고치던 것이 있으면 묻는다. 저작한 것을 말없이 버리지 않는다 — 되돌릴 수 없다.
|
|
1006
|
-
그만두면 보고 있던 도형의 주소로 되돌려 화면과 주소가 어긋나지 않게 한다.
|
|
1007
|
-
*/
|
|
1008
|
-
if (this.dirty && !confirm(i18next.t('figure.text.discard-changes-and-start-new'))) {
|
|
1009
|
-
navigate(`figure-modeller/${this.figure.id}`, true);
|
|
1010
|
-
return;
|
|
1011
|
-
}
|
|
1012
1058
|
this.startNew();
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (shouldStartNewFigure(changes, !!this.figure, this.figure?.id)) {
|
|
1062
|
+
this.startNew();
|
|
1063
|
+
return;
|
|
1013
1064
|
}
|
|
1014
1065
|
}
|
|
1015
1066
|
async load(id) {
|
|
1067
|
+
const generation = ++this.loadGeneration;
|
|
1016
1068
|
const figure = await fetchFigure(id);
|
|
1069
|
+
if (generation !== this.loadGeneration || !this.active)
|
|
1070
|
+
return;
|
|
1071
|
+
this.proposalSession = undefined;
|
|
1072
|
+
this.proposalBaseSource = undefined;
|
|
1017
1073
|
this.figure = figure;
|
|
1018
1074
|
/*
|
|
1019
1075
|
그림이 없으면 이 자리에서 채운다(아래 `fillThumbnail`). 기다리지 않는다 — 화면이 먼저다.
|
|
@@ -1044,8 +1100,16 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1044
1100
|
this.selected = -1;
|
|
1045
1101
|
this.dirty = false;
|
|
1046
1102
|
this.violations = [];
|
|
1103
|
+
this.undoStack = [];
|
|
1104
|
+
this.redoStack = [];
|
|
1105
|
+
this.lastCoalesceKey = undefined;
|
|
1047
1106
|
}
|
|
1048
1107
|
startNew() {
|
|
1108
|
+
++this.loadGeneration;
|
|
1109
|
+
this.proposalSession = undefined;
|
|
1110
|
+
this.proposalBaseSource = undefined;
|
|
1111
|
+
this.proposalFeedback = [];
|
|
1112
|
+
this.mode = 'edit';
|
|
1049
1113
|
// 타입 이름을 사람이 정하기 전까지는 임시 이름을 쓴다. 저장할 때 확정한다.
|
|
1050
1114
|
const draftType = `FIGURE_${Date.now().toString(36).toUpperCase()}`;
|
|
1051
1115
|
const { board, parts } = startingModel(draftType);
|
|
@@ -1056,6 +1120,9 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1056
1120
|
this.selected = -1;
|
|
1057
1121
|
this.dirty = true;
|
|
1058
1122
|
this.violations = [];
|
|
1123
|
+
this.undoStack = [];
|
|
1124
|
+
this.redoStack = [];
|
|
1125
|
+
this.lastCoalesceKey = undefined;
|
|
1059
1126
|
}
|
|
1060
1127
|
rename(name) {
|
|
1061
1128
|
if (!this.figure)
|
|
@@ -1075,13 +1142,56 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1075
1142
|
this.figure = { ...this.figure, ...patch };
|
|
1076
1143
|
this.dirty = true;
|
|
1077
1144
|
}
|
|
1145
|
+
snapshotState() {
|
|
1146
|
+
return {
|
|
1147
|
+
board: this.board ? structuredClone(this.board) : undefined,
|
|
1148
|
+
parts: structuredClone(this.parts),
|
|
1149
|
+
selected: this.selected
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
recordHistory(coalesceKey) {
|
|
1153
|
+
const now = Date.now();
|
|
1154
|
+
if (coalesceKey && this.lastCoalesceKey === coalesceKey && now - this.lastHistoryTime < 600) {
|
|
1155
|
+
this.lastHistoryTime = now;
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
this.lastCoalesceKey = coalesceKey;
|
|
1159
|
+
this.lastHistoryTime = now;
|
|
1160
|
+
this.undoStack = [...this.undoStack.slice(-49), this.snapshotState()];
|
|
1161
|
+
this.redoStack = [];
|
|
1162
|
+
}
|
|
1163
|
+
undo() {
|
|
1164
|
+
if (this.undoStack.length === 0)
|
|
1165
|
+
return;
|
|
1166
|
+
const prev = this.undoStack[this.undoStack.length - 1];
|
|
1167
|
+
this.undoStack = this.undoStack.slice(0, -1);
|
|
1168
|
+
this.redoStack = [this.snapshotState(), ...this.redoStack.slice(0, 49)];
|
|
1169
|
+
this.board = prev.board ? structuredClone(prev.board) : undefined;
|
|
1170
|
+
this.parts = structuredClone(prev.parts);
|
|
1171
|
+
this.selected = this.parts.length ? Math.max(-1, Math.min(prev.selected, this.parts.length - 1)) : -1;
|
|
1172
|
+
this.dirty = true;
|
|
1173
|
+
this.lastCoalesceKey = undefined;
|
|
1174
|
+
}
|
|
1175
|
+
redo() {
|
|
1176
|
+
if (this.redoStack.length === 0)
|
|
1177
|
+
return;
|
|
1178
|
+
const next = this.redoStack[0];
|
|
1179
|
+
this.redoStack = this.redoStack.slice(1);
|
|
1180
|
+
this.undoStack = [...this.undoStack.slice(-49), this.snapshotState()];
|
|
1181
|
+
this.board = next.board ? structuredClone(next.board) : undefined;
|
|
1182
|
+
this.parts = structuredClone(next.parts);
|
|
1183
|
+
this.selected = this.parts.length ? Math.max(-1, Math.min(next.selected, this.parts.length - 1)) : -1;
|
|
1184
|
+
this.dirty = true;
|
|
1185
|
+
this.lastCoalesceKey = undefined;
|
|
1186
|
+
}
|
|
1078
1187
|
/**
|
|
1079
1188
|
* 부품 목록을 갈아 끼운다.
|
|
1080
1189
|
*
|
|
1081
1190
|
* 편집 규칙은 `part-edits` 가 갖는다 — 화면과 테스트가 같은 것을 쓰게 하려고 떼어
|
|
1082
1191
|
* 놓았다. 여기서는 결과를 담고 선택 인덱스를 정리한다.
|
|
1083
1192
|
*/
|
|
1084
|
-
put(parts, selected = this.selected) {
|
|
1193
|
+
put(parts, selected = this.selected, coalesceKey) {
|
|
1194
|
+
this.recordHistory(coalesceKey);
|
|
1085
1195
|
this.parts = parts;
|
|
1086
1196
|
this.selected = Math.min(selected, parts.length - 1);
|
|
1087
1197
|
this.dirty = true;
|
|
@@ -1102,7 +1212,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1102
1212
|
this.put(edits.movePart(this.parts, index, to), to);
|
|
1103
1213
|
}
|
|
1104
1214
|
updatePart(index, part) {
|
|
1105
|
-
this.put(edits.updatePart(this.parts, index, part));
|
|
1215
|
+
this.put(edits.updatePart(this.parts, index, part), this.selected, `part:${part.name || index}`);
|
|
1106
1216
|
}
|
|
1107
1217
|
/**
|
|
1108
1218
|
* 판 자체의 값을 갈아 끼운다 — 능력처럼 부품보다 위에 있는 것들.
|
|
@@ -1111,6 +1221,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1111
1221
|
* 고칠 때마다 부품 목록이 새 배열이 되고, 그리는 쪽이 전부 다시 그린다.
|
|
1112
1222
|
*/
|
|
1113
1223
|
updateBoard(board) {
|
|
1224
|
+
this.recordHistory('board');
|
|
1114
1225
|
this.board = board;
|
|
1115
1226
|
this.dirty = true;
|
|
1116
1227
|
}
|
|
@@ -1127,6 +1238,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1127
1238
|
changeDetailLevel(level) {
|
|
1128
1239
|
if (!this.board)
|
|
1129
1240
|
return;
|
|
1241
|
+
this.recordHistory('detailLevel');
|
|
1130
1242
|
this.board = { ...this.board, detailLevel: level };
|
|
1131
1243
|
this.dirty = true;
|
|
1132
1244
|
}
|
|
@@ -1163,7 +1275,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1163
1275
|
partChanged(name, part) {
|
|
1164
1276
|
const at = this.parts.findIndex(item => item.name === name);
|
|
1165
1277
|
if (at >= 0)
|
|
1166
|
-
this.put(edits.updatePart(this.parts, at, part));
|
|
1278
|
+
this.put(edits.updatePart(this.parts, at, part), this.selected, `drag:${name}`);
|
|
1167
1279
|
}
|
|
1168
1280
|
/**
|
|
1169
1281
|
* 정밀 배치 도구.
|
|
@@ -1365,6 +1477,14 @@ __decorate([
|
|
|
1365
1477
|
state(),
|
|
1366
1478
|
__metadata("design:type", Array)
|
|
1367
1479
|
], FigureModellerPage.prototype, "proposalFeedback", void 0);
|
|
1480
|
+
__decorate([
|
|
1481
|
+
state(),
|
|
1482
|
+
__metadata("design:type", Array)
|
|
1483
|
+
], FigureModellerPage.prototype, "undoStack", void 0);
|
|
1484
|
+
__decorate([
|
|
1485
|
+
state(),
|
|
1486
|
+
__metadata("design:type", Array)
|
|
1487
|
+
], FigureModellerPage.prototype, "redoStack", void 0);
|
|
1368
1488
|
FigureModellerPage = __decorate([
|
|
1369
1489
|
customElement('figure-modeller-page')
|
|
1370
1490
|
], FigureModellerPage);
|