@things-factory/figure-ui 10.1.5 → 10.1.6
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 +35 -3
- package/client/modeller/figure-ask.ts +5 -5
- package/client/modeller/figure-canvas.ts +1 -1
- package/client/modeller/figure-capabilities.ts +372 -0
- package/client/modeller/figure-history-panel.ts +171 -18
- package/client/modeller/figure-inspector.ts +392 -76
- package/client/modeller/figure-parts.ts +15 -15
- package/client/modeller/figure-preview.ts +2 -2
- package/client/modeller/figure-report.ts +102 -37
- package/client/modeller/figure-settings.ts +20 -20
- package/client/modeller/figure-side.ts +37 -9
- package/client/modeller/figure-source.test.ts +15 -1
- package/client/modeller/figure-source.ts +31 -15
- package/client/modeller/figure-thumbnail.ts +49 -4
- package/client/modeller/maturity.ts +10 -10
- package/client/pages/figure-list-page.ts +152 -66
- package/client/pages/figure-modeller-page.ts +260 -43
- package/client/types.ts +25 -0
- package/client/viewparts/figure-thumb.ts +172 -0
- package/dist-client/graphql/index.d.ts +8 -1
- package/dist-client/graphql/index.js +32 -3
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-ask.js +5 -5
- package/dist-client/modeller/figure-ask.js.map +1 -1
- package/dist-client/modeller/figure-canvas.js +1 -1
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-capabilities.d.ts +42 -0
- package/dist-client/modeller/figure-capabilities.js +344 -0
- package/dist-client/modeller/figure-capabilities.js.map +1 -0
- package/dist-client/modeller/figure-history-panel.d.ts +15 -1
- package/dist-client/modeller/figure-history-panel.js +165 -18
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +84 -0
- package/dist-client/modeller/figure-inspector.js +379 -75
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-parts.js +15 -15
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +2 -2
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-report.d.ts +18 -1
- package/dist-client/modeller/figure-report.js +105 -37
- package/dist-client/modeller/figure-report.js.map +1 -1
- package/dist-client/modeller/figure-settings.js +19 -19
- package/dist-client/modeller/figure-settings.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +1 -0
- package/dist-client/modeller/figure-side.js +36 -8
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +15 -8
- package/dist-client/modeller/figure-source.js +16 -7
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/modeller/figure-thumbnail.js +40 -4
- package/dist-client/modeller/figure-thumbnail.js.map +1 -1
- package/dist-client/modeller/maturity.js +10 -10
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/pages/figure-list-page.d.ts +1 -4
- package/dist-client/pages/figure-list-page.js +150 -63
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +45 -0
- package/dist-client/pages/figure-modeller-page.js +259 -43
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +23 -0
- package/dist-client/types.js.map +1 -1
- package/dist-client/viewparts/figure-thumb.d.ts +29 -0
- package/dist-client/viewparts/figure-thumb.js +168 -0
- package/dist-client/viewparts/figure-thumb.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -5
- package/test/i18n-prefix-guard.test.ts +129 -0
- package/test/korean-register-guard.test.ts +83 -0
- package/translations/en.json +307 -264
- package/translations/ja.json +275 -264
- package/translations/ko.json +307 -264
- package/translations/ms.json +275 -264
- package/translations/zh.json +275 -264
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js'
|
|
2
|
+
|
|
3
|
+
import { css, html, LitElement } from 'lit'
|
|
4
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
5
|
+
|
|
6
|
+
import { i18next, localize } from '@operato/i18n'
|
|
7
|
+
import { getPathInfo } from '@operato/utils'
|
|
8
|
+
|
|
9
|
+
import type { Figure } from '../types.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 그림을 부르는 주소.
|
|
13
|
+
*
|
|
14
|
+
* **base64 를 목록 응답에 실어 오지 않는다.** data URL 은 브라우저가 캐시할 수 없어, 카탈로그를
|
|
15
|
+
* 열 때마다 그림 전부가 다시 온다([확인함] 표본 14개 227KB). 주소로 부르면 캐시가 되고, 판을
|
|
16
|
+
* 가리는 `v` 가 바뀐 것만 다시 받는다. 내주는 자리는 `figure-thumbnail-router` 다.
|
|
17
|
+
*
|
|
18
|
+
* 도메인 조각(`/domain/{subdomain}`)은 셸이 쓰는 것과 같은 함수로 읽는다 — 화면이 주소를
|
|
19
|
+
* 손으로 짜맞추면 도메인이 바뀌는 배치에서 조용히 틀린다.
|
|
20
|
+
*/
|
|
21
|
+
function thumbnailSrc(figure: Figure): string | undefined {
|
|
22
|
+
if (!figure.thumbnailUpdatedAt) {
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { contextPath } = getPathInfo(location.pathname)
|
|
27
|
+
const stamp = new Date(figure.thumbnailUpdatedAt).getTime()
|
|
28
|
+
|
|
29
|
+
return `${contextPath}/figure-thumbnail/${figure.id}?v=${stamp}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 카드의 그림 칸.
|
|
34
|
+
*
|
|
35
|
+
* ## 왜 컴포넌트인가
|
|
36
|
+
*
|
|
37
|
+
* 카탈로그는 `ox-grist` 의 CARD 모드로 그린다. 격자에게 그림 칸은 컬럼 하나이고, 그 칸을 그리는
|
|
38
|
+
* 것은 컬럼의 `record.renderer` 다. 그림 위에 얹히는 것들(초안 표시, 삭제)은 **그림 칸 안에**
|
|
39
|
+
* 있어야 하므로 renderer 가 그려야 하는데, renderer 의 결과는 `ox-card-field` 의 shadow root
|
|
40
|
+
* 안으로 들어간다 — 화면의 `static styles` 가 닿지 않고, 인라인 style 로는 `:hover` 를 쓸 수 없다.
|
|
41
|
+
*
|
|
42
|
+
* 그래서 그림 칸만 컴포넌트로 둔다. 자기 스타일을 갖고 있으니 hover 도 테마도 정상으로 돈다.
|
|
43
|
+
* 격자로 옮기기 전 손으로 만들었던 카드의 위쪽 절반이 그대로 이 자리다.
|
|
44
|
+
*
|
|
45
|
+
* ## 삭제가 왜 여기 있나
|
|
46
|
+
*
|
|
47
|
+
* 격자의 gutter 줄에 두면 카드 아래에 단추 줄이 하나 생긴다. 감춰도 자리가 남고, 자리를 없애면
|
|
48
|
+
* 눌릴 곳이 사라진다. 되돌릴 수 없는 조작은 **고르는 눈길에 걸리지 않아야** 하므로 그림 오른쪽
|
|
49
|
+
* 위, 손이 갔을 때만 나오는 자리에 둔다.
|
|
50
|
+
*/
|
|
51
|
+
@customElement('figure-thumb')
|
|
52
|
+
export class FigureThumb extends localize(i18next)(LitElement) {
|
|
53
|
+
static styles = css`
|
|
54
|
+
:host {
|
|
55
|
+
position: relative;
|
|
56
|
+
display: block;
|
|
57
|
+
width: 100%;
|
|
58
|
+
height: 100%;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
img {
|
|
63
|
+
display: block;
|
|
64
|
+
width: 100%;
|
|
65
|
+
height: 100%;
|
|
66
|
+
/* 형상을 자르지 않는다 — 긴 것(컨베이어)과 높은 것(시그널타워)이 한 줄에 선다 */
|
|
67
|
+
object-fit: contain;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* 아직 그림이 없는 것을 빈 사각형으로 두지 않는다 — 왜 없는지 말한다 */
|
|
71
|
+
div[empty] {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
gap: var(--spacing-tiny, 4px);
|
|
77
|
+
width: 100%;
|
|
78
|
+
height: 100%;
|
|
79
|
+
font: var(--label-font, inherit);
|
|
80
|
+
font-size: 0.7rem;
|
|
81
|
+
color: var(--md-sys-color-outline);
|
|
82
|
+
}
|
|
83
|
+
div[empty] md-icon {
|
|
84
|
+
--md-icon-size: 26px;
|
|
85
|
+
color: var(--md-sys-color-outline-variant);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
초안 표시.
|
|
90
|
+
|
|
91
|
+
발행된 것에는 아무 말도 하지 않는다 — 카탈로그에 있는 것은 대개 발행된 것이라, 카드마다
|
|
92
|
+
「발행됨」이 붙으면 아무것도 가르지 않고 자리만 먹는다. 초안만 말하면 이 표시가 있는 카드가
|
|
93
|
+
곧 「아직 쓰면 안 되는 것」이 된다.
|
|
94
|
+
*/
|
|
95
|
+
span[draft] {
|
|
96
|
+
position: absolute;
|
|
97
|
+
top: 6px;
|
|
98
|
+
left: 6px;
|
|
99
|
+
padding: 1px 7px;
|
|
100
|
+
font: var(--label-font, inherit);
|
|
101
|
+
font-size: 0.68rem;
|
|
102
|
+
line-height: 1.5;
|
|
103
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
104
|
+
background-color: var(--md-sys-color-secondary-container);
|
|
105
|
+
border-radius: 4px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
button[remove] {
|
|
109
|
+
position: absolute;
|
|
110
|
+
top: 5px;
|
|
111
|
+
right: 5px;
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
justify-content: center;
|
|
115
|
+
padding: 3px;
|
|
116
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
117
|
+
/* 그림이 밝을 수도 어두울 수도 있다 — 옅은 면을 깔아 아이콘이 어디서나 읽히게 한다 */
|
|
118
|
+
background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 82%, transparent);
|
|
119
|
+
border: none;
|
|
120
|
+
border-radius: 4px;
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
opacity: 0;
|
|
123
|
+
transition: opacity 0.12s ease-out;
|
|
124
|
+
}
|
|
125
|
+
button[remove] md-icon {
|
|
126
|
+
--md-icon-size: 18px;
|
|
127
|
+
}
|
|
128
|
+
:host(:hover) button[remove],
|
|
129
|
+
button[remove]:focus-visible {
|
|
130
|
+
opacity: 1;
|
|
131
|
+
}
|
|
132
|
+
button[remove]:hover {
|
|
133
|
+
color: var(--md-sys-color-on-error, #fff);
|
|
134
|
+
background-color: var(--md-sys-color-error);
|
|
135
|
+
}
|
|
136
|
+
`
|
|
137
|
+
|
|
138
|
+
@property({ type: Object }) figure!: Figure
|
|
139
|
+
|
|
140
|
+
render() {
|
|
141
|
+
const figure = this.figure
|
|
142
|
+
if (!figure) {
|
|
143
|
+
return html``
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const src = thumbnailSrc(figure)
|
|
147
|
+
|
|
148
|
+
return html`
|
|
149
|
+
${src
|
|
150
|
+
? html`<img src=${src} alt=${figure.name || ''} loading="lazy" />`
|
|
151
|
+
: html`
|
|
152
|
+
<div empty>
|
|
153
|
+
<md-icon>deployed_code</md-icon>
|
|
154
|
+
${i18next.t('figure.text.thumbnail-not-made-yet')}
|
|
155
|
+
</div>
|
|
156
|
+
`}
|
|
157
|
+
${figure.state === 'released' ? '' : html`<span draft>${i18next.t('figure.label.draft')}</span>`}
|
|
158
|
+
|
|
159
|
+
<button
|
|
160
|
+
remove
|
|
161
|
+
title=${i18next.t('figure.button.delete-figure')}
|
|
162
|
+
@click=${(e: Event) => {
|
|
163
|
+
/* 카드 클릭(=저작면 열기)까지 올라가지 않게 여기서 멈춘다 */
|
|
164
|
+
e.stopPropagation()
|
|
165
|
+
this.dispatchEvent(new CustomEvent('remove'))
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
<md-icon>delete</md-icon>
|
|
169
|
+
</button>
|
|
170
|
+
`
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Figure, FigureFinding, FigureListResult, FigurePatch, FigureProposal, FigureVersion, NewFigure } from '../types.js';
|
|
1
|
+
import type { Figure, FigureFinding, FigureInspection, FigureListResult, FigurePatch, FigureProposal, FigureVersion, NewFigure } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* 목록을 가져온다.
|
|
4
4
|
*
|
|
@@ -57,5 +57,12 @@ export declare function proposeFigure(request: {
|
|
|
57
57
|
export declare function releaseFigure(id: string, comment?: string): Promise<Figure>;
|
|
58
58
|
/** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */
|
|
59
59
|
export declare function revertFigureVersion(id: string, version: number): Promise<SaveResult>;
|
|
60
|
+
/**
|
|
61
|
+
* 발행해도 되나 — **누르기 전에** 묻는다.
|
|
62
|
+
*
|
|
63
|
+
* 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이
|
|
64
|
+
* 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.
|
|
65
|
+
*/
|
|
66
|
+
export declare function inspectFigure(id: string): Promise<FigureInspection>;
|
|
60
67
|
/** 발행된 판들 — 최근 것부터 열 개. */
|
|
61
68
|
export declare function fetchFigureVersions(id: string): Promise<FigureVersion[]>;
|
|
@@ -23,8 +23,10 @@ function unwrap(response, field) {
|
|
|
23
23
|
/**
|
|
24
24
|
* 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.
|
|
25
25
|
*
|
|
26
|
-
* `
|
|
27
|
-
*
|
|
26
|
+
* **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림
|
|
27
|
+
* 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로
|
|
28
|
+
* 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를
|
|
29
|
+
* 그것으로 알고, 판을 가리는 값으로도 쓴다.
|
|
28
30
|
*/
|
|
29
31
|
const FIGURE_LIST_FIELDS = `
|
|
30
32
|
id
|
|
@@ -37,7 +39,6 @@ const FIGURE_LIST_FIELDS = `
|
|
|
37
39
|
score
|
|
38
40
|
triangles
|
|
39
41
|
groups
|
|
40
|
-
thumbnail
|
|
41
42
|
thumbnailUpdatedAt
|
|
42
43
|
updatedAt
|
|
43
44
|
updater { id name }
|
|
@@ -209,6 +210,34 @@ export async function revertFigureVersion(id, version) {
|
|
|
209
210
|
});
|
|
210
211
|
return unwrap(response, 'revertFigureVersion');
|
|
211
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* 발행해도 되나 — **누르기 전에** 묻는다.
|
|
215
|
+
*
|
|
216
|
+
* 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이
|
|
217
|
+
* 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.
|
|
218
|
+
*/
|
|
219
|
+
export async function inspectFigure(id) {
|
|
220
|
+
const response = await client.query({
|
|
221
|
+
query: gql `
|
|
222
|
+
query InspectFigure($id: String!) {
|
|
223
|
+
inspectFigure(id: $id) {
|
|
224
|
+
blocked
|
|
225
|
+
findings {
|
|
226
|
+
code
|
|
227
|
+
message
|
|
228
|
+
why
|
|
229
|
+
how
|
|
230
|
+
at
|
|
231
|
+
blocking
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
`,
|
|
236
|
+
variables: { id },
|
|
237
|
+
fetchPolicy: 'network-only'
|
|
238
|
+
});
|
|
239
|
+
return unwrap(response, 'inspectFigure');
|
|
240
|
+
}
|
|
212
241
|
/** 발행된 판들 — 최근 것부터 열 개. */
|
|
213
242
|
export async function fetchFigureVersions(id) {
|
|
214
243
|
const response = await client.query({
|
|
@@ -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;AAYzC;;;;;;;;;;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;;;;;;;;;;;;;;;CAe1B,CAAA;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAKrC;IACC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;oBAGM,kBAAkB;;;;KAIjC;QACD,SAAS,EAAE;YACT,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,EAAE;YAC7B,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE;YACjE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;SAC1F;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;;;;;KAKzB;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;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EAAU,EAAE,OAAgB;IAC9D,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;QACnC,QAAQ,EAAE,GAAG,CAAA;;;;;;;;;KASZ;QACD,SAAS,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE;KAC3B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAS,QAAQ,EAAE,eAAe,CAAC,CAAA;AAClD,CAAC;AAED,kDAAkD;AAClD,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,EAAU,EAAE,OAAe;IACnE,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;QACnC,QAAQ,EAAE,GAAG,CAAA;;;qBAGI,kBAAkB;;;;KAIlC;QACD,SAAS,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE;KAC3B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAa,QAAQ,EAAE,qBAAqB,CAAC,CAAA;AAC5D,CAAC;AAED,2BAA2B;AAC3B,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,EAAU;IAClD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;KAaT;QACD,SAAS,EAAE,EAAE,EAAE,EAAE;QACjB,WAAW,EAAE,cAAc;KAC5B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAkB,QAAQ,EAAE,gBAAgB,CAAC,IAAI,EAAE,CAAA;AAClE,CAAC","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"]}
|
|
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;AAazC;;;;;;;;;;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;;;;;;;GAOG;AACH,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;CAc1B,CAAA;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAKrC;IACC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;oBAGM,kBAAkB;;;;KAIjC;QACD,SAAS,EAAE;YACT,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,EAAE;YAC7B,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE;YACjE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;SAC1F;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;;;;;KAKzB;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;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EAAU,EAAE,OAAgB;IAC9D,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;QACnC,QAAQ,EAAE,GAAG,CAAA;;;;;;;;;KASZ;QACD,SAAS,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE;KAC3B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAS,QAAQ,EAAE,eAAe,CAAC,CAAA;AAClD,CAAC;AAED,kDAAkD;AAClD,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,EAAU,EAAE,OAAe;IACnE,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;QACnC,QAAQ,EAAE,GAAG,CAAA;;;qBAGI,kBAAkB;;;;KAIlC;QACD,SAAS,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE;KAC3B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAa,QAAQ,EAAE,qBAAqB,CAAC,CAAA;AAC5D,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EAAU;IAC5C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;KAcT;QACD,SAAS,EAAE,EAAE,EAAE,EAAE;QACjB,WAAW,EAAE,cAAc;KAC5B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAmB,QAAQ,EAAE,eAAe,CAAC,CAAA;AAC5D,CAAC;AAED,2BAA2B;AAC3B,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,EAAU;IAClD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;KAaT;QACD,SAAS,EAAE,EAAE,EAAE,EAAE;QACjB,WAAW,EAAE,cAAc;KAC5B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAkB,QAAQ,EAAE,gBAAgB,CAAC,IAAI,EAAE,CAAA;AAClE,CAAC","sourcesContent":["import gql from 'graphql-tag'\nimport { client } from '@operato/graphql'\n\nimport type {\n Figure,\n FigureFinding,\n FigureInspection,\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 * **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림\n * 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로\n * 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를\n * 그것으로 알고, 판을 가리는 값으로도 쓴다.\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 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/**\n * 발행해도 되나 — **누르기 전에** 묻는다.\n *\n * 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이\n * 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.\n */\nexport async function inspectFigure(id: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigure($id: String!) {\n inspectFigure(id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigure')\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"]}
|
|
@@ -176,21 +176,21 @@ let FigureAsk = class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
176
176
|
return html `
|
|
177
177
|
<form @submit=${(e) => this.submit(e)}>
|
|
178
178
|
<md-icon lead>auto_awesome</md-icon>
|
|
179
|
-
<span mode>${i18next.t(revising ? 'text.ask-to-revise' : 'text.ask-to-create')}</span>
|
|
179
|
+
<span mode>${i18next.t(revising ? 'figure.text.ask-to-revise' : 'figure.text.ask-to-create')}</span>
|
|
180
180
|
<input
|
|
181
181
|
ask
|
|
182
182
|
?disabled=${this.asking}
|
|
183
|
-
placeholder=${i18next.t(revising ? 'text.ask-revise-example' : 'text.ask-create-example')}
|
|
183
|
+
placeholder=${i18next.t(revising ? 'figure.text.ask-revise-example' : 'figure.text.ask-create-example')}
|
|
184
184
|
/>
|
|
185
|
-
<label picture ?has=${!!this.picture} title=${i18next.t('text.picture-is-for-proportions')}>
|
|
185
|
+
<label picture ?has=${!!this.picture} title=${i18next.t('figure.text.picture-is-for-proportions')}>
|
|
186
186
|
<md-icon>image</md-icon>
|
|
187
187
|
${this.picture
|
|
188
188
|
? html `<span filename>${this.picture.name}</span>`
|
|
189
|
-
: i18next.t('button.attach-a-picture')}
|
|
189
|
+
: i18next.t('figure.button.attach-a-picture')}
|
|
190
190
|
<input type="file" accept="image/*" @change=${(e) => this.pick(e)} />
|
|
191
191
|
</label>
|
|
192
192
|
<button go ?disabled=${this.asking}>
|
|
193
|
-
${this.asking ? i18next.t('text.asking') : i18next.t('button.ask')}
|
|
193
|
+
${this.asking ? i18next.t('figure.text.asking') : i18next.t('figure.button.ask')}
|
|
194
194
|
</button>
|
|
195
195
|
</form>
|
|
196
196
|
|
|
@@ -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 /*\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"]}
|
|
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 ? 'figure.text.ask-to-revise' : 'figure.text.ask-to-create')}</span>\n <input\n ask\n ?disabled=${this.asking}\n placeholder=${i18next.t(revising ? 'figure.text.ask-revise-example' : 'figure.text.ask-create-example')}\n />\n <label picture ?has=${!!this.picture} title=${i18next.t('figure.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('figure.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('figure.text.asking') : i18next.t('figure.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"]}
|
|
@@ -216,7 +216,7 @@ let FigureCanvas = class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
216
216
|
standUp() {
|
|
217
217
|
const board = this.board;
|
|
218
218
|
if (!board) {
|
|
219
|
-
this.cannot = i18next.t('text.canvas-appears-once-a-part-exists');
|
|
219
|
+
this.cannot = i18next.t('figure.text.canvas-appears-once-a-part-exists');
|
|
220
220
|
return;
|
|
221
221
|
}
|
|
222
222
|
const signature = `${board.width}x${board.height}x${board.depth}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"figure-canvas.js","sourceRoot":"","sources":["../../client/modeller/figure-canvas.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { create, Model } from '@hatiolab/things-scene'\n\nimport { gridStep } from './part-edits.js'\nimport { DEFAULT_VIEW, WORKBENCH } from './figure-view.js'\nimport type { ViewSettings } from './figure-view.js'\nimport type { BoardModel, PartModel } from './figure-source.js'\n\n/**\n * 판의 종이 색 — 거의 흰색.\n *\n * 2D 편집 도구(손잡이 · 안내선 · 선택 테)가 어두운 색으로 그려진다. 종이가 어두우면\n * 그것들이 안 보여 편집을 할 수 없다. 순백이 아닌 것은 판의 가장자리가 작업면과\n * 구별되게 하려는 것이다.\n */\nconst PAPER = '#f4f4f4'\n\n/**\n * 달라진 값만 골라 낸다. 같은 것뿐이면 `null`.\n *\n * 값 비교는 JSON 으로 한다 — 경로(`path`)처럼 배열인 값이 있어서 얕은 비교로는 늘\n * 다르다고 나온다.\n */\nfunction changedKeys(\n now: Record<string, unknown>,\n wanted: Record<string, unknown>\n): Record<string, unknown> | null {\n const diff: Record<string, unknown> = {}\n let any = false\n\n for (const [key, value] of Object.entries(wanted)) {\n const before = now[key]\n const same =\n before === value || (before !== undefined && JSON.stringify(before) === JSON.stringify(value))\n if (!same) {\n diff[key] = value\n any = true\n }\n }\n\n return any ? diff : null\n}\n\n/** 씬이 돌려주는 것 중 우리가 쓰는 부분. */\ninterface SceneHandle {\n dispose?: () => void\n resize?: () => void\n /** 모델 전체가 보이게 화면에 맞춘다. 'ratio' 는 비율을 지키며 맞춘다. */\n fit?: (type?: string) => void\n /**\n * **판**이다 — 루트가 아니다.\n *\n * `create()` 가 돌려주는 `root` 는 `model-layer` 이고, 그 위에 진짜 루트가 하나 더\n * 있다. 부품은 판에 붙고, 선택 알림은 루트가 낸다. 둘을 섞으면 알림이 안 온다 —\n * 실제로 그랬다.\n */\n root?: SceneBoard\n}\n\ninterface SceneBoard {\n invalidate?: () => void\n /** 보기 설정을 넣는 자리. 씬이 스스로 반응한다. */\n set?: (patch: Record<string, unknown>) => void\n /** 3D 카메라를 모델 전체가 보이게 맞춘다. 10.1.4 부터 있다. */\n fitCameraToModel?: (margin?: number) => void\n components?: SceneComponent[]\n selected?: SceneComponent[]\n app?: unknown\n addComponent?: (component: unknown) => void\n on?: (event: string, handler: (...args: unknown[]) => void) => void\n /** 판 위의 루트. 선택 알림이 여기서 난다. */\n root?: {\n selected?: SceneComponent[]\n on?: (event: string, handler: (...args: unknown[]) => void) => void\n }\n}\n\ninterface SceneComponent {\n state: Record<string, unknown>\n set: (props: Record<string, unknown> | string, value?: unknown) => void\n on?: (event: string, handler: (...args: unknown[]) => void) => void\n remove?: () => void\n}\n\n/**\n * 편집 캔버스 — 부품들을 보드 위에 세우고, 여기서 고치게 한다.\n *\n * ## 미리보기가 아니다\n *\n * 한동안 이름이 figure-preview 였다. 원본을 컴파일해 컴포넌트 하나로 세우던 시절에는\n * 정말 보기만 하는 화면이었다. 부품을 컴포넌트로 바꾸면서 편집이 여기서 일어나게 되었다.\n * 부품을 고르고, 2D 로 바꿔 편집하고, 기즈모로 끄는 일이 전부 이 안에서 일어난다.\n * 이름만 남아 있었다.\n *\n * ## 부품이 컴포넌트다\n *\n * 한동안 원본 데이터를 컴파일해 **컴포넌트 하나**로 놓고, 그 안에서 부품을 찾아 다뤘다.\n * 배포용 형태를 편집 화면에 그대로 쓴 것이다. 그래서 어느 부품이 선택됐나 · 무엇을 감쌀까 ·\n * 무엇을 끌까를 전부 따로 뚫어야 했다.\n *\n * 이제 부품 하나가 컴포넌트 하나다. 선택 · 기즈모 · 윤곽선 · 다중 선택 · 되돌리기가\n * 씬의 기본 동작으로 그냥 된다.\n *\n * ## 보드가 곧 기준 상자다\n *\n * 보드의 `width` · `height` · `depth` 가 `FigureSource.base` 다. 부품 좌표를 변환할 때\n * 보드 중심만 빼면 된다 — `figure-source.ts` 가 그 일을 한다.\n *\n * ## 다시 세우지 않는다\n *\n * 부품이 바뀌면 씬을 통째로 다시 세우지 않고 **바뀐 컴포넌트만 고친다.** 다시 세우면\n * 고른 것과 카메라가 날아가서, 숫자 하나 고칠 때마다 화면이 처음으로 돌아간다.\n * 판 크기가 바뀔 때만 다시 세운다 — 판은 씬을 만들 때 정해진다.\n */\n@customElement('figure-canvas')\nexport class FigureCanvas extends localize(i18next)(LitElement) {\n static styles = css`\n /*\n 판 바깥을 우리가 칠한다.\n\n 무대에는 셋이 있다 — 판의 면(2D 는 종이, 3D 는 바닥판), 판 바깥, 좌표 바탕.\n 어느 것도 원본 데이터에 저장되지 않는다. 보려고 만든 것이므로 편집하기 좋게 잡는다.\n\n 2D 에는 바깥을 칠하는 사람이 없다. 씬의 2D 는 캔버스를 투명으로 지우고\n 끝이라(layer.ts 의 clearRect), 뒤에 있는 것이 그대로 비친다. 테마 토큰을\n 깔아 두었더니 어두운 데서 까맣게 나왔다.\n\n 3D 하늘과 같은 값을 쓴다. 갈리면 2D 로 갔을 때 바탕색이 바뀐다.\n */\n :host {\n position: relative;\n display: flex;\n overflow: hidden;\n background-color: ${unsafeCSS(WORKBENCH)};\n }\n\n /*\n 씬이 이 칸을 자기 것으로 삼으면서 인라인 스타일로 position: relative 를 박는다.\n\n 그러면 우리가 CSS 로 준 position: absolute 가 밀리고, 같이 준 inset: 0 도 함께 뜻을\n 잃는다. relative 인 블록은 상위 배치에서 크기를 받는데, 자식(캔버스)이 전부 절대 배치라\n 내용 폭이 0 이다. 그래서 폭 0 짜리 칸이 되고 캔버스도 폭 0 이 된다.\n\n 화면은 그냥 회색으로 남는다. 게다가 마우스 좌표를 NDC 로 바꿀 때 폭으로 나누므로\n 값이 무한대가 되어 기즈모를 집을 수 없다 — \"기즈모가 안 잡힌다\"의 실제 원인이\n 이것이었다.\n\n 그래서 width 와 height 를 직접 준다. 인라인으로 position 이 무엇으로 바뀌든 칸은\n 부모를 채운다.\n */\n div[stage] {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n }\n\n /* 세울 수 없는 상태를 빈 화면으로 두지 않는다 — 왜 안 보이는지 말한다 */\n div[cannot] {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--spacing-large, 12px);\n text-align: center;\n font: var(--label-font, inherit);\n line-height: 1.6;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: ${unsafeCSS(WORKBENCH)};\n }\n `\n\n /**\n * 편집 중인 내용. **씬 모델 그대로 받는다.**\n *\n * 한동안 `FigureSource` 로 받아 여기서 변환했다. 그런데 모델러 페이지가 갖고 있는 것이\n * 이미 씬 모델이므로, 그러면 값을 하나 고칠 때마다 왕복 변환이 한 번씩 일어난다.\n * 무손실이긴 하지만 하는 일이 없다 — 보드가 곧 보드이고 부품이 곧 컴포넌트다.\n */\n @property({ type: Object }) board?: BoardModel\n /** 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다(`figure-view.ts`). */\n @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW\n @property({ type: Array }) parts: PartModel[] = []\n /** 지금 고른 부품 이름. 목록에서 고른 것을 캔버스에 알린다. */\n @property({ type: String }) picked?: string\n\n /**\n * 보기 전용 — 씬에서 일어난 일을 밖에 알리지 않는다.\n *\n * AI 후보를 나란히 세울 때 쓴다. 후보는 아직 편집 대상이 아닌데, 씬은 컴포넌트를\n * 만들면서 상태를 정리하고 그때 변경 이벤트를 낸다. 그 이벤트가 그대로 올라가면\n * **후보의 값이 편집 중인 데이터를 덮어쓴다** — 실제로 그랬다. 받지 않기로 한 부품\n * 위치가 오류보고없이 반영되어 있었고, 후보를 버려도 되돌아오지 않았다.\n */\n @property({ type: Boolean }) viewOnly = false\n\n @query('div[stage]') private stage!: HTMLDivElement\n\n /** 세울 수 없는 사유. 없으면 세운 것이다. */\n @state() private cannot = ''\n\n private scene?: SceneHandle\n\n /**\n * 지금 세워진 것을 그림으로 낸다 — 카드에 쓸 썸네일.\n *\n * 부르는 쪽은 이 부품을 **숨은 자리에 480×360 으로 하나 더 세워** 부른다\n * (`figure-thumbnail.ts`). 보이는 저작면을 찍지 않는 이유는, 저작자가 돌려 둔 각도와 확대,\n * 그리고 창 크기에 따라 달라지는 칸 비율이 그대로 카드에 박히기 때문이다 — 나사를 확대해 보던\n * 중에 저장하면 그 자산의 얼굴이 나사가 된다. 목록은 견주는 자리라 프레임이 일정해야 한다.\n *\n * 못 찍으면 `undefined` 다. 빈 그림을 만들어 「그림이 있다」고 말하지 않는다.\n */\n async snapshot(width = 480, height = 360): Promise<string | undefined> {\n /*\n **3D 를 낼 수 있을 때만 찍는다.**\n\n 씬의 `toDataURL` 은 **2D 씬을 렌더한다.** 그것으로 찍어 보니 흰 종이 한 장이 나왔다\n (480×291 · 834바이트 · 순백 — 2D 판의 면인 `fillStyle: PAPER` 이 화면 전체였다). 3D 픽셀은\n `model-layer.renderer3d`(THREE.WebGLRenderer) 쪽에 있고, 그 렌더러는\n `preserveDrawingBuffer: false` 라 **그린 직후 같은 task 에서** 읽어야 한다. 그 절차를 아는\n 것은 씬이므로, things-scene 이 그 일을 공개 API 로 내주면 여기서 부른다.\n\n 아직 없으면 `undefined` 다. 흰 종이를 썸네일이라고 저장하면 카드가 「그림이 있다」고 말하면서\n 아무것도 보여 주지 않고, 저장할 때마다 판 번호만 올라간다 — 없는 것보다 나쁘다.\n */\n const layer = this.scene?.root as\n | (SceneBoard & {\n snapshot3d?: (\n width?: number,\n height?: number,\n type?: string,\n quality?: number\n ) => Promise<string | undefined>\n })\n | undefined\n\n if (!layer?.snapshot3d) {\n return undefined\n }\n\n try {\n /*\n 알파가 필요 없고 카드에 들어갈 그림이라 webp 로 찍는다. png 는 같은 그림이 서너 배 무겁고,\n 그 무게가 도형마다 DB 와 목록 응답에 실린다. webp 를 못 만드는 브라우저는 캔버스가 알아서\n png 를 돌려준다 — 우리가 판정하지 않는다.\n */\n const url = await layer.snapshot3d(width, height, 'image/webp', 0.85)\n return typeof url === 'string' && url.startsWith('data:') ? url : undefined\n } catch (e) {\n /* 원인을 지어내지 않는다. 못 찍은 것이 저장을 막지는 않는다. */\n console.warn(`[thumbnail] 찍지 못했습니다 — ${(e as Error).message}`)\n return undefined\n }\n }\n /** 지금 씬이 서 있는 판. 이것이 달라지면 다시 세운다. */\n private standing?: string\n /** 칸 크기를 따라가게 지켜본다. 안 지켜보면 0x0 으로 고정된다. */\n private watcher?: ResizeObserver\n\n render() {\n return html`\n <div stage></div>\n ${this.cannot ? html`<div cannot>${this.cannot}</div>` : ''}\n `\n }\n\n willUpdate(changed: Map<string, unknown>) {\n if (!this.hasUpdated) return\n\n if (changed.has('board') || changed.has('parts')) this.standUp()\n if (changed.has('view')) this.applyView()\n if (changed.has('picked')) this.showPicked()\n }\n\n firstUpdated() {\n this.standUp()\n\n // 씬은 세울 때의 칸 크기를 읽는다. 저작면이 아직 배치되기 전이면 0x0 으로 굳어\n // **캔버스는 붙었는데 아무것도 안 보이는** 상태가 된다 — 실제로 그렇게 났다.\n this.watcher = new ResizeObserver(() => this.scene?.resize?.())\n this.watcher.observe(this)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.watcher?.disconnect()\n this.watcher = undefined\n this.teardown()\n }\n\n /**\n * 받은 것을 씬에 반영한다.\n *\n * **판이 그대로면 다시 세우지 않는다.** 다시 세우면 고른 것과 카메라가 날아가서,\n * 숫자 하나 고칠 때마다 화면이 처음으로 돌아간다.\n */\n private standUp() {\n const board = this.board\n if (!board) {\n this.cannot = i18next.t('text.canvas-appears-once-a-part-exists')\n return\n }\n\n const signature = `${board.width}x${board.height}x${board.depth}`\n if (this.scene && this.standing === signature) {\n this.syncParts()\n return\n }\n\n this.teardown()\n\n try {\n this.scene = create({\n target: this.stage,\n model: {\n type: 'model-layer',\n width: board.width,\n height: board.height,\n depth: board.depth,\n threed: true,\n environment: 'studio',\n /*\n 표본을 화면보다 더 쓴다. MSAA 만으로는 실루엣의 계단이 남는다 — 픽셀\n 비율만 올려 보니 눈에 띄게 매끄러워졌다. 저작면은 작은 칸에 자산 하나라\n 그릴 것이 적으니 표본을 더 써도 된다.\n */\n pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),\n /*\n 좌표 바탕을 씬에게 맡긴다. 우리가 만들어 얹었더니 2D 로 갔다 오면\n 사라졌다 — 그때 씬이 통째로 다시 서면서 새 THREE.Scene 을 만들기 때문이다.\n 보드는 이 값을 안 주므로 안 보인다.\n */\n ...this.sceneView(),\n gridStep: gridStep(board),\n sky: 'color',\n /*\n 판의 면 — 2D 는 밝은 종이, 3D 는 없음.\n\n 종이는 밝아야 한다: 2D 편집 손잡이와 안내선이 어두운 색이다. 바닥판은\n 없어야 한다: 좌표 모눈과 겹쳐 싸우고 원점 아래쪽을 가린다.\n */\n fillStyle: PAPER,\n components: this.parts.map(part => ({ ...part }))\n },\n // **편집 모드**로 만든다. 보드의 3D 모델러가 그 모드에서 카메라 시점 버튼 ·\n // 기즈모 모드 · 도움말 · 단축키를 띄운다.\n mode: 1\n }) as SceneHandle\n\n this.standing = signature\n this.watchScene()\n this.scene.resize?.()\n /*\n 열자마자 모델 전체가 보이게 맞춘다.\n\n 카메라는 2D 뷰포트에서 만들어진다. 그런데 그 뷰포트의 처음 배율은 컨테이너\n 크기에서 오지 모델 크기에서 오지 않는다. 그래서 가로로 긴 모델(컨베이어가\n 그렇다)을 열면 화면 밖으로 나가 벨트 한 조각만 보였다.\n\n **다시 세울 때만 맞춘다.** 창 크기가 바뀔 때마다 맞추면 사용자가 돌려 둔 시점과\n 확대가 매번 되돌아간다.\n */\n this.scene.fit?.('ratio')\n /*\n 3D 카메라는 2D 쪽 fit 으로 안 움직인다. 처음 3D 로 설 때 모델 크기를 보지 않기\n 때문이다. 씬이 내주는 자리로 따로 맞춘다.\n\n `?.` 로 부르는 것은 배포된 판에 아직 없을 수 있어서다. 없으면 2D 만 맞고 3D 는\n 기본 시점으로 선다 — 여기서 카메라 계산을 흉내 내지 않는다.\n */\n this.scene.root?.fitCameraToModel?.()\n this.cannot = ''\n } catch (e) {\n // 원인을 지어내지 않는다 — 던진 말을 그대로 보여 준다.\n this.cannot = (e as Error).message\n }\n }\n\n /**\n * 지금 판 위에 있는 부품 컴포넌트를 이름으로 찾는다.\n *\n * **참조를 들고 있지 않는다.** 전에는 세울 때 한 번 색인해 두고 계속 그 Map 을 썼다.\n * 그런데 씬은 우리 모르게 판을 다시 세울 때가 있다 — 2D 와 3D 를 오갈 때가 그렇다.\n * 그러면 Map 안의 것들은 부모가 없는 죽은 객체가 되고, 그 뒤로는\n *\n * - 목록에서 골라도 선택이 안 된다. 루트의 `selected` 게터가 부모 없는 것을\n * 걸러 내므로 빈 배열이 되고, 테두리도 기즈모도 붙지 않는다\n * - 속성을 고쳐도 3D 가 안 따라온다. 죽은 객체에 값을 넣기 때문이다\n *\n * 둘 다 오류보고없이 일어난다. 실제로 화면에서 「기즈모가 안 잡힌다」로 나타났다.\n *\n * 부품은 많아야 20개다. 매번 순회하는 값이 참조를 갖고 있다가 틀리는 값보다 훨씬 싸다.\n */\n private partsOnBoard(): Map<string, SceneComponent> {\n const found = new Map<string, SceneComponent>()\n for (const component of this.scene?.root?.components ?? []) {\n const name = component.state?.name\n if (typeof name === 'string') found.set(name, component)\n }\n return found\n }\n\n /**\n * 보기 설정을 씬이 아는 이름으로 낸다.\n *\n * 옮겨 적는 표를 두지 않는다 — `ViewSettings` 의 이름이 곧 `model-layer` 의 상태\n * 이름이다. 표를 두면 씬이 새 설정을 받아도 이쪽이 모른다.\n */\n private sceneView(): Record<string, unknown> {\n const { grid, gridLabels, floor, environment, skyColor, ...light } = this.view ?? DEFAULT_VIEW\n\n return { grid, gridLabels, floor, environment, skyColor, ...light }\n }\n\n /**\n * 설정이 바뀌면 **씬에 그대로 넣는다.** 다시 세우지 않는다.\n *\n * 씬이 이 값들에 스스로 반응한다(`three-capability` 의 `onchange`) — 조명 · 반사 ·\n * 바닥 · 좌표 바탕이 그 자리에서 바뀐다. 다시 세우면 고른 것과 카메라가 날아가서,\n * 밝기 하나 끄는 동안 화면이 계속 처음으로 돌아간다.\n */\n private applyView(): void {\n const board = this.scene?.root\n if (!board) return\n\n board.set?.(this.sceneView())\n board.invalidate?.()\n }\n\n /**\n * 부품 목록을 씬에 맞춘다. **다시 세우지 않고 바뀐 것만 고친다.**\n *\n * 다시 세우면 고른 것과 카메라가 날아가서, 숫자 하나 고칠 때마다 화면이 처음으로\n * 돌아간다.\n */\n private syncParts() {\n const root = this.scene?.root\n if (!root) return\n\n const wanted = new Map(this.parts.map(part => [part.name, part]))\n const standing = this.partsOnBoard()\n\n // 없어진 것을 제거한다\n for (const [name, component] of standing) {\n if (!wanted.has(name)) component.remove?.()\n }\n\n for (const part of this.parts) {\n const found = standing.get(part.name)\n if (found) {\n /*\n **달라진 값만 넣는다.**\n\n 같은 값을 다시 넣어도 씬은 변경 이벤트를 낸다. 그 이벤트가 다시 편집 데이터를\n 만들고, 그것이 다시 여기로 돌아오면 끝없이 반복된다. 기즈모를 끄는 중이라면 그\n 되먹임이 끌기와 싸운다.\n */\n const diff = changedKeys(found.state, part as unknown as Record<string, unknown>)\n if (diff) found.set(diff)\n continue\n }\n // 새 부품. 씬의 공장으로 만들어 판에 붙인다.\n const made = Model.compile({ ...part }, root.app as never) as SceneComponent | null\n if (made) root.addComponent?.(made)\n }\n\n root.invalidate?.()\n }\n\n /**\n * 씬에서 일어난 일을 밖에 알린다.\n *\n * **자리가 아니라 이름으로 알린다.** 목록의 몇 번째인지로 알리면, 알림이 오는 사이에\n * 목록이 바뀌면 엉뚱한 부품을 가리킨다.\n */\n private watchScene() {\n const board = this.scene?.root\n const root = board?.root\n if (!board || !root?.on || this.viewOnly) return\n\n root.on('selected', (after: unknown) => {\n const chosen = (after as SceneComponent[] | undefined)?.[0]\n const name = chosen?.state?.name\n if (typeof name === 'string') this.tell('pick-part', { name })\n })\n\n /*\n 기즈모로 끌거나 2D 에서 옮기면 컴포넌트 상태가 바뀐다. 그것을 그대로 올려 보낸다 —\n 씬이 쓰는 용어 그대로다. 저장 형식으로 옮기는 일은 변환 함수가 한다.\n\n **바뀜은 판으로 올라오고, 바뀐 컴포넌트는 끝 인자의 `origin` 에 있다.** 자식이\n `trigger('change', after, before)` 를 하면 씬이 `{ origin, name, deliverer }` 를\n 뒤에 붙여 부모에게 넘긴다. 첫 인자를 컴포넌트로 읽으면 아무 일도 안 일어난다 —\n 실제로 그랬다.\n */\n board.on?.('change', (...args: unknown[]) => {\n const hint = args[args.length - 1] as { origin?: SceneComponent } | undefined\n const changed = hint?.origin\n const name = changed?.state?.name\n if (typeof name !== 'string') return\n\n const was = this.parts.find(part => part.name === name)\n if (!was) return\n\n // 씬 모델 그대로 올려 보낸다. 모델러 페이지가 갖고 있는 것도 씬 모델이라 변환할 것이 없다.\n this.tell('part-changed', { name, part: { ...was, ...changed!.state } as PartModel })\n })\n }\n\n /** 목록에서 고른 것을 캔버스의 선택에 맞춘다. */\n private showPicked() {\n const board = this.scene?.root\n const root = board?.root\n if (!board || !root) return\n\n const component = this.picked ? this.partsOnBoard().get(this.picked) : undefined\n root.selected = component ? [component] : []\n board.invalidate?.()\n }\n\n private tell(what: string, detail: object) {\n this.dispatchEvent(new CustomEvent(what, { detail, bubbles: true, composed: true }))\n }\n\n private teardown() {\n this.scene?.dispose?.()\n this.scene = undefined\n this.standing = undefined\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"figure-canvas.js","sourceRoot":"","sources":["../../client/modeller/figure-canvas.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { create, Model } from '@hatiolab/things-scene'\n\nimport { gridStep } from './part-edits.js'\nimport { DEFAULT_VIEW, WORKBENCH } from './figure-view.js'\nimport type { ViewSettings } from './figure-view.js'\nimport type { BoardModel, PartModel } from './figure-source.js'\n\n/**\n * 판의 종이 색 — 거의 흰색.\n *\n * 2D 편집 도구(손잡이 · 안내선 · 선택 테)가 어두운 색으로 그려진다. 종이가 어두우면\n * 그것들이 안 보여 편집을 할 수 없다. 순백이 아닌 것은 판의 가장자리가 작업면과\n * 구별되게 하려는 것이다.\n */\nconst PAPER = '#f4f4f4'\n\n/**\n * 달라진 값만 골라 낸다. 같은 것뿐이면 `null`.\n *\n * 값 비교는 JSON 으로 한다 — 경로(`path`)처럼 배열인 값이 있어서 얕은 비교로는 늘\n * 다르다고 나온다.\n */\nfunction changedKeys(\n now: Record<string, unknown>,\n wanted: Record<string, unknown>\n): Record<string, unknown> | null {\n const diff: Record<string, unknown> = {}\n let any = false\n\n for (const [key, value] of Object.entries(wanted)) {\n const before = now[key]\n const same =\n before === value || (before !== undefined && JSON.stringify(before) === JSON.stringify(value))\n if (!same) {\n diff[key] = value\n any = true\n }\n }\n\n return any ? diff : null\n}\n\n/** 씬이 돌려주는 것 중 우리가 쓰는 부분. */\ninterface SceneHandle {\n dispose?: () => void\n resize?: () => void\n /** 모델 전체가 보이게 화면에 맞춘다. 'ratio' 는 비율을 지키며 맞춘다. */\n fit?: (type?: string) => void\n /**\n * **판**이다 — 루트가 아니다.\n *\n * `create()` 가 돌려주는 `root` 는 `model-layer` 이고, 그 위에 진짜 루트가 하나 더\n * 있다. 부품은 판에 붙고, 선택 알림은 루트가 낸다. 둘을 섞으면 알림이 안 온다 —\n * 실제로 그랬다.\n */\n root?: SceneBoard\n}\n\ninterface SceneBoard {\n invalidate?: () => void\n /** 보기 설정을 넣는 자리. 씬이 스스로 반응한다. */\n set?: (patch: Record<string, unknown>) => void\n /** 3D 카메라를 모델 전체가 보이게 맞춘다. 10.1.4 부터 있다. */\n fitCameraToModel?: (margin?: number) => void\n components?: SceneComponent[]\n selected?: SceneComponent[]\n app?: unknown\n addComponent?: (component: unknown) => void\n on?: (event: string, handler: (...args: unknown[]) => void) => void\n /** 판 위의 루트. 선택 알림이 여기서 난다. */\n root?: {\n selected?: SceneComponent[]\n on?: (event: string, handler: (...args: unknown[]) => void) => void\n }\n}\n\ninterface SceneComponent {\n state: Record<string, unknown>\n set: (props: Record<string, unknown> | string, value?: unknown) => void\n on?: (event: string, handler: (...args: unknown[]) => void) => void\n remove?: () => void\n}\n\n/**\n * 편집 캔버스 — 부품들을 보드 위에 세우고, 여기서 고치게 한다.\n *\n * ## 미리보기가 아니다\n *\n * 한동안 이름이 figure-preview 였다. 원본을 컴파일해 컴포넌트 하나로 세우던 시절에는\n * 정말 보기만 하는 화면이었다. 부품을 컴포넌트로 바꾸면서 편집이 여기서 일어나게 되었다.\n * 부품을 고르고, 2D 로 바꿔 편집하고, 기즈모로 끄는 일이 전부 이 안에서 일어난다.\n * 이름만 남아 있었다.\n *\n * ## 부품이 컴포넌트다\n *\n * 한동안 원본 데이터를 컴파일해 **컴포넌트 하나**로 놓고, 그 안에서 부품을 찾아 다뤘다.\n * 배포용 형태를 편집 화면에 그대로 쓴 것이다. 그래서 어느 부품이 선택됐나 · 무엇을 감쌀까 ·\n * 무엇을 끌까를 전부 따로 뚫어야 했다.\n *\n * 이제 부품 하나가 컴포넌트 하나다. 선택 · 기즈모 · 윤곽선 · 다중 선택 · 되돌리기가\n * 씬의 기본 동작으로 그냥 된다.\n *\n * ## 보드가 곧 기준 상자다\n *\n * 보드의 `width` · `height` · `depth` 가 `FigureSource.base` 다. 부품 좌표를 변환할 때\n * 보드 중심만 빼면 된다 — `figure-source.ts` 가 그 일을 한다.\n *\n * ## 다시 세우지 않는다\n *\n * 부품이 바뀌면 씬을 통째로 다시 세우지 않고 **바뀐 컴포넌트만 고친다.** 다시 세우면\n * 고른 것과 카메라가 날아가서, 숫자 하나 고칠 때마다 화면이 처음으로 돌아간다.\n * 판 크기가 바뀔 때만 다시 세운다 — 판은 씬을 만들 때 정해진다.\n */\n@customElement('figure-canvas')\nexport class FigureCanvas extends localize(i18next)(LitElement) {\n static styles = css`\n /*\n 판 바깥을 우리가 칠한다.\n\n 무대에는 셋이 있다 — 판의 면(2D 는 종이, 3D 는 바닥판), 판 바깥, 좌표 바탕.\n 어느 것도 원본 데이터에 저장되지 않는다. 보려고 만든 것이므로 편집하기 좋게 잡는다.\n\n 2D 에는 바깥을 칠하는 사람이 없다. 씬의 2D 는 캔버스를 투명으로 지우고\n 끝이라(layer.ts 의 clearRect), 뒤에 있는 것이 그대로 비친다. 테마 토큰을\n 깔아 두었더니 어두운 데서 까맣게 나왔다.\n\n 3D 하늘과 같은 값을 쓴다. 갈리면 2D 로 갔을 때 바탕색이 바뀐다.\n */\n :host {\n position: relative;\n display: flex;\n overflow: hidden;\n background-color: ${unsafeCSS(WORKBENCH)};\n }\n\n /*\n 씬이 이 칸을 자기 것으로 삼으면서 인라인 스타일로 position: relative 를 박는다.\n\n 그러면 우리가 CSS 로 준 position: absolute 가 밀리고, 같이 준 inset: 0 도 함께 뜻을\n 잃는다. relative 인 블록은 상위 배치에서 크기를 받는데, 자식(캔버스)이 전부 절대 배치라\n 내용 폭이 0 이다. 그래서 폭 0 짜리 칸이 되고 캔버스도 폭 0 이 된다.\n\n 화면은 그냥 회색으로 남는다. 게다가 마우스 좌표를 NDC 로 바꿀 때 폭으로 나누므로\n 값이 무한대가 되어 기즈모를 집을 수 없다 — \"기즈모가 안 잡힌다\"의 실제 원인이\n 이것이었다.\n\n 그래서 width 와 height 를 직접 준다. 인라인으로 position 이 무엇으로 바뀌든 칸은\n 부모를 채운다.\n */\n div[stage] {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n }\n\n /* 세울 수 없는 상태를 빈 화면으로 두지 않는다 — 왜 안 보이는지 말한다 */\n div[cannot] {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--spacing-large, 12px);\n text-align: center;\n font: var(--label-font, inherit);\n line-height: 1.6;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: ${unsafeCSS(WORKBENCH)};\n }\n `\n\n /**\n * 편집 중인 내용. **씬 모델 그대로 받는다.**\n *\n * 한동안 `FigureSource` 로 받아 여기서 변환했다. 그런데 모델러 페이지가 갖고 있는 것이\n * 이미 씬 모델이므로, 그러면 값을 하나 고칠 때마다 왕복 변환이 한 번씩 일어난다.\n * 무손실이긴 하지만 하는 일이 없다 — 보드가 곧 보드이고 부품이 곧 컴포넌트다.\n */\n @property({ type: Object }) board?: BoardModel\n /** 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다(`figure-view.ts`). */\n @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW\n @property({ type: Array }) parts: PartModel[] = []\n /** 지금 고른 부품 이름. 목록에서 고른 것을 캔버스에 알린다. */\n @property({ type: String }) picked?: string\n\n /**\n * 보기 전용 — 씬에서 일어난 일을 밖에 알리지 않는다.\n *\n * AI 후보를 나란히 세울 때 쓴다. 후보는 아직 편집 대상이 아닌데, 씬은 컴포넌트를\n * 만들면서 상태를 정리하고 그때 변경 이벤트를 낸다. 그 이벤트가 그대로 올라가면\n * **후보의 값이 편집 중인 데이터를 덮어쓴다** — 실제로 그랬다. 받지 않기로 한 부품\n * 위치가 오류보고없이 반영되어 있었고, 후보를 버려도 되돌아오지 않았다.\n */\n @property({ type: Boolean }) viewOnly = false\n\n @query('div[stage]') private stage!: HTMLDivElement\n\n /** 세울 수 없는 사유. 없으면 세운 것이다. */\n @state() private cannot = ''\n\n private scene?: SceneHandle\n\n /**\n * 지금 세워진 것을 그림으로 낸다 — 카드에 쓸 썸네일.\n *\n * 부르는 쪽은 이 부품을 **숨은 자리에 480×360 으로 하나 더 세워** 부른다\n * (`figure-thumbnail.ts`). 보이는 저작면을 찍지 않는 이유는, 저작자가 돌려 둔 각도와 확대,\n * 그리고 창 크기에 따라 달라지는 칸 비율이 그대로 카드에 박히기 때문이다 — 나사를 확대해 보던\n * 중에 저장하면 그 자산의 얼굴이 나사가 된다. 목록은 견주는 자리라 프레임이 일정해야 한다.\n *\n * 못 찍으면 `undefined` 다. 빈 그림을 만들어 「그림이 있다」고 말하지 않는다.\n */\n async snapshot(width = 480, height = 360): Promise<string | undefined> {\n /*\n **3D 를 낼 수 있을 때만 찍는다.**\n\n 씬의 `toDataURL` 은 **2D 씬을 렌더한다.** 그것으로 찍어 보니 흰 종이 한 장이 나왔다\n (480×291 · 834바이트 · 순백 — 2D 판의 면인 `fillStyle: PAPER` 이 화면 전체였다). 3D 픽셀은\n `model-layer.renderer3d`(THREE.WebGLRenderer) 쪽에 있고, 그 렌더러는\n `preserveDrawingBuffer: false` 라 **그린 직후 같은 task 에서** 읽어야 한다. 그 절차를 아는\n 것은 씬이므로, things-scene 이 그 일을 공개 API 로 내주면 여기서 부른다.\n\n 아직 없으면 `undefined` 다. 흰 종이를 썸네일이라고 저장하면 카드가 「그림이 있다」고 말하면서\n 아무것도 보여 주지 않고, 저장할 때마다 판 번호만 올라간다 — 없는 것보다 나쁘다.\n */\n const layer = this.scene?.root as\n | (SceneBoard & {\n snapshot3d?: (\n width?: number,\n height?: number,\n type?: string,\n quality?: number\n ) => Promise<string | undefined>\n })\n | undefined\n\n if (!layer?.snapshot3d) {\n return undefined\n }\n\n try {\n /*\n 알파가 필요 없고 카드에 들어갈 그림이라 webp 로 찍는다. png 는 같은 그림이 서너 배 무겁고,\n 그 무게가 도형마다 DB 와 목록 응답에 실린다. webp 를 못 만드는 브라우저는 캔버스가 알아서\n png 를 돌려준다 — 우리가 판정하지 않는다.\n */\n const url = await layer.snapshot3d(width, height, 'image/webp', 0.85)\n return typeof url === 'string' && url.startsWith('data:') ? url : undefined\n } catch (e) {\n /* 원인을 지어내지 않는다. 못 찍은 것이 저장을 막지는 않는다. */\n console.warn(`[thumbnail] 찍지 못했습니다 — ${(e as Error).message}`)\n return undefined\n }\n }\n /** 지금 씬이 서 있는 판. 이것이 달라지면 다시 세운다. */\n private standing?: string\n /** 칸 크기를 따라가게 지켜본다. 안 지켜보면 0x0 으로 고정된다. */\n private watcher?: ResizeObserver\n\n render() {\n return html`\n <div stage></div>\n ${this.cannot ? html`<div cannot>${this.cannot}</div>` : ''}\n `\n }\n\n willUpdate(changed: Map<string, unknown>) {\n if (!this.hasUpdated) return\n\n if (changed.has('board') || changed.has('parts')) this.standUp()\n if (changed.has('view')) this.applyView()\n if (changed.has('picked')) this.showPicked()\n }\n\n firstUpdated() {\n this.standUp()\n\n // 씬은 세울 때의 칸 크기를 읽는다. 저작면이 아직 배치되기 전이면 0x0 으로 굳어\n // **캔버스는 붙었는데 아무것도 안 보이는** 상태가 된다 — 실제로 그렇게 났다.\n this.watcher = new ResizeObserver(() => this.scene?.resize?.())\n this.watcher.observe(this)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.watcher?.disconnect()\n this.watcher = undefined\n this.teardown()\n }\n\n /**\n * 받은 것을 씬에 반영한다.\n *\n * **판이 그대로면 다시 세우지 않는다.** 다시 세우면 고른 것과 카메라가 날아가서,\n * 숫자 하나 고칠 때마다 화면이 처음으로 돌아간다.\n */\n private standUp() {\n const board = this.board\n if (!board) {\n this.cannot = i18next.t('figure.text.canvas-appears-once-a-part-exists')\n return\n }\n\n const signature = `${board.width}x${board.height}x${board.depth}`\n if (this.scene && this.standing === signature) {\n this.syncParts()\n return\n }\n\n this.teardown()\n\n try {\n this.scene = create({\n target: this.stage,\n model: {\n type: 'model-layer',\n width: board.width,\n height: board.height,\n depth: board.depth,\n threed: true,\n environment: 'studio',\n /*\n 표본을 화면보다 더 쓴다. MSAA 만으로는 실루엣의 계단이 남는다 — 픽셀\n 비율만 올려 보니 눈에 띄게 매끄러워졌다. 저작면은 작은 칸에 자산 하나라\n 그릴 것이 적으니 표본을 더 써도 된다.\n */\n pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),\n /*\n 좌표 바탕을 씬에게 맡긴다. 우리가 만들어 얹었더니 2D 로 갔다 오면\n 사라졌다 — 그때 씬이 통째로 다시 서면서 새 THREE.Scene 을 만들기 때문이다.\n 보드는 이 값을 안 주므로 안 보인다.\n */\n ...this.sceneView(),\n gridStep: gridStep(board),\n sky: 'color',\n /*\n 판의 면 — 2D 는 밝은 종이, 3D 는 없음.\n\n 종이는 밝아야 한다: 2D 편집 손잡이와 안내선이 어두운 색이다. 바닥판은\n 없어야 한다: 좌표 모눈과 겹쳐 싸우고 원점 아래쪽을 가린다.\n */\n fillStyle: PAPER,\n components: this.parts.map(part => ({ ...part }))\n },\n // **편집 모드**로 만든다. 보드의 3D 모델러가 그 모드에서 카메라 시점 버튼 ·\n // 기즈모 모드 · 도움말 · 단축키를 띄운다.\n mode: 1\n }) as SceneHandle\n\n this.standing = signature\n this.watchScene()\n this.scene.resize?.()\n /*\n 열자마자 모델 전체가 보이게 맞춘다.\n\n 카메라는 2D 뷰포트에서 만들어진다. 그런데 그 뷰포트의 처음 배율은 컨테이너\n 크기에서 오지 모델 크기에서 오지 않는다. 그래서 가로로 긴 모델(컨베이어가\n 그렇다)을 열면 화면 밖으로 나가 벨트 한 조각만 보였다.\n\n **다시 세울 때만 맞춘다.** 창 크기가 바뀔 때마다 맞추면 사용자가 돌려 둔 시점과\n 확대가 매번 되돌아간다.\n */\n this.scene.fit?.('ratio')\n /*\n 3D 카메라는 2D 쪽 fit 으로 안 움직인다. 처음 3D 로 설 때 모델 크기를 보지 않기\n 때문이다. 씬이 내주는 자리로 따로 맞춘다.\n\n `?.` 로 부르는 것은 배포된 판에 아직 없을 수 있어서다. 없으면 2D 만 맞고 3D 는\n 기본 시점으로 선다 — 여기서 카메라 계산을 흉내 내지 않는다.\n */\n this.scene.root?.fitCameraToModel?.()\n this.cannot = ''\n } catch (e) {\n // 원인을 지어내지 않는다 — 던진 말을 그대로 보여 준다.\n this.cannot = (e as Error).message\n }\n }\n\n /**\n * 지금 판 위에 있는 부품 컴포넌트를 이름으로 찾는다.\n *\n * **참조를 들고 있지 않는다.** 전에는 세울 때 한 번 색인해 두고 계속 그 Map 을 썼다.\n * 그런데 씬은 우리 모르게 판을 다시 세울 때가 있다 — 2D 와 3D 를 오갈 때가 그렇다.\n * 그러면 Map 안의 것들은 부모가 없는 죽은 객체가 되고, 그 뒤로는\n *\n * - 목록에서 골라도 선택이 안 된다. 루트의 `selected` 게터가 부모 없는 것을\n * 걸러 내므로 빈 배열이 되고, 테두리도 기즈모도 붙지 않는다\n * - 속성을 고쳐도 3D 가 안 따라온다. 죽은 객체에 값을 넣기 때문이다\n *\n * 둘 다 오류보고없이 일어난다. 실제로 화면에서 「기즈모가 안 잡힌다」로 나타났다.\n *\n * 부품은 많아야 20개다. 매번 순회하는 값이 참조를 갖고 있다가 틀리는 값보다 훨씬 싸다.\n */\n private partsOnBoard(): Map<string, SceneComponent> {\n const found = new Map<string, SceneComponent>()\n for (const component of this.scene?.root?.components ?? []) {\n const name = component.state?.name\n if (typeof name === 'string') found.set(name, component)\n }\n return found\n }\n\n /**\n * 보기 설정을 씬이 아는 이름으로 낸다.\n *\n * 옮겨 적는 표를 두지 않는다 — `ViewSettings` 의 이름이 곧 `model-layer` 의 상태\n * 이름이다. 표를 두면 씬이 새 설정을 받아도 이쪽이 모른다.\n */\n private sceneView(): Record<string, unknown> {\n const { grid, gridLabels, floor, environment, skyColor, ...light } = this.view ?? DEFAULT_VIEW\n\n return { grid, gridLabels, floor, environment, skyColor, ...light }\n }\n\n /**\n * 설정이 바뀌면 **씬에 그대로 넣는다.** 다시 세우지 않는다.\n *\n * 씬이 이 값들에 스스로 반응한다(`three-capability` 의 `onchange`) — 조명 · 반사 ·\n * 바닥 · 좌표 바탕이 그 자리에서 바뀐다. 다시 세우면 고른 것과 카메라가 날아가서,\n * 밝기 하나 끄는 동안 화면이 계속 처음으로 돌아간다.\n */\n private applyView(): void {\n const board = this.scene?.root\n if (!board) return\n\n board.set?.(this.sceneView())\n board.invalidate?.()\n }\n\n /**\n * 부품 목록을 씬에 맞춘다. **다시 세우지 않고 바뀐 것만 고친다.**\n *\n * 다시 세우면 고른 것과 카메라가 날아가서, 숫자 하나 고칠 때마다 화면이 처음으로\n * 돌아간다.\n */\n private syncParts() {\n const root = this.scene?.root\n if (!root) return\n\n const wanted = new Map(this.parts.map(part => [part.name, part]))\n const standing = this.partsOnBoard()\n\n // 없어진 것을 제거한다\n for (const [name, component] of standing) {\n if (!wanted.has(name)) component.remove?.()\n }\n\n for (const part of this.parts) {\n const found = standing.get(part.name)\n if (found) {\n /*\n **달라진 값만 넣는다.**\n\n 같은 값을 다시 넣어도 씬은 변경 이벤트를 낸다. 그 이벤트가 다시 편집 데이터를\n 만들고, 그것이 다시 여기로 돌아오면 끝없이 반복된다. 기즈모를 끄는 중이라면 그\n 되먹임이 끌기와 싸운다.\n */\n const diff = changedKeys(found.state, part as unknown as Record<string, unknown>)\n if (diff) found.set(diff)\n continue\n }\n // 새 부품. 씬의 공장으로 만들어 판에 붙인다.\n const made = Model.compile({ ...part }, root.app as never) as SceneComponent | null\n if (made) root.addComponent?.(made)\n }\n\n root.invalidate?.()\n }\n\n /**\n * 씬에서 일어난 일을 밖에 알린다.\n *\n * **자리가 아니라 이름으로 알린다.** 목록의 몇 번째인지로 알리면, 알림이 오는 사이에\n * 목록이 바뀌면 엉뚱한 부품을 가리킨다.\n */\n private watchScene() {\n const board = this.scene?.root\n const root = board?.root\n if (!board || !root?.on || this.viewOnly) return\n\n root.on('selected', (after: unknown) => {\n const chosen = (after as SceneComponent[] | undefined)?.[0]\n const name = chosen?.state?.name\n if (typeof name === 'string') this.tell('pick-part', { name })\n })\n\n /*\n 기즈모로 끌거나 2D 에서 옮기면 컴포넌트 상태가 바뀐다. 그것을 그대로 올려 보낸다 —\n 씬이 쓰는 용어 그대로다. 저장 형식으로 옮기는 일은 변환 함수가 한다.\n\n **바뀜은 판으로 올라오고, 바뀐 컴포넌트는 끝 인자의 `origin` 에 있다.** 자식이\n `trigger('change', after, before)` 를 하면 씬이 `{ origin, name, deliverer }` 를\n 뒤에 붙여 부모에게 넘긴다. 첫 인자를 컴포넌트로 읽으면 아무 일도 안 일어난다 —\n 실제로 그랬다.\n */\n board.on?.('change', (...args: unknown[]) => {\n const hint = args[args.length - 1] as { origin?: SceneComponent } | undefined\n const changed = hint?.origin\n const name = changed?.state?.name\n if (typeof name !== 'string') return\n\n const was = this.parts.find(part => part.name === name)\n if (!was) return\n\n // 씬 모델 그대로 올려 보낸다. 모델러 페이지가 갖고 있는 것도 씬 모델이라 변환할 것이 없다.\n this.tell('part-changed', { name, part: { ...was, ...changed!.state } as PartModel })\n })\n }\n\n /** 목록에서 고른 것을 캔버스의 선택에 맞춘다. */\n private showPicked() {\n const board = this.scene?.root\n const root = board?.root\n if (!board || !root) return\n\n const component = this.picked ? this.partsOnBoard().get(this.picked) : undefined\n root.selected = component ? [component] : []\n board.invalidate?.()\n }\n\n private tell(what: string, detail: object) {\n this.dispatchEvent(new CustomEvent(what, { detail, bubbles: true, composed: true }))\n }\n\n private teardown() {\n this.scene?.dispose?.()\n this.scene = undefined\n this.standing = undefined\n }\n}\n"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import type { BoardModel, PartModel } from './figure-source.js';
|
|
3
|
+
declare const FigureCapabilities_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
4
|
+
export declare class FigureCapabilities extends FigureCapabilities_base {
|
|
5
|
+
static styles: import("lit").CSSResult[];
|
|
6
|
+
board?: BoardModel;
|
|
7
|
+
parts: PartModel[];
|
|
8
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
9
|
+
/**
|
|
10
|
+
* 자리와 문 — **여기서는 읽기만 한다.**
|
|
11
|
+
*
|
|
12
|
+
* 지정은 속성 탭에서 한다. 부품을 고치는 곳이 한 군데여야 하기 때문이다 — 목록을 여기 또
|
|
13
|
+
* 두면 같은 부품을 두 화면에서 고치게 되고, 저작자는 어느 쪽이 이기는지 매번 생각해야 한다.
|
|
14
|
+
*
|
|
15
|
+
* 그래도 요약은 남긴다. 「어느 부품이 자리인가」는 부품을 하나씩 눌러 보며 알아낼 일이
|
|
16
|
+
* 아니다 — 자리는 그려지지 않으므로 화면을 봐서는 영영 못 찾는다.
|
|
17
|
+
*/
|
|
18
|
+
private renderAnchors;
|
|
19
|
+
/** 지정된 부품 한 줄. 누르면 그 부품이 골라진다 — 고치는 자리는 속성 탭이다. */
|
|
20
|
+
private renderMarked;
|
|
21
|
+
/** 이 부품을 고르게 한다. 부품 목록에서 누른 것과 같은 알림이다. */
|
|
22
|
+
private pick;
|
|
23
|
+
/**
|
|
24
|
+
* 사다리 한 칸.
|
|
25
|
+
*
|
|
26
|
+
* `checked` 를 `live()` 로 묶는다. 그냥 두면 Lit 이 **마지막으로 그린 값**과 견주므로,
|
|
27
|
+
* 브라우저가 DOM 을 바꿨는데 모델은 안 바뀐 경우 되돌리지 않는다. 실제로 그랬다 — 고르는
|
|
28
|
+
* 도중 도형이 다시 실렸더니 점은 「흐른다」에 있고 강조 상자는 「그리기만 한다」에 있었다.
|
|
29
|
+
* 화면이 모델과 다른 말을 하는 상태다. `live()` 는 **지금 DOM 의 값**과 견준다.
|
|
30
|
+
*/
|
|
31
|
+
private rung;
|
|
32
|
+
/**
|
|
33
|
+
* 사다리를 내리면 자리도 함께 거둔다.
|
|
34
|
+
*
|
|
35
|
+
* 능력을 「그리기만 한다」로 되돌려 놓고 부품에 자리 표시가 남아 있으면, 형식은 자리를
|
|
36
|
+
* 그대로 실어 보내는데 씬은 그것을 쓸 클래스가 없다. **선언은 있고 길이 없는** 상태가
|
|
37
|
+
* 되고, 그 도형은 멀쩡해 보이면서 아무것도 못 받는다.
|
|
38
|
+
*/
|
|
39
|
+
private setRung;
|
|
40
|
+
private tellBoard;
|
|
41
|
+
}
|
|
42
|
+
export {};
|