@things-factory/figure-ui 10.1.13 → 10.1.15
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 +12 -0
- package/client/modeller/figure-canvas.ts +26 -4
- package/client/modeller/figure-inspector.ts +48 -7
- package/client/modeller/figure-side.ts +1 -0
- package/client/modeller/figure-source.test.ts +2 -0
- package/client/modeller/figure-source.ts +17 -21
- package/client/pages/figure-modeller-page.ts +521 -161
- package/dist-client/modeller/figure-ai-target.d.ts +10 -0
- package/dist-client/modeller/figure-ai-target.js +10 -0
- package/dist-client/modeller/figure-ai-target.js.map +1 -1
- package/dist-client/modeller/figure-canvas.d.ts +2 -0
- package/dist-client/modeller/figure-canvas.js +29 -4
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +4 -1
- package/dist-client/modeller/figure-inspector.js +47 -6
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-side.js +1 -0
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +15 -21
- package/dist-client/modeller/figure-source.js +5 -0
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +20 -0
- package/dist-client/pages/figure-modeller-page.js +531 -163
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/ai-proposal-contract.test.ts +21 -2
- package/test/modeller-undo-redo.test.ts +33 -0
- package/translations/en.json +12 -1
- package/translations/ja.json +2 -0
- package/translations/ko.json +13 -1
- package/translations/ms.json +2 -0
- package/translations/zh.json +2 -0
|
@@ -8,12 +8,12 @@ import '../modeller/figure-preview.js';
|
|
|
8
8
|
import { shouldStartNewFigure } from '../modeller/new-figure-entry.js';
|
|
9
9
|
import '../modeller/figure-parts.js';
|
|
10
10
|
import '../modeller/figure-inspector.js';
|
|
11
|
-
import { css, html } from 'lit';
|
|
11
|
+
import { css, html, nothing } from 'lit';
|
|
12
12
|
import { customElement, state } from 'lit/decorators.js';
|
|
13
13
|
import { i18next, localize } from '@operato/i18n';
|
|
14
14
|
import { navigate, PageView } from '@operato/shell';
|
|
15
15
|
import { openOverlay } from '@operato/layout';
|
|
16
|
-
import { requestFigureAI } from '../modeller/figure-ai-target.js';
|
|
16
|
+
import { requestFigureAI, consumePendingFigureProposal } from '../modeller/figure-ai-target.js';
|
|
17
17
|
import { validate } from '@hatiolab/figure-model';
|
|
18
18
|
import * as edits from '../modeller/part-edits.js';
|
|
19
19
|
import * as proposals from '../modeller/proposal.js';
|
|
@@ -130,8 +130,16 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
130
130
|
* 미저장 표시(`dirty`)는 그대로 남으므로 데이터를 잃지는 않았다. 잃은 것은 **사유**다.
|
|
131
131
|
*/
|
|
132
132
|
this.saveFailure = '';
|
|
133
|
+
this.showSaveModal = false;
|
|
134
|
+
this.saveModalType = '';
|
|
135
|
+
this.saveModalName = '';
|
|
136
|
+
this.saveModalError = '';
|
|
133
137
|
/** 인라인 요청창과 AI 도크가 함께 쓰는 현재 Figure의 제한된 세션 피드백. */
|
|
134
138
|
this.proposalFeedback = [];
|
|
139
|
+
this.undoStack = [];
|
|
140
|
+
this.redoStack = [];
|
|
141
|
+
this.sceneRevision = 0;
|
|
142
|
+
this.lastHistoryTime = 0;
|
|
135
143
|
this.announceAiContext = () => {
|
|
136
144
|
if (!this.active)
|
|
137
145
|
return;
|
|
@@ -141,14 +149,53 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
141
149
|
};
|
|
142
150
|
this.acceptDockProposal = (event) => {
|
|
143
151
|
const detail = event.detail;
|
|
144
|
-
if (!
|
|
152
|
+
if (!detail)
|
|
145
153
|
return;
|
|
146
|
-
if (detail?.
|
|
154
|
+
if (detail?.baseSource && detail.baseSource !== JSON.stringify(this.folded)) {
|
|
155
|
+
try {
|
|
156
|
+
const parsedBase = typeof detail.baseSource === 'string' ? JSON.parse(detail.baseSource) : detail.baseSource;
|
|
157
|
+
if (parsedBase?.type && this.folded?.type && parsedBase.type !== this.folded.type) {
|
|
158
|
+
// type 불일치 시에만 차단하고 그 외 미세 차이는 수용
|
|
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);
|
|
166
|
+
if (source) {
|
|
167
|
+
this.applySourceDirectly(source, detail.proposal);
|
|
168
|
+
detail.accepted = true;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
this.onFigureAiStage = (event) => {
|
|
172
|
+
const detail = event.detail;
|
|
173
|
+
const source = detail?.source || (detail?.proposal?.source ? (typeof detail.proposal.source === 'string' ? JSON.parse(detail.proposal.source) : detail.proposal.source) : undefined);
|
|
174
|
+
if (source) {
|
|
175
|
+
this.applySourceDirectly(source, detail.proposal);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
this.onSaveRequest = () => {
|
|
179
|
+
if (this.active)
|
|
180
|
+
this.save();
|
|
181
|
+
};
|
|
182
|
+
this.handleKeyDown = (e) => {
|
|
183
|
+
if (!this.active || this.mode !== 'edit' || this.proposalSession)
|
|
147
184
|
return;
|
|
148
|
-
|
|
185
|
+
const target = e.composedPath()[0];
|
|
186
|
+
const tag = target?.tagName?.toLowerCase();
|
|
187
|
+
if (tag === 'input' || tag === 'textarea' || target?.isContentEditable)
|
|
149
188
|
return;
|
|
150
|
-
|
|
151
|
-
|
|
189
|
+
if ((e.metaKey || e.ctrlKey) && !e.altKey) {
|
|
190
|
+
if (e.key === 'z' && !e.shiftKey) {
|
|
191
|
+
e.preventDefault();
|
|
192
|
+
this.undo();
|
|
193
|
+
}
|
|
194
|
+
else if ((e.key === 'z' && e.shiftKey) || e.key === 'y') {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
this.redo();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
152
199
|
};
|
|
153
200
|
this.loadGeneration = 0;
|
|
154
201
|
}
|
|
@@ -223,90 +270,9 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
223
270
|
display: none;
|
|
224
271
|
}
|
|
225
272
|
|
|
226
|
-
/*
|
|
227
|
-
머리줄 — 이름 · 타입 · 미저장 표시 · 말로 시키는 줄 · 저장을 **한 줄에** 담는다.
|
|
228
|
-
|
|
229
|
-
이름은 페이지 제목에도 나온다(셸이 context.title 로 그린다). 그래도 여기 입력칸을 두는
|
|
230
|
-
이유는 **고치는 자리**가 필요해서다 — 제목은 읽는 것이고 이 칸은 바꾸는 것이다. 두 줄로
|
|
231
|
-
벌리는 대신 한 줄에 모아 캔버스에 높이를 넘긴다.
|
|
232
|
-
*/
|
|
233
|
-
div[header] {
|
|
234
|
-
display: flex;
|
|
235
|
-
align-items: center;
|
|
236
|
-
gap: var(--spacing-medium, 8px);
|
|
237
|
-
padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
|
|
238
|
-
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
239
|
-
}
|
|
240
|
-
button[ai-support] {
|
|
241
|
-
height: 28px;
|
|
242
|
-
padding: 0 12px;
|
|
243
|
-
border: 1px solid var(--md-sys-color-outline-variant);
|
|
244
|
-
border-radius: 6px;
|
|
245
|
-
background: var(--md-sys-color-primary-container);
|
|
246
|
-
color: var(--md-sys-color-on-primary-container);
|
|
247
|
-
font: inherit;
|
|
248
|
-
cursor: pointer;
|
|
249
|
-
}
|
|
250
|
-
input[name] {
|
|
251
|
-
flex: none;
|
|
252
|
-
width: 180px;
|
|
253
|
-
padding: 5px 9px;
|
|
254
|
-
font: var(--input-field-font, inherit);
|
|
255
|
-
font-size: 0.82rem;
|
|
256
|
-
color: var(--md-sys-color-on-surface);
|
|
257
|
-
background-color: var(--md-sys-color-surface-container-lowest);
|
|
258
|
-
border: 1px solid var(--md-sys-color-outline-variant);
|
|
259
|
-
border-radius: 6px;
|
|
260
|
-
}
|
|
261
|
-
/* 타입 이름은 만든 뒤 고칠 수 없다 — 그 사실이 화면에서 보여야 한다 */
|
|
262
|
-
code[type] {
|
|
263
|
-
padding: 4px 8px;
|
|
264
|
-
font-family: var(--mono-font, monospace);
|
|
265
|
-
font-size: 0.76rem;
|
|
266
|
-
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
267
|
-
background-color: var(--md-sys-color-surface-container);
|
|
268
|
-
border-radius: 6px;
|
|
269
|
-
}
|
|
270
|
-
span[dirty] {
|
|
271
|
-
font: var(--label-font, inherit);
|
|
272
|
-
font-size: 0.74rem;
|
|
273
|
-
color: var(--md-sys-color-tertiary);
|
|
274
|
-
}
|
|
275
273
|
div[spacer] {
|
|
276
274
|
flex: 1;
|
|
277
275
|
}
|
|
278
|
-
button[save] {
|
|
279
|
-
padding: 6px 16px;
|
|
280
|
-
font: var(--label-font, inherit);
|
|
281
|
-
font-size: 0.76rem;
|
|
282
|
-
font-weight: 600;
|
|
283
|
-
color: var(--md-sys-color-on-primary, #fff);
|
|
284
|
-
background-color: var(--md-sys-color-primary);
|
|
285
|
-
border: none;
|
|
286
|
-
border-radius: 6px;
|
|
287
|
-
cursor: pointer;
|
|
288
|
-
}
|
|
289
|
-
button[save][disabled] {
|
|
290
|
-
opacity: 0.4;
|
|
291
|
-
cursor: default;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/*
|
|
295
|
-
저장이 거절된 사유. 단추 **바로 왼쪽**에 둔다 — 누른 자리에서 답을 봐야 한다. 길면 줄이지만
|
|
296
|
-
전문은 title 로 남긴다(서버 문장이 길 수 있다).
|
|
297
|
-
*/
|
|
298
|
-
span[save-failed] {
|
|
299
|
-
max-width: 280px;
|
|
300
|
-
overflow: hidden;
|
|
301
|
-
text-overflow: ellipsis;
|
|
302
|
-
white-space: nowrap;
|
|
303
|
-
padding: 4px 8px;
|
|
304
|
-
font: var(--label-font, inherit);
|
|
305
|
-
font-size: 0.72rem;
|
|
306
|
-
color: var(--md-sys-color-on-error-container, var(--md-sys-color-on-surface));
|
|
307
|
-
background-color: var(--md-sys-color-error-container);
|
|
308
|
-
border-radius: 6px;
|
|
309
|
-
}
|
|
310
276
|
|
|
311
277
|
/*
|
|
312
278
|
칸을 벡터 저작도구의 관례대로 놓는다 — 왼쪽에 무엇이 있나(레이어), 오른쪽에
|
|
@@ -329,16 +295,17 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
329
295
|
}
|
|
330
296
|
|
|
331
297
|
/*
|
|
332
|
-
편집 · 미리보기
|
|
298
|
+
편집 · 미리보기 전환 및 조작 띠.
|
|
333
299
|
|
|
334
300
|
가운데 칸 위에 얇게 둔다. 무엇을 보고 있는지가 늘 보여야 하고, 두 화면이 같은
|
|
335
301
|
자리를 쓰므로 어느 쪽인지 모르면 편집이 안 먹는 것처럼 읽힌다.
|
|
336
302
|
*/
|
|
337
303
|
div[modes] {
|
|
338
304
|
display: flex;
|
|
305
|
+
align-items: center;
|
|
339
306
|
flex: none;
|
|
340
307
|
gap: 2px;
|
|
341
|
-
padding:
|
|
308
|
+
padding: 4px var(--spacing-medium, 8px);
|
|
342
309
|
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
343
310
|
}
|
|
344
311
|
div[modes] button[mode] {
|
|
@@ -359,13 +326,36 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
359
326
|
color: var(--md-sys-color-on-primary-container);
|
|
360
327
|
background-color: var(--md-sys-color-primary-container);
|
|
361
328
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
329
|
+
div[modes] div[separator] {
|
|
330
|
+
width: 1px;
|
|
331
|
+
height: 16px;
|
|
332
|
+
margin: 0 4px;
|
|
333
|
+
background-color: var(--md-sys-color-outline-variant);
|
|
334
|
+
}
|
|
335
|
+
div[modes] button[history] {
|
|
336
|
+
display: flex;
|
|
337
|
+
align-items: center;
|
|
338
|
+
justify-content: center;
|
|
339
|
+
width: 26px;
|
|
340
|
+
height: 26px;
|
|
341
|
+
padding: 0;
|
|
342
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
343
|
+
background: none;
|
|
344
|
+
border: none;
|
|
345
|
+
border-radius: 6px;
|
|
346
|
+
cursor: pointer;
|
|
347
|
+
}
|
|
348
|
+
div[modes] button[history]:hover:not([disabled]) {
|
|
349
|
+
background-color: var(--md-sys-color-surface-container);
|
|
350
|
+
color: var(--md-sys-color-on-surface);
|
|
351
|
+
}
|
|
352
|
+
div[modes] button[history][disabled] {
|
|
353
|
+
opacity: 0.3;
|
|
354
|
+
cursor: default;
|
|
355
|
+
}
|
|
356
|
+
div[modes] button[history] md-icon {
|
|
357
|
+
--md-icon-size: 18px;
|
|
358
|
+
}
|
|
369
359
|
div[modes] button[collapse] {
|
|
370
360
|
display: flex;
|
|
371
361
|
align-items: center;
|
|
@@ -383,6 +373,67 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
383
373
|
div[modes] button[collapse] md-icon {
|
|
384
374
|
--md-icon-size: 18px;
|
|
385
375
|
}
|
|
376
|
+
div[modes] span[dirty] {
|
|
377
|
+
align-self: center;
|
|
378
|
+
margin-right: 6px;
|
|
379
|
+
font: var(--label-font, inherit);
|
|
380
|
+
font-size: 0.72rem;
|
|
381
|
+
color: var(--md-sys-color-tertiary);
|
|
382
|
+
}
|
|
383
|
+
div[modes] span[save-failed] {
|
|
384
|
+
align-self: center;
|
|
385
|
+
max-width: 240px;
|
|
386
|
+
overflow: hidden;
|
|
387
|
+
text-overflow: ellipsis;
|
|
388
|
+
white-space: nowrap;
|
|
389
|
+
margin-right: 6px;
|
|
390
|
+
padding: 3px 7px;
|
|
391
|
+
font: var(--label-font, inherit);
|
|
392
|
+
font-size: 0.7rem;
|
|
393
|
+
color: var(--md-sys-color-on-error-container, var(--md-sys-color-on-surface));
|
|
394
|
+
background-color: var(--md-sys-color-error-container);
|
|
395
|
+
border-radius: 4px;
|
|
396
|
+
}
|
|
397
|
+
div[modes] button[save] {
|
|
398
|
+
padding: 4px 14px;
|
|
399
|
+
margin-right: 4px;
|
|
400
|
+
font: var(--label-font, inherit);
|
|
401
|
+
font-size: 0.74rem;
|
|
402
|
+
font-weight: 600;
|
|
403
|
+
color: var(--md-sys-color-on-primary, #fff);
|
|
404
|
+
background-color: var(--md-sys-color-primary);
|
|
405
|
+
border: none;
|
|
406
|
+
border-radius: 6px;
|
|
407
|
+
cursor: pointer;
|
|
408
|
+
}
|
|
409
|
+
div[modes] button[save]:hover:not([disabled]) {
|
|
410
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
411
|
+
}
|
|
412
|
+
div[modes] button[save][disabled] {
|
|
413
|
+
opacity: 0.35;
|
|
414
|
+
cursor: default;
|
|
415
|
+
}
|
|
416
|
+
div[modes] button[ai-support] {
|
|
417
|
+
display: inline-flex;
|
|
418
|
+
align-items: center;
|
|
419
|
+
gap: 4px;
|
|
420
|
+
padding: 3px 10px;
|
|
421
|
+
margin-right: 6px;
|
|
422
|
+
font: var(--label-font, inherit);
|
|
423
|
+
font-size: 0.74rem;
|
|
424
|
+
font-weight: 600;
|
|
425
|
+
color: var(--md-sys-color-primary);
|
|
426
|
+
background: var(--md-sys-color-primary-container);
|
|
427
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
428
|
+
border-radius: 6px;
|
|
429
|
+
cursor: pointer;
|
|
430
|
+
}
|
|
431
|
+
div[modes] button[ai-support]:hover {
|
|
432
|
+
background-color: var(--md-sys-color-surface-container-high);
|
|
433
|
+
}
|
|
434
|
+
div[modes] button[ai-support] md-icon {
|
|
435
|
+
--md-icon-size: 15px;
|
|
436
|
+
}
|
|
386
437
|
|
|
387
438
|
/*
|
|
388
439
|
후보가 오면 가운데를 둘로 나눈다.
|
|
@@ -569,6 +620,86 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
569
620
|
line-height: 1.5;
|
|
570
621
|
color: var(--md-sys-color-error);
|
|
571
622
|
}
|
|
623
|
+
.save-modal-backdrop {
|
|
624
|
+
position: fixed;
|
|
625
|
+
inset: 0;
|
|
626
|
+
background: rgba(0, 0, 0, 0.5);
|
|
627
|
+
backdrop-filter: blur(2px);
|
|
628
|
+
display: flex;
|
|
629
|
+
align-items: center;
|
|
630
|
+
justify-content: center;
|
|
631
|
+
z-index: 1000;
|
|
632
|
+
}
|
|
633
|
+
.save-modal-card {
|
|
634
|
+
background: var(--md-sys-color-surface-container-high, #fff);
|
|
635
|
+
color: var(--md-sys-color-on-surface, #1f1f1f);
|
|
636
|
+
border-radius: 12px;
|
|
637
|
+
padding: 24px;
|
|
638
|
+
width: 380px;
|
|
639
|
+
max-width: 90vw;
|
|
640
|
+
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.28);
|
|
641
|
+
display: flex;
|
|
642
|
+
flex-direction: column;
|
|
643
|
+
gap: 12px;
|
|
644
|
+
}
|
|
645
|
+
.save-modal-title {
|
|
646
|
+
margin: 0;
|
|
647
|
+
font-size: 1.15rem;
|
|
648
|
+
font-weight: 700;
|
|
649
|
+
}
|
|
650
|
+
.save-modal-desc {
|
|
651
|
+
margin: 0;
|
|
652
|
+
font-size: 0.8rem;
|
|
653
|
+
color: var(--md-sys-color-on-surface-variant, #666);
|
|
654
|
+
}
|
|
655
|
+
.save-modal-field {
|
|
656
|
+
display: flex;
|
|
657
|
+
flex-direction: column;
|
|
658
|
+
gap: 4px;
|
|
659
|
+
}
|
|
660
|
+
.save-modal-field label {
|
|
661
|
+
font-size: 0.76rem;
|
|
662
|
+
font-weight: 600;
|
|
663
|
+
color: var(--md-sys-color-on-surface-variant, #555);
|
|
664
|
+
}
|
|
665
|
+
.save-modal-field input {
|
|
666
|
+
padding: 8px 10px;
|
|
667
|
+
border: 1px solid var(--md-sys-color-outline, #ccc);
|
|
668
|
+
border-radius: 6px;
|
|
669
|
+
font-size: 0.88rem;
|
|
670
|
+
background: var(--md-sys-color-surface, #fff);
|
|
671
|
+
color: inherit;
|
|
672
|
+
}
|
|
673
|
+
.save-modal-error {
|
|
674
|
+
color: var(--md-sys-color-error, #b00020);
|
|
675
|
+
font-size: 0.78rem;
|
|
676
|
+
}
|
|
677
|
+
.save-modal-actions {
|
|
678
|
+
display: flex;
|
|
679
|
+
justify-content: flex-end;
|
|
680
|
+
gap: 8px;
|
|
681
|
+
margin-top: 8px;
|
|
682
|
+
}
|
|
683
|
+
.save-modal-actions button {
|
|
684
|
+
padding: 6px 14px;
|
|
685
|
+
border-radius: 6px;
|
|
686
|
+
font-size: 0.82rem;
|
|
687
|
+
cursor: pointer;
|
|
688
|
+
border: none;
|
|
689
|
+
}
|
|
690
|
+
.save-modal-actions .btn-cancel {
|
|
691
|
+
background: none;
|
|
692
|
+
color: var(--md-sys-color-on-surface, inherit);
|
|
693
|
+
}
|
|
694
|
+
.save-modal-actions .btn-confirm {
|
|
695
|
+
background: var(--md-sys-color-primary, #0066cc);
|
|
696
|
+
color: var(--md-sys-color-on-primary, #fff);
|
|
697
|
+
font-weight: 600;
|
|
698
|
+
}
|
|
699
|
+
.save-modal-actions .btn-confirm:disabled {
|
|
700
|
+
opacity: 0.5;
|
|
701
|
+
cursor: not-allowed;
|
|
702
|
+
}
|
|
572
703
|
`; }
|
|
573
704
|
willUpdate(changed) {
|
|
574
705
|
if (changed.has('board') || changed.has('parts')) {
|
|
@@ -596,12 +727,44 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
596
727
|
}));
|
|
597
728
|
}
|
|
598
729
|
}
|
|
730
|
+
applySourceDirectly(rawSource, proposal) {
|
|
731
|
+
const source = typeof rawSource === 'string' ? JSON.parse(rawSource) : rawSource;
|
|
732
|
+
if (!source || !Array.isArray(source.parts))
|
|
733
|
+
return;
|
|
734
|
+
this.recordHistory();
|
|
735
|
+
try {
|
|
736
|
+
const { board, parts } = fromFigureSource(source);
|
|
737
|
+
this.board = board;
|
|
738
|
+
this.parts = parts;
|
|
739
|
+
this.dirty = true;
|
|
740
|
+
this.selected = parts.length ? 0 : -1;
|
|
741
|
+
if (source.type) {
|
|
742
|
+
this.figure = { ...(this.figure || {}), type: source.type };
|
|
743
|
+
if (this.board) {
|
|
744
|
+
this.board = { ...this.board, figureType: source.type };
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
this.proposalSession = undefined;
|
|
748
|
+
this.sceneRevision++;
|
|
749
|
+
this.requestUpdate();
|
|
750
|
+
document.dispatchEvent(new CustomEvent('notify', { detail: { message: `${source.type || 'Figure'} 모델이 3D 뷰어에 반영되었습니다.` } }));
|
|
751
|
+
}
|
|
752
|
+
catch (e) {
|
|
753
|
+
console.error('[FigureModeller] Failed to apply AI proposal source:', e);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
599
756
|
connectedCallback() {
|
|
600
757
|
super.connectedCallback();
|
|
601
758
|
window.addEventListener('figure-ai-context-request', this.announceAiContext);
|
|
602
759
|
window.addEventListener('figure-ai-review', this.acceptDockProposal);
|
|
760
|
+
window.addEventListener('figure-ai-stage', this.onFigureAiStage);
|
|
761
|
+
window.addEventListener('figure-save-request', this.onSaveRequest);
|
|
762
|
+
window.addEventListener('keydown', this.handleKeyDown);
|
|
603
763
|
}
|
|
604
764
|
disconnectedCallback() {
|
|
765
|
+
window.removeEventListener('keydown', this.handleKeyDown);
|
|
766
|
+
window.removeEventListener('figure-save-request', this.onSaveRequest);
|
|
767
|
+
window.removeEventListener('figure-ai-stage', this.onFigureAiStage);
|
|
605
768
|
window.removeEventListener('figure-ai-context-request', this.announceAiContext);
|
|
606
769
|
window.removeEventListener('figure-ai-review', this.acceptDockProposal);
|
|
607
770
|
window.dispatchEvent(new CustomEvent('figure-ai-context', { detail: undefined }));
|
|
@@ -626,32 +789,12 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
626
789
|
render() {
|
|
627
790
|
const source = this.folded;
|
|
628
791
|
return html `
|
|
629
|
-
<div header>
|
|
630
|
-
<!--
|
|
631
|
-
이름과 타입은 **속성 패널**에 있다(figure-inspector 의 도형 절). 여기 있던 이름 칸은
|
|
632
|
-
하네스 시절의 자리였다 — 앱에서는 셸이 페이지 제목으로 같은 이름을 그리므로 글자가 두 번
|
|
633
|
-
나오고, 캔버스가 주인공인 화면에서 그만큼 높이를 잃었다.
|
|
634
|
-
-->
|
|
635
|
-
${this.dirty ? html `<span dirty>${i18next.t('figure.text.unsaved-changes')}</span>` : ''}
|
|
636
|
-
<!--
|
|
637
|
-
말로 시키는 줄이 머리줄의 남는 폭을 쓴다. 따로 두면 화면 위가 두 줄이 되고, 그 위 줄은
|
|
638
|
-
이름을 페이지 제목과 겹쳐 적는다.
|
|
639
|
-
-->
|
|
640
|
-
<button ai-support @click=${() => {
|
|
641
|
-
requestFigureAI({ figureId: this.figure?.id, source: this.folded });
|
|
642
|
-
openOverlay('figure-ai-dock', { backdrop: false });
|
|
643
|
-
}}>AI 지원</button>
|
|
644
|
-
${this.saveFailure ? html `<span save-failed title=${this.saveFailure}>${this.saveFailure}</span>` : ''}
|
|
645
|
-
<button save ?disabled=${!this.canSave} @click=${() => this.save()}>
|
|
646
|
-
${this.saving ? i18next.t('figure.text.saving-figure') : i18next.t('figure.button.save')}
|
|
647
|
-
</button>
|
|
648
|
-
</div>
|
|
649
|
-
|
|
650
792
|
<div
|
|
651
793
|
edits
|
|
652
794
|
?collapse-left=${this.collapsed.left}
|
|
653
795
|
?collapse-right=${this.collapsed.right}
|
|
654
796
|
@figure-rename=${(e) => this.rename(e.detail.name)}
|
|
797
|
+
@figure-type-change=${(e) => this.typeChanged(e.detail.type)}
|
|
655
798
|
@figure-catalog=${(e) => this.catalog(e.detail)}
|
|
656
799
|
@figure-released=${(e) => this.released(e.detail.figure)}
|
|
657
800
|
@figure-reverted=${() => this.figure?.id && this.load(this.figure.id)}
|
|
@@ -687,10 +830,12 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
687
830
|
<div lane>
|
|
688
831
|
<h4>${i18next.t('figure.label.now')}</h4>
|
|
689
832
|
<figure-canvas
|
|
833
|
+
.figureId=${this.figure?.id || this.figure?.type}
|
|
690
834
|
.board=${this.board}
|
|
691
835
|
.parts=${this.parts}
|
|
692
836
|
.picked=${this.pickedName}
|
|
693
837
|
.view=${this.view}
|
|
838
|
+
.revision=${this.sceneRevision}
|
|
694
839
|
></figure-canvas>
|
|
695
840
|
</div>
|
|
696
841
|
<div lane candidate>
|
|
@@ -711,9 +856,12 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
711
856
|
: this.mode === 'preview'
|
|
712
857
|
? html `<figure-preview .source=${source}></figure-preview>`
|
|
713
858
|
: html `<figure-canvas
|
|
859
|
+
.figureId=${this.figure?.id || this.figure?.type}
|
|
714
860
|
.board=${this.board}
|
|
715
861
|
.parts=${this.parts}
|
|
716
862
|
.picked=${this.pickedName}
|
|
863
|
+
.view=${this.view}
|
|
864
|
+
.revision=${this.sceneRevision}
|
|
717
865
|
></figure-canvas>`}
|
|
718
866
|
</div>
|
|
719
867
|
|
|
@@ -733,6 +881,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
733
881
|
@update-view=${(e) => (this.view = e.detail.view)}
|
|
734
882
|
></figure-side>
|
|
735
883
|
</div>
|
|
884
|
+
${this.renderSaveModal()}
|
|
736
885
|
`;
|
|
737
886
|
}
|
|
738
887
|
/**
|
|
@@ -765,7 +914,44 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
765
914
|
`;
|
|
766
915
|
return html `<div modes>
|
|
767
916
|
${this.renderCollapse('left')}${this.proposalSession ? '' : html `${one('edit')}${one('preview')}`}
|
|
917
|
+
${this.mode === 'edit' && !this.proposalSession
|
|
918
|
+
? html `
|
|
919
|
+
<div separator></div>
|
|
920
|
+
<button
|
|
921
|
+
history
|
|
922
|
+
?disabled=${this.undoStack.length === 0}
|
|
923
|
+
title="${i18next.t('figure.button.undo')} (Ctrl+Z)"
|
|
924
|
+
@click=${() => this.undo()}
|
|
925
|
+
>
|
|
926
|
+
<md-icon>undo</md-icon>
|
|
927
|
+
</button>
|
|
928
|
+
<button
|
|
929
|
+
history
|
|
930
|
+
?disabled=${this.redoStack.length === 0}
|
|
931
|
+
title="${i18next.t('figure.button.redo')} (Ctrl+Y)"
|
|
932
|
+
@click=${() => this.redo()}
|
|
933
|
+
>
|
|
934
|
+
<md-icon>redo</md-icon>
|
|
935
|
+
</button>
|
|
936
|
+
`
|
|
937
|
+
: ''}
|
|
768
938
|
<div spacer></div>
|
|
939
|
+
<button
|
|
940
|
+
ai-support
|
|
941
|
+
title=${i18next.t('figure.button.ask')}
|
|
942
|
+
@click=${() => {
|
|
943
|
+
requestFigureAI({ figureId: this.figure?.id, source: this.folded });
|
|
944
|
+
openOverlay('figure-ai-dock', { backdrop: false });
|
|
945
|
+
}}
|
|
946
|
+
>
|
|
947
|
+
<md-icon>auto_awesome</md-icon>
|
|
948
|
+
<span>AI</span>
|
|
949
|
+
</button>
|
|
950
|
+
${this.saveFailure ? html `<span save-failed title=${this.saveFailure}>${this.saveFailure}</span>` : ''}
|
|
951
|
+
${this.dirty ? html `<span dirty>${i18next.t('figure.text.unsaved-changes')}</span>` : ''}
|
|
952
|
+
<button save ?disabled=${!this.canSave} @click=${() => this.save()}>
|
|
953
|
+
${this.saving ? i18next.t('figure.text.saving-figure') : i18next.t('figure.button.save')}
|
|
954
|
+
</button>
|
|
769
955
|
${this.renderCollapse('right')}
|
|
770
956
|
</div>`;
|
|
771
957
|
}
|
|
@@ -956,6 +1142,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
956
1142
|
return;
|
|
957
1143
|
if (this.proposalBaseSource !== undefined && this.proposalBaseSource !== JSON.stringify(this.folded))
|
|
958
1144
|
return;
|
|
1145
|
+
this.recordHistory();
|
|
959
1146
|
const taken = proposals.applyProposal(this.folded, this.proposalSession.source, this.proposalSession.picked);
|
|
960
1147
|
const { board, parts } = fromFigureSource(taken);
|
|
961
1148
|
this.board = board;
|
|
@@ -1008,7 +1195,11 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1008
1195
|
* 저장이 거절되면 `dirty` 를 그대로 두므로 다시 누를 수 있다.
|
|
1009
1196
|
*/
|
|
1010
1197
|
get canSave() {
|
|
1011
|
-
if (this.saving || !this.dirty || !this.folded
|
|
1198
|
+
if (this.saving || !this.dirty || !this.folded) {
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
// 기존 저장된 모델은 이름이 있어야 바로 저장 가능. 신규 모델은 저장 팝업에서 이름을 입력할 수 있음
|
|
1202
|
+
if (this.figure?.id && !this.figure?.name) {
|
|
1012
1203
|
return false;
|
|
1013
1204
|
}
|
|
1014
1205
|
return validate(this.folded).errors.length === 0;
|
|
@@ -1020,9 +1211,13 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1020
1211
|
}
|
|
1021
1212
|
const id = lifecycle?.resourceId;
|
|
1022
1213
|
if (id) {
|
|
1023
|
-
if (id !== this.figure?.id) {
|
|
1214
|
+
if (changes.active || id !== this.figure?.id) {
|
|
1024
1215
|
await this.load(id);
|
|
1025
1216
|
}
|
|
1217
|
+
const pending = consumePendingFigureProposal();
|
|
1218
|
+
if (pending?.source) {
|
|
1219
|
+
this.applySourceDirectly(pending.source, pending.proposal);
|
|
1220
|
+
}
|
|
1026
1221
|
return;
|
|
1027
1222
|
}
|
|
1028
1223
|
/*
|
|
@@ -1037,10 +1232,34 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1037
1232
|
들고 있는 것이 **저장된 도형이면**(id 가 있으면) 새로 시작한다. 아직 저장 안 한 새 도형을
|
|
1038
1233
|
들고 있으면 그대로 둔다 — 페이지를 다시 들렀다고 저작 중인 것을 버리지 않는다.
|
|
1039
1234
|
*/
|
|
1235
|
+
if (!this.figure) {
|
|
1236
|
+
this.startNew();
|
|
1237
|
+
const pending = consumePendingFigureProposal();
|
|
1238
|
+
if (pending?.source) {
|
|
1239
|
+
this.applySourceDirectly(pending.source, pending.proposal);
|
|
1240
|
+
}
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (this.figure.id) {
|
|
1244
|
+
this.startNew();
|
|
1245
|
+
const pending = consumePendingFigureProposal();
|
|
1246
|
+
if (pending?.source) {
|
|
1247
|
+
this.applySourceDirectly(pending.source, pending.proposal);
|
|
1248
|
+
}
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1040
1251
|
if (shouldStartNewFigure(changes, !!this.figure, this.figure?.id)) {
|
|
1041
1252
|
this.startNew();
|
|
1253
|
+
const pending = consumePendingFigureProposal();
|
|
1254
|
+
if (pending?.source) {
|
|
1255
|
+
this.applySourceDirectly(pending.source, pending.proposal);
|
|
1256
|
+
}
|
|
1042
1257
|
return;
|
|
1043
1258
|
}
|
|
1259
|
+
const pending = consumePendingFigureProposal();
|
|
1260
|
+
if (pending?.source) {
|
|
1261
|
+
this.applySourceDirectly(pending.source, pending.proposal);
|
|
1262
|
+
}
|
|
1044
1263
|
}
|
|
1045
1264
|
async load(id) {
|
|
1046
1265
|
const generation = ++this.loadGeneration;
|
|
@@ -1079,6 +1298,9 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1079
1298
|
this.selected = -1;
|
|
1080
1299
|
this.dirty = false;
|
|
1081
1300
|
this.violations = [];
|
|
1301
|
+
this.undoStack = [];
|
|
1302
|
+
this.redoStack = [];
|
|
1303
|
+
this.lastCoalesceKey = undefined;
|
|
1082
1304
|
}
|
|
1083
1305
|
startNew() {
|
|
1084
1306
|
++this.loadGeneration;
|
|
@@ -1096,6 +1318,9 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1096
1318
|
this.selected = -1;
|
|
1097
1319
|
this.dirty = true;
|
|
1098
1320
|
this.violations = [];
|
|
1321
|
+
this.undoStack = [];
|
|
1322
|
+
this.redoStack = [];
|
|
1323
|
+
this.lastCoalesceKey = undefined;
|
|
1099
1324
|
}
|
|
1100
1325
|
rename(name) {
|
|
1101
1326
|
if (!this.figure)
|
|
@@ -1103,6 +1328,16 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1103
1328
|
this.figure = { ...this.figure, name };
|
|
1104
1329
|
this.dirty = true;
|
|
1105
1330
|
}
|
|
1331
|
+
typeChanged(type) {
|
|
1332
|
+
if (!type || this.figure?.id)
|
|
1333
|
+
return;
|
|
1334
|
+
const sanitized = type.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase();
|
|
1335
|
+
if (this.board)
|
|
1336
|
+
this.board.figureType = sanitized;
|
|
1337
|
+
this.figure = { ...this.figure, type: sanitized };
|
|
1338
|
+
this.folded = this.board ? toFigureSource(this.board, this.parts) : undefined;
|
|
1339
|
+
this.dirty = true;
|
|
1340
|
+
}
|
|
1106
1341
|
/**
|
|
1107
1342
|
* 분류와 태그를 고친다.
|
|
1108
1343
|
*
|
|
@@ -1115,13 +1350,56 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1115
1350
|
this.figure = { ...this.figure, ...patch };
|
|
1116
1351
|
this.dirty = true;
|
|
1117
1352
|
}
|
|
1353
|
+
snapshotState() {
|
|
1354
|
+
return {
|
|
1355
|
+
board: this.board ? structuredClone(this.board) : undefined,
|
|
1356
|
+
parts: structuredClone(this.parts),
|
|
1357
|
+
selected: this.selected
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
recordHistory(coalesceKey) {
|
|
1361
|
+
const now = Date.now();
|
|
1362
|
+
if (coalesceKey && this.lastCoalesceKey === coalesceKey && now - this.lastHistoryTime < 600) {
|
|
1363
|
+
this.lastHistoryTime = now;
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
this.lastCoalesceKey = coalesceKey;
|
|
1367
|
+
this.lastHistoryTime = now;
|
|
1368
|
+
this.undoStack = [...this.undoStack.slice(-49), this.snapshotState()];
|
|
1369
|
+
this.redoStack = [];
|
|
1370
|
+
}
|
|
1371
|
+
undo() {
|
|
1372
|
+
if (this.undoStack.length === 0)
|
|
1373
|
+
return;
|
|
1374
|
+
const prev = this.undoStack[this.undoStack.length - 1];
|
|
1375
|
+
this.undoStack = this.undoStack.slice(0, -1);
|
|
1376
|
+
this.redoStack = [this.snapshotState(), ...this.redoStack.slice(0, 49)];
|
|
1377
|
+
this.board = prev.board ? structuredClone(prev.board) : undefined;
|
|
1378
|
+
this.parts = structuredClone(prev.parts);
|
|
1379
|
+
this.selected = this.parts.length ? Math.max(-1, Math.min(prev.selected, this.parts.length - 1)) : -1;
|
|
1380
|
+
this.dirty = true;
|
|
1381
|
+
this.lastCoalesceKey = undefined;
|
|
1382
|
+
}
|
|
1383
|
+
redo() {
|
|
1384
|
+
if (this.redoStack.length === 0)
|
|
1385
|
+
return;
|
|
1386
|
+
const next = this.redoStack[0];
|
|
1387
|
+
this.redoStack = this.redoStack.slice(1);
|
|
1388
|
+
this.undoStack = [...this.undoStack.slice(-49), this.snapshotState()];
|
|
1389
|
+
this.board = next.board ? structuredClone(next.board) : undefined;
|
|
1390
|
+
this.parts = structuredClone(next.parts);
|
|
1391
|
+
this.selected = this.parts.length ? Math.max(-1, Math.min(next.selected, this.parts.length - 1)) : -1;
|
|
1392
|
+
this.dirty = true;
|
|
1393
|
+
this.lastCoalesceKey = undefined;
|
|
1394
|
+
}
|
|
1118
1395
|
/**
|
|
1119
1396
|
* 부품 목록을 갈아 끼운다.
|
|
1120
1397
|
*
|
|
1121
1398
|
* 편집 규칙은 `part-edits` 가 갖는다 — 화면과 테스트가 같은 것을 쓰게 하려고 떼어
|
|
1122
1399
|
* 놓았다. 여기서는 결과를 담고 선택 인덱스를 정리한다.
|
|
1123
1400
|
*/
|
|
1124
|
-
put(parts, selected = this.selected) {
|
|
1401
|
+
put(parts, selected = this.selected, coalesceKey) {
|
|
1402
|
+
this.recordHistory(coalesceKey);
|
|
1125
1403
|
this.parts = parts;
|
|
1126
1404
|
this.selected = Math.min(selected, parts.length - 1);
|
|
1127
1405
|
this.dirty = true;
|
|
@@ -1142,7 +1420,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1142
1420
|
this.put(edits.movePart(this.parts, index, to), to);
|
|
1143
1421
|
}
|
|
1144
1422
|
updatePart(index, part) {
|
|
1145
|
-
this.put(edits.updatePart(this.parts, index, part));
|
|
1423
|
+
this.put(edits.updatePart(this.parts, index, part), this.selected, `part:${part.name || index}`);
|
|
1146
1424
|
}
|
|
1147
1425
|
/**
|
|
1148
1426
|
* 판 자체의 값을 갈아 끼운다 — 능력처럼 부품보다 위에 있는 것들.
|
|
@@ -1151,6 +1429,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1151
1429
|
* 고칠 때마다 부품 목록이 새 배열이 되고, 그리는 쪽이 전부 다시 그린다.
|
|
1152
1430
|
*/
|
|
1153
1431
|
updateBoard(board) {
|
|
1432
|
+
this.recordHistory('board');
|
|
1154
1433
|
this.board = board;
|
|
1155
1434
|
this.dirty = true;
|
|
1156
1435
|
}
|
|
@@ -1167,6 +1446,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1167
1446
|
changeDetailLevel(level) {
|
|
1168
1447
|
if (!this.board)
|
|
1169
1448
|
return;
|
|
1449
|
+
this.recordHistory('detailLevel');
|
|
1170
1450
|
this.board = { ...this.board, detailLevel: level };
|
|
1171
1451
|
this.dirty = true;
|
|
1172
1452
|
}
|
|
@@ -1203,7 +1483,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1203
1483
|
partChanged(name, part) {
|
|
1204
1484
|
const at = this.parts.findIndex(item => item.name === name);
|
|
1205
1485
|
if (at >= 0)
|
|
1206
|
-
this.put(edits.updatePart(this.parts, at, part));
|
|
1486
|
+
this.put(edits.updatePart(this.parts, at, part), this.selected, `drag:${name}`);
|
|
1207
1487
|
}
|
|
1208
1488
|
/**
|
|
1209
1489
|
* 정밀 배치 도구.
|
|
@@ -1286,48 +1566,50 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1286
1566
|
console.warn(`[thumbnail] ${id} 를 채우지 못했습니다 — ${e.message}`);
|
|
1287
1567
|
}
|
|
1288
1568
|
}
|
|
1289
|
-
|
|
1569
|
+
save() {
|
|
1290
1570
|
if (!this.canSave || !this.folded || !this.figure) {
|
|
1291
1571
|
return;
|
|
1292
1572
|
}
|
|
1573
|
+
if (!this.figure.id) {
|
|
1574
|
+
// 신규 저장: 타입 코드와 표시 이름을 명확히 팝업으로 확인/입력받음
|
|
1575
|
+
let defaultType = this.board?.figureType || this.figure.type || '';
|
|
1576
|
+
if (!defaultType || defaultType.startsWith('FIGURE_')) {
|
|
1577
|
+
defaultType = 'FIGURE';
|
|
1578
|
+
}
|
|
1579
|
+
this.saveModalType = defaultType;
|
|
1580
|
+
this.saveModalName = this.figure.name || '';
|
|
1581
|
+
this.saveModalError = '';
|
|
1582
|
+
this.showSaveModal = true;
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
void this.doSave();
|
|
1586
|
+
}
|
|
1587
|
+
async doSave() {
|
|
1588
|
+
if (!this.folded || !this.figure)
|
|
1589
|
+
return;
|
|
1293
1590
|
this.saving = true;
|
|
1294
1591
|
this.saveFailure = '';
|
|
1592
|
+
this.saveModalError = '';
|
|
1295
1593
|
try {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
못 찍으면 보내지 않는다(`undefined`). 빈 그림을 넣어 「그림이 있다」고 말하지 않는다.
|
|
1304
|
-
*/
|
|
1305
|
-
const thumbnail = await captureThumbnail(this.board, this.parts);
|
|
1306
|
-
if (this.figure.id) {
|
|
1307
|
-
const result = await updateFigure(this.figure.id, {
|
|
1308
|
-
name: this.figure.name,
|
|
1309
|
-
/*
|
|
1310
|
-
카탈로그의 값도 함께 보낸다. 화면에서 고쳐 놓고 저장에 안 실으면 저장 뒤에 조용히
|
|
1311
|
-
사라진다 — 저작자는 저장했다고 보고 다시 열면 없다.
|
|
1312
|
-
*/
|
|
1313
|
-
category: this.figure.category,
|
|
1314
|
-
tags: this.figure.tags,
|
|
1315
|
-
source: sourceJson,
|
|
1316
|
-
thumbnail
|
|
1317
|
-
});
|
|
1318
|
-
this.figure = result.figure;
|
|
1319
|
-
this.violations = result.violations;
|
|
1320
|
-
}
|
|
1321
|
-
else {
|
|
1322
|
-
// 새로 만들 때만 타입 이름이 정해진다. 이미 쓰이는 이름이면 저장 전에 막는다 —
|
|
1323
|
-
// 만든 뒤에는 고칠 수 없으므로 여기서 걸러야 한다.
|
|
1594
|
+
if (!this.figure.id) {
|
|
1595
|
+
const typeToSave = (this.saveModalType || this.board?.figureType || this.figure.type || 'FIGURE').trim().toUpperCase();
|
|
1596
|
+
const nameToSave = (this.saveModalName || this.figure.name || typeToSave).trim();
|
|
1597
|
+
if (!typeToSave) {
|
|
1598
|
+
throw new Error('타입 이름을 입력하세요.');
|
|
1599
|
+
}
|
|
1324
1600
|
const taken = await fetchFigureTypeNames();
|
|
1325
|
-
if (taken.includes(
|
|
1326
|
-
throw new Error(i18next.t('figure.text.type-name-already-taken', { type:
|
|
1601
|
+
if (taken.includes(typeToSave)) {
|
|
1602
|
+
throw new Error(i18next.t('figure.text.type-name-already-taken', { type: typeToSave }));
|
|
1327
1603
|
}
|
|
1604
|
+
if (this.board)
|
|
1605
|
+
this.board.figureType = typeToSave;
|
|
1606
|
+
this.figure = { ...this.figure, type: typeToSave, name: nameToSave };
|
|
1607
|
+
this.folded = { ...this.folded, type: typeToSave };
|
|
1608
|
+
const sourceJson = JSON.stringify(this.folded);
|
|
1609
|
+
const thumbnail = await captureThumbnail(this.board, this.parts);
|
|
1328
1610
|
const result = await createFigure({
|
|
1329
|
-
type:
|
|
1330
|
-
name:
|
|
1611
|
+
type: typeToSave,
|
|
1612
|
+
name: nameToSave,
|
|
1331
1613
|
category: this.figure.category,
|
|
1332
1614
|
tags: this.figure.tags,
|
|
1333
1615
|
source: sourceJson,
|
|
@@ -1335,23 +1617,81 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1335
1617
|
});
|
|
1336
1618
|
this.figure = result.figure;
|
|
1337
1619
|
this.violations = result.violations;
|
|
1620
|
+
this.showSaveModal = false;
|
|
1621
|
+
this.dirty = false;
|
|
1338
1622
|
navigate(`figure-modeller/${result.figure.id}`);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
// 기존 모델 업데이트: folded.type이 figure.type과 일치하도록 보장
|
|
1626
|
+
if (this.figure.type && this.folded.type !== this.figure.type) {
|
|
1627
|
+
this.folded = { ...this.folded, type: this.figure.type };
|
|
1628
|
+
if (this.board)
|
|
1629
|
+
this.board.figureType = this.figure.type;
|
|
1339
1630
|
}
|
|
1631
|
+
const sourceJson = JSON.stringify(this.folded);
|
|
1632
|
+
const thumbnail = await captureThumbnail(this.board, this.parts);
|
|
1633
|
+
const result = await updateFigure(this.figure.id, {
|
|
1634
|
+
name: this.figure.name,
|
|
1635
|
+
category: this.figure.category,
|
|
1636
|
+
tags: this.figure.tags,
|
|
1637
|
+
source: sourceJson,
|
|
1638
|
+
thumbnail
|
|
1639
|
+
});
|
|
1640
|
+
this.figure = result.figure;
|
|
1641
|
+
this.violations = result.violations;
|
|
1340
1642
|
this.dirty = false;
|
|
1341
1643
|
}
|
|
1342
1644
|
catch (e) {
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
`dirty` 는 건드리지 않는다. 저장이 안 됐으므로 미저장인 것이 사실이다.
|
|
1348
|
-
*/
|
|
1349
|
-
this.saveFailure = e.message;
|
|
1645
|
+
const msg = e.message;
|
|
1646
|
+
this.saveFailure = msg;
|
|
1647
|
+
this.saveModalError = msg;
|
|
1350
1648
|
}
|
|
1351
1649
|
finally {
|
|
1352
1650
|
this.saving = false;
|
|
1353
1651
|
}
|
|
1354
1652
|
}
|
|
1653
|
+
renderSaveModal() {
|
|
1654
|
+
if (!this.showSaveModal)
|
|
1655
|
+
return nothing;
|
|
1656
|
+
return html `
|
|
1657
|
+
<div class="save-modal-backdrop" @click=${(e) => { if (e.target === e.currentTarget)
|
|
1658
|
+
this.showSaveModal = false; }}>
|
|
1659
|
+
<div class="save-modal-card" role="dialog" aria-modal="true">
|
|
1660
|
+
<h3 class="save-modal-title">${i18next.t('figure.title.save-new-figure', { defaultValue: '도형 초안 저장' })}</h3>
|
|
1661
|
+
<p class="save-modal-desc">${i18next.t('figure.text.save-new-figure-desc', { defaultValue: '보드와 캔버스에서 식별할 고유 타입 코드와 이름을 정합니다.' })}</p>
|
|
1662
|
+
|
|
1663
|
+
<div class="save-modal-field">
|
|
1664
|
+
<label>${i18next.t('figure.label.figure-type', { defaultValue: '타입 코드 (영문 대문자)' })}</label>
|
|
1665
|
+
<input
|
|
1666
|
+
.value=${this.saveModalType}
|
|
1667
|
+
placeholder="예: OHT, AGV_LIFTER"
|
|
1668
|
+
@input=${(e) => (this.saveModalType = e.target.value.trim().toUpperCase())}
|
|
1669
|
+
/>
|
|
1670
|
+
</div>
|
|
1671
|
+
|
|
1672
|
+
<div class="save-modal-field">
|
|
1673
|
+
<label>${i18next.t('figure.label.name', { defaultValue: '도형 이름' })}</label>
|
|
1674
|
+
<input
|
|
1675
|
+
.value=${this.saveModalName}
|
|
1676
|
+
placeholder="예: 오버헤드 호이스트 트랜스포트"
|
|
1677
|
+
@input=${(e) => (this.saveModalName = e.target.value)}
|
|
1678
|
+
/>
|
|
1679
|
+
</div>
|
|
1680
|
+
|
|
1681
|
+
${this.saveModalError ? html `<div class="save-modal-error">${this.saveModalError}</div>` : nothing}
|
|
1682
|
+
|
|
1683
|
+
<div class="save-modal-actions">
|
|
1684
|
+
<button class="btn-cancel" @click=${() => (this.showSaveModal = false)}>
|
|
1685
|
+
${i18next.t('figure.button.cancel', { defaultValue: '취소' })}
|
|
1686
|
+
</button>
|
|
1687
|
+
<button class="btn-confirm" ?disabled=${this.saving || !this.saveModalType} @click=${() => this.doSave()}>
|
|
1688
|
+
${this.saving ? i18next.t('figure.text.saving-figure', { defaultValue: '저장 중...' }) : i18next.t('figure.button.save', { defaultValue: '저장' })}
|
|
1689
|
+
</button>
|
|
1690
|
+
</div>
|
|
1691
|
+
</div>
|
|
1692
|
+
</div>
|
|
1693
|
+
`;
|
|
1694
|
+
}
|
|
1355
1695
|
};
|
|
1356
1696
|
__decorate([
|
|
1357
1697
|
state(),
|
|
@@ -1397,6 +1737,22 @@ __decorate([
|
|
|
1397
1737
|
state(),
|
|
1398
1738
|
__metadata("design:type", Object)
|
|
1399
1739
|
], FigureModellerPage.prototype, "saveFailure", void 0);
|
|
1740
|
+
__decorate([
|
|
1741
|
+
state(),
|
|
1742
|
+
__metadata("design:type", Object)
|
|
1743
|
+
], FigureModellerPage.prototype, "showSaveModal", void 0);
|
|
1744
|
+
__decorate([
|
|
1745
|
+
state(),
|
|
1746
|
+
__metadata("design:type", Object)
|
|
1747
|
+
], FigureModellerPage.prototype, "saveModalType", void 0);
|
|
1748
|
+
__decorate([
|
|
1749
|
+
state(),
|
|
1750
|
+
__metadata("design:type", Object)
|
|
1751
|
+
], FigureModellerPage.prototype, "saveModalName", void 0);
|
|
1752
|
+
__decorate([
|
|
1753
|
+
state(),
|
|
1754
|
+
__metadata("design:type", Object)
|
|
1755
|
+
], FigureModellerPage.prototype, "saveModalError", void 0);
|
|
1400
1756
|
__decorate([
|
|
1401
1757
|
state(),
|
|
1402
1758
|
__metadata("design:type", Object)
|
|
@@ -1405,6 +1761,18 @@ __decorate([
|
|
|
1405
1761
|
state(),
|
|
1406
1762
|
__metadata("design:type", Array)
|
|
1407
1763
|
], FigureModellerPage.prototype, "proposalFeedback", void 0);
|
|
1764
|
+
__decorate([
|
|
1765
|
+
state(),
|
|
1766
|
+
__metadata("design:type", Array)
|
|
1767
|
+
], FigureModellerPage.prototype, "undoStack", void 0);
|
|
1768
|
+
__decorate([
|
|
1769
|
+
state(),
|
|
1770
|
+
__metadata("design:type", Array)
|
|
1771
|
+
], FigureModellerPage.prototype, "redoStack", void 0);
|
|
1772
|
+
__decorate([
|
|
1773
|
+
state(),
|
|
1774
|
+
__metadata("design:type", Object)
|
|
1775
|
+
], FigureModellerPage.prototype, "sceneRevision", void 0);
|
|
1408
1776
|
FigureModellerPage = __decorate([
|
|
1409
1777
|
customElement('figure-modeller-page')
|
|
1410
1778
|
], FigureModellerPage);
|