@things-factory/figure-ui 10.1.3 → 10.1.5
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/graphql/index.ts +87 -12
- package/client/modeller/figure-ask.ts +14 -4
- package/client/modeller/figure-canvas.ts +53 -0
- package/client/modeller/figure-history-panel.ts +324 -0
- package/client/modeller/figure-inspector.ts +78 -2
- package/client/modeller/figure-side.ts +22 -1
- package/client/modeller/figure-thumbnail.ts +78 -0
- package/client/pages/figure-list-page.ts +391 -195
- package/client/pages/figure-modeller-page.ts +114 -19
- package/client/types.ts +17 -0
- package/dist-client/graphql/index.d.ts +24 -4
- package/dist-client/graphql/index.js +70 -9
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-ask.js +14 -4
- package/dist-client/modeller/figure-ask.js.map +1 -1
- package/dist-client/modeller/figure-canvas.d.ts +11 -0
- package/dist-client/modeller/figure-canvas.js +42 -0
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.d.ts +42 -0
- package/dist-client/modeller/figure-history-panel.js +343 -0
- package/dist-client/modeller/figure-history-panel.js.map +1 -0
- package/dist-client/modeller/figure-inspector.d.ts +17 -0
- package/dist-client/modeller/figure-inspector.js +77 -2
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +7 -0
- package/dist-client/modeller/figure-side.js +43 -0
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/figure-thumbnail.d.ts +3 -0
- package/dist-client/modeller/figure-thumbnail.js +59 -0
- package/dist-client/modeller/figure-thumbnail.js.map +1 -0
- package/dist-client/pages/figure-list-page.d.ts +45 -17
- package/dist-client/pages/figure-list-page.js +374 -209
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +27 -0
- package/dist-client/pages/figure-modeller-page.js +109 -19
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-client/types.d.ts +19 -0
- package/dist-client/types.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +5 -3
- package/translations/en.json +19 -0
- package/translations/ja.json +21 -2
- package/translations/ko.json +21 -2
- package/translations/ms.json +21 -2
- package/translations/zh.json +21 -2
- package/client/viewparts/figure-card.ts +0 -226
- package/dist-client/viewparts/figure-card.d.ts +0 -24
- package/dist-client/viewparts/figure-card.js +0 -221
- package/dist-client/viewparts/figure-card.js.map +0 -1
|
@@ -2,6 +2,7 @@ import '@material/web/icon/icon.js'
|
|
|
2
2
|
import '../modeller/figure-side.js'
|
|
3
3
|
import '../modeller/figure-ask.js'
|
|
4
4
|
import '../modeller/figure-canvas.js'
|
|
5
|
+
import { captureThumbnail } from '../modeller/figure-thumbnail.js'
|
|
5
6
|
import '../modeller/figure-preview.js'
|
|
6
7
|
import '../modeller/figure-parts.js'
|
|
7
8
|
import '../modeller/figure-inspector.js'
|
|
@@ -188,7 +189,13 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
188
189
|
display: none;
|
|
189
190
|
}
|
|
190
191
|
|
|
191
|
-
/*
|
|
192
|
+
/*
|
|
193
|
+
머리줄 — 이름 · 타입 · 미저장 표시 · 말로 시키는 줄 · 저장을 **한 줄에** 담는다.
|
|
194
|
+
|
|
195
|
+
이름은 페이지 제목에도 나온다(셸이 context.title 로 그린다). 그래도 여기 입력칸을 두는
|
|
196
|
+
이유는 **고치는 자리**가 필요해서다 — 제목은 읽는 것이고 이 칸은 바꾸는 것이다. 두 줄로
|
|
197
|
+
벌리는 대신 한 줄에 모아 캔버스에 높이를 넘긴다.
|
|
198
|
+
*/
|
|
192
199
|
div[header] {
|
|
193
200
|
display: flex;
|
|
194
201
|
align-items: center;
|
|
@@ -196,9 +203,13 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
196
203
|
padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
|
|
197
204
|
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
198
205
|
}
|
|
206
|
+
div[header] figure-ask {
|
|
207
|
+
flex: 1;
|
|
208
|
+
min-width: 0;
|
|
209
|
+
}
|
|
199
210
|
input[name] {
|
|
200
211
|
flex: none;
|
|
201
|
-
width:
|
|
212
|
+
width: 180px;
|
|
202
213
|
padding: 5px 9px;
|
|
203
214
|
font: var(--input-field-font, inherit);
|
|
204
215
|
font-size: 0.82rem;
|
|
@@ -522,7 +533,18 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
522
533
|
|
|
523
534
|
get context() {
|
|
524
535
|
return {
|
|
525
|
-
title: this.figure?.name ?? i18next.t('title.new-figure')
|
|
536
|
+
title: this.figure?.name ?? i18next.t('title.new-figure'),
|
|
537
|
+
/*
|
|
538
|
+
**제목 줄은 끈다.** 이름은 그대로 둔다.
|
|
539
|
+
|
|
540
|
+
이 화면의 주인공은 캔버스다. 이름은 속성 패널에서 보고 고치고, 브라우저 탭에도
|
|
541
|
+
「operato figure · 작업자」로 나온다. 그 위에 제목 줄을 한 겹 더 얹으면 같은 말이 세 번이고,
|
|
542
|
+
그만큼 캔버스가 낮아진다.
|
|
543
|
+
|
|
544
|
+
`title` 을 비우는 것으로 끄지 않는 이유는 그 값을 MDI 탭도 읽기 때문이다 — 비우면 탭 이름이
|
|
545
|
+
함께 사라진다. 이름은 하나이고 그리는 자리마다 켜고 끈다(`ox-page-title-bar`).
|
|
546
|
+
*/
|
|
547
|
+
heading: false
|
|
526
548
|
}
|
|
527
549
|
}
|
|
528
550
|
|
|
@@ -531,30 +553,33 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
531
553
|
|
|
532
554
|
return html`
|
|
533
555
|
<div header>
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
/>
|
|
540
|
-
<code type title=${i18next.t('text.type-name-cannot-change')}>${this.board?.figureType ?? ''}</code>
|
|
556
|
+
<!--
|
|
557
|
+
이름과 타입은 **속성 패널**에 있다(figure-inspector 의 도형 절). 여기 있던 이름 칸은
|
|
558
|
+
하네스 시절의 자리였다 — 앱에서는 셸이 페이지 제목으로 같은 이름을 그리므로 글자가 두 번
|
|
559
|
+
나오고, 캔버스가 주인공인 화면에서 그만큼 높이를 잃었다.
|
|
560
|
+
-->
|
|
541
561
|
${this.dirty ? html`<span dirty>${i18next.t('text.unsaved-changes')}</span>` : ''}
|
|
542
|
-
|
|
562
|
+
<!--
|
|
563
|
+
말로 시키는 줄이 머리줄의 남는 폭을 쓴다. 따로 두면 화면 위가 두 줄이 되고, 그 위 줄은
|
|
564
|
+
이름을 페이지 제목과 겹쳐 적는다.
|
|
565
|
+
-->
|
|
566
|
+
<figure-ask
|
|
567
|
+
.source=${source}
|
|
568
|
+
.type=${source?.type ?? ''}
|
|
569
|
+
@proposed=${(e: CustomEvent) => this.receive(e.detail.proposal)}
|
|
570
|
+
></figure-ask>
|
|
543
571
|
<button save ?disabled=${!this.canSave} @click=${() => this.save()}>
|
|
544
572
|
${this.saving ? i18next.t('text.saving') : i18next.t('button.save')}
|
|
545
573
|
</button>
|
|
546
574
|
</div>
|
|
547
575
|
|
|
548
|
-
<figure-ask
|
|
549
|
-
.source=${source}
|
|
550
|
-
.type=${source?.type ?? ''}
|
|
551
|
-
@proposed=${(e: CustomEvent) => this.receive(e.detail.proposal)}
|
|
552
|
-
></figure-ask>
|
|
553
|
-
|
|
554
576
|
<div
|
|
555
577
|
edits
|
|
556
578
|
?collapse-left=${this.collapsed.left}
|
|
557
579
|
?collapse-right=${this.collapsed.right}
|
|
580
|
+
@figure-rename=${(e: CustomEvent) => this.rename(e.detail.name)}
|
|
581
|
+
@figure-released=${(e: CustomEvent) => this.released(e.detail.figure)}
|
|
582
|
+
@figure-reverted=${() => this.figure?.id && this.load(this.figure.id)}
|
|
558
583
|
@add-part=${(e: CustomEvent) => this.addPart(e.detail.primitive)}
|
|
559
584
|
@select-part=${(e: CustomEvent) => (this.selected = e.detail.index)}
|
|
560
585
|
@remove-part=${(e: CustomEvent) => this.removePart(e.detail.index)}
|
|
@@ -614,6 +639,11 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
614
639
|
</div>
|
|
615
640
|
|
|
616
641
|
<figure-side
|
|
642
|
+
.figureName=${this.figure?.name ?? ''}
|
|
643
|
+
.figureId=${this.figure?.id ?? ''}
|
|
644
|
+
.figureState=${this.figure?.state}
|
|
645
|
+
.figureVersion=${this.figure?.version ?? 0}
|
|
646
|
+
.dirty=${this.dirty}
|
|
617
647
|
.board=${this.board}
|
|
618
648
|
.parts=${this.parts}
|
|
619
649
|
.source=${source}
|
|
@@ -862,6 +892,11 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
862
892
|
|
|
863
893
|
this.figure = figure
|
|
864
894
|
|
|
895
|
+
/* 그림이 없으면 이 자리에서 채운다(아래 `fillThumbnail`). 기다리지 않는다 — 화면이 먼저다. */
|
|
896
|
+
if (!figure.thumbnail && figure.id) {
|
|
897
|
+
void this.fillThumbnail(figure.id)
|
|
898
|
+
}
|
|
899
|
+
|
|
865
900
|
// 서버가 준 것을 그대로 쓴다. 읽지 못하면 오류보고없이 빈 상태로 바꾸지 않는다.
|
|
866
901
|
// 그러면 사용자가 저장을 선택해 원본을 덮어쓴다.
|
|
867
902
|
const model = figure.source ? fromFigureSource(JSON.parse(figure.source) as FigureSource) : undefined
|
|
@@ -1025,6 +1060,55 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1025
1060
|
if (at >= 0) this.selected = at
|
|
1026
1061
|
}
|
|
1027
1062
|
|
|
1063
|
+
/**
|
|
1064
|
+
* 그림이 없는 도형을 **열었을 때 한 번** 찍어 채운다.
|
|
1065
|
+
*
|
|
1066
|
+
* ## 왜 여기인가
|
|
1067
|
+
*
|
|
1068
|
+
* 보드는 저장할 때 서버가 찍는다. 그런데 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는
|
|
1069
|
+
* 것이 자주 실패한다. 도형을 그리는 GPU 는 이미 보는 사람 손에 있고, 씬은 지금 여기 서 있다.
|
|
1070
|
+
*
|
|
1071
|
+
* 저작면을 거치지 않고 생긴 것들이 그림이 없다 — 씨앗으로 넣은 표본, AI 가 낸 후보, 반입한 자산.
|
|
1072
|
+
* 그것들을 한 번 열어 보면 그때 채워진다.
|
|
1073
|
+
*
|
|
1074
|
+
* ## 저장이 아니다
|
|
1075
|
+
*
|
|
1076
|
+
* 그림만 보낸다. 정본을 건드리지 않으므로 미저장 표시도 켜지 않는다 — 열어 봤다는 것이 고쳤다는
|
|
1077
|
+
* 뜻이 되면 저작자가 저장할지 말지를 매번 고민한다.
|
|
1078
|
+
*/
|
|
1079
|
+
/**
|
|
1080
|
+
* 발행되었다 — **정본을 든 자리가 그 사실을 받는다.**
|
|
1081
|
+
*
|
|
1082
|
+
* 이력 패널은 발행을 시키고 결과를 알릴 뿐, 이 페이지의 상태를 고쳐 쓰지 않는다. 판 번호와 상태가
|
|
1083
|
+
* 여기서 갱신되어야 머리줄·이력 탭이 같은 것을 본다.
|
|
1084
|
+
*
|
|
1085
|
+
* 정본(source)은 발행이 건드리지 않으므로 다시 불러오지 않는다 — 저작 중인 화면을 되감지 않는다.
|
|
1086
|
+
*/
|
|
1087
|
+
private released(figure: Figure): void {
|
|
1088
|
+
if (!this.figure || figure?.id !== this.figure.id) {
|
|
1089
|
+
return
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
this.figure = { ...this.figure, version: figure.version, state: figure.state, updatedAt: figure.updatedAt }
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
private async fillThumbnail(id: string): Promise<void> {
|
|
1096
|
+
await this.updateComplete
|
|
1097
|
+
|
|
1098
|
+
const thumbnail = await captureThumbnail(this.board, this.parts)
|
|
1099
|
+
if (!thumbnail || this.figure?.id !== id) {
|
|
1100
|
+
return
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
try {
|
|
1104
|
+
await updateFigure(id, { thumbnail })
|
|
1105
|
+
this.figure = { ...this.figure, thumbnail }
|
|
1106
|
+
} catch (e) {
|
|
1107
|
+
/* 볼 권한만 있는 사람일 수 있다. 화면은 그대로 두고 조용히 넘어간다. */
|
|
1108
|
+
console.warn(`[thumbnail] ${id} 를 채우지 못했습니다 — ${(e as Error).message}`)
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1028
1112
|
private async save() {
|
|
1029
1113
|
if (!this.canSave || !this.folded || !this.figure) {
|
|
1030
1114
|
return
|
|
@@ -1033,11 +1117,21 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1033
1117
|
this.saving = true
|
|
1034
1118
|
try {
|
|
1035
1119
|
const sourceJson = JSON.stringify(this.folded)
|
|
1120
|
+
/*
|
|
1121
|
+
보이는 것을 그대로 찍어 함께 보낸다.
|
|
1122
|
+
|
|
1123
|
+
카드가 그림 없이 서 있으면 카탈로그에서 무엇인지 못 고른다. 찍는 자리를 저장에 두는 이유는
|
|
1124
|
+
**그 순간이 정본이 바뀌는 순간**이라서다 — 따로 찍는 절차를 두면 그림과 정본이 갈린다.
|
|
1125
|
+
|
|
1126
|
+
못 찍으면 보내지 않는다(`undefined`). 빈 그림을 넣어 「그림이 있다」고 말하지 않는다.
|
|
1127
|
+
*/
|
|
1128
|
+
const thumbnail = await captureThumbnail(this.board, this.parts)
|
|
1036
1129
|
|
|
1037
1130
|
if (this.figure.id) {
|
|
1038
1131
|
const result = await updateFigure(this.figure.id, {
|
|
1039
1132
|
name: this.figure.name,
|
|
1040
|
-
source: sourceJson
|
|
1133
|
+
source: sourceJson,
|
|
1134
|
+
thumbnail
|
|
1041
1135
|
})
|
|
1042
1136
|
this.figure = result.figure
|
|
1043
1137
|
this.violations = result.violations
|
|
@@ -1052,7 +1146,8 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1052
1146
|
const result = await createFigure({
|
|
1053
1147
|
type: this.folded.type,
|
|
1054
1148
|
name: this.figure.name,
|
|
1055
|
-
source: sourceJson
|
|
1149
|
+
source: sourceJson,
|
|
1150
|
+
thumbnail
|
|
1056
1151
|
})
|
|
1057
1152
|
this.figure = result.figure
|
|
1058
1153
|
this.violations = result.violations
|
package/client/types.ts
CHANGED
|
@@ -30,6 +30,23 @@ export interface Figure {
|
|
|
30
30
|
version?: number
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* 발행된 판 하나.
|
|
35
|
+
*
|
|
36
|
+
* 저작 중 저장은 여기 오지 않는다 — 판본은 발행에서만 생긴다.
|
|
37
|
+
*/
|
|
38
|
+
export interface FigureVersion {
|
|
39
|
+
version: number
|
|
40
|
+
/** 발행하며 남긴 말. */
|
|
41
|
+
comment?: string
|
|
42
|
+
state?: 'draft' | 'released'
|
|
43
|
+
updatedAt?: string
|
|
44
|
+
updater?: { id: string; name: string }
|
|
45
|
+
score?: number
|
|
46
|
+
triangles?: number
|
|
47
|
+
groups?: number
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
export interface FigureListResult {
|
|
34
51
|
items: Figure[]
|
|
35
52
|
total: number
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import type { Figure, FigureFinding, FigureListResult, FigurePatch, FigureProposal, NewFigure } from '../types.js';
|
|
1
|
+
import type { Figure, FigureFinding, FigureListResult, FigurePatch, FigureProposal, FigureVersion, NewFigure } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 목록을 가져온다.
|
|
4
|
+
*
|
|
5
|
+
* 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어
|
|
6
|
+
* filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,
|
|
7
|
+
* 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.
|
|
8
|
+
*/
|
|
2
9
|
export declare function fetchFigureList(params: {
|
|
3
10
|
page?: number;
|
|
4
11
|
limit?: number;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
filters?: unknown[];
|
|
13
|
+
sortings?: {
|
|
14
|
+
name: string;
|
|
15
|
+
desc?: boolean;
|
|
16
|
+
}[];
|
|
8
17
|
}): Promise<FigureListResult>;
|
|
9
18
|
/** 저작면에 필요한 전부 — `source` 를 포함한다. */
|
|
10
19
|
export declare function fetchFigure(id: string): Promise<Figure>;
|
|
@@ -39,3 +48,14 @@ export declare function proposeFigure(request: {
|
|
|
39
48
|
palette: string[];
|
|
40
49
|
image?: File;
|
|
41
50
|
}): Promise<FigureProposal>;
|
|
51
|
+
/**
|
|
52
|
+
* 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.
|
|
53
|
+
*
|
|
54
|
+
* 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은
|
|
55
|
+
* 발행을 푼다(초안으로 돌아온다).
|
|
56
|
+
*/
|
|
57
|
+
export declare function releaseFigure(id: string, comment?: string): Promise<Figure>;
|
|
58
|
+
/** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */
|
|
59
|
+
export declare function revertFigureVersion(id: string, version: number): Promise<SaveResult>;
|
|
60
|
+
/** 발행된 판들 — 최근 것부터 열 개. */
|
|
61
|
+
export declare function fetchFigureVersions(id: string): Promise<FigureVersion[]>;
|
|
@@ -33,6 +33,7 @@ const FIGURE_LIST_FIELDS = `
|
|
|
33
33
|
description
|
|
34
34
|
category
|
|
35
35
|
state
|
|
36
|
+
version
|
|
36
37
|
score
|
|
37
38
|
triangles
|
|
38
39
|
groups
|
|
@@ -41,12 +42,14 @@ const FIGURE_LIST_FIELDS = `
|
|
|
41
42
|
updatedAt
|
|
42
43
|
updater { id name }
|
|
43
44
|
`;
|
|
45
|
+
/**
|
|
46
|
+
* 목록을 가져온다.
|
|
47
|
+
*
|
|
48
|
+
* 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어
|
|
49
|
+
* filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,
|
|
50
|
+
* 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.
|
|
51
|
+
*/
|
|
44
52
|
export async function fetchFigureList(params) {
|
|
45
|
-
const filters = [];
|
|
46
|
-
if (params.category)
|
|
47
|
-
filters.push({ name: 'category', operator: 'eq', value: params.category });
|
|
48
|
-
if (params.state)
|
|
49
|
-
filters.push({ name: 'state', operator: 'eq', value: params.state });
|
|
50
53
|
const response = await client.query({
|
|
51
54
|
query: gql `
|
|
52
55
|
query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
|
|
@@ -57,9 +60,9 @@ export async function fetchFigureList(params) {
|
|
|
57
60
|
}
|
|
58
61
|
`,
|
|
59
62
|
variables: {
|
|
60
|
-
filters,
|
|
63
|
+
filters: params.filters ?? [],
|
|
61
64
|
pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },
|
|
62
|
-
sortings: [{ name: 'updatedAt', desc: true }]
|
|
65
|
+
sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]
|
|
63
66
|
},
|
|
64
67
|
fetchPolicy: 'network-only'
|
|
65
68
|
});
|
|
@@ -74,7 +77,6 @@ export async function fetchFigure(id) {
|
|
|
74
77
|
${FIGURE_LIST_FIELDS}
|
|
75
78
|
source
|
|
76
79
|
properties
|
|
77
|
-
version
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
`,
|
|
@@ -119,7 +121,7 @@ export async function updateFigure(id, patch) {
|
|
|
119
121
|
mutation: gql `
|
|
120
122
|
mutation ($id: String!, $patch: FigurePatch!) {
|
|
121
123
|
updateFigure(id: $id, patch: $patch) {
|
|
122
|
-
figure { ${FIGURE_LIST_FIELDS} source
|
|
124
|
+
figure { ${FIGURE_LIST_FIELDS} source }
|
|
123
125
|
violations { code message at }
|
|
124
126
|
}
|
|
125
127
|
}
|
|
@@ -170,4 +172,63 @@ export async function proposeFigure(request) {
|
|
|
170
172
|
});
|
|
171
173
|
return unwrap(response, 'proposeFigure');
|
|
172
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.
|
|
177
|
+
*
|
|
178
|
+
* 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은
|
|
179
|
+
* 발행을 푼다(초안으로 돌아온다).
|
|
180
|
+
*/
|
|
181
|
+
export async function releaseFigure(id, comment) {
|
|
182
|
+
const response = await client.mutate({
|
|
183
|
+
mutation: gql `
|
|
184
|
+
mutation ReleaseFigure($id: String!, $comment: String) {
|
|
185
|
+
releaseFigure(id: $id, comment: $comment) {
|
|
186
|
+
id
|
|
187
|
+
version
|
|
188
|
+
state
|
|
189
|
+
updatedAt
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
`,
|
|
193
|
+
variables: { id, comment }
|
|
194
|
+
});
|
|
195
|
+
return unwrap(response, 'releaseFigure');
|
|
196
|
+
}
|
|
197
|
+
/** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */
|
|
198
|
+
export async function revertFigureVersion(id, version) {
|
|
199
|
+
const response = await client.mutate({
|
|
200
|
+
mutation: gql `
|
|
201
|
+
mutation RevertFigureVersion($id: String!, $version: Float!) {
|
|
202
|
+
revertFigureVersion(id: $id, version: $version) {
|
|
203
|
+
figure { ${FIGURE_LIST_FIELDS} source }
|
|
204
|
+
violations { code message at }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
`,
|
|
208
|
+
variables: { id, version }
|
|
209
|
+
});
|
|
210
|
+
return unwrap(response, 'revertFigureVersion');
|
|
211
|
+
}
|
|
212
|
+
/** 발행된 판들 — 최근 것부터 열 개. */
|
|
213
|
+
export async function fetchFigureVersions(id) {
|
|
214
|
+
const response = await client.query({
|
|
215
|
+
query: gql `
|
|
216
|
+
query FigureVersions($id: String!) {
|
|
217
|
+
figureVersions(id: $id) {
|
|
218
|
+
version
|
|
219
|
+
comment
|
|
220
|
+
state
|
|
221
|
+
updatedAt
|
|
222
|
+
updater { id name }
|
|
223
|
+
score
|
|
224
|
+
triangles
|
|
225
|
+
groups
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
`,
|
|
229
|
+
variables: { id },
|
|
230
|
+
fetchPolicy: 'network-only'
|
|
231
|
+
});
|
|
232
|
+
return unwrap(response, 'figureVersions') ?? [];
|
|
233
|
+
}
|
|
173
234
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAIzC;;;;;;;;;;GAUG;AACH,SAAS,MAAM,CAAI,QAAqF,EAAE,KAAa;IACrH,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,CAAA;IAC1C,IAAI,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,CAAA;IAE/B,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,OAAO,KAAK,gBAAgB,CAAC,CAAA;IAC/C,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAM,CAAA;AAClC,CAAC;AAGD;;;;;GAKG;AACH,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;CAc1B,CAAA;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAMrC;IACC,MAAM,OAAO,GAAyD,EAAE,CAAA;IACxE,IAAI,MAAM,CAAC,QAAQ;QAAE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAA;IAC/F,IAAI,MAAM,CAAC,KAAK;QAAE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAA;IAEtF,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;oBAGM,kBAAkB;;;;KAIjC;QACD,SAAS,EAAE;YACT,OAAO;YACP,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE;YACjE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;SAC9C;QACD,WAAW,EAAE,cAAc;KAC5B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAmB,QAAQ,EAAE,SAAS,CAAC,CAAA;AACtD,CAAC;AAED,qCAAqC;AACrC,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAAU;IAC1C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;YAGF,kBAAkB;;;;;;KAMzB;QACD,SAAS,EAAE,EAAE,EAAE,EAAE;QACjB,WAAW,EAAE,cAAc;KAC5B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAS,QAAQ,EAAE,QAAQ,CAAC,CAAA;AAC3C,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB;IACxC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;;KAIT;QACD,WAAW,EAAE,cAAc;KAC5B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAkB,QAAQ,EAAE,iBAAiB,CAAC,IAAI,EAAE,CAAA;AACnE,CAAC;AAQD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAiB;IAClD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;QACnC,QAAQ,EAAE,GAAG,CAAA;;;qBAGI,kBAAkB;;;;KAIlC;QACD,SAAS,EAAE,EAAE,MAAM,EAAE;KACtB,CAAC,CAAA;IAEF,OAAO,MAAM,CAAa,QAAQ,EAAE,cAAc,CAAC,CAAA;AACrD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,EAAU,EAAE,KAAkB;IAC/D,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;QACnC,QAAQ,EAAE,GAAG,CAAA;;;qBAGI,kBAAkB;;;;KAIlC;QACD,SAAS,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE;KACzB,CAAC,CAAA;IAEF,OAAO,MAAM,CAAa,QAAQ,EAAE,cAAc,CAAC,CAAA;AACrD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,EAAU;IAC3C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;QACnC,QAAQ,EAAE,GAAG,CAAA;;;;KAIZ;QACD,SAAS,EAAE,EAAE,EAAE,EAAE;KAClB,CAAC,CAAA;IAEF,OAAO,MAAM,CAAU,QAAQ,EAAE,cAAc,CAAC,CAAA;AAClD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAMnC;IACC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;QACnC,QAAQ,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;KAeZ;QACD,SAAS,EAAE,EAAE,OAAO,EAAE;KACvB,CAAC,CAAA;IAEF,OAAO,MAAM,CAAiB,QAAQ,EAAE,eAAe,CAAC,CAAA;AAC1D,CAAC","sourcesContent":["import gql from 'graphql-tag'\nimport { client } from '@operato/graphql'\n\nimport type { Figure, FigureFinding, FigureListResult, FigurePatch, FigureProposal, NewFigure } from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n throw new Error(`서버가 ${field} 를 돌려주지 않았습니다.`)\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * `score`·`triangles` 를 함께 받는 이유는 카드에 그것을 보여 주기 때문이다. 저작자가\n * 목록에서 「이 Figure 가 무거운가」를 바로 알아야 한다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n type\n name\n description\n category\n state\n score\n triangles\n groups\n thumbnail\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n search?: string\n category?: string\n state?: string\n}): Promise<FigureListResult> {\n const filters: { name: string; operator: string; value: unknown }[] = []\n if (params.category) filters.push({ name: 'category', operator: 'eq', value: params.category })\n if (params.state) filters.push({ name: 'state', operator: 'eq', value: params.state })\n\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n figures(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} }\n total\n }\n }\n `,\n variables: {\n filters,\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, 'figures')\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n version\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source version }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 저작 보조에게 후보를 시킨다.\n *\n * 팔레트를 **클라이언트가 보낸다.** 서버는 팔레트를 모른다 — things-scene 이 갖고\n * 있는데 그 패키지의 node 조건은 팔레트가 없는 번들로 간다. 그리는 쪽이 어떤 토큰을\n * 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.\n *\n * 돌아오는 것은 **후보**다. 저장되지 않았다 — 저작자가 받아야 정본이 된다.\n */\nexport async function proposeFigure(request: {\n prompt: string\n base?: string\n type?: string\n palette: string[]\n image?: File\n}): Promise<FigureProposal> {\n const response = await client.mutate({\n mutation: gql`\n mutation ProposeFigure($request: ProposeRequest!) {\n proposeFigure(request: $request) {\n source\n score\n grade\n triangles\n groups\n attempts\n violations {\n code\n message\n }\n }\n }\n `,\n variables: { request }\n })\n\n return unwrap<FigureProposal>(response, 'proposeFigure')\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { client } from '@operato/graphql'\n\nimport type {\n Figure,\n FigureFinding,\n FigureListResult,\n FigurePatch,\n FigureProposal,\n FigureVersion,\n NewFigure\n} from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n throw new Error(`서버가 ${field} 를 돌려주지 않았습니다.`)\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * `score`·`triangles` 를 함께 받는 이유는 카드에 그것을 보여 주기 때문이다. 저작자가\n * 목록에서 「이 Figure 가 무거운가」를 바로 알아야 한다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n type\n name\n description\n category\n state\n version\n score\n triangles\n groups\n thumbnail\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n}): Promise<FigureListResult> {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n figures(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} }\n total\n }\n }\n `,\n variables: {\n filters: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, 'figures')\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 저작 보조에게 후보를 시킨다.\n *\n * 팔레트를 **클라이언트가 보낸다.** 서버는 팔레트를 모른다 — things-scene 이 갖고\n * 있는데 그 패키지의 node 조건은 팔레트가 없는 번들로 간다. 그리는 쪽이 어떤 토큰을\n * 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.\n *\n * 돌아오는 것은 **후보**다. 저장되지 않았다 — 저작자가 받아야 정본이 된다.\n */\nexport async function proposeFigure(request: {\n prompt: string\n base?: string\n type?: string\n palette: string[]\n image?: File\n}): Promise<FigureProposal> {\n const response = await client.mutate({\n mutation: gql`\n mutation ProposeFigure($request: ProposeRequest!) {\n proposeFigure(request: $request) {\n source\n score\n grade\n triangles\n groups\n attempts\n violations {\n code\n message\n }\n }\n }\n `,\n variables: { request }\n })\n\n return unwrap<FigureProposal>(response, 'proposeFigure')\n}\n\n/**\n * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */\nexport async function revertFigureVersion(id: string, version: number): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!) {\n revertFigureVersion(id: $id, version: $version) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, version }\n })\n\n return unwrap<SaveResult>(response, 'revertFigureVersion')\n}\n\n/** 발행된 판들 — 최근 것부터 열 개. */\nexport async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {\n const response = await client.query({\n query: gql`\n query FigureVersions($id: String!) {\n figureVersions(id: $id) {\n version\n comment\n state\n updatedAt\n updater { id name }\n score\n triangles\n groups\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureVersion[]>(response, 'figureVersions') ?? []\n}\n"]}
|
|
@@ -32,10 +32,19 @@ let FigureAsk = class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
32
32
|
this.failure = '';
|
|
33
33
|
}
|
|
34
34
|
static { this.styles = css `
|
|
35
|
+
/*
|
|
36
|
+
이 부품은 **자기 줄을 갖지 않는다.**
|
|
37
|
+
|
|
38
|
+
전에는 테두리와 여백으로 스스로 한 줄이 되었다. 저작면의 머리줄 바로 아래에 놓이니
|
|
39
|
+
화면 위쪽이 두 줄이 되었고, 위 줄은 이름을 페이지 제목과 겹쳐 적고 있었다. 캔버스가
|
|
40
|
+
주인공인 화면에서 그 두 줄은 비싸다.
|
|
41
|
+
|
|
42
|
+
이제 머리줄 안에 들어가 그 줄의 남는 폭을 쓴다. 테두리·여백은 담는 줄이 갖는다.
|
|
43
|
+
*/
|
|
35
44
|
:host {
|
|
36
|
-
display:
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
display: flex;
|
|
46
|
+
flex: 1;
|
|
47
|
+
min-width: 0;
|
|
39
48
|
font: var(--label-font, inherit);
|
|
40
49
|
/*
|
|
41
50
|
이 줄의 글자 크기를 여기서 정한다.
|
|
@@ -50,9 +59,10 @@ let FigureAsk = class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
50
59
|
|
|
51
60
|
form {
|
|
52
61
|
display: flex;
|
|
62
|
+
flex: 1;
|
|
63
|
+
min-width: 0;
|
|
53
64
|
align-items: center;
|
|
54
65
|
gap: var(--spacing-medium, 8px);
|
|
55
|
-
padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
md-icon[lead] {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"figure-ask.js","sourceRoot":"","sources":["../../client/modeller/figure-ask.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport type { FigureSource } from '@hatiolab/figure-model'\nimport { activePalette } from '@hatiolab/things-scene'\n\nimport { proposeFigure } from '../graphql/index.js'\nimport type { FigureProposal } from '../types.js'\n\n/**\n * 말로 시키는 줄.\n *\n * ## 무엇을 시키는지가 화면에 보여야 한다\n *\n * 정본이 있으면 **고치는 것**이고, 없으면 **만드는 것**이다. 같은 칸으로 두 가지를\n * 하면 저작자가 「이게 지금 것을 덮어쓰나」를 매번 걱정한다. 그래서 무엇을 할지\n * 칸 옆에 적어 둔다.\n *\n * ## 후보를 여기서 받지 않는다\n *\n * 이 컴포넌트는 시키고 결과를 **알릴 뿐**이다. 받을지 버릴지는 저작면이 정한다 —\n * 정본은 한 벌이고 그 한 벌을 가진 자리가 정해야 한다.\n *\n * ## 팔레트를 함께 보낸다\n *\n * 서버는 팔레트를 모른다. 그리는 쪽이 어떤 토큰을 풀 수 있는지 아는 유일한 자리다.\n */\n@customElement('figure-ask')\nexport class FigureAsk extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n display: block;\n background-color: var(--md-sys-color-surface-container-lowest);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n font: var(--label-font, inherit);\n /*\n 이 줄의 글자 크기를 여기서 정한다.\n\n 안 적어 두면 브라우저 기본 크기(16px)를 물려받는다. 아래 패널들은 전부 12px\n 언저리라 위 두 줄만 커 보이고, 단추가 화면 전체와 따로 논다.\n\n form 요소(input · button)는 글꼴을 물려받지 않으므로 각자 적어야 한다.\n */\n font-size: 0.76rem;\n }\n\n form {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n }\n\n md-icon[lead] {\n --md-icon-size: 18px;\n flex: none;\n color: var(--md-sys-color-primary);\n }\n\n input[ask] {\n flex: 1;\n min-width: 0;\n padding: 6px 10px;\n font: var(--input-field-font, inherit);\n font-size: 0.78rem;\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n }\n input[ask]:focus {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: -1px;\n }\n input[ask][disabled] {\n opacity: 0.5;\n }\n\n /* 무엇을 시키는 것인지 — 만드나 고치나 */\n span[mode] {\n flex: none;\n font-size: 0.72rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* 그림 붙이기. 붙인 것이 있으면 이름을 보여 준다 */\n label[picture] {\n display: flex;\n align-items: center;\n gap: 4px;\n flex: none;\n padding: 5px 9px;\n font-size: 0.74rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n cursor: pointer;\n }\n label[picture]:hover {\n background-color: var(--md-sys-color-surface-container);\n }\n label[picture][has] {\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border-color: transparent;\n }\n label[picture] md-icon {\n --md-icon-size: 16px;\n }\n label[picture] input {\n display: none;\n }\n span[filename] {\n max-width: 8em;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /*\n 채운 파랑 단추를 저장과 나눠 갖지 않는다.\n\n 머리줄의 저장이 이 화면의 주 동작이다. 바로 아래 줄에 같은 파랑 단추가 하나 더\n 있으면 어느 쪽이 주인지 눈이 못 고른다. 이쪽은 톤 단추로 한 단계 내린다.\n */\n button[go] {\n flex: none;\n padding: 6px 14px;\n font: var(--label-font, inherit);\n font-size: 0.76rem;\n font-weight: 600;\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border: none;\n border-radius: 6px;\n cursor: pointer;\n }\n button[go]:hover:not([disabled]) {\n background-color: color-mix(in srgb, var(--md-sys-color-primary) 24%, var(--md-sys-color-primary-container));\n }\n button[go][disabled] {\n opacity: 0.4;\n cursor: default;\n }\n\n /* 못 만들었을 때. 사유를 삼키지 않는다 — 말을 고쳐 다시 시킬 단서다 */\n div[failed] {\n display: flex;\n gap: 6px;\n padding: 0 var(--spacing-large, 12px) var(--spacing-medium, 8px) var(--spacing-large, 12px);\n font-size: 0.74rem;\n line-height: 1.5;\n color: var(--md-sys-color-error);\n }\n div[failed] md-icon {\n --md-icon-size: 16px;\n flex: none;\n }\n `\n\n /** 지금 정본. 있으면 고치는 것이고 없으면 만드는 것이다. */\n @property({ type: Object }) source?: FigureSource\n /** 새로 만들 때 쓸 타입 이름. */\n @property({ type: String }) type = ''\n\n @state() private asking = false\n @state() private failure = ''\n @state() private picture?: File\n\n render() {\n const revising = !!this.source\n\n return html`\n <form @submit=${(e: Event) => this.submit(e)}>\n <md-icon lead>auto_awesome</md-icon>\n <span mode>${i18next.t(revising ? 'text.ask-to-revise' : 'text.ask-to-create')}</span>\n <input\n ask\n ?disabled=${this.asking}\n placeholder=${i18next.t(revising ? 'text.ask-revise-example' : 'text.ask-create-example')}\n />\n <label picture ?has=${!!this.picture} title=${i18next.t('text.picture-is-for-proportions')}>\n <md-icon>image</md-icon>\n ${this.picture\n ? html`<span filename>${this.picture.name}</span>`\n : i18next.t('button.attach-a-picture')}\n <input type=\"file\" accept=\"image/*\" @change=${(e: Event) => this.pick(e)} />\n </label>\n <button go ?disabled=${this.asking}>\n ${this.asking ? i18next.t('text.asking') : i18next.t('button.ask')}\n </button>\n </form>\n\n ${this.failure\n ? html`\n <div failed>\n <md-icon>error</md-icon>\n <span>${this.failure}</span>\n </div>\n `\n : nothing}\n `\n }\n\n private pick(e: Event) {\n const input = e.target as HTMLInputElement\n this.picture = input.files?.[0]\n }\n\n private async submit(e: Event) {\n e.preventDefault()\n\n const input = this.renderRoot.querySelector('input[ask]') as HTMLInputElement\n const prompt = input?.value?.trim()\n if (!prompt || this.asking) {\n return\n }\n\n this.asking = true\n this.failure = ''\n\n try {\n const proposal = await proposeFigure({\n prompt,\n base: this.source ? JSON.stringify(this.source) : undefined,\n type: this.source ? undefined : this.type,\n // 그리는 쪽이 풀 수 있는 토큰만 보낸다. 서버는 이 목록 밖의 색을 거절한다.\n palette: Object.keys(activePalette() as Record<string, string>),\n image: this.picture\n })\n\n this.dispatchEvent(\n new CustomEvent('proposed', { detail: { proposal }, bubbles: true, composed: true })\n )\n input.value = ''\n this.picture = undefined\n } catch (err) {\n // 사유를 그대로 보여 준다. 「실패했습니다」로 뭉개면 말을 어떻게 고칠지 모른다.\n this.failure = (err as Error).message\n } finally {\n this.asking = false\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"figure-ask.js","sourceRoot":"","sources":["../../client/modeller/figure-ask.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport type { FigureSource } from '@hatiolab/figure-model'\nimport { activePalette } from '@hatiolab/things-scene'\n\nimport { proposeFigure } from '../graphql/index.js'\nimport type { FigureProposal } from '../types.js'\n\n/**\n * 말로 시키는 줄.\n *\n * ## 무엇을 시키는지가 화면에 보여야 한다\n *\n * 정본이 있으면 **고치는 것**이고, 없으면 **만드는 것**이다. 같은 칸으로 두 가지를\n * 하면 저작자가 「이게 지금 것을 덮어쓰나」를 매번 걱정한다. 그래서 무엇을 할지\n * 칸 옆에 적어 둔다.\n *\n * ## 후보를 여기서 받지 않는다\n *\n * 이 컴포넌트는 시키고 결과를 **알릴 뿐**이다. 받을지 버릴지는 저작면이 정한다 —\n * 정본은 한 벌이고 그 한 벌을 가진 자리가 정해야 한다.\n *\n * ## 팔레트를 함께 보낸다\n *\n * 서버는 팔레트를 모른다. 그리는 쪽이 어떤 토큰을 풀 수 있는지 아는 유일한 자리다.\n */\n@customElement('figure-ask')\nexport class FigureAsk extends localize(i18next)(LitElement) {\n static styles = css`\n /*\n 이 부품은 **자기 줄을 갖지 않는다.**\n\n 전에는 테두리와 여백으로 스스로 한 줄이 되었다. 저작면의 머리줄 바로 아래에 놓이니\n 화면 위쪽이 두 줄이 되었고, 위 줄은 이름을 페이지 제목과 겹쳐 적고 있었다. 캔버스가\n 주인공인 화면에서 그 두 줄은 비싸다.\n\n 이제 머리줄 안에 들어가 그 줄의 남는 폭을 쓴다. 테두리·여백은 담는 줄이 갖는다.\n */\n :host {\n display: flex;\n flex: 1;\n min-width: 0;\n font: var(--label-font, inherit);\n /*\n 이 줄의 글자 크기를 여기서 정한다.\n\n 안 적어 두면 브라우저 기본 크기(16px)를 물려받는다. 아래 패널들은 전부 12px\n 언저리라 위 두 줄만 커 보이고, 단추가 화면 전체와 따로 논다.\n\n form 요소(input · button)는 글꼴을 물려받지 않으므로 각자 적어야 한다.\n */\n font-size: 0.76rem;\n }\n\n form {\n display: flex;\n flex: 1;\n min-width: 0;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n }\n\n md-icon[lead] {\n --md-icon-size: 18px;\n flex: none;\n color: var(--md-sys-color-primary);\n }\n\n input[ask] {\n flex: 1;\n min-width: 0;\n padding: 6px 10px;\n font: var(--input-field-font, inherit);\n font-size: 0.78rem;\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n }\n input[ask]:focus {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: -1px;\n }\n input[ask][disabled] {\n opacity: 0.5;\n }\n\n /* 무엇을 시키는 것인지 — 만드나 고치나 */\n span[mode] {\n flex: none;\n font-size: 0.72rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* 그림 붙이기. 붙인 것이 있으면 이름을 보여 준다 */\n label[picture] {\n display: flex;\n align-items: center;\n gap: 4px;\n flex: none;\n padding: 5px 9px;\n font-size: 0.74rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n cursor: pointer;\n }\n label[picture]:hover {\n background-color: var(--md-sys-color-surface-container);\n }\n label[picture][has] {\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border-color: transparent;\n }\n label[picture] md-icon {\n --md-icon-size: 16px;\n }\n label[picture] input {\n display: none;\n }\n span[filename] {\n max-width: 8em;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /*\n 채운 파랑 단추를 저장과 나눠 갖지 않는다.\n\n 머리줄의 저장이 이 화면의 주 동작이다. 바로 아래 줄에 같은 파랑 단추가 하나 더\n 있으면 어느 쪽이 주인지 눈이 못 고른다. 이쪽은 톤 단추로 한 단계 내린다.\n */\n button[go] {\n flex: none;\n padding: 6px 14px;\n font: var(--label-font, inherit);\n font-size: 0.76rem;\n font-weight: 600;\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border: none;\n border-radius: 6px;\n cursor: pointer;\n }\n button[go]:hover:not([disabled]) {\n background-color: color-mix(in srgb, var(--md-sys-color-primary) 24%, var(--md-sys-color-primary-container));\n }\n button[go][disabled] {\n opacity: 0.4;\n cursor: default;\n }\n\n /* 못 만들었을 때. 사유를 삼키지 않는다 — 말을 고쳐 다시 시킬 단서다 */\n div[failed] {\n display: flex;\n gap: 6px;\n padding: 0 var(--spacing-large, 12px) var(--spacing-medium, 8px) var(--spacing-large, 12px);\n font-size: 0.74rem;\n line-height: 1.5;\n color: var(--md-sys-color-error);\n }\n div[failed] md-icon {\n --md-icon-size: 16px;\n flex: none;\n }\n `\n\n /** 지금 정본. 있으면 고치는 것이고 없으면 만드는 것이다. */\n @property({ type: Object }) source?: FigureSource\n /** 새로 만들 때 쓸 타입 이름. */\n @property({ type: String }) type = ''\n\n @state() private asking = false\n @state() private failure = ''\n @state() private picture?: File\n\n render() {\n const revising = !!this.source\n\n return html`\n <form @submit=${(e: Event) => this.submit(e)}>\n <md-icon lead>auto_awesome</md-icon>\n <span mode>${i18next.t(revising ? 'text.ask-to-revise' : 'text.ask-to-create')}</span>\n <input\n ask\n ?disabled=${this.asking}\n placeholder=${i18next.t(revising ? 'text.ask-revise-example' : 'text.ask-create-example')}\n />\n <label picture ?has=${!!this.picture} title=${i18next.t('text.picture-is-for-proportions')}>\n <md-icon>image</md-icon>\n ${this.picture\n ? html`<span filename>${this.picture.name}</span>`\n : i18next.t('button.attach-a-picture')}\n <input type=\"file\" accept=\"image/*\" @change=${(e: Event) => this.pick(e)} />\n </label>\n <button go ?disabled=${this.asking}>\n ${this.asking ? i18next.t('text.asking') : i18next.t('button.ask')}\n </button>\n </form>\n\n ${this.failure\n ? html`\n <div failed>\n <md-icon>error</md-icon>\n <span>${this.failure}</span>\n </div>\n `\n : nothing}\n `\n }\n\n private pick(e: Event) {\n const input = e.target as HTMLInputElement\n this.picture = input.files?.[0]\n }\n\n private async submit(e: Event) {\n e.preventDefault()\n\n const input = this.renderRoot.querySelector('input[ask]') as HTMLInputElement\n const prompt = input?.value?.trim()\n if (!prompt || this.asking) {\n return\n }\n\n this.asking = true\n this.failure = ''\n\n try {\n const proposal = await proposeFigure({\n prompt,\n base: this.source ? JSON.stringify(this.source) : undefined,\n type: this.source ? undefined : this.type,\n // 그리는 쪽이 풀 수 있는 토큰만 보낸다. 서버는 이 목록 밖의 색을 거절한다.\n palette: Object.keys(activePalette() as Record<string, string>),\n image: this.picture\n })\n\n this.dispatchEvent(\n new CustomEvent('proposed', { detail: { proposal }, bubbles: true, composed: true })\n )\n input.value = ''\n this.picture = undefined\n } catch (err) {\n // 사유를 그대로 보여 준다. 「실패했습니다」로 뭉개면 말을 어떻게 고칠지 모른다.\n this.failure = (err as Error).message\n } finally {\n this.asking = false\n }\n }\n}\n"]}
|
|
@@ -60,6 +60,17 @@ export declare class FigureCanvas extends FigureCanvas_base {
|
|
|
60
60
|
/** 세울 수 없는 사유. 없으면 세운 것이다. */
|
|
61
61
|
private cannot;
|
|
62
62
|
private scene?;
|
|
63
|
+
/**
|
|
64
|
+
* 지금 세워진 것을 그림으로 낸다 — 카드에 쓸 썸네일.
|
|
65
|
+
*
|
|
66
|
+
* 부르는 쪽은 이 부품을 **숨은 자리에 480×360 으로 하나 더 세워** 부른다
|
|
67
|
+
* (`figure-thumbnail.ts`). 보이는 저작면을 찍지 않는 이유는, 저작자가 돌려 둔 각도와 확대,
|
|
68
|
+
* 그리고 창 크기에 따라 달라지는 칸 비율이 그대로 카드에 박히기 때문이다 — 나사를 확대해 보던
|
|
69
|
+
* 중에 저장하면 그 자산의 얼굴이 나사가 된다. 목록은 견주는 자리라 프레임이 일정해야 한다.
|
|
70
|
+
*
|
|
71
|
+
* 못 찍으면 `undefined` 다. 빈 그림을 만들어 「그림이 있다」고 말하지 않는다.
|
|
72
|
+
*/
|
|
73
|
+
snapshot(width?: number, height?: number): Promise<string | undefined>;
|
|
63
74
|
/** 지금 씬이 서 있는 판. 이것이 달라지면 다시 세운다. */
|
|
64
75
|
private standing?;
|
|
65
76
|
/** 칸 크기를 따라가게 지켜본다. 안 지켜보면 0x0 으로 고정된다. */
|
|
@@ -136,6 +136,48 @@ let FigureCanvas = class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
136
136
|
background-color: ${unsafeCSS(WORKBENCH)};
|
|
137
137
|
}
|
|
138
138
|
`; }
|
|
139
|
+
/**
|
|
140
|
+
* 지금 세워진 것을 그림으로 낸다 — 카드에 쓸 썸네일.
|
|
141
|
+
*
|
|
142
|
+
* 부르는 쪽은 이 부품을 **숨은 자리에 480×360 으로 하나 더 세워** 부른다
|
|
143
|
+
* (`figure-thumbnail.ts`). 보이는 저작면을 찍지 않는 이유는, 저작자가 돌려 둔 각도와 확대,
|
|
144
|
+
* 그리고 창 크기에 따라 달라지는 칸 비율이 그대로 카드에 박히기 때문이다 — 나사를 확대해 보던
|
|
145
|
+
* 중에 저장하면 그 자산의 얼굴이 나사가 된다. 목록은 견주는 자리라 프레임이 일정해야 한다.
|
|
146
|
+
*
|
|
147
|
+
* 못 찍으면 `undefined` 다. 빈 그림을 만들어 「그림이 있다」고 말하지 않는다.
|
|
148
|
+
*/
|
|
149
|
+
async snapshot(width = 480, height = 360) {
|
|
150
|
+
/*
|
|
151
|
+
**3D 를 낼 수 있을 때만 찍는다.**
|
|
152
|
+
|
|
153
|
+
씬의 `toDataURL` 은 **2D 씬을 렌더한다.** 그것으로 찍어 보니 흰 종이 한 장이 나왔다
|
|
154
|
+
(480×291 · 834바이트 · 순백 — 2D 판의 면인 `fillStyle: PAPER` 이 화면 전체였다). 3D 픽셀은
|
|
155
|
+
`model-layer.renderer3d`(THREE.WebGLRenderer) 쪽에 있고, 그 렌더러는
|
|
156
|
+
`preserveDrawingBuffer: false` 라 **그린 직후 같은 task 에서** 읽어야 한다. 그 절차를 아는
|
|
157
|
+
것은 씬이므로, things-scene 이 그 일을 공개 API 로 내주면 여기서 부른다.
|
|
158
|
+
|
|
159
|
+
아직 없으면 `undefined` 다. 흰 종이를 썸네일이라고 저장하면 카드가 「그림이 있다」고 말하면서
|
|
160
|
+
아무것도 보여 주지 않고, 저장할 때마다 판 번호만 올라간다 — 없는 것보다 나쁘다.
|
|
161
|
+
*/
|
|
162
|
+
const layer = this.scene?.root;
|
|
163
|
+
if (!layer?.snapshot3d) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
/*
|
|
168
|
+
알파가 필요 없고 카드에 들어갈 그림이라 webp 로 찍는다. png 는 같은 그림이 서너 배 무겁고,
|
|
169
|
+
그 무게가 도형마다 DB 와 목록 응답에 실린다. webp 를 못 만드는 브라우저는 캔버스가 알아서
|
|
170
|
+
png 를 돌려준다 — 우리가 판정하지 않는다.
|
|
171
|
+
*/
|
|
172
|
+
const url = await layer.snapshot3d(width, height, 'image/webp', 0.85);
|
|
173
|
+
return typeof url === 'string' && url.startsWith('data:') ? url : undefined;
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
/* 원인을 지어내지 않는다. 못 찍은 것이 저장을 막지는 않는다. */
|
|
177
|
+
console.warn(`[thumbnail] 찍지 못했습니다 — ${e.message}`);
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
139
181
|
render() {
|
|
140
182
|
return html `
|
|
141
183
|
<div stage></div>
|