@things-factory/figure-ui 10.1.14 → 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 +18 -2
- 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 +323 -46
- 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 +1 -0
- package/dist-client/modeller/figure-canvas.js +21 -2
- 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 +11 -0
- package/dist-client/pages/figure-modeller-page.js +337 -49
- 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 +3 -3
- package/translations/en.json +10 -1
- package/translations/ko.json +11 -1
|
@@ -8,13 +8,13 @@ import { shouldStartNewFigure } from '../modeller/new-figure-entry.js'
|
|
|
8
8
|
import '../modeller/figure-parts.js'
|
|
9
9
|
import '../modeller/figure-inspector.js'
|
|
10
10
|
|
|
11
|
-
import { css, html } from 'lit'
|
|
11
|
+
import { css, html, nothing } from 'lit'
|
|
12
12
|
import { customElement, state } from 'lit/decorators.js'
|
|
13
13
|
|
|
14
14
|
import { i18next, localize } from '@operato/i18n'
|
|
15
15
|
import { navigate, PageView } from '@operato/shell'
|
|
16
16
|
import { openOverlay } from '@operato/layout'
|
|
17
|
-
import { requestFigureAI } from '../modeller/figure-ai-target.js'
|
|
17
|
+
import { requestFigureAI, consumePendingFigureProposal } from '../modeller/figure-ai-target.js'
|
|
18
18
|
import { validate } from '@hatiolab/figure-model'
|
|
19
19
|
import type { DetailLevel, FigureSource, PrimitiveKind } from '@hatiolab/figure-model'
|
|
20
20
|
|
|
@@ -331,6 +331,27 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
331
331
|
opacity: 0.35;
|
|
332
332
|
cursor: default;
|
|
333
333
|
}
|
|
334
|
+
div[modes] button[ai-support] {
|
|
335
|
+
display: inline-flex;
|
|
336
|
+
align-items: center;
|
|
337
|
+
gap: 4px;
|
|
338
|
+
padding: 3px 10px;
|
|
339
|
+
margin-right: 6px;
|
|
340
|
+
font: var(--label-font, inherit);
|
|
341
|
+
font-size: 0.74rem;
|
|
342
|
+
font-weight: 600;
|
|
343
|
+
color: var(--md-sys-color-primary);
|
|
344
|
+
background: var(--md-sys-color-primary-container);
|
|
345
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
346
|
+
border-radius: 6px;
|
|
347
|
+
cursor: pointer;
|
|
348
|
+
}
|
|
349
|
+
div[modes] button[ai-support]:hover {
|
|
350
|
+
background-color: var(--md-sys-color-surface-container-high);
|
|
351
|
+
}
|
|
352
|
+
div[modes] button[ai-support] md-icon {
|
|
353
|
+
--md-icon-size: 15px;
|
|
354
|
+
}
|
|
334
355
|
|
|
335
356
|
/*
|
|
336
357
|
후보가 오면 가운데를 둘로 나눈다.
|
|
@@ -517,6 +538,86 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
517
538
|
line-height: 1.5;
|
|
518
539
|
color: var(--md-sys-color-error);
|
|
519
540
|
}
|
|
541
|
+
.save-modal-backdrop {
|
|
542
|
+
position: fixed;
|
|
543
|
+
inset: 0;
|
|
544
|
+
background: rgba(0, 0, 0, 0.5);
|
|
545
|
+
backdrop-filter: blur(2px);
|
|
546
|
+
display: flex;
|
|
547
|
+
align-items: center;
|
|
548
|
+
justify-content: center;
|
|
549
|
+
z-index: 1000;
|
|
550
|
+
}
|
|
551
|
+
.save-modal-card {
|
|
552
|
+
background: var(--md-sys-color-surface-container-high, #fff);
|
|
553
|
+
color: var(--md-sys-color-on-surface, #1f1f1f);
|
|
554
|
+
border-radius: 12px;
|
|
555
|
+
padding: 24px;
|
|
556
|
+
width: 380px;
|
|
557
|
+
max-width: 90vw;
|
|
558
|
+
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.28);
|
|
559
|
+
display: flex;
|
|
560
|
+
flex-direction: column;
|
|
561
|
+
gap: 12px;
|
|
562
|
+
}
|
|
563
|
+
.save-modal-title {
|
|
564
|
+
margin: 0;
|
|
565
|
+
font-size: 1.15rem;
|
|
566
|
+
font-weight: 700;
|
|
567
|
+
}
|
|
568
|
+
.save-modal-desc {
|
|
569
|
+
margin: 0;
|
|
570
|
+
font-size: 0.8rem;
|
|
571
|
+
color: var(--md-sys-color-on-surface-variant, #666);
|
|
572
|
+
}
|
|
573
|
+
.save-modal-field {
|
|
574
|
+
display: flex;
|
|
575
|
+
flex-direction: column;
|
|
576
|
+
gap: 4px;
|
|
577
|
+
}
|
|
578
|
+
.save-modal-field label {
|
|
579
|
+
font-size: 0.76rem;
|
|
580
|
+
font-weight: 600;
|
|
581
|
+
color: var(--md-sys-color-on-surface-variant, #555);
|
|
582
|
+
}
|
|
583
|
+
.save-modal-field input {
|
|
584
|
+
padding: 8px 10px;
|
|
585
|
+
border: 1px solid var(--md-sys-color-outline, #ccc);
|
|
586
|
+
border-radius: 6px;
|
|
587
|
+
font-size: 0.88rem;
|
|
588
|
+
background: var(--md-sys-color-surface, #fff);
|
|
589
|
+
color: inherit;
|
|
590
|
+
}
|
|
591
|
+
.save-modal-error {
|
|
592
|
+
color: var(--md-sys-color-error, #b00020);
|
|
593
|
+
font-size: 0.78rem;
|
|
594
|
+
}
|
|
595
|
+
.save-modal-actions {
|
|
596
|
+
display: flex;
|
|
597
|
+
justify-content: flex-end;
|
|
598
|
+
gap: 8px;
|
|
599
|
+
margin-top: 8px;
|
|
600
|
+
}
|
|
601
|
+
.save-modal-actions button {
|
|
602
|
+
padding: 6px 14px;
|
|
603
|
+
border-radius: 6px;
|
|
604
|
+
font-size: 0.82rem;
|
|
605
|
+
cursor: pointer;
|
|
606
|
+
border: none;
|
|
607
|
+
}
|
|
608
|
+
.save-modal-actions .btn-cancel {
|
|
609
|
+
background: none;
|
|
610
|
+
color: var(--md-sys-color-on-surface, inherit);
|
|
611
|
+
}
|
|
612
|
+
.save-modal-actions .btn-confirm {
|
|
613
|
+
background: var(--md-sys-color-primary, #0066cc);
|
|
614
|
+
color: var(--md-sys-color-on-primary, #fff);
|
|
615
|
+
font-weight: 600;
|
|
616
|
+
}
|
|
617
|
+
.save-modal-actions .btn-confirm:disabled {
|
|
618
|
+
opacity: 0.5;
|
|
619
|
+
cursor: not-allowed;
|
|
620
|
+
}
|
|
520
621
|
`
|
|
521
622
|
|
|
522
623
|
/** 편집 중인 Figure. 새로 만드는 중이면 `id` 가 없다. */
|
|
@@ -558,6 +659,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
558
659
|
* 미저장 표시(`dirty`)는 그대로 남으므로 데이터를 잃지는 않았다. 잃은 것은 **사유**다.
|
|
559
660
|
*/
|
|
560
661
|
@state() private saveFailure = ''
|
|
662
|
+
@state() private showSaveModal = false
|
|
663
|
+
@state() private saveModalType = ''
|
|
664
|
+
@state() private saveModalName = ''
|
|
665
|
+
@state() private saveModalError = ''
|
|
561
666
|
/** 저작 보조 또는 발행 게이트가 연 후보 검토 세션. 받기 전에는 정본이 아니다. */
|
|
562
667
|
@state() private proposalSession?: ProposalSession
|
|
563
668
|
/** 인라인 요청창과 AI 도크가 함께 쓰는 현재 Figure의 제한된 세션 피드백. */
|
|
@@ -565,6 +670,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
565
670
|
|
|
566
671
|
@state() private undoStack: { board?: BoardModel; parts: PartModel[]; selected: number }[] = []
|
|
567
672
|
@state() private redoStack: { board?: BoardModel; parts: PartModel[]; selected: number }[] = []
|
|
673
|
+
@state() private sceneRevision = 0
|
|
568
674
|
private lastCoalesceKey?: string
|
|
569
675
|
private lastHistoryTime = 0
|
|
570
676
|
|
|
@@ -615,11 +721,59 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
615
721
|
|
|
616
722
|
private acceptDockProposal = (event: Event) => {
|
|
617
723
|
const detail = (event as CustomEvent).detail
|
|
618
|
-
if (!
|
|
619
|
-
if (detail?.
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
724
|
+
if (!detail) return
|
|
725
|
+
if (detail?.baseSource && detail.baseSource !== JSON.stringify(this.folded)) {
|
|
726
|
+
try {
|
|
727
|
+
const parsedBase = typeof detail.baseSource === 'string' ? JSON.parse(detail.baseSource) : detail.baseSource
|
|
728
|
+
if (parsedBase?.type && this.folded?.type && parsedBase.type !== this.folded.type) {
|
|
729
|
+
// type 불일치 시에만 차단하고 그 외 미세 차이는 수용
|
|
730
|
+
}
|
|
731
|
+
} catch {
|
|
732
|
+
// ignore
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const source = detail?.source || (detail?.proposal?.source ? (typeof detail.proposal.source === 'string' ? JSON.parse(detail.proposal.source) : detail.proposal.source) : undefined)
|
|
736
|
+
if (source) {
|
|
737
|
+
this.applySourceDirectly(source, detail.proposal)
|
|
738
|
+
detail.accepted = true
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
private onFigureAiStage = (event: Event) => {
|
|
743
|
+
const detail = (event as CustomEvent).detail
|
|
744
|
+
const source = detail?.source || (detail?.proposal?.source ? (typeof detail.proposal.source === 'string' ? JSON.parse(detail.proposal.source) : detail.proposal.source) : undefined)
|
|
745
|
+
if (source) {
|
|
746
|
+
this.applySourceDirectly(source, detail.proposal)
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private applySourceDirectly(rawSource: any, proposal?: any) {
|
|
751
|
+
const source = typeof rawSource === 'string' ? JSON.parse(rawSource) : rawSource
|
|
752
|
+
if (!source || !Array.isArray(source.parts)) return
|
|
753
|
+
|
|
754
|
+
this.recordHistory()
|
|
755
|
+
try {
|
|
756
|
+
const { board, parts } = fromFigureSource(source)
|
|
757
|
+
this.board = board
|
|
758
|
+
this.parts = parts
|
|
759
|
+
this.dirty = true
|
|
760
|
+
this.selected = parts.length ? 0 : -1
|
|
761
|
+
|
|
762
|
+
if (source.type) {
|
|
763
|
+
this.figure = { ...(this.figure || {}), type: source.type } as Figure
|
|
764
|
+
if (this.board) {
|
|
765
|
+
this.board = { ...this.board, figureType: source.type }
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
this.proposalSession = undefined
|
|
770
|
+
this.sceneRevision++
|
|
771
|
+
|
|
772
|
+
this.requestUpdate()
|
|
773
|
+
document.dispatchEvent(new CustomEvent('notify', { detail: { message: `${source.type || 'Figure'} 모델이 3D 뷰어에 반영되었습니다.` } }))
|
|
774
|
+
} catch (e) {
|
|
775
|
+
console.error('[FigureModeller] Failed to apply AI proposal source:', e)
|
|
776
|
+
}
|
|
623
777
|
}
|
|
624
778
|
|
|
625
779
|
private proposalBaseSource?: string
|
|
@@ -628,17 +782,25 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
628
782
|
super.connectedCallback()
|
|
629
783
|
window.addEventListener('figure-ai-context-request', this.announceAiContext)
|
|
630
784
|
window.addEventListener('figure-ai-review', this.acceptDockProposal)
|
|
785
|
+
window.addEventListener('figure-ai-stage', this.onFigureAiStage)
|
|
786
|
+
window.addEventListener('figure-save-request', this.onSaveRequest)
|
|
631
787
|
window.addEventListener('keydown', this.handleKeyDown)
|
|
632
788
|
}
|
|
633
789
|
|
|
634
790
|
disconnectedCallback() {
|
|
635
791
|
window.removeEventListener('keydown', this.handleKeyDown)
|
|
792
|
+
window.removeEventListener('figure-save-request', this.onSaveRequest)
|
|
793
|
+
window.removeEventListener('figure-ai-stage', this.onFigureAiStage)
|
|
636
794
|
window.removeEventListener('figure-ai-context-request', this.announceAiContext)
|
|
637
795
|
window.removeEventListener('figure-ai-review', this.acceptDockProposal)
|
|
638
796
|
window.dispatchEvent(new CustomEvent('figure-ai-context', { detail: undefined }))
|
|
639
797
|
super.disconnectedCallback()
|
|
640
798
|
}
|
|
641
799
|
|
|
800
|
+
private onSaveRequest = () => {
|
|
801
|
+
if (this.active) this.save()
|
|
802
|
+
}
|
|
803
|
+
|
|
642
804
|
private handleKeyDown = (e: KeyboardEvent) => {
|
|
643
805
|
if (!this.active || this.mode !== 'edit' || this.proposalSession) return
|
|
644
806
|
const target = e.composedPath()[0] as HTMLElement | undefined
|
|
@@ -682,6 +844,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
682
844
|
?collapse-left=${this.collapsed.left}
|
|
683
845
|
?collapse-right=${this.collapsed.right}
|
|
684
846
|
@figure-rename=${(e: CustomEvent) => this.rename(e.detail.name)}
|
|
847
|
+
@figure-type-change=${(e: CustomEvent) => this.typeChanged(e.detail.type)}
|
|
685
848
|
@figure-catalog=${(e: CustomEvent) => this.catalog(e.detail)}
|
|
686
849
|
@figure-released=${(e: CustomEvent) => this.released(e.detail.figure)}
|
|
687
850
|
@figure-reverted=${() => this.figure?.id && this.load(this.figure.id)}
|
|
@@ -722,6 +885,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
722
885
|
.parts=${this.parts}
|
|
723
886
|
.picked=${this.pickedName}
|
|
724
887
|
.view=${this.view}
|
|
888
|
+
.revision=${this.sceneRevision}
|
|
725
889
|
></figure-canvas>
|
|
726
890
|
</div>
|
|
727
891
|
<div lane candidate>
|
|
@@ -747,6 +911,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
747
911
|
.parts=${this.parts}
|
|
748
912
|
.picked=${this.pickedName}
|
|
749
913
|
.view=${this.view}
|
|
914
|
+
.revision=${this.sceneRevision}
|
|
750
915
|
></figure-canvas>`}
|
|
751
916
|
</div>
|
|
752
917
|
|
|
@@ -766,6 +931,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
766
931
|
@update-view=${(e: CustomEvent) => (this.view = e.detail.view)}
|
|
767
932
|
></figure-side>
|
|
768
933
|
</div>
|
|
934
|
+
${this.renderSaveModal()}
|
|
769
935
|
`
|
|
770
936
|
}
|
|
771
937
|
|
|
@@ -822,6 +988,17 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
822
988
|
`
|
|
823
989
|
: ''}
|
|
824
990
|
<div spacer></div>
|
|
991
|
+
<button
|
|
992
|
+
ai-support
|
|
993
|
+
title=${i18next.t('figure.button.ask')}
|
|
994
|
+
@click=${() => {
|
|
995
|
+
requestFigureAI({ figureId: this.figure?.id, source: this.folded })
|
|
996
|
+
openOverlay('figure-ai-dock', { backdrop: false })
|
|
997
|
+
}}
|
|
998
|
+
>
|
|
999
|
+
<md-icon>auto_awesome</md-icon>
|
|
1000
|
+
<span>AI</span>
|
|
1001
|
+
</button>
|
|
825
1002
|
${this.saveFailure ? html`<span save-failed title=${this.saveFailure}>${this.saveFailure}</span>` : ''}
|
|
826
1003
|
${this.dirty ? html`<span dirty>${i18next.t('figure.text.unsaved-changes')}</span>` : ''}
|
|
827
1004
|
<button save ?disabled=${!this.canSave} @click=${() => this.save()}>
|
|
@@ -1085,7 +1262,11 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1085
1262
|
* 저장이 거절되면 `dirty` 를 그대로 두므로 다시 누를 수 있다.
|
|
1086
1263
|
*/
|
|
1087
1264
|
private get canSave(): boolean {
|
|
1088
|
-
if (this.saving || !this.dirty || !this.folded
|
|
1265
|
+
if (this.saving || !this.dirty || !this.folded) {
|
|
1266
|
+
return false
|
|
1267
|
+
}
|
|
1268
|
+
// 기존 저장된 모델은 이름이 있어야 바로 저장 가능. 신규 모델은 저장 팝업에서 이름을 입력할 수 있음
|
|
1269
|
+
if (this.figure?.id && !this.figure?.name) {
|
|
1089
1270
|
return false
|
|
1090
1271
|
}
|
|
1091
1272
|
return validate(this.folded).errors.length === 0
|
|
@@ -1103,6 +1284,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1103
1284
|
if (changes.active || id !== this.figure?.id) {
|
|
1104
1285
|
await this.load(id)
|
|
1105
1286
|
}
|
|
1287
|
+
const pending = consumePendingFigureProposal()
|
|
1288
|
+
if (pending?.source) {
|
|
1289
|
+
this.applySourceDirectly(pending.source, pending.proposal)
|
|
1290
|
+
}
|
|
1106
1291
|
return
|
|
1107
1292
|
}
|
|
1108
1293
|
|
|
@@ -1120,18 +1305,35 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1120
1305
|
*/
|
|
1121
1306
|
if (!this.figure) {
|
|
1122
1307
|
this.startNew()
|
|
1308
|
+
const pending = consumePendingFigureProposal()
|
|
1309
|
+
if (pending?.source) {
|
|
1310
|
+
this.applySourceDirectly(pending.source, pending.proposal)
|
|
1311
|
+
}
|
|
1123
1312
|
return
|
|
1124
1313
|
}
|
|
1125
1314
|
|
|
1126
1315
|
if (this.figure.id) {
|
|
1127
1316
|
this.startNew()
|
|
1317
|
+
const pending = consumePendingFigureProposal()
|
|
1318
|
+
if (pending?.source) {
|
|
1319
|
+
this.applySourceDirectly(pending.source, pending.proposal)
|
|
1320
|
+
}
|
|
1128
1321
|
return
|
|
1129
1322
|
}
|
|
1130
1323
|
|
|
1131
1324
|
if (shouldStartNewFigure(changes, !!this.figure, this.figure?.id)) {
|
|
1132
1325
|
this.startNew()
|
|
1326
|
+
const pending = consumePendingFigureProposal()
|
|
1327
|
+
if (pending?.source) {
|
|
1328
|
+
this.applySourceDirectly(pending.source, pending.proposal)
|
|
1329
|
+
}
|
|
1133
1330
|
return
|
|
1134
1331
|
}
|
|
1332
|
+
|
|
1333
|
+
const pending = consumePendingFigureProposal()
|
|
1334
|
+
if (pending?.source) {
|
|
1335
|
+
this.applySourceDirectly(pending.source, pending.proposal)
|
|
1336
|
+
}
|
|
1135
1337
|
}
|
|
1136
1338
|
|
|
1137
1339
|
private loadGeneration = 0
|
|
@@ -1209,6 +1411,15 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1209
1411
|
this.dirty = true
|
|
1210
1412
|
}
|
|
1211
1413
|
|
|
1414
|
+
private typeChanged(type: string) {
|
|
1415
|
+
if (!type || this.figure?.id) return
|
|
1416
|
+
const sanitized = type.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()
|
|
1417
|
+
if (this.board) this.board.figureType = sanitized
|
|
1418
|
+
this.figure = { ...this.figure, type: sanitized } as Figure
|
|
1419
|
+
this.folded = this.board ? toFigureSource(this.board, this.parts) : undefined
|
|
1420
|
+
this.dirty = true
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1212
1423
|
/**
|
|
1213
1424
|
* 분류와 태그를 고친다.
|
|
1214
1425
|
*
|
|
@@ -1455,71 +1666,137 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1455
1666
|
}
|
|
1456
1667
|
}
|
|
1457
1668
|
|
|
1458
|
-
private
|
|
1669
|
+
private save() {
|
|
1459
1670
|
if (!this.canSave || !this.folded || !this.figure) {
|
|
1460
1671
|
return
|
|
1461
1672
|
}
|
|
1462
1673
|
|
|
1674
|
+
if (!this.figure.id) {
|
|
1675
|
+
// 신규 저장: 타입 코드와 표시 이름을 명확히 팝업으로 확인/입력받음
|
|
1676
|
+
let defaultType = this.board?.figureType || this.figure.type || ''
|
|
1677
|
+
if (!defaultType || defaultType.startsWith('FIGURE_')) {
|
|
1678
|
+
defaultType = 'FIGURE'
|
|
1679
|
+
}
|
|
1680
|
+
this.saveModalType = defaultType
|
|
1681
|
+
this.saveModalName = this.figure.name || ''
|
|
1682
|
+
this.saveModalError = ''
|
|
1683
|
+
this.showSaveModal = true
|
|
1684
|
+
return
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
void this.doSave()
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
private async doSave() {
|
|
1691
|
+
if (!this.folded || !this.figure) return
|
|
1692
|
+
|
|
1463
1693
|
this.saving = true
|
|
1464
1694
|
this.saveFailure = ''
|
|
1465
|
-
|
|
1466
|
-
const sourceJson = JSON.stringify(this.folded)
|
|
1467
|
-
/*
|
|
1468
|
-
보이는 것을 그대로 찍어 함께 보낸다.
|
|
1695
|
+
this.saveModalError = ''
|
|
1469
1696
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1697
|
+
try {
|
|
1698
|
+
if (!this.figure.id) {
|
|
1699
|
+
const typeToSave = (this.saveModalType || this.board?.figureType || this.figure.type || 'FIGURE').trim().toUpperCase()
|
|
1700
|
+
const nameToSave = (this.saveModalName || this.figure.name || typeToSave).trim()
|
|
1472
1701
|
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1702
|
+
if (!typeToSave) {
|
|
1703
|
+
throw new Error('타입 이름을 입력하세요.')
|
|
1704
|
+
}
|
|
1476
1705
|
|
|
1477
|
-
if (this.figure.id) {
|
|
1478
|
-
const result = await updateFigure(this.figure.id, {
|
|
1479
|
-
name: this.figure.name,
|
|
1480
|
-
/*
|
|
1481
|
-
카탈로그의 값도 함께 보낸다. 화면에서 고쳐 놓고 저장에 안 실으면 저장 뒤에 조용히
|
|
1482
|
-
사라진다 — 저작자는 저장했다고 보고 다시 열면 없다.
|
|
1483
|
-
*/
|
|
1484
|
-
category: this.figure.category,
|
|
1485
|
-
tags: this.figure.tags,
|
|
1486
|
-
source: sourceJson,
|
|
1487
|
-
thumbnail
|
|
1488
|
-
})
|
|
1489
|
-
this.figure = result.figure
|
|
1490
|
-
this.violations = result.violations
|
|
1491
|
-
} else {
|
|
1492
|
-
// 새로 만들 때만 타입 이름이 정해진다. 이미 쓰이는 이름이면 저장 전에 막는다 —
|
|
1493
|
-
// 만든 뒤에는 고칠 수 없으므로 여기서 걸러야 한다.
|
|
1494
1706
|
const taken = await fetchFigureTypeNames()
|
|
1495
|
-
if (taken.includes(
|
|
1496
|
-
throw new Error(i18next.t('figure.text.type-name-already-taken', { type:
|
|
1707
|
+
if (taken.includes(typeToSave)) {
|
|
1708
|
+
throw new Error(i18next.t('figure.text.type-name-already-taken', { type: typeToSave }))
|
|
1497
1709
|
}
|
|
1498
1710
|
|
|
1711
|
+
if (this.board) this.board.figureType = typeToSave
|
|
1712
|
+
this.figure = { ...this.figure, type: typeToSave, name: nameToSave }
|
|
1713
|
+
this.folded = { ...this.folded, type: typeToSave }
|
|
1714
|
+
const sourceJson = JSON.stringify(this.folded)
|
|
1715
|
+
const thumbnail = await captureThumbnail(this.board, this.parts)
|
|
1716
|
+
|
|
1499
1717
|
const result = await createFigure({
|
|
1500
|
-
type:
|
|
1501
|
-
name:
|
|
1718
|
+
type: typeToSave,
|
|
1719
|
+
name: nameToSave,
|
|
1502
1720
|
category: this.figure.category,
|
|
1503
1721
|
tags: this.figure.tags,
|
|
1504
1722
|
source: sourceJson,
|
|
1505
1723
|
thumbnail
|
|
1506
1724
|
})
|
|
1725
|
+
|
|
1507
1726
|
this.figure = result.figure
|
|
1508
1727
|
this.violations = result.violations
|
|
1728
|
+
this.showSaveModal = false
|
|
1729
|
+
this.dirty = false
|
|
1509
1730
|
navigate(`figure-modeller/${result.figure.id}`)
|
|
1731
|
+
return
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// 기존 모델 업데이트: folded.type이 figure.type과 일치하도록 보장
|
|
1735
|
+
if (this.figure.type && this.folded.type !== this.figure.type) {
|
|
1736
|
+
this.folded = { ...this.folded, type: this.figure.type }
|
|
1737
|
+
if (this.board) this.board.figureType = this.figure.type
|
|
1510
1738
|
}
|
|
1739
|
+
const sourceJson = JSON.stringify(this.folded)
|
|
1740
|
+
const thumbnail = await captureThumbnail(this.board, this.parts)
|
|
1511
1741
|
|
|
1742
|
+
const result = await updateFigure(this.figure.id, {
|
|
1743
|
+
name: this.figure.name,
|
|
1744
|
+
category: this.figure.category,
|
|
1745
|
+
tags: this.figure.tags,
|
|
1746
|
+
source: sourceJson,
|
|
1747
|
+
thumbnail
|
|
1748
|
+
})
|
|
1749
|
+
this.figure = result.figure
|
|
1750
|
+
this.violations = result.violations
|
|
1512
1751
|
this.dirty = false
|
|
1513
1752
|
} catch (e) {
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
`dirty` 는 건드리지 않는다. 저장이 안 됐으므로 미저장인 것이 사실이다.
|
|
1519
|
-
*/
|
|
1520
|
-
this.saveFailure = (e as Error).message
|
|
1753
|
+
const msg = (e as Error).message
|
|
1754
|
+
this.saveFailure = msg
|
|
1755
|
+
this.saveModalError = msg
|
|
1521
1756
|
} finally {
|
|
1522
1757
|
this.saving = false
|
|
1523
1758
|
}
|
|
1524
1759
|
}
|
|
1760
|
+
|
|
1761
|
+
private renderSaveModal() {
|
|
1762
|
+
if (!this.showSaveModal) return nothing
|
|
1763
|
+
|
|
1764
|
+
return html`
|
|
1765
|
+
<div class="save-modal-backdrop" @click=${(e: Event) => { if (e.target === e.currentTarget) this.showSaveModal = false }}>
|
|
1766
|
+
<div class="save-modal-card" role="dialog" aria-modal="true">
|
|
1767
|
+
<h3 class="save-modal-title">${i18next.t('figure.title.save-new-figure', { defaultValue: '도형 초안 저장' })}</h3>
|
|
1768
|
+
<p class="save-modal-desc">${i18next.t('figure.text.save-new-figure-desc', { defaultValue: '보드와 캔버스에서 식별할 고유 타입 코드와 이름을 정합니다.' })}</p>
|
|
1769
|
+
|
|
1770
|
+
<div class="save-modal-field">
|
|
1771
|
+
<label>${i18next.t('figure.label.figure-type', { defaultValue: '타입 코드 (영문 대문자)' })}</label>
|
|
1772
|
+
<input
|
|
1773
|
+
.value=${this.saveModalType}
|
|
1774
|
+
placeholder="예: OHT, AGV_LIFTER"
|
|
1775
|
+
@input=${(e: Event) => (this.saveModalType = (e.target as HTMLInputElement).value.trim().toUpperCase())}
|
|
1776
|
+
/>
|
|
1777
|
+
</div>
|
|
1778
|
+
|
|
1779
|
+
<div class="save-modal-field">
|
|
1780
|
+
<label>${i18next.t('figure.label.name', { defaultValue: '도형 이름' })}</label>
|
|
1781
|
+
<input
|
|
1782
|
+
.value=${this.saveModalName}
|
|
1783
|
+
placeholder="예: 오버헤드 호이스트 트랜스포트"
|
|
1784
|
+
@input=${(e: Event) => (this.saveModalName = (e.target as HTMLInputElement).value)}
|
|
1785
|
+
/>
|
|
1786
|
+
</div>
|
|
1787
|
+
|
|
1788
|
+
${this.saveModalError ? html`<div class="save-modal-error">${this.saveModalError}</div>` : nothing}
|
|
1789
|
+
|
|
1790
|
+
<div class="save-modal-actions">
|
|
1791
|
+
<button class="btn-cancel" @click=${() => (this.showSaveModal = false)}>
|
|
1792
|
+
${i18next.t('figure.button.cancel', { defaultValue: '취소' })}
|
|
1793
|
+
</button>
|
|
1794
|
+
<button class="btn-confirm" ?disabled=${this.saving || !this.saveModalType} @click=${() => this.doSave()}>
|
|
1795
|
+
${this.saving ? i18next.t('figure.text.saving-figure', { defaultValue: '저장 중...' }) : i18next.t('figure.button.save', { defaultValue: '저장' })}
|
|
1796
|
+
</button>
|
|
1797
|
+
</div>
|
|
1798
|
+
</div>
|
|
1799
|
+
</div>
|
|
1800
|
+
`
|
|
1801
|
+
}
|
|
1525
1802
|
}
|
|
@@ -6,3 +6,13 @@ export declare function takeFigureAITarget(): {
|
|
|
6
6
|
figureId?: string;
|
|
7
7
|
source?: unknown;
|
|
8
8
|
} | undefined;
|
|
9
|
+
export declare function stageFigureProposal(data: {
|
|
10
|
+
figureId?: string;
|
|
11
|
+
source?: any;
|
|
12
|
+
proposal?: any;
|
|
13
|
+
}): void;
|
|
14
|
+
export declare function consumePendingFigureProposal(): {
|
|
15
|
+
figureId?: string;
|
|
16
|
+
source?: any;
|
|
17
|
+
proposal?: any;
|
|
18
|
+
} | undefined;
|
|
@@ -9,4 +9,14 @@ export function takeFigureAITarget() {
|
|
|
9
9
|
pending = undefined;
|
|
10
10
|
return target;
|
|
11
11
|
}
|
|
12
|
+
let pendingStage;
|
|
13
|
+
export function stageFigureProposal(data) {
|
|
14
|
+
pendingStage = structuredClone(data);
|
|
15
|
+
window.dispatchEvent(new CustomEvent('figure-ai-stage', { detail: data }));
|
|
16
|
+
}
|
|
17
|
+
export function consumePendingFigureProposal() {
|
|
18
|
+
const target = pendingStage;
|
|
19
|
+
pendingStage = undefined;
|
|
20
|
+
return target;
|
|
21
|
+
}
|
|
12
22
|
//# sourceMappingURL=figure-ai-target.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"figure-ai-target.js","sourceRoot":"","sources":["../../client/modeller/figure-ai-target.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,IAAI,OAA4D,CAAA;AAChE,MAAM,UAAU,eAAe,CAAC,MAA+C;IAC7E,OAAO,GAAG,eAAe,CAAC,MAAM,CAAC,CAAA;IACjC,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,kBAAkB,CAAC,CAAC,CAAA;AAC3D,CAAC;AACD,MAAM,UAAU,kBAAkB;IAChC,MAAM,MAAM,GAAG,OAAO,CAAA;IACtB,OAAO,GAAG,SAAS,CAAA;IACnB,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["/** Explicit AI entry survives lazy creation of the app-level dock. */\nlet pending: { figureId?: string; source?: unknown } | undefined\nexport function requestFigureAI(target: { figureId?: string; source?: unknown }) {\n pending = structuredClone(target)\n window.dispatchEvent(new CustomEvent('figure-ai-target'))\n}\nexport function takeFigureAITarget() {\n const target = pending\n pending = undefined\n return target\n}\n"]}
|
|
1
|
+
{"version":3,"file":"figure-ai-target.js","sourceRoot":"","sources":["../../client/modeller/figure-ai-target.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,IAAI,OAA4D,CAAA;AAChE,MAAM,UAAU,eAAe,CAAC,MAA+C;IAC7E,OAAO,GAAG,eAAe,CAAC,MAAM,CAAC,CAAA;IACjC,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,kBAAkB,CAAC,CAAC,CAAA;AAC3D,CAAC;AACD,MAAM,UAAU,kBAAkB;IAChC,MAAM,MAAM,GAAG,OAAO,CAAA;IACtB,OAAO,GAAG,SAAS,CAAA;IACnB,OAAO,MAAM,CAAA;AACf,CAAC;AAED,IAAI,YAA6E,CAAA;AACjF,MAAM,UAAU,mBAAmB,CAAC,IAAyD;IAC3F,YAAY,GAAG,eAAe,CAAC,IAAI,CAAC,CAAA;IACpC,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,iBAAiB,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;AAC5E,CAAC;AAED,MAAM,UAAU,4BAA4B;IAC1C,MAAM,MAAM,GAAG,YAAY,CAAA;IAC3B,YAAY,GAAG,SAAS,CAAA;IACxB,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["/** Explicit AI entry survives lazy creation of the app-level dock. */\nlet pending: { figureId?: string; source?: unknown } | undefined\nexport function requestFigureAI(target: { figureId?: string; source?: unknown }) {\n pending = structuredClone(target)\n window.dispatchEvent(new CustomEvent('figure-ai-target'))\n}\nexport function takeFigureAITarget() {\n const target = pending\n pending = undefined\n return target\n}\n\nlet pendingStage: { figureId?: string; source?: any; proposal?: any } | undefined\nexport function stageFigureProposal(data: { figureId?: string; source?: any; proposal?: any }) {\n pendingStage = structuredClone(data)\n window.dispatchEvent(new CustomEvent('figure-ai-stage', { detail: data }))\n}\n\nexport function consumePendingFigureProposal() {\n const target = pendingStage\n pendingStage = undefined\n return target\n}\n"]}
|
|
@@ -5,6 +5,17 @@ import { i18next, localize } from '@operato/i18n';
|
|
|
5
5
|
import { create, Model } from '@hatiolab/things-scene';
|
|
6
6
|
import { gridStep } from './part-edits.js';
|
|
7
7
|
import { DEFAULT_VIEW, WORKBENCH } from './figure-view.js';
|
|
8
|
+
function toScenePlacement(placement) {
|
|
9
|
+
switch (placement) {
|
|
10
|
+
case 'ceiling':
|
|
11
|
+
return 'inverted';
|
|
12
|
+
case 'center':
|
|
13
|
+
return 'space';
|
|
14
|
+
case 'floor':
|
|
15
|
+
default:
|
|
16
|
+
return 'floor';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
8
19
|
/**
|
|
9
20
|
* 판의 종이 색 — 거의 흰색.
|
|
10
21
|
*
|
|
@@ -68,6 +79,7 @@ let FigureCanvas = class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
68
79
|
/** 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다(`figure-view.ts`). */
|
|
69
80
|
this.view = DEFAULT_VIEW;
|
|
70
81
|
this.parts = [];
|
|
82
|
+
this.revision = 0;
|
|
71
83
|
/**
|
|
72
84
|
* 보기 전용 — 씬에서 일어난 일을 밖에 알리지 않는다.
|
|
73
85
|
*
|
|
@@ -187,7 +199,9 @@ let FigureCanvas = class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
187
199
|
willUpdate(changed) {
|
|
188
200
|
if (!this.hasUpdated)
|
|
189
201
|
return;
|
|
190
|
-
if (changed.has('
|
|
202
|
+
if (changed.has('revision'))
|
|
203
|
+
this.standing = undefined;
|
|
204
|
+
if (changed.has('board') || changed.has('parts') || changed.has('figureId') || changed.has('revision'))
|
|
191
205
|
this.standUp();
|
|
192
206
|
if (changed.has('view'))
|
|
193
207
|
this.applyView();
|
|
@@ -226,7 +240,7 @@ let FigureCanvas = class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
226
240
|
this.cannot = i18next.t('figure.text.canvas-appears-once-a-part-exists');
|
|
227
241
|
return;
|
|
228
242
|
}
|
|
229
|
-
const signature = `${this.figureId ?? ''}:${board.width}x${board.height}x${board.depth}`;
|
|
243
|
+
const signature = `${this.figureId ?? ''}:${board.width}x${board.height}x${board.depth}:${board.placement ?? 'floor'}`;
|
|
230
244
|
if (this.scene && this.standing === signature) {
|
|
231
245
|
this.syncParts();
|
|
232
246
|
return;
|
|
@@ -240,6 +254,7 @@ let FigureCanvas = class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
240
254
|
width: board.width,
|
|
241
255
|
height: board.height,
|
|
242
256
|
depth: board.depth,
|
|
257
|
+
placement: toScenePlacement(board.placement),
|
|
243
258
|
threed: true,
|
|
244
259
|
environment: 'studio',
|
|
245
260
|
/*
|
|
@@ -461,6 +476,10 @@ __decorate([
|
|
|
461
476
|
property({ type: Array }),
|
|
462
477
|
__metadata("design:type", Array)
|
|
463
478
|
], FigureCanvas.prototype, "parts", void 0);
|
|
479
|
+
__decorate([
|
|
480
|
+
property({ type: Number }),
|
|
481
|
+
__metadata("design:type", Object)
|
|
482
|
+
], FigureCanvas.prototype, "revision", void 0);
|
|
464
483
|
__decorate([
|
|
465
484
|
property({ type: String }),
|
|
466
485
|
__metadata("design:type", String)
|