@things-factory/figure-ui 10.1.16 → 10.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/figure-animations.ts +331 -124
- package/client/modeller/figure-canvas.ts +32 -48
- package/client/modeller/figure-capabilities.ts +10 -10
- package/client/modeller/figure-inspector.ts +39 -28
- package/client/modeller/figure-parts.ts +1 -1
- package/client/modeller/figure-preview.ts +151 -41
- package/client/modeller/figure-report.ts +24 -6
- package/client/modeller/figure-settings.ts +45 -2
- package/client/modeller/figure-side.ts +6 -6
- package/client/modeller/figure-source.ts +62 -32
- package/client/modeller/figure-thumbnail.ts +5 -5
- package/client/modeller/figure-view.ts +53 -0
- package/client/modeller/part-edits.ts +36 -36
- package/client/modeller/proposal.ts +27 -4
- package/client/pages/figure-modeller-page.ts +65 -58
- package/dist-client/modeller/figure-animations.d.ts +37 -8
- package/dist-client/modeller/figure-animations.js +293 -122
- package/dist-client/modeller/figure-animations.js.map +1 -1
- package/dist-client/modeller/figure-canvas.d.ts +7 -7
- package/dist-client/modeller/figure-canvas.js +27 -40
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-capabilities.d.ts +2 -2
- package/dist-client/modeller/figure-capabilities.js +9 -9
- package/dist-client/modeller/figure-capabilities.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +16 -5
- package/dist-client/modeller/figure-inspector.js +38 -27
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-parts.d.ts +1 -1
- package/dist-client/modeller/figure-parts.js +1 -1
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +56 -6
- package/dist-client/modeller/figure-preview.js +132 -39
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-report.d.ts +9 -0
- package/dist-client/modeller/figure-report.js +23 -5
- package/dist-client/modeller/figure-report.js.map +1 -1
- package/dist-client/modeller/figure-settings.js +42 -2
- package/dist-client/modeller/figure-settings.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +2 -2
- package/dist-client/modeller/figure-side.js +5 -5
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +25 -15
- package/dist-client/modeller/figure-source.js +42 -21
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/modeller/figure-thumbnail.d.ts +2 -2
- package/dist-client/modeller/figure-thumbnail.js +3 -3
- package/dist-client/modeller/figure-thumbnail.js.map +1 -1
- package/dist-client/modeller/figure-view.d.ts +45 -0
- package/dist-client/modeller/figure-view.js +27 -0
- package/dist-client/modeller/figure-view.js.map +1 -1
- package/dist-client/modeller/part-edits.d.ts +16 -16
- package/dist-client/modeller/part-edits.js +35 -35
- package/dist-client/modeller/part-edits.js.map +1 -1
- package/dist-client/modeller/proposal.d.ts +1 -1
- package/dist-client/modeller/proposal.js +16 -3
- package/dist-client/modeller/proposal.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +9 -10
- package/dist-client/pages/figure-modeller-page.js +61 -54
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/ai-proposal-contract.test.ts +433 -12
- package/test/expect.ts +55 -0
- package/test/figure-source.test.ts +572 -0
- package/test/i18n-prefix-guard.test.ts +9 -12
- package/{client/modeller → test}/part-edits.test.ts +50 -45
- package/translations/en.json +46 -35
- package/translations/ja.json +16 -21
- package/translations/ko.json +56 -45
- package/translations/ms.json +16 -21
- package/translations/zh.json +16 -21
- package/client/modeller/figure-ask.ts +0 -265
- package/client/modeller/figure-source.test.ts +0 -352
- package/dist-client/modeller/figure-ask.d.ts +0 -40
- package/dist-client/modeller/figure-ask.js +0 -282
- package/dist-client/modeller/figure-ask.js.map +0 -1
|
@@ -6,6 +6,9 @@ import { compile, validate } from '@hatiolab/figure-model'
|
|
|
6
6
|
import type { FigureSource } from '@hatiolab/figure-model'
|
|
7
7
|
import { create, registerFigure } from '@hatiolab/things-scene'
|
|
8
8
|
|
|
9
|
+
import { DEFAULT_VIEW, sceneLook, WORKBENCH } from './figure-view.js'
|
|
10
|
+
import type { ViewSettings } from './figure-view.js'
|
|
11
|
+
|
|
9
12
|
/**
|
|
10
13
|
* 저작 중인 것을 **보드에 놓았을 때**의 모습.
|
|
11
14
|
*
|
|
@@ -29,9 +32,6 @@ import { create, registerFigure } from '@hatiolab/things-scene'
|
|
|
29
32
|
* 뷰 모드로 세운다.
|
|
30
33
|
*/
|
|
31
34
|
|
|
32
|
-
/** 배경. 편집 캔버스와 같은 값을 쓴다 — 갈리면 두 화면이 다른 도구처럼 보인다. */
|
|
33
|
-
const WORKBENCH = '#7c7c7c'
|
|
34
|
-
|
|
35
35
|
/** 얼마나 키워 보나. */
|
|
36
36
|
export const PREVIEW_FACTOR = 2
|
|
37
37
|
|
|
@@ -57,6 +57,18 @@ export const PREVIEW_WAYS: readonly { name: string; axes: readonly ('x' | 'y' |
|
|
|
57
57
|
{ name: 'x·y·z', axes: ['x', 'y', 'z'] }
|
|
58
58
|
]
|
|
59
59
|
|
|
60
|
+
/*
|
|
61
|
+
* 높이는 여기서 안 정한다.
|
|
62
|
+
*
|
|
63
|
+
* 한동안 배치 기준을 높이로 옮기는 계산을 이 파일이 들고 있었다. 이제 씬이 한다 —
|
|
64
|
+
* `FigureRealObject.effectiveZPos` 가 청사진의 `placement` 와 레이어의 `heights` 로 정한다
|
|
65
|
+
* (ADR-0045, `operato-twin/design/04-decisions.md`). 보드가 그렇게 세우므로 「보드에 놓았을
|
|
66
|
+
* 때의 모습」을 말하는 이 화면도 그렇게 세워야 한다 — 여기서 `zPos` 를 적으면 **적은 값이
|
|
67
|
+
* 이겨서**, 씬이 무엇을 할지가 이 화면에서만 안 보인다.
|
|
68
|
+
*
|
|
69
|
+
* 규칙이 두 벌이면 갈린다. 오늘 하루가 그 이야기였다.
|
|
70
|
+
*/
|
|
71
|
+
|
|
60
72
|
interface SceneHandle {
|
|
61
73
|
dispose?: () => void
|
|
62
74
|
resize?: () => void
|
|
@@ -64,6 +76,8 @@ interface SceneHandle {
|
|
|
64
76
|
root?: {
|
|
65
77
|
invalidate?: () => void
|
|
66
78
|
fitCameraToModel?: (margin?: number) => void
|
|
79
|
+
/** 보기 설정을 넣는 자리. 씬이 스스로 반응한다. */
|
|
80
|
+
set?: (patch: Record<string, unknown>) => void
|
|
67
81
|
/** 세워 둔 인스턴스들. clip 을 몰 때 전부에 같은 값을 준다. */
|
|
68
82
|
components?: { set?: (property: string, value: unknown) => void }[]
|
|
69
83
|
}
|
|
@@ -103,11 +117,11 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
103
117
|
}
|
|
104
118
|
|
|
105
119
|
/*
|
|
106
|
-
|
|
120
|
+
움직임 조작. **저작자가 제 애니메이션과 파라미터를 만져 볼 유일한 자리다.**
|
|
107
121
|
|
|
108
|
-
|
|
109
|
-
중에는 그 사실을 주는 것이 없어서, 손대지 않으면
|
|
110
|
-
|
|
122
|
+
도면에서는 트윈이 이 값을 준다 — 「닙이 40% 닫혔다」는 사실이 어디선가 온다. 저작
|
|
123
|
+
중에는 그 사실을 주는 것이 없어서, 손대지 않으면 파라미터는 기본값에 머문 채 영영
|
|
124
|
+
안 움직인다. 포크가 안 올라가는 것을 저작자가 「안 되는구나」로 읽게 된다.
|
|
111
125
|
|
|
112
126
|
**미리보기 것이지 자산의 것이 아니다.** 여기서 민 값은 저장되지 않는다.
|
|
113
127
|
*/
|
|
@@ -162,6 +176,14 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
162
176
|
/** 저작 중인 것. 저장 형식으로 받는다 — 보드가 소비하는 것이 이 형식이다. */
|
|
163
177
|
@property({ type: Object }) source?: FigureSource
|
|
164
178
|
|
|
179
|
+
/**
|
|
180
|
+
* 보기 설정. 저장되지 않는다(`figure-view.ts`).
|
|
181
|
+
*
|
|
182
|
+
* 한동안 이 화면만 조명과 환경을 **하드코딩**했다. 저작자가 환경 탭에서 무엇을 바꿔도
|
|
183
|
+
* 편집 캔버스만 달라지고 여기는 그대로였다 — 두 화면이 같은 것을 다르게 보여 주었다.
|
|
184
|
+
*/
|
|
185
|
+
@property({ type: Object }) view: ViewSettings = DEFAULT_VIEW
|
|
186
|
+
|
|
165
187
|
@query('div[stage]') private stage!: HTMLDivElement
|
|
166
188
|
|
|
167
189
|
/** 세울 수 없는 사유. 없으면 세운 것이다. */
|
|
@@ -172,11 +194,14 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
172
194
|
private standing?: string
|
|
173
195
|
private watcher?: ResizeObserver
|
|
174
196
|
|
|
175
|
-
/** 지금 민 clip
|
|
197
|
+
/** 지금 민 clip 배속. clip 이름 → 수. **저장되지 않는다.** */
|
|
176
198
|
@state() private driving: Record<string, number> = {}
|
|
199
|
+
/** 지금 민 파라미터 값. 이름 → `range` 의 단위로 잰 수. **저장되지 않는다.** */
|
|
200
|
+
@state() private giving: Record<string, number> = {}
|
|
177
201
|
|
|
178
202
|
render() {
|
|
179
203
|
const clips = this.source?.animations ?? []
|
|
204
|
+
const parameters = this.source?.parameters ?? []
|
|
180
205
|
|
|
181
206
|
return html`
|
|
182
207
|
<div stage></div>
|
|
@@ -185,31 +210,54 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
185
210
|
: html`<div legend>
|
|
186
211
|
${PREVIEW_WAYS.map(way => html`<span>${way.name}</span>`)}
|
|
187
212
|
</div>
|
|
188
|
-
${clips.length > 0 ? this.
|
|
213
|
+
${clips.length + parameters.length > 0 ? this.renderDrivers(clips, parameters) : ''}`}
|
|
189
214
|
`
|
|
190
215
|
}
|
|
191
216
|
|
|
192
217
|
/**
|
|
193
|
-
*
|
|
218
|
+
* 움직임 하나에 손잡이 하나.
|
|
194
219
|
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* 뜻인지 매번
|
|
220
|
+
* 두 갈래를 **눈금이 다른 채로** 낸다. clip 은 배속이라 0 이 멈춤이고 0~3 이 뜻이
|
|
221
|
+
* 있으며, 파라미터는 물리량이라 그 `range` 가 곧 눈금이고 단위가 따라 붙는다. 하나의
|
|
222
|
+
* 0~1 로 뭉뚱그리면 「1」이 무슨 뜻인지 매번 헷갈리고, 1200mm 를 넣어 보려던 저작자가
|
|
223
|
+
* 넣을 자리를 못 찾는다.
|
|
198
224
|
*/
|
|
199
|
-
private
|
|
225
|
+
private renderDrivers(
|
|
226
|
+
clips: NonNullable<FigureSource['animations']>,
|
|
227
|
+
parameters: NonNullable<FigureSource['parameters']>
|
|
228
|
+
) {
|
|
200
229
|
return html`
|
|
201
230
|
<div clips>
|
|
231
|
+
${parameters.map(parameter => {
|
|
232
|
+
const range = parameter.range
|
|
233
|
+
const value = this.giving[parameter.name] ?? parameter.default ?? range?.min ?? 0
|
|
234
|
+
const step = range ? Math.max((range.max - range.min) / 100, 0.01) : 1
|
|
235
|
+
|
|
236
|
+
return html`
|
|
237
|
+
<label>
|
|
238
|
+
<span>${parameter.label ?? parameter.name} <span drive>${range?.unit ?? ''}</span></span>
|
|
239
|
+
<input
|
|
240
|
+
type="range"
|
|
241
|
+
min=${range?.min ?? 0}
|
|
242
|
+
max=${range?.max ?? 1}
|
|
243
|
+
step=${step}
|
|
244
|
+
.value=${String(value)}
|
|
245
|
+
@input=${(e: Event) => this.give(parameter.name, Number((e.target as HTMLInputElement).value))}
|
|
246
|
+
/>
|
|
247
|
+
<output>${Math.round(value)}</output>
|
|
248
|
+
</label>
|
|
249
|
+
`
|
|
250
|
+
})}
|
|
202
251
|
${clips.map(clip => {
|
|
203
|
-
const
|
|
204
|
-
const value = this.driving[clip.name] ?? (loop ? 1 : 0)
|
|
252
|
+
const value = this.driving[clip.name] ?? 1
|
|
205
253
|
|
|
206
254
|
return html`
|
|
207
255
|
<label>
|
|
208
|
-
<span>${clip.name} <span drive
|
|
256
|
+
<span>${clip.name} <span drive>배속</span></span>
|
|
209
257
|
<input
|
|
210
258
|
type="range"
|
|
211
259
|
min="0"
|
|
212
|
-
max
|
|
260
|
+
max="3"
|
|
213
261
|
step="0.05"
|
|
214
262
|
.value=${String(value)}
|
|
215
263
|
@input=${(e: Event) => this.drive(clip.name, Number((e.target as HTMLInputElement).value))}
|
|
@@ -230,15 +278,53 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
230
278
|
*/
|
|
231
279
|
private drive(name: string, value: number): void {
|
|
232
280
|
this.driving = { ...this.driving, [name]: value }
|
|
281
|
+
this.tellInstances('clips', this.driving)
|
|
282
|
+
}
|
|
233
283
|
|
|
284
|
+
/**
|
|
285
|
+
* 파라미터 값을 준다 — **`range` 의 단위 그대로다.**
|
|
286
|
+
*
|
|
287
|
+
* 0~1 로 바꿔서 주지 않는다. 그 환산은 런타임의 일이고, 여기서 한 번 더 하면 두 곳이
|
|
288
|
+
* 갈릴 수 있다. 저작자가 화면에서 보는 1200 이 씬에 그대로 간다.
|
|
289
|
+
*/
|
|
290
|
+
private give(name: string, value: number): void {
|
|
291
|
+
this.giving = { ...this.giving, [name]: value }
|
|
292
|
+
this.tellInstances('parameters', this.giving)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private tellInstances(key: 'clips' | 'parameters', values: Record<string, number>): void {
|
|
234
296
|
for (const component of this.scene?.root?.components ?? []) {
|
|
235
|
-
component.set?.(
|
|
297
|
+
component.set?.(key, { ...values })
|
|
236
298
|
}
|
|
237
299
|
}
|
|
238
300
|
|
|
301
|
+
/**
|
|
302
|
+
* 보기 설정이 바뀌면 **다시 세우지 않고 씬에 넣는다** — 두 개만 빼고.
|
|
303
|
+
*
|
|
304
|
+
* 조명·환경·바탕은 씬이 스스로 반응한다(`three-capability` 의 `onchange`). 슬라이더를
|
|
305
|
+
* 끄는 동안 다시 세우면 카메라가 매 프레임 처음으로 돌아간다.
|
|
306
|
+
*
|
|
307
|
+
* 좌표 기준과 천장 높이는 다르다. 씬이 그 둘로 인스턴스 높이를 정하는데, 그 계산은 세울
|
|
308
|
+
* 때 한 번 돈다 — 이미 세워 둔 것에 넣어 봐야 자리가 안 바뀐다. 그것만 다시 세운다.
|
|
309
|
+
*/
|
|
239
310
|
willUpdate(changed: Map<string, unknown>) {
|
|
240
311
|
if (!this.hasUpdated) return
|
|
241
|
-
|
|
312
|
+
|
|
313
|
+
if (changed.has('source')) {
|
|
314
|
+
this.standUp()
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!changed.has('view')) return
|
|
319
|
+
|
|
320
|
+
const before = changed.get('view') as ViewSettings | undefined
|
|
321
|
+
const now = this.view ?? DEFAULT_VIEW
|
|
322
|
+
if (before?.placement !== now.placement || before?.ceilingHeight !== now.ceilingHeight) {
|
|
323
|
+
this.standUp()
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.scene?.root?.set?.(sceneLook(now))
|
|
242
328
|
}
|
|
243
329
|
|
|
244
330
|
firstUpdated() {
|
|
@@ -268,7 +354,9 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
268
354
|
return
|
|
269
355
|
}
|
|
270
356
|
|
|
271
|
-
const
|
|
357
|
+
const view = this.view ?? DEFAULT_VIEW
|
|
358
|
+
/* 씬이 이 둘로 인스턴스 높이를 정한다 — 서명에 안 넣으면 다시 세워도 그대로 선다. */
|
|
359
|
+
const signature = JSON.stringify([source, view.placement, view.ceilingHeight])
|
|
272
360
|
if (this.scene && this.standing === signature) return
|
|
273
361
|
|
|
274
362
|
const { errors } = validate(source)
|
|
@@ -304,24 +392,16 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
304
392
|
const COLUMNS = 4
|
|
305
393
|
const cell = { x: base.x * biggest * 1.25, z: base.z * biggest * 1.25 }
|
|
306
394
|
const rows = Math.ceil(PREVIEW_WAYS.length / COLUMNS)
|
|
307
|
-
const board = {
|
|
308
|
-
width: Math.round(cell.x * COLUMNS),
|
|
309
|
-
height: Math.round(cell.z * rows),
|
|
310
|
-
/*
|
|
311
|
-
판의 높이도 가장 큰 인스턴스를 담아야 한다.
|
|
312
|
-
|
|
313
|
-
안 주면 카메라가 평면만 보고 자리를 잡아서, 키가 큰 모델은 밑동만 보인다.
|
|
314
|
-
시그널타워를 2 배로 놓으면 2200 인데 판이 그것을 모르면 받침만 화면에 든다.
|
|
315
|
-
*/
|
|
316
|
-
depth: Math.round(base.y * biggest * 1.1)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
395
|
/*
|
|
320
396
|
씬의 낱말이 형식과 다르다 — `width` 가 형식의 x, `height` 가 z, `depth` 가 y 다.
|
|
321
397
|
옮기는 일을 여기 한 줄에 모아 둔다.
|
|
322
398
|
|
|
323
399
|
`depth` 를 늘 적는 이유가 있다. 안 적으면 평면 두 축의 기하평균으로 잡혀서,
|
|
324
400
|
「y 만 키운다」가 x·z 를 안 건드리는데도 높이가 안 오른다.
|
|
401
|
+
|
|
402
|
+
높이는 **배치 기준이 정한다.** 바닥 기반은 땅에서 올라오고, 천정 기반은 천장에
|
|
403
|
+
붙어 내려온다. 여덟을 나란히 세우는 자리라 이것이 눈에 바로 보인다 — 바닥 기반이면
|
|
404
|
+
밑면이, 천정 기반이면 윗면이 한 줄이 된다.
|
|
325
405
|
*/
|
|
326
406
|
const components = PREVIEW_WAYS.map((way, at) => {
|
|
327
407
|
const grow = growth(way)
|
|
@@ -329,6 +409,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
329
409
|
const row = Math.floor(at / COLUMNS)
|
|
330
410
|
const width = Math.round(base.x * grow.x)
|
|
331
411
|
const height = Math.round(base.z * grow.z)
|
|
412
|
+
const tall = Math.round(base.y * grow.y)
|
|
332
413
|
|
|
333
414
|
return {
|
|
334
415
|
type: source.type,
|
|
@@ -338,23 +419,52 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
338
419
|
top: Math.round(row * cell.z + (cell.z - height) / 2),
|
|
339
420
|
width,
|
|
340
421
|
height,
|
|
341
|
-
depth:
|
|
422
|
+
depth: tall
|
|
342
423
|
}
|
|
343
424
|
})
|
|
344
425
|
|
|
426
|
+
const draft = {
|
|
427
|
+
width: Math.round(cell.x * COLUMNS),
|
|
428
|
+
height: Math.round(cell.z * rows),
|
|
429
|
+
/*
|
|
430
|
+
판의 높이도 가장 큰 인스턴스를 담아야 한다. 안 주면 카메라가 평면만 보고 자리를
|
|
431
|
+
잡아서, 키가 큰 모델은 밑동만 보인다 — 시그널타워를 2 배로 놓으면 2200 인데 판이
|
|
432
|
+
그것을 모르면 받침만 화면에 든다.
|
|
433
|
+
|
|
434
|
+
어디에 올려 놓을지는 **씬이 정하므로**(`FigureRealObject.effectiveZPos`), 여기서는
|
|
435
|
+
어느 경우에도 담기는 경계만 잡는다. 바닥 기반은 가장 큰 것의 키까지, 천정 기반은
|
|
436
|
+
천장까지다. 배치를 높이로 옮기는 계산을 여기서 다시 쓰면 그것이 두 벌째가 된다.
|
|
437
|
+
*/
|
|
438
|
+
depth: Math.round(Math.max(view.ceilingHeight, base.y * biggest) * 1.1)
|
|
439
|
+
}
|
|
440
|
+
|
|
345
441
|
this.scene = create({
|
|
346
442
|
target: this.stage,
|
|
347
443
|
model: {
|
|
348
444
|
type: 'model-layer',
|
|
349
|
-
width:
|
|
350
|
-
height:
|
|
351
|
-
depth:
|
|
445
|
+
width: draft.width,
|
|
446
|
+
height: draft.height,
|
|
447
|
+
depth: draft.depth,
|
|
352
448
|
threed: true,
|
|
353
|
-
environment: 'studio',
|
|
354
|
-
grid: true,
|
|
355
449
|
sky: 'color',
|
|
356
|
-
|
|
357
|
-
|
|
450
|
+
/*
|
|
451
|
+
보기 설정을 그대로 싣는다.
|
|
452
|
+
|
|
453
|
+
여기만 조명·환경·바탕을 하드코딩하고 있었다. 저작자가 환경 탭에서 조명을
|
|
454
|
+
올려도 편집 캔버스만 밝아지고 이 화면은 그대로여서, 같은 것을 두 화면이
|
|
455
|
+
다르게 보여 주었다.
|
|
456
|
+
|
|
457
|
+
`sceneLook` 이 좌표 기준과 천장 높이를 빼고 준다 — 씬이 아는 이름이 아니거나
|
|
458
|
+
(`ceilingHeight`) 편집 캔버스에 새면 안 되는 것(`placement`)이라서다. 둘 다 아래에
|
|
459
|
+
씬의 낱말로 따로 싣는다.
|
|
460
|
+
*/
|
|
461
|
+
...sceneLook(view),
|
|
462
|
+
placement: view.placement,
|
|
463
|
+
/*
|
|
464
|
+
부착면의 높이. 씬이 이 자리를 갖고(`real-object.ts` 의 `sceneHeights`), 천정 기반
|
|
465
|
+
도형이 여기서 제 높이를 얻는다. 보드에서는 현장이 이 값을 준다.
|
|
466
|
+
*/
|
|
467
|
+
heights: { floor: 0, ceiling: view.ceilingHeight },
|
|
358
468
|
pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),
|
|
359
469
|
components
|
|
360
470
|
},
|
|
@@ -6,7 +6,7 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js'
|
|
|
6
6
|
|
|
7
7
|
import { i18next, localize } from '@operato/i18n'
|
|
8
8
|
import { ScrollbarStyles } from '@operato/styles'
|
|
9
|
-
import { compile, costCurveSvg, costOf, scoreOf, sizingReport, validate } from '@hatiolab/figure-model'
|
|
9
|
+
import { blocksRelease, compile, costCurveSvg, costOf, scoreOf, sizingReport, validate } from '@hatiolab/figure-model'
|
|
10
10
|
import type { FigureCost, FigureScore, FigureSource } from '@hatiolab/figure-model'
|
|
11
11
|
|
|
12
12
|
import { capacityOf } from './scene-budget.js'
|
|
@@ -439,7 +439,16 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
439
439
|
/** 추이 그림을 제 크기로 펼쳤나. */
|
|
440
440
|
@state() private expanded = false
|
|
441
441
|
@state() private errors: { code: string; message: string; at?: string }[] = []
|
|
442
|
-
|
|
442
|
+
/**
|
|
443
|
+
* 형식이 권하지 않는 것 — **그중 둘은 발행을 막는다.**
|
|
444
|
+
*
|
|
445
|
+
* 등급을 형식에서 받는다(`blocksRelease`). 화면이 제 목록을 들면 형식에 코드가 하나 늘 때
|
|
446
|
+
* 조용히 「알림」으로 그려지고, 저작자는 발행이 막힌 것을 **눌러 보고 나서야** 안다.
|
|
447
|
+
*
|
|
448
|
+
* `actual` 은 얼마나 넘쳤나다. 「상자를 벗어난다」만으로는 2mm 인지 250mm 인지 모르고,
|
|
449
|
+
* 그 둘은 고치는 품이 다르다.
|
|
450
|
+
*/
|
|
451
|
+
@state() private violations: { code: string; message: string; blocking: boolean; actual?: number }[] = []
|
|
443
452
|
/**
|
|
444
453
|
* 크기를 바꿔 봤을 때 갈라지는가 — **발행을 막는 것**이다.
|
|
445
454
|
*
|
|
@@ -864,9 +873,13 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
864
873
|
)}
|
|
865
874
|
${this.violations.map(
|
|
866
875
|
v => html`
|
|
867
|
-
<li violation>
|
|
868
|
-
<md-icon
|
|
869
|
-
<span
|
|
876
|
+
<li ?violation=${!v.blocking}>
|
|
877
|
+
<md-icon>${v.blocking ? 'block' : 'info'}</md-icon>
|
|
878
|
+
<span>
|
|
879
|
+
${v.message}${v.blocking && v.actual !== undefined
|
|
880
|
+
? html` <code at>${v.actual}</code>`
|
|
881
|
+
: ''}
|
|
882
|
+
</span>
|
|
870
883
|
</li>
|
|
871
884
|
`
|
|
872
885
|
)}
|
|
@@ -909,7 +922,12 @@ export class FigureReport extends localize(i18next)(LitElement) {
|
|
|
909
922
|
|
|
910
923
|
const result = validate(source)
|
|
911
924
|
this.errors = result.errors.map(e => ({ code: e.code, message: e.message, at: e.path }))
|
|
912
|
-
this.violations = result.violations.map(v => ({
|
|
925
|
+
this.violations = result.violations.map(v => ({
|
|
926
|
+
code: v.code,
|
|
927
|
+
message: v.message,
|
|
928
|
+
blocking: blocksRelease(v.code),
|
|
929
|
+
actual: v.actual
|
|
930
|
+
}))
|
|
913
931
|
|
|
914
932
|
if (this.errors.length > 0) {
|
|
915
933
|
// 형식을 어겼으면 컴파일이 던진다. 셈을 비우고 오류만 보여 준다.
|
|
@@ -4,7 +4,7 @@ import { customElement, property } from 'lit/decorators.js'
|
|
|
4
4
|
import { i18next, localize } from '@operato/i18n'
|
|
5
5
|
import { ScrollbarStyles } from '@operato/styles'
|
|
6
6
|
|
|
7
|
-
import { DEFAULT_VIEW, ENVIRONMENTS } from './figure-view.js'
|
|
7
|
+
import { DEFAULT_VIEW, ENVIRONMENTS, SCENE_REFERENCES } from './figure-view.js'
|
|
8
8
|
import type { EnvironmentName, ViewSettings } from './figure-view.js'
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -99,6 +99,7 @@ export class FigureSettings extends localize(i18next)(LitElement) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
select,
|
|
102
|
+
input[type='number'],
|
|
102
103
|
input[type='color'] {
|
|
103
104
|
width: 100%;
|
|
104
105
|
padding: 5px 6px;
|
|
@@ -158,10 +159,52 @@ export class FigureSettings extends localize(i18next)(LitElement) {
|
|
|
158
159
|
const view = this.view ?? DEFAULT_VIEW
|
|
159
160
|
|
|
160
161
|
return html`
|
|
162
|
+
<!--
|
|
163
|
+
놓이는 보드.
|
|
164
|
+
|
|
165
|
+
이 묶음이 맨 위인 것은, 나머지가 「어떻게 보이나」인데 이것은 「무엇으로 보고
|
|
166
|
+
있나」이기 때문이다. 미리보기가 보드에 놓인 모습을 말하는 자리이므로, 어떤 보드를
|
|
167
|
+
가정하는지가 먼저 정해져야 그 다음이 뜻을 갖는다.
|
|
168
|
+
|
|
169
|
+
도형 탭의 「배치 기준」과 짝이지 같은 것이 아니다. 저쪽은 이 도형이 어느 면에
|
|
170
|
+
붙는가(저장된다), 이쪽은 그 면을 가진 보드가 어느 쪽을 기준으로 재는가(저장 안 된다).
|
|
171
|
+
-->
|
|
161
172
|
<section>
|
|
162
|
-
<h3>${i18next.t('figure.label.
|
|
173
|
+
<h3>${i18next.t('figure.label.assumed-board')}</h3>
|
|
163
174
|
<p quiet>${i18next.t('figure.text.view-settings-are-not-saved')}</p>
|
|
164
175
|
|
|
176
|
+
<label field>
|
|
177
|
+
<span>${i18next.t('figure.label.scene-reference')}</span>
|
|
178
|
+
<select
|
|
179
|
+
.value=${view.placement}
|
|
180
|
+
@change=${(e: Event) =>
|
|
181
|
+
this.put({ placement: (e.target as HTMLSelectElement).value as ViewSettings['placement'] })}
|
|
182
|
+
>
|
|
183
|
+
${SCENE_REFERENCES.map(
|
|
184
|
+
name => html`<option value=${name} ?selected=${view.placement === name}>
|
|
185
|
+
${i18next.t(`figure.label.scene-reference-${name}`)}
|
|
186
|
+
</option>`
|
|
187
|
+
)}
|
|
188
|
+
</select>
|
|
189
|
+
</label>
|
|
190
|
+
<p quiet>${i18next.t('figure.text.scene-reference-explained')}</p>
|
|
191
|
+
|
|
192
|
+
<label field>
|
|
193
|
+
<span>${i18next.t('figure.label.ceiling-height')}</span>
|
|
194
|
+
<input
|
|
195
|
+
type="number"
|
|
196
|
+
min="0"
|
|
197
|
+
step="100"
|
|
198
|
+
.value=${String(view.ceilingHeight)}
|
|
199
|
+
@change=${(e: Event) => this.put({ ceilingHeight: Number((e.target as HTMLInputElement).value) || 0 })}
|
|
200
|
+
/>
|
|
201
|
+
</label>
|
|
202
|
+
<p quiet>${i18next.t('figure.text.ceiling-height-explained')}</p>
|
|
203
|
+
</section>
|
|
204
|
+
|
|
205
|
+
<section>
|
|
206
|
+
<h3>${i18next.t('figure.label.guides')}</h3>
|
|
207
|
+
|
|
165
208
|
${this.toggle('grid', view.grid, i18next.t('figure.label.coordinate-ground'), i18next.t('figure.text.coordinate-ground-explained'))}
|
|
166
209
|
${this.toggle('gridLabels', view.gridLabels, i18next.t('figure.label.axis-labels'))}
|
|
167
210
|
${this.toggle('floor', view.floor, i18next.t('figure.label.scene-floor'), i18next.t('figure.text.floor-explained'))}
|
|
@@ -12,7 +12,7 @@ import { customElement, property, state } from 'lit/decorators.js'
|
|
|
12
12
|
import { i18next, localize } from '@operato/i18n'
|
|
13
13
|
import type { FigureSource } from '@hatiolab/figure-model'
|
|
14
14
|
|
|
15
|
-
import type {
|
|
15
|
+
import type { FigureDraft, PartModel } from './figure-source.js'
|
|
16
16
|
import { DEFAULT_VIEW } from './figure-view.js'
|
|
17
17
|
import type { ViewSettings } from './figure-view.js'
|
|
18
18
|
|
|
@@ -156,7 +156,7 @@ export class FigureSide extends localize(i18next)(LitElement) {
|
|
|
156
156
|
@property({ type: Number }) figureVersion = 0
|
|
157
157
|
@property({ type: Boolean }) dirty = false
|
|
158
158
|
|
|
159
|
-
@property({ type: Object })
|
|
159
|
+
@property({ type: Object }) draft?: FigureDraft
|
|
160
160
|
@property({ type: Array }) parts: PartModel[] = []
|
|
161
161
|
@property({ type: Object }) source?: FigureSource
|
|
162
162
|
@property({ type: Number }) selected = -1
|
|
@@ -175,7 +175,7 @@ export class FigureSide extends localize(i18next)(LitElement) {
|
|
|
175
175
|
* 0 이면 아무것도 안 붙인다 — 「0」을 붙이면 없는 것이 있는 것처럼 자리를 차지한다.
|
|
176
176
|
*/
|
|
177
177
|
private get clipCount(): number | undefined {
|
|
178
|
-
const n = this.
|
|
178
|
+
const n = (this.draft?.animations?.length ?? 0) + (this.draft?.parameters?.length ?? 0)
|
|
179
179
|
return n === 0 ? undefined : n
|
|
180
180
|
}
|
|
181
181
|
|
|
@@ -196,20 +196,20 @@ export class FigureSide extends localize(i18next)(LitElement) {
|
|
|
196
196
|
.figureName=${this.figureName}
|
|
197
197
|
.figureCategory=${this.figureCategory}
|
|
198
198
|
.figureTags=${this.figureTags}
|
|
199
|
-
.
|
|
199
|
+
.draft=${this.draft}
|
|
200
200
|
.parts=${this.parts}
|
|
201
201
|
.selected=${this.selected}
|
|
202
202
|
></figure-inspector>
|
|
203
203
|
|
|
204
204
|
<figure-capabilities
|
|
205
205
|
?off=${this.tab !== 'capability'}
|
|
206
|
-
.
|
|
206
|
+
.draft=${this.draft}
|
|
207
207
|
.parts=${this.parts}
|
|
208
208
|
></figure-capabilities>
|
|
209
209
|
|
|
210
210
|
<figure-animations
|
|
211
211
|
?off=${this.tab !== 'animation'}
|
|
212
|
-
.
|
|
212
|
+
.draft=${this.draft}
|
|
213
213
|
.parts=${this.parts}
|
|
214
214
|
></figure-animations>
|
|
215
215
|
|