@things-factory/figure-ui 10.1.5 → 10.1.7
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 +36 -3
- package/client/modeller/figure-animations.ts +712 -0
- 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 +439 -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 +91 -15
- 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 +204 -66
- package/client/pages/figure-modeller-page.ts +158 -42
- package/client/types.ts +31 -0
- package/client/viewparts/figure-thumb.ts +250 -0
- package/dist-client/graphql/index.d.ts +8 -1
- package/dist-client/graphql/index.js +33 -3
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-animations.d.ts +93 -0
- package/dist-client/modeller/figure-animations.js +655 -0
- package/dist-client/modeller/figure-animations.js.map +1 -0
- 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 +98 -0
- package/dist-client/modeller/figure-inspector.js +425 -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 +24 -0
- package/dist-client/modeller/figure-side.js +94 -14
- 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 +11 -4
- package/dist-client/pages/figure-list-page.js +201 -63
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +33 -0
- package/dist-client/pages/figure-modeller-page.js +156 -42
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +29 -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 +242 -0
- package/dist-client/viewparts/figure-thumb.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/test/i18n-prefix-guard.test.ts +171 -0
- package/test/korean-register-guard.test.ts +83 -0
- package/translations/en.json +351 -264
- package/translations/ja.json +276 -264
- package/translations/ko.json +351 -264
- package/translations/ms.json +276 -264
- package/translations/zh.json +276 -264
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js'
|
|
1
2
|
import './figure-inspector.js'
|
|
3
|
+
import './figure-capabilities.js'
|
|
4
|
+
import './figure-animations.js'
|
|
2
5
|
import './figure-report.js'
|
|
3
6
|
import './figure-settings.js'
|
|
4
7
|
import './figure-history-panel.js'
|
|
@@ -13,7 +16,7 @@ import type { BoardModel, PartModel } from './figure-source.js'
|
|
|
13
16
|
import { DEFAULT_VIEW } from './figure-view.js'
|
|
14
17
|
import type { ViewSettings } from './figure-view.js'
|
|
15
18
|
|
|
16
|
-
type Tab = 'properties' | 'verdict' | 'view' | 'history'
|
|
19
|
+
type Tab = 'properties' | 'capability' | 'animation' | 'verdict' | 'view' | 'history'
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* 저작면 오른쪽 칸 — 속성과 판정을 탭으로 겹쳐 둔다.
|
|
@@ -65,13 +68,22 @@ export class FigureSide extends localize(i18next)(LitElement) {
|
|
|
65
68
|
}
|
|
66
69
|
button[tab] {
|
|
67
70
|
flex: 1;
|
|
71
|
+
/*
|
|
72
|
+
**아이콘 하나가 한 칸을 쓴다.**
|
|
73
|
+
|
|
74
|
+
전에는 이름을 적었는데, 넷이 좁은 칸을 나눠 쓰면서 접혀 「성숙 / 도」가 되었다. 접히면
|
|
75
|
+
그 탭만 두 줄이 되어 높이가 서로 달라진다. 다섯이 되면서 더는 줄여 짜 넣을 수 없어
|
|
76
|
+
아이콘으로 바꿨고, 이름은 title 과 aria-label 로 남는다.
|
|
77
|
+
|
|
78
|
+
배지는 여전히 자리를 차지하지 않고 **아이콘 위에 겹친다**(아래 span[badge]). 개수는
|
|
79
|
+
곁들이는 것이다.
|
|
80
|
+
*/
|
|
81
|
+
position: relative;
|
|
68
82
|
display: flex;
|
|
69
83
|
align-items: center;
|
|
70
84
|
justify-content: center;
|
|
71
|
-
|
|
72
|
-
padding:
|
|
73
|
-
font: var(--label-font, inherit);
|
|
74
|
-
font-size: 0.76rem;
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
padding: 7px 6px;
|
|
75
87
|
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
76
88
|
background: none;
|
|
77
89
|
border: none;
|
|
@@ -88,17 +100,34 @@ export class FigureSide extends localize(i18next)(LitElement) {
|
|
|
88
100
|
border-bottom-color: var(--md-sys-color-primary);
|
|
89
101
|
}
|
|
90
102
|
|
|
91
|
-
/*
|
|
103
|
+
/*
|
|
104
|
+
판정을 덮어 두어도 개수는 여기 남는다.
|
|
105
|
+
|
|
106
|
+
자리를 먹지 않게 오른쪽 위에 겹쳐 둔다 — 탭 이름을 밀지 않으므로 이름이 접히지 않는다.
|
|
107
|
+
숫자가 커져도(1,200 같은 것) 이름 위로 넘칠 뿐 줄을 바꾸지 않는다.
|
|
108
|
+
*/
|
|
109
|
+
button[tab] md-icon {
|
|
110
|
+
font-size: 20px;
|
|
111
|
+
width: 20px;
|
|
112
|
+
height: 20px;
|
|
113
|
+
}
|
|
92
114
|
span[badge] {
|
|
93
|
-
|
|
94
|
-
|
|
115
|
+
position: absolute;
|
|
116
|
+
/* 이름 위에 겹치되 위로 붙지 않게 — 탭 가운데 글자와 눈높이를 조금 맞춘다 */
|
|
117
|
+
top: 5px;
|
|
118
|
+
right: 2px;
|
|
119
|
+
padding: 0 5px;
|
|
120
|
+
font-size: 0.62rem;
|
|
95
121
|
font-weight: 700;
|
|
122
|
+
line-height: 1.5;
|
|
96
123
|
color: var(--md-sys-color-on-primary-container);
|
|
97
124
|
background-color: var(--md-sys-color-primary-container);
|
|
98
125
|
border-radius: 999em;
|
|
126
|
+
pointer-events: none;
|
|
99
127
|
}
|
|
100
128
|
|
|
101
129
|
figure-inspector,
|
|
130
|
+
figure-animations,
|
|
102
131
|
figure-report,
|
|
103
132
|
figure-settings {
|
|
104
133
|
flex: 1;
|
|
@@ -118,6 +147,9 @@ export class FigureSide extends localize(i18next)(LitElement) {
|
|
|
118
147
|
곳이라 변환된 FigureSource 를 받는다. 변환은 모델러 페이지가 한 번만 해서 내려보낸다.
|
|
119
148
|
*/
|
|
120
149
|
@property({ type: String }) figureName = ''
|
|
150
|
+
/** 카탈로그 분류·태그. 속성 탭이 보여 주고 바꾼다. */
|
|
151
|
+
@property({ type: String }) figureCategory?: string
|
|
152
|
+
@property({ type: Array }) figureTags?: string[]
|
|
121
153
|
/** 이력 탭이 발행하고 되돌리는 데 필요한 것들. 정본은 저작면이 든다. */
|
|
122
154
|
@property({ type: String }) figureId?: string
|
|
123
155
|
@property({ type: String }) figureState?: 'draft' | 'released'
|
|
@@ -135,23 +167,51 @@ export class FigureSide extends localize(i18next)(LitElement) {
|
|
|
135
167
|
/** 판정이 알려 준 개수. 탭 이름표에 붙는다. */
|
|
136
168
|
@state() private instances?: number
|
|
137
169
|
|
|
170
|
+
/**
|
|
171
|
+
* 이 도형이 든 clip 수. 탭 아이콘에 붙는다.
|
|
172
|
+
*
|
|
173
|
+
* 아이콘만으로는 「여기에 무언가 있다」를 알 수 없다. 대부분의 도형은 움직이지 않으므로
|
|
174
|
+
* 이 탭은 늘 비어 있을 것이고, 그러면 움직이는 도형을 열었을 때 그 사실이 눈에 걸려야 한다.
|
|
175
|
+
* 0 이면 아무것도 안 붙인다 — 「0」을 붙이면 없는 것이 있는 것처럼 자리를 차지한다.
|
|
176
|
+
*/
|
|
177
|
+
private get clipCount(): number | undefined {
|
|
178
|
+
const n = this.board?.animations?.length ?? 0
|
|
179
|
+
return n === 0 ? undefined : n
|
|
180
|
+
}
|
|
181
|
+
|
|
138
182
|
render() {
|
|
139
183
|
return html`
|
|
140
184
|
<div tabs>
|
|
141
|
-
${this.renderTab('properties', i18next.t('label.properties'))}
|
|
142
|
-
${this.renderTab('
|
|
143
|
-
${this.renderTab('
|
|
144
|
-
${this.renderTab('
|
|
185
|
+
${this.renderTab('properties', 'tune', i18next.t('figure.label.properties'))}
|
|
186
|
+
${this.renderTab('capability', 'conveyor_belt', i18next.t('figure.label.tab-capability'))}
|
|
187
|
+
${this.renderTab('animation', 'animation', i18next.t('figure.label.animation'), this.clipCount)}
|
|
188
|
+
${this.renderTab('verdict', 'insights', i18next.t('figure.label.maturity'), this.instances)}
|
|
189
|
+
${this.renderTab('view', 'wb_sunny', i18next.t('figure.label.tab-environment'))}
|
|
190
|
+
${this.renderTab('history', 'history', i18next.t('figure.label.history'))}
|
|
145
191
|
</div>
|
|
146
192
|
|
|
147
193
|
<figure-inspector
|
|
148
194
|
?off=${this.tab !== 'properties'}
|
|
149
195
|
.figureName=${this.figureName}
|
|
196
|
+
.figureCategory=${this.figureCategory}
|
|
197
|
+
.figureTags=${this.figureTags}
|
|
150
198
|
.board=${this.board}
|
|
151
199
|
.parts=${this.parts}
|
|
152
200
|
.selected=${this.selected}
|
|
153
201
|
></figure-inspector>
|
|
154
202
|
|
|
203
|
+
<figure-capabilities
|
|
204
|
+
?off=${this.tab !== 'capability'}
|
|
205
|
+
.board=${this.board}
|
|
206
|
+
.parts=${this.parts}
|
|
207
|
+
></figure-capabilities>
|
|
208
|
+
|
|
209
|
+
<figure-animations
|
|
210
|
+
?off=${this.tab !== 'animation'}
|
|
211
|
+
.board=${this.board}
|
|
212
|
+
.parts=${this.parts}
|
|
213
|
+
></figure-animations>
|
|
214
|
+
|
|
155
215
|
<figure-report
|
|
156
216
|
?off=${this.tab !== 'verdict'}
|
|
157
217
|
.source=${this.source}
|
|
@@ -173,10 +233,26 @@ export class FigureSide extends localize(i18next)(LitElement) {
|
|
|
173
233
|
`
|
|
174
234
|
}
|
|
175
235
|
|
|
176
|
-
|
|
236
|
+
/**
|
|
237
|
+
* 탭 하나 — **아이콘이 이름을 대신한다.**
|
|
238
|
+
*
|
|
239
|
+
* 탭이 다섯이 되면서 이름이 좁은 칸을 나눠 쓰다 접혔다. 앞으로 더 늘어날 자리이므로 이름을
|
|
240
|
+
* 줄여 짜 넣는 대신 아이콘으로 바꾼다.
|
|
241
|
+
*
|
|
242
|
+
* **이름을 버리지는 않는다.** 아이콘만으로는 무엇인지 모르는 사람이 있고, 특히 「능력」처럼
|
|
243
|
+
* 이 도구에만 있는 개념이 그렇다. 그래서 이름은 `title` 로 남아 마우스를 올리면 나오고,
|
|
244
|
+
* `aria-label` 로 읽는 기계에도 그대로 간다.
|
|
245
|
+
*/
|
|
246
|
+
private renderTab(tab: Tab, icon: string, label: string, badge?: number) {
|
|
177
247
|
return html`
|
|
178
|
-
<button
|
|
179
|
-
|
|
248
|
+
<button
|
|
249
|
+
tab
|
|
250
|
+
?on=${this.tab === tab}
|
|
251
|
+
title=${label}
|
|
252
|
+
aria-label=${label}
|
|
253
|
+
@click=${() => (this.tab = tab)}
|
|
254
|
+
>
|
|
255
|
+
<md-icon>${icon}</md-icon>${badge == null ? '' : html`<span badge>${badge.toLocaleString()}</span>`}
|
|
180
256
|
</button>
|
|
181
257
|
`
|
|
182
258
|
}
|
|
@@ -149,7 +149,6 @@ const FULL: FigureSource = {
|
|
|
149
149
|
base: { x: 3000, y: 800, z: 2000 },
|
|
150
150
|
detailLevel: 'L',
|
|
151
151
|
styleKit: 'factory',
|
|
152
|
-
anchor: { x: 0, y: -400, z: 0 },
|
|
153
152
|
parts: [
|
|
154
153
|
{
|
|
155
154
|
name: 'deck',
|
|
@@ -200,8 +199,23 @@ const FULL: FigureSource = {
|
|
|
200
199
|
material: { token: 'palette.neutral' },
|
|
201
200
|
sizing: 'repeat',
|
|
202
201
|
repeat: { axis: 'x', pitch: 200 }
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
/*
|
|
205
|
+
물건을 놓는 자리이면서 드나드는 문이다 — AGV 의 상판이 그렇다. **그려지지 않으므로**
|
|
206
|
+
재질이 없다.
|
|
207
|
+
|
|
208
|
+
그리지 않는 것이라 화면으로는 왕복에서 누락됐는지 알 수 없다. 자리를 잃어버린 도형은
|
|
209
|
+
멀쩡해 보이면서 아무것도 받지 못한다.
|
|
210
|
+
*/
|
|
211
|
+
name: 'seat',
|
|
212
|
+
primitive: 'cube',
|
|
213
|
+
transform: { position: { x: 0, y: 420, z: 0 }, size: { x: 700, y: 500, z: 500 } },
|
|
214
|
+
capability: { roles: ['slot', 'port-in', 'port-out'], accepts: ['PALLET'], capacity: 2 }
|
|
203
215
|
}
|
|
204
216
|
],
|
|
217
|
+
/* 이 도형이 지는 능력. 부품이 아니라 도형 전체의 것이다. */
|
|
218
|
+
capabilities: ['FlowNode'],
|
|
205
219
|
/*
|
|
206
220
|
움직임. 편집면이 아직 안 만지는 자리이며, **바로 그래서 여기 있다.**
|
|
207
221
|
|
|
@@ -82,14 +82,6 @@ export interface BoardModel {
|
|
|
82
82
|
figureType?: string
|
|
83
83
|
detailLevel?: DetailLevel
|
|
84
84
|
styleKit?: string
|
|
85
|
-
/**
|
|
86
|
-
* 형상 전체의 기준점.
|
|
87
|
-
*
|
|
88
|
-
* **부품의 `anchor` 와 다른 것이다.** 이쪽은 점(`Vec3`)이고 저쪽은 축별로 어느 면을
|
|
89
|
-
* 붙잡느냐다. 한 형식 안에 같은 낱말이 둘인 것은 좋지 않으나, 이름을 바꾸면 저장된
|
|
90
|
-
* 원본이 따라와야 하므로 여기서 정하지 않는다.
|
|
91
|
-
*/
|
|
92
|
-
anchor?: Vec3
|
|
93
85
|
/**
|
|
94
86
|
* 움직임. **부품 바깥에 있다** — clip 하나가 부품 여럿을 함께 움직이기 때문이다.
|
|
95
87
|
*
|
|
@@ -97,6 +89,14 @@ export interface BoardModel {
|
|
|
97
89
|
* 저장하는 것만으로 **조용히 지워지기** 때문이다.
|
|
98
90
|
*/
|
|
99
91
|
animations?: FigureSource['animations']
|
|
92
|
+
/**
|
|
93
|
+
* 이 도형이 지는 능력. 씬의 capability mixin 이름이다.
|
|
94
|
+
*
|
|
95
|
+
* **부품이 아니라 도형 전체의 것이다** — 「이 컨베이어는 흐름의 마디다」는 부품 하나가
|
|
96
|
+
* 아니라 도형이 지는 성질이다. 부품 쪽 `capability` 는 그 능력을 **어느 부품이 받쳐
|
|
97
|
+
* 주나**를 말하는 다른 칸이다.
|
|
98
|
+
*/
|
|
99
|
+
capabilities?: FigureSource['capabilities']
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/** 부품 컴포넌트의 상태. 씬의 용어 그대로다. */
|
|
@@ -159,6 +159,13 @@ export interface PartModel {
|
|
|
159
159
|
anchor?: FigurePart['anchor']
|
|
160
160
|
repeat?: FigurePart['repeat']
|
|
161
161
|
label?: FigurePart['label']
|
|
162
|
+
/**
|
|
163
|
+
* 이 부품이 능력의 어느 자리인가 — 물건이 앉는 자리(slot)이거나 드나드는 문(port)이다.
|
|
164
|
+
*
|
|
165
|
+
* 이 칸이 있는 부품은 **그려지지 않는다.** 저작면에서는 어디인지 보여야 하니 그리지만,
|
|
166
|
+
* 청사진에서는 `groups` 가 아니라 `anchors` 로 간다.
|
|
167
|
+
*/
|
|
168
|
+
capability?: FigurePart['capability']
|
|
162
169
|
}
|
|
163
170
|
|
|
164
171
|
const RAD_TO_DEG = 180 / Math.PI
|
|
@@ -223,11 +230,11 @@ export function toFigureSource(board: BoardModel, parts: PartModel[]): FigureSou
|
|
|
223
230
|
return withoutEmpty({
|
|
224
231
|
type: board.figureType ?? '',
|
|
225
232
|
base: { x: board.width, y: board.depth, z: board.height },
|
|
226
|
-
anchor: board.anchor,
|
|
227
233
|
detailLevel: board.detailLevel,
|
|
228
234
|
styleKit: board.styleKit,
|
|
229
235
|
parts: parts.map(part => partTo(part, half)),
|
|
230
|
-
animations: board.animations
|
|
236
|
+
animations: board.animations,
|
|
237
|
+
capabilities: board.capabilities
|
|
231
238
|
}) as FigureSource
|
|
232
239
|
}
|
|
233
240
|
|
|
@@ -239,6 +246,13 @@ function partTo(part: PartModel, half: Vec3): FigurePart {
|
|
|
239
246
|
})
|
|
240
247
|
|
|
241
248
|
const shape = withoutEmpty({ round: part.round, path: part.path, hollow: part.hollow })
|
|
249
|
+
/*
|
|
250
|
+
재질이 하나도 없으면 **칸 자체를 안 적는다.**
|
|
251
|
+
|
|
252
|
+
빈 객체를 적으면 형식이 「재질은 있는데 무엇으로 칠할지 모른다」로 읽어
|
|
253
|
+
`material-unresolvable` 을 낸다. 그리지 않는 자리(`capability`)에는 재질이 없는 것이
|
|
254
|
+
정상이고, 그 부품이 조용히 오류를 달고 다니게 된다.
|
|
255
|
+
*/
|
|
242
256
|
const material = withoutEmpty({
|
|
243
257
|
token: part.token,
|
|
244
258
|
preset: part.preset,
|
|
@@ -262,13 +276,14 @@ function partTo(part: PartModel, half: Vec3): FigurePart {
|
|
|
262
276
|
}),
|
|
263
277
|
segments: part.segments,
|
|
264
278
|
shape: Object.keys(shape).length > 0 ? shape : undefined,
|
|
265
|
-
material,
|
|
279
|
+
material: Object.keys(material).length > 0 ? material : undefined,
|
|
266
280
|
materialSlot: part.materialSlot,
|
|
267
281
|
keepRound: part.keepRound,
|
|
268
282
|
sizing: part.sizing,
|
|
269
283
|
anchor: part.anchor,
|
|
270
284
|
repeat: part.repeat,
|
|
271
|
-
label: part.label
|
|
285
|
+
label: part.label,
|
|
286
|
+
capability: part.capability
|
|
272
287
|
}) as FigurePart
|
|
273
288
|
}
|
|
274
289
|
|
|
@@ -283,8 +298,8 @@ export function fromFigureSource(source: FigureSource): { board: BoardModel; par
|
|
|
283
298
|
figureType: source.type,
|
|
284
299
|
detailLevel: source.detailLevel,
|
|
285
300
|
styleKit: source.styleKit,
|
|
286
|
-
|
|
287
|
-
|
|
301
|
+
animations: source.animations,
|
|
302
|
+
capabilities: source.capabilities
|
|
288
303
|
}) as BoardModel
|
|
289
304
|
|
|
290
305
|
return { board, parts: source.parts.map(part => partFrom(part, half)) }
|
|
@@ -333,6 +348,7 @@ function partFrom(part: FigurePart, half: Vec3): PartModel {
|
|
|
333
348
|
sizing: part.sizing,
|
|
334
349
|
anchor: part.anchor,
|
|
335
350
|
repeat: part.repeat,
|
|
336
|
-
label: part.label
|
|
351
|
+
label: part.label,
|
|
352
|
+
capability: part.capability
|
|
337
353
|
}) as PartModel
|
|
338
354
|
}
|
|
@@ -34,11 +34,40 @@ interface Shooter extends HTMLElement {
|
|
|
34
34
|
snapshot?: (width?: number, height?: number) => Promise<string | undefined>
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* 씬이 서고 카메라가 자리를 잡을 틈 — 프레임 두 장 뒤에 한 번 더 쉰다. 씬에 그 신호가 없다.
|
|
39
|
+
*
|
|
40
|
+
* **rAF 만 기다리면 안 된다.** 탭이 뒤로 가거나 창이 가려지면 브라우저가 rAF 를 멈추고, 그러면
|
|
41
|
+
* 이 약속이 영원히 안 풀린다. 그 위에 저장이 얹혀 있어서 저장이 통째로 멈췄다 — 단추는 잠긴 채로,
|
|
42
|
+
* 아무 말도 없이. [확인함] 2026-09-10 포커스 없는 탭에서 `saving: true` 로 고정.
|
|
43
|
+
*
|
|
44
|
+
* 그래서 시계와 함께 경주시킨다. 늦게 깨어난 쪽은 버려지고, 화면이 안 그려지는 상황이면 아래
|
|
45
|
+
* `snapshot` 이 빈 것을 돌려주거나 시간이 다 된다 — 어느 쪽이든 **저장은 계속 간다.**
|
|
46
|
+
*/
|
|
38
47
|
function settle(): Promise<void> {
|
|
39
|
-
|
|
48
|
+
const frames = new Promise<void>(resolve =>
|
|
40
49
|
requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 150)))
|
|
41
50
|
)
|
|
51
|
+
|
|
52
|
+
return Promise.race([frames, delay(600)])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function delay(ms: number): Promise<void> {
|
|
56
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 그림을 기다리는 시간의 한도.
|
|
61
|
+
*
|
|
62
|
+
* 3D 를 세워 한 장 찍는 데 걸리는 시간은 형상과 기계에 따라 다르지만, 이 한도의 목적은 빠르기가
|
|
63
|
+
* 아니라 **저장이 멈추지 않는 것**이다. 넘으면 그림 없이 저장한다 — 정본이 그림보다 중하고,
|
|
64
|
+
* 그림은 다음에 저작면을 열 때 다시 채운다(`figure-modeller-page` 의 fillThumbnail).
|
|
65
|
+
*/
|
|
66
|
+
const PATIENCE = 5000
|
|
67
|
+
|
|
68
|
+
/** 시간이 다 되면 없는 것으로 답한다. 던지지 않는다 — 그림이 없는 것은 실패가 아니다. */
|
|
69
|
+
function within<T>(work: Promise<T>, ms: number): Promise<T | undefined> {
|
|
70
|
+
return Promise.race([work, delay(ms).then(() => undefined)])
|
|
42
71
|
}
|
|
43
72
|
|
|
44
73
|
export async function captureThumbnail(
|
|
@@ -66,9 +95,25 @@ export async function captureThumbnail(
|
|
|
66
95
|
|
|
67
96
|
try {
|
|
68
97
|
host.appendChild(shooter)
|
|
69
|
-
await settle()
|
|
70
98
|
|
|
71
|
-
|
|
99
|
+
/*
|
|
100
|
+
**한도 안에 못 찍으면 그림 없이 간다.** 이 함수를 부르는 자리가 저장이고, 저장은 그림을
|
|
101
|
+
기다리다 멈춰서는 안 된다. 못 찍은 것은 조용히 넘기지 않고 콘솔에 남긴다 — 그림이 계속
|
|
102
|
+
안 생기면 그 줄이 단서가 된다.
|
|
103
|
+
*/
|
|
104
|
+
const shot = await within(
|
|
105
|
+
(async () => {
|
|
106
|
+
await settle()
|
|
107
|
+
return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined
|
|
108
|
+
})(),
|
|
109
|
+
PATIENCE
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if (!shot) {
|
|
113
|
+
console.warn(`[thumbnail] ${PATIENCE}ms 안에 못 찍었습니다 — 그림 없이 저장합니다`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return shot
|
|
72
117
|
} catch (e) {
|
|
73
118
|
console.warn(`[thumbnail] 세우지 못했습니다 — ${(e as Error).message}`)
|
|
74
119
|
return undefined
|
|
@@ -70,7 +70,7 @@ export function maturityOf(source: FigureSource, cost: FigureCost, score: Figure
|
|
|
70
70
|
function scale(cost: FigureCost): MaturityAxis {
|
|
71
71
|
const capacity = capacityFrom(cost.groups, cost.triangles)
|
|
72
72
|
if (!capacity) {
|
|
73
|
-
return { key: 'scale', blocked: 'text.maturity-blocked-no-parts' }
|
|
73
|
+
return { key: 'scale', blocked: 'figure.text.maturity-blocked-no-parts' }
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
return {
|
|
@@ -79,7 +79,7 @@ function scale(cost: FigureCost): MaturityAxis {
|
|
|
79
79
|
// 목표를 넘긴 여유는 다른 축을 고치는 데 쓰는 편이 낫다.
|
|
80
80
|
ratio: Math.min(1, capacity.instances / BOARD_ASSETS),
|
|
81
81
|
reading: String(capacity.instances),
|
|
82
|
-
because: 'text.maturity-scale-because'
|
|
82
|
+
because: 'figure.text.maturity-scale-because'
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -102,8 +102,8 @@ function consistency(source: FigureSource, cost: FigureCost): MaturityAxis {
|
|
|
102
102
|
return {
|
|
103
103
|
key: 'consistency',
|
|
104
104
|
ratio: (3 - faults) / 3,
|
|
105
|
-
readingKey: faults === 0 ? 'text.maturity-consistency-clean' : 'text.maturity-consistency-faults',
|
|
106
|
-
because: 'text.maturity-consistency-because'
|
|
105
|
+
readingKey: faults === 0 ? 'figure.text.maturity-consistency-clean' : 'figure.text.maturity-consistency-faults',
|
|
106
|
+
because: 'figure.text.maturity-consistency-because'
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -117,7 +117,7 @@ function consistency(source: FigureSource, cost: FigureCost): MaturityAxis {
|
|
|
117
117
|
function reuse(score: FigureScore): MaturityAxis {
|
|
118
118
|
const { distinct, parts, saved } = score.reuse
|
|
119
119
|
if (parts === 0) {
|
|
120
|
-
return { key: 'reuse', blocked: 'text.maturity-blocked-no-parts' }
|
|
120
|
+
return { key: 'reuse', blocked: 'figure.text.maturity-blocked-no-parts' }
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
return {
|
|
@@ -125,7 +125,7 @@ function reuse(score: FigureScore): MaturityAxis {
|
|
|
125
125
|
// ratio 가 낮을수록 많이 되쓴다 — 뒤집어서 높은 쪽이 좋게 만든다
|
|
126
126
|
ratio: 1 - distinct / parts,
|
|
127
127
|
reading: String(saved),
|
|
128
|
-
because: 'text.maturity-reuse-because'
|
|
128
|
+
because: 'figure.text.maturity-reuse-because'
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -148,7 +148,7 @@ function detail(source: FigureSource, cost: FigureCost): MaturityAxis {
|
|
|
148
148
|
// 절반쯤 쓴 것을 제일 좋게 본다. 텅 비어도, 넘쳐도 등급과 안 맞는다.
|
|
149
149
|
ratio: used > 1 ? Math.max(0, 2 - used) : Math.min(1, used * 2),
|
|
150
150
|
reading: `${cost.parts} / ${limit}`,
|
|
151
|
-
because: 'text.maturity-detail-because'
|
|
151
|
+
because: 'figure.text.maturity-detail-because'
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -162,7 +162,7 @@ function detail(source: FigureSource, cost: FigureCost): MaturityAxis {
|
|
|
162
162
|
* **아무 형상도 보여 줄 수 없는 상태**다. 축 자체가 아직 없는 것이다.
|
|
163
163
|
*/
|
|
164
164
|
function reactive(source: FigureSource): MaturityAxis {
|
|
165
|
-
return { key: 'reactive', blocked: 'text.maturity-blocked-no-binding' }
|
|
165
|
+
return { key: 'reactive', blocked: 'figure.text.maturity-blocked-no-binding' }
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
@@ -205,7 +205,7 @@ function reactive(source: FigureSource): MaturityAxis {
|
|
|
205
205
|
function readable(source: FigureSource, cost: FigureCost): MaturityAxis {
|
|
206
206
|
const regions = topViewRegions(source)
|
|
207
207
|
if (regions.total === 0) {
|
|
208
|
-
return { key: 'readable', blocked: 'text.maturity-blocked-no-parts' }
|
|
208
|
+
return { key: 'readable', blocked: 'figure.text.maturity-blocked-no-parts' }
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
// 배경도 한 구역으로 센다 — 발자국을 얼마나 채우나가 식별성의 일부다.
|
|
@@ -221,7 +221,7 @@ function readable(source: FigureSource, cost: FigureCost): MaturityAxis {
|
|
|
221
221
|
key: 'readable',
|
|
222
222
|
ratio: Math.min(1, entropy / ceiling),
|
|
223
223
|
reading: `${regions.byMaterial.size}`,
|
|
224
|
-
because: 'text.maturity-readable-because'
|
|
224
|
+
because: 'figure.text.maturity-readable-because'
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
|