@things-factory/figure-ui 10.1.22 → 10.1.23
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-canvas.ts +5 -5
- package/client/modeller/figure-inspector.ts +2 -2
- package/client/modeller/figure-preview.ts +7 -7
- package/client/modeller/figure-settings.ts +4 -4
- package/client/modeller/figure-thumbnail.ts +1 -1
- package/client/modeller/figure-view.ts +6 -6
- package/client/modeller/part-edits.ts +1 -1
- package/client/modeller/scene-budget.ts +1 -1
- package/client/pages/figure-list-page.ts +1 -1
- package/client/pages/figure-modeller-page.ts +4 -4
- package/client/types.ts +2 -2
- package/dist-client/modeller/figure-canvas.d.ts +1 -1
- package/dist-client/modeller/figure-canvas.js +5 -5
- package/dist-client/modeller/figure-canvas.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +1 -1
- package/dist-client/modeller/figure-inspector.js +2 -2
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +3 -3
- package/dist-client/modeller/figure-preview.js +5 -5
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-settings.js +4 -4
- package/dist-client/modeller/figure-settings.js.map +1 -1
- package/dist-client/modeller/figure-thumbnail.js +1 -1
- package/dist-client/modeller/figure-thumbnail.js.map +1 -1
- package/dist-client/modeller/figure-view.d.ts +6 -6
- package/dist-client/modeller/figure-view.js.map +1 -1
- package/dist-client/modeller/part-edits.js +1 -1
- package/dist-client/modeller/part-edits.js.map +1 -1
- package/dist-client/modeller/scene-budget.d.ts +1 -1
- package/dist-client/modeller/scene-budget.js +1 -1
- package/dist-client/modeller/scene-budget.js.map +1 -1
- package/dist-client/pages/figure-list-page.js +1 -1
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +3 -3
- package/dist-client/pages/figure-modeller-page.js +4 -4
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +2 -2
- package/dist-client/types.js.map +1 -1
- package/package.json +2 -2
- package/test/figure-source.test.ts +3 -3
- package/test/i18n-prefix-guard.test.ts +19 -1
- package/translations/en.json +25 -14
- package/translations/ja.json +8 -8
- package/translations/ko.json +13 -13
- package/translations/ms.json +8 -8
- package/translations/zh.json +8 -8
|
@@ -346,7 +346,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
346
346
|
/*
|
|
347
347
|
좌표 바탕을 씬에게 맡긴다. 우리가 만들어 얹었더니 2D 로 갔다 오면
|
|
348
348
|
사라졌다 — 그때 씬이 통째로 다시 서면서 새 THREE.Scene 을 만들기 때문이다.
|
|
349
|
-
|
|
349
|
+
도면은 이 값을 안 주므로 안 보인다.
|
|
350
350
|
*/
|
|
351
351
|
...this.sceneView(),
|
|
352
352
|
gridStep: gridStep(draft),
|
|
@@ -360,7 +360,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
360
360
|
fillStyle: PAPER,
|
|
361
361
|
components: this.parts.map(part => ({ ...part }))
|
|
362
362
|
},
|
|
363
|
-
// **편집 모드**로 만든다.
|
|
363
|
+
// **편집 모드**로 만든다. 도면의 3D 모델러가 그 모드에서 카메라 시점 버튼 ·
|
|
364
364
|
// 기즈모 모드 · 도움말 · 단축키를 띄운다.
|
|
365
365
|
mode: 1
|
|
366
366
|
}) as SceneHandle
|
|
@@ -409,7 +409,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
409
409
|
*
|
|
410
410
|
* 부품은 많아야 20개다. 매번 순회하는 값이 참조를 갖고 있다가 틀리는 값보다 훨씬 싸다.
|
|
411
411
|
*/
|
|
412
|
-
private
|
|
412
|
+
private partsOnCanvas(): Map<string, SceneComponent> {
|
|
413
413
|
const found = new Map<string, SceneComponent>()
|
|
414
414
|
for (const component of this.scene?.root?.components ?? []) {
|
|
415
415
|
const name = component.state?.name
|
|
@@ -454,7 +454,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
454
454
|
if (!root) return
|
|
455
455
|
|
|
456
456
|
const wanted = new Map(this.parts.map(part => [part.name, part]))
|
|
457
|
-
const standing = this.
|
|
457
|
+
const standing = this.partsOnCanvas()
|
|
458
458
|
|
|
459
459
|
// 없어진 것을 제거한다
|
|
460
460
|
for (const [name, component] of standing) {
|
|
@@ -532,7 +532,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
532
532
|
const root = draft?.root
|
|
533
533
|
if (!draft || !root) return
|
|
534
534
|
|
|
535
|
-
const component = this.picked ? this.
|
|
535
|
+
const component = this.picked ? this.partsOnCanvas().get(this.picked) : undefined
|
|
536
536
|
root.selected = component ? [component] : []
|
|
537
537
|
draft.invalidate?.()
|
|
538
538
|
}
|
|
@@ -453,7 +453,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
453
453
|
/*
|
|
454
454
|
정밀 제작 도구.
|
|
455
455
|
|
|
456
|
-
|
|
456
|
+
도면 편집기는 자산을 배치하는 곳이라 대충 놓아도 되지만, 여기는 자산을 만드는
|
|
457
457
|
곳이다. 「몸통 한가운데」·「바닥에 붙이기」·「반대쪽에도 하나」를 손으로 계산하면
|
|
458
458
|
늘 조금씩 어긋나고, 기준 치수를 바꾸면 전부 다시 해야 한다.
|
|
459
459
|
*/
|
|
@@ -944,7 +944,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
944
944
|
*
|
|
945
945
|
* ## 타입은 읽기만 한다
|
|
946
946
|
*
|
|
947
|
-
* 저장되는 식별자다. 배치된
|
|
947
|
+
* 저장되는 식별자다. 배치된 도면이 이 이름으로 타입을 찾으므로 만든 뒤에는 못 바꾼다.
|
|
948
948
|
*/
|
|
949
949
|
private renderFigure(compact = false) {
|
|
950
950
|
const fields = html`
|
|
@@ -10,7 +10,7 @@ import { DEFAULT_VIEW, sceneLook, WORKBENCH } from './figure-view.js'
|
|
|
10
10
|
import type { ViewSettings } from './figure-view.js'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* 저작 중인 것을
|
|
13
|
+
* 저작 중인 것을 **도면에 놓았을 때**의 모습.
|
|
14
14
|
*
|
|
15
15
|
* ## 편집 캔버스로는 볼 수 없다
|
|
16
16
|
*
|
|
@@ -18,7 +18,7 @@ import type { ViewSettings } from './figure-view.js'
|
|
|
18
18
|
* 값 절반은 **크기가 달라졌을 때 어떻게 반응하는가**에 있다. 기둥만 늘어나는 시그널타워,
|
|
19
19
|
* 롤러 개수가 느는 컨베이어는 기준 크기에서는 아무것도 보여 주지 못한다.
|
|
20
20
|
*
|
|
21
|
-
* 그래서 여기서는 저작한 것을 실제로 컴파일해 `FigureInstance` 로 세운다.
|
|
21
|
+
* 그래서 여기서는 저작한 것을 실제로 컴파일해 `FigureInstance` 로 세운다. 도면이 하는
|
|
22
22
|
* 일을 그대로 한다 — 청사진을 등록하고, 타입 이름만 적은 컴포넌트를 놓는다.
|
|
23
23
|
*
|
|
24
24
|
* ## 여러 크기를 한 번에 놓는다
|
|
@@ -62,7 +62,7 @@ export const PREVIEW_WAYS: readonly { name: string; axes: readonly ('x' | 'y' |
|
|
|
62
62
|
*
|
|
63
63
|
* 한동안 배치 기준을 높이로 옮기는 계산을 이 파일이 들고 있었다. 이제 씬이 한다 —
|
|
64
64
|
* `FigureRealObject.effectiveZPos` 가 청사진의 `placement` 와 레이어의 `heights` 로 정한다
|
|
65
|
-
* (ADR-0045, `operato-twin/design/04-decisions.md`).
|
|
65
|
+
* (ADR-0045, `operato-twin/design/04-decisions.md`). 도면이 그렇게 세우므로 「도면에 놓았을
|
|
66
66
|
* 때의 모습」을 말하는 이 화면도 그렇게 세워야 한다 — 여기서 `zPos` 를 적으면 **적은 값이
|
|
67
67
|
* 이겨서**, 씬이 무엇을 할지가 이 화면에서만 안 보인다.
|
|
68
68
|
*
|
|
@@ -173,7 +173,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
173
173
|
}
|
|
174
174
|
`
|
|
175
175
|
|
|
176
|
-
/** 저작 중인 것. 저장 형식으로 받는다 —
|
|
176
|
+
/** 저작 중인 것. 저장 형식으로 받는다 — 도면이 소비하는 것이 이 형식이다. */
|
|
177
177
|
@property({ type: Object }) source?: FigureSource
|
|
178
178
|
|
|
179
179
|
/**
|
|
@@ -374,8 +374,8 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
374
374
|
registerFigure(compile(source) as never)
|
|
375
375
|
|
|
376
376
|
/*
|
|
377
|
-
|
|
378
|
-
청사진도 원본도
|
|
377
|
+
도면이 하는 일을 그대로 한다. 컴포넌트에는 타입 이름과 자리·크기만 적는다 —
|
|
378
|
+
청사진도 원본도 도면에 들어가지 않는다.
|
|
379
379
|
|
|
380
380
|
가장 큰 것이 들어갈 만큼 판을 잡고, 세로로 줄을 나눠 나란히 놓는다.
|
|
381
381
|
*/
|
|
@@ -462,7 +462,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
462
462
|
placement: view.placement,
|
|
463
463
|
/*
|
|
464
464
|
부착면의 높이. 씬이 이 자리를 갖고(`real-object.ts` 의 `sceneHeights`), 천정 기반
|
|
465
|
-
도형이 여기서 제 높이를 얻는다.
|
|
465
|
+
도형이 여기서 제 높이를 얻는다. 도면에서는 현장이 이 값을 준다.
|
|
466
466
|
*/
|
|
467
467
|
heights: { floor: 0, ceiling: view.ceilingHeight },
|
|
468
468
|
pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),
|
|
@@ -160,17 +160,17 @@ export class FigureSettings extends localize(i18next)(LitElement) {
|
|
|
160
160
|
|
|
161
161
|
return html`
|
|
162
162
|
<!--
|
|
163
|
-
놓이는
|
|
163
|
+
놓이는 도면.
|
|
164
164
|
|
|
165
165
|
이 묶음이 맨 위인 것은, 나머지가 「어떻게 보이나」인데 이것은 「무엇으로 보고
|
|
166
|
-
있나」이기 때문이다. 미리보기가
|
|
166
|
+
있나」이기 때문이다. 미리보기가 도면에 놓인 모습을 말하는 자리이므로, 어떤 도면을
|
|
167
167
|
가정하는지가 먼저 정해져야 그 다음이 뜻을 갖는다.
|
|
168
168
|
|
|
169
169
|
도형 탭의 「배치 기준」과 짝이지 같은 것이 아니다. 저쪽은 이 도형이 어느 면에
|
|
170
|
-
붙는가(저장된다), 이쪽은 그 면을 가진
|
|
170
|
+
붙는가(저장된다), 이쪽은 그 면을 가진 도면이 어느 쪽을 기준으로 재는가(저장 안 된다).
|
|
171
171
|
-->
|
|
172
172
|
<section>
|
|
173
|
-
<h3>${i18next.t('figure.label.assumed-
|
|
173
|
+
<h3>${i18next.t('figure.label.assumed-drawing')}</h3>
|
|
174
174
|
<p quiet>${i18next.t('figure.text.view-settings-are-not-saved')}</p>
|
|
175
175
|
|
|
176
176
|
<label field>
|
|
@@ -19,7 +19,7 @@ import type { FigureDraft, PartModel } from './figure-source.js'
|
|
|
19
19
|
*
|
|
20
20
|
* ## 왜 서버가 아니라 브라우저인가
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* 도면은 저장할 때 서버가 찍는데, 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는 것이 자주
|
|
23
23
|
* 실패한다. 도형을 그릴 GPU 는 이미 보는 사람 손에 있다.
|
|
24
24
|
*/
|
|
25
25
|
|
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
*/
|
|
21
21
|
export interface ViewSettings {
|
|
22
22
|
/**
|
|
23
|
-
* 어떤
|
|
23
|
+
* 어떤 도면에 놓인 것으로 보나 — 씬 전체의 좌표 모드.
|
|
24
24
|
*
|
|
25
25
|
* **도형의 `placement` 와 다른 것이다.** 도형의 것은 「이 종류는 바닥에 서나, 천장에
|
|
26
|
-
* 매달리나」이고, 이것은 「지금 보고 있는
|
|
27
|
-
* 기준
|
|
26
|
+
* 매달리나」이고, 이것은 「지금 보고 있는 도면이 어느 쪽을 기준으로 재나」다. 바닥
|
|
27
|
+
* 기준 도면에 천정 기반 OHT 를 놓으면 도면은 안 뒤집히고 OHT 가 천장 높이에 매달린다.
|
|
28
28
|
*
|
|
29
29
|
* 한동안 도형의 `placement` 를 이 자리에 흘려 넣었다. 모델러에 도형이 하나뿐이라
|
|
30
|
-
* 그럴듯해 보였지만, 그러면 도형의 성질이 씬 전체를 뒤집는다 —
|
|
31
|
-
* 없고(
|
|
30
|
+
* 그럴듯해 보였지만, 그러면 도형의 성질이 씬 전체를 뒤집는다 — 도면에서는 성립할 수가
|
|
31
|
+
* 없고(도면에는 도형이 여럿이다), 저작 중에는 배치를 고를 때마다 저작자가 적어 둔
|
|
32
32
|
* 높이가 전부 다른 숫자로 보였다.
|
|
33
33
|
*
|
|
34
34
|
* 씬의 낱말 그대로다: `floor` 바닥 기준 · `space` 중심 기준(바닥 없음) · `inverted`
|
|
@@ -39,7 +39,7 @@ export interface ViewSettings {
|
|
|
39
39
|
/**
|
|
40
40
|
* 천장 높이 (mm). 천정 기반 도형을 미리보기에서 어디에 매달 것인가.
|
|
41
41
|
*
|
|
42
|
-
* 도형은 제가 놓일 방의 높이를 모른다.
|
|
42
|
+
* 도형은 제가 놓일 방의 높이를 모른다. 도면은 알지만 미리보기에는 도면이 없으므로,
|
|
43
43
|
* 저작자가 가정할 값을 여기서 정한다.
|
|
44
44
|
*/
|
|
45
45
|
ceilingHeight: number
|
|
@@ -117,7 +117,7 @@ export function updatePart(parts: PartModel[], index: number, part: PartModel):
|
|
|
117
117
|
/**
|
|
118
118
|
* 정밀 배치 도구 — 좌표를 손으로 계산하지 않게 한다.
|
|
119
119
|
*
|
|
120
|
-
*
|
|
120
|
+
* 도면 편집기는 자산을 **배치**하는 도구라 대략적인 위치로도 충분하다. 여기는 자산을
|
|
121
121
|
* **만드는** 도구라 다르다. 모터를 몸통 정중앙에 놓거나, 다리를 네 귀퉁이에 대칭으로
|
|
122
122
|
* 놓거나, 부품을 바닥에 정확히 붙이는 작업은 손으로 계산하면 늘 조금씩 어긋난다.
|
|
123
123
|
*/
|
|
@@ -19,7 +19,7 @@ import type { FigureCost } from '@hatiolab/figure-model'
|
|
|
19
19
|
* 삼각형 ≤ 50 만
|
|
20
20
|
*
|
|
21
21
|
* 그 문서가 스스로 이렇게 적어 두었다 — **「측정 없이 정한 목표다. 저작도구가 서면
|
|
22
|
-
* 실제
|
|
22
|
+
* 실제 도면으로 재고 조정한다」**(미결 D-20). 그래서 화면에도 근거를 함께 띄운다.
|
|
23
23
|
* 숫자만 크게 내걸면 재 본 값처럼 읽힌다.
|
|
24
24
|
*/
|
|
25
25
|
/**
|
|
@@ -102,7 +102,7 @@ function startingModel(type: string): { draft: FigureDraft; parts: PartModel[] }
|
|
|
102
102
|
* 오른쪽 판단 — 늘 떠 있다
|
|
103
103
|
*
|
|
104
104
|
* 미리보기가 씬 위에서 실제로 동작하는 것이 이 도구의 전제다. **편집한 것이 곧 실제 결과물**이라야
|
|
105
|
-
* "모델러에서는 되는데
|
|
105
|
+
* "모델러에서는 되는데 도면에서는 다르다"가 생기지 않는다.
|
|
106
106
|
*
|
|
107
107
|
* ## 편집 데이터는 씬 모델 하나뿐이다
|
|
108
108
|
*
|
|
@@ -978,7 +978,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
978
978
|
* ## 무엇을 보고 있나
|
|
979
979
|
*
|
|
980
980
|
* 편집과 미리보기는 같은 자리를 놓고 다투지 않는다. 둘 다 넓이가 필요하고, 동시에
|
|
981
|
-
* 봐야 할 일이 없다. 부품을 고치는 동안에는 편집만 보면 되고, 다 고친 뒤에
|
|
981
|
+
* 봐야 할 일이 없다. 부품을 고치는 동안에는 편집만 보면 되고, 다 고친 뒤에 도면에서
|
|
982
982
|
* 어떻게 되는지 보면 된다.
|
|
983
983
|
*
|
|
984
984
|
* 후보가 와 있을 때는 그 두 단추를 내지 않는다. 그때는 가운데가 이미 둘로 나뉘어
|
|
@@ -1666,7 +1666,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1666
1666
|
*
|
|
1667
1667
|
* ## 왜 여기인가
|
|
1668
1668
|
*
|
|
1669
|
-
*
|
|
1669
|
+
* 도면은 저장할 때 서버가 찍는다. 그런데 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는
|
|
1670
1670
|
* 것이 자주 실패한다. 도형을 그리는 GPU 는 이미 보는 사람 손에 있고, 씬은 지금 여기 서 있다.
|
|
1671
1671
|
*
|
|
1672
1672
|
* 저작면을 거치지 않고 생긴 것들이 그림이 없다 — 씨앗으로 넣은 표본, AI 가 낸 후보, 반입한 자산.
|
|
@@ -1810,7 +1810,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1810
1810
|
<div class="save-modal-backdrop" @click=${(e: Event) => { if (e.target === e.currentTarget) this.showSaveModal = false }}>
|
|
1811
1811
|
<div class="save-modal-card" role="dialog" aria-modal="true">
|
|
1812
1812
|
<h3 class="save-modal-title">${i18next.t('figure.title.save-new-figure', { defaultValue: '도형 초안 저장' })}</h3>
|
|
1813
|
-
<p class="save-modal-desc">${i18next.t('figure.text.save-new-figure-desc', { defaultValue: '
|
|
1813
|
+
<p class="save-modal-desc">${i18next.t('figure.text.save-new-figure-desc', { defaultValue: '도면과 캔버스에서 식별할 고유 타입 코드와 이름을 정합니다.' })}</p>
|
|
1814
1814
|
|
|
1815
1815
|
<div class="save-modal-field">
|
|
1816
1816
|
<label>${i18next.t('figure.label.figure-type', { defaultValue: '타입 코드 (영문 대문자)' })}</label>
|
package/client/types.ts
CHANGED
|
@@ -76,8 +76,8 @@ export interface NewFigure {
|
|
|
76
76
|
/**
|
|
77
77
|
* 수정.
|
|
78
78
|
*
|
|
79
|
-
* `type` 이 없다 — 저장되는 식별자라 고칠 수 없다. 배치된
|
|
80
|
-
* 찾으므로, 바꾸면 그
|
|
79
|
+
* `type` 이 없다 — 저장되는 식별자라 고칠 수 없다. 배치된 도면이 그 이름으로 타입을
|
|
80
|
+
* 찾으므로, 바꾸면 그 도면이 빈 화면이 된다.
|
|
81
81
|
*/
|
|
82
82
|
export interface FigurePatch {
|
|
83
83
|
name?: string
|
|
@@ -254,7 +254,7 @@ let FigureCanvas = class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
254
254
|
/*
|
|
255
255
|
좌표 바탕을 씬에게 맡긴다. 우리가 만들어 얹었더니 2D 로 갔다 오면
|
|
256
256
|
사라졌다 — 그때 씬이 통째로 다시 서면서 새 THREE.Scene 을 만들기 때문이다.
|
|
257
|
-
|
|
257
|
+
도면은 이 값을 안 주므로 안 보인다.
|
|
258
258
|
*/
|
|
259
259
|
...this.sceneView(),
|
|
260
260
|
gridStep: gridStep(draft),
|
|
@@ -268,7 +268,7 @@ let FigureCanvas = class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
268
268
|
fillStyle: PAPER,
|
|
269
269
|
components: this.parts.map(part => ({ ...part }))
|
|
270
270
|
},
|
|
271
|
-
// **편집 모드**로 만든다.
|
|
271
|
+
// **편집 모드**로 만든다. 도면의 3D 모델러가 그 모드에서 카메라 시점 버튼 ·
|
|
272
272
|
// 기즈모 모드 · 도움말 · 단축키를 띄운다.
|
|
273
273
|
mode: 1
|
|
274
274
|
});
|
|
@@ -316,7 +316,7 @@ let FigureCanvas = class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
316
316
|
*
|
|
317
317
|
* 부품은 많아야 20개다. 매번 순회하는 값이 참조를 갖고 있다가 틀리는 값보다 훨씬 싸다.
|
|
318
318
|
*/
|
|
319
|
-
|
|
319
|
+
partsOnCanvas() {
|
|
320
320
|
const found = new Map();
|
|
321
321
|
for (const component of this.scene?.root?.components ?? []) {
|
|
322
322
|
const name = component.state?.name;
|
|
@@ -359,7 +359,7 @@ let FigureCanvas = class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
359
359
|
if (!root)
|
|
360
360
|
return;
|
|
361
361
|
const wanted = new Map(this.parts.map(part => [part.name, part]));
|
|
362
|
-
const standing = this.
|
|
362
|
+
const standing = this.partsOnCanvas();
|
|
363
363
|
// 없어진 것을 제거한다
|
|
364
364
|
for (const [name, component] of standing) {
|
|
365
365
|
if (!wanted.has(name)) {
|
|
@@ -434,7 +434,7 @@ let FigureCanvas = class FigureCanvas extends localize(i18next)(LitElement) {
|
|
|
434
434
|
const root = draft?.root;
|
|
435
435
|
if (!draft || !root)
|
|
436
436
|
return;
|
|
437
|
-
const component = this.picked ? this.
|
|
437
|
+
const component = this.picked ? this.partsOnCanvas().get(this.picked) : undefined;
|
|
438
438
|
root.selected = component ? [component] : [];
|
|
439
439
|
draft.invalidate?.();
|
|
440
440
|
}
|
|
@@ -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, sceneLook, WORKBENCH } from './figure-view.js'\nimport type { ViewSettings } from './figure-view.js'\nimport type { FigureDraft, 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?: SceneLayer\n}\n\ninterface SceneLayer {\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 removeComponent?: (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 removeSelf?: (completely?: boolean) => void\n dispose?: () => 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: String }) figureId?: string\n @property({ type: Object }) draft?: FigureDraft\n /** 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다(`figure-view.ts`). */\n @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW\n @property({ type: Array }) parts: PartModel[] = []\n @property({ type: Number }) revision = 0\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 | (SceneLayer & {\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('revision')) this.standing = undefined\n if (changed.has('draft') || changed.has('parts') || changed.has('figureId') || changed.has('revision')) 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 draft = this.draft\n if (!draft) {\n this.teardown()\n this.cannot = i18next.t('figure.text.canvas-appears-once-a-part-exists')\n return\n }\n\n // 새 Figure의 빈 초안에서는 이전 자산의 씬을 반드시 버린다.\n if (this.parts.length === 0) {\n this.teardown()\n this.cannot = i18next.t('figure.text.canvas-appears-once-a-part-exists')\n return\n }\n\n const signature = `${this.figureId ?? ''}:${draft.width}x${draft.height}x${draft.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: draft.width,\n height: draft.height,\n depth: draft.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(draft),\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 return sceneLook(this.view ?? DEFAULT_VIEW)\n }\n\n /**\n * 설정이 바뀌면 **씬에 그대로 넣는다.** 다시 세우지 않는다.\n *\n * 씬이 이 값들에 스스로 반응한다(`three-capability` 의 `onchange`) — 조명 · 반사 ·\n * 바닥 · 좌표 바탕이 그 자리에서 바뀐다. 다시 세우면 고른 것과 카메라가 날아가서,\n * 밝기 하나 끄는 동안 화면이 계속 처음으로 돌아간다.\n */\n private applyView(): void {\n const draft = this.scene?.root\n if (!draft) return\n\n draft.set?.(this.sceneView())\n draft.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)) {\n root.removeComponent?.(component)\n component.dispose?.()\n }\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 draft = this.scene?.root\n const root = draft?.root\n if (!draft || !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 draft.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 draft = this.scene?.root\n const root = draft?.root\n if (!draft || !root) return\n\n const component = this.picked ? this.partsOnBoard().get(this.picked) : undefined\n root.selected = component ? [component] : []\n draft.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, sceneLook, WORKBENCH } from './figure-view.js'\nimport type { ViewSettings } from './figure-view.js'\nimport type { FigureDraft, 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?: SceneLayer\n}\n\ninterface SceneLayer {\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 removeComponent?: (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 removeSelf?: (completely?: boolean) => void\n dispose?: () => 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: String }) figureId?: string\n @property({ type: Object }) draft?: FigureDraft\n /** 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다(`figure-view.ts`). */\n @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW\n @property({ type: Array }) parts: PartModel[] = []\n @property({ type: Number }) revision = 0\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 | (SceneLayer & {\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('revision')) this.standing = undefined\n if (changed.has('draft') || changed.has('parts') || changed.has('figureId') || changed.has('revision')) 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 draft = this.draft\n if (!draft) {\n this.teardown()\n this.cannot = i18next.t('figure.text.canvas-appears-once-a-part-exists')\n return\n }\n\n // 새 Figure의 빈 초안에서는 이전 자산의 씬을 반드시 버린다.\n if (this.parts.length === 0) {\n this.teardown()\n this.cannot = i18next.t('figure.text.canvas-appears-once-a-part-exists')\n return\n }\n\n const signature = `${this.figureId ?? ''}:${draft.width}x${draft.height}x${draft.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: draft.width,\n height: draft.height,\n depth: draft.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(draft),\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 partsOnCanvas(): 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 return sceneLook(this.view ?? DEFAULT_VIEW)\n }\n\n /**\n * 설정이 바뀌면 **씬에 그대로 넣는다.** 다시 세우지 않는다.\n *\n * 씬이 이 값들에 스스로 반응한다(`three-capability` 의 `onchange`) — 조명 · 반사 ·\n * 바닥 · 좌표 바탕이 그 자리에서 바뀐다. 다시 세우면 고른 것과 카메라가 날아가서,\n * 밝기 하나 끄는 동안 화면이 계속 처음으로 돌아간다.\n */\n private applyView(): void {\n const draft = this.scene?.root\n if (!draft) return\n\n draft.set?.(this.sceneView())\n draft.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.partsOnCanvas()\n\n // 없어진 것을 제거한다\n for (const [name, component] of standing) {\n if (!wanted.has(name)) {\n root.removeComponent?.(component)\n component.dispose?.()\n }\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 draft = this.scene?.root\n const root = draft?.root\n if (!draft || !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 draft.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 draft = this.scene?.root\n const root = draft?.root\n if (!draft || !root) return\n\n const component = this.picked ? this.partsOnCanvas().get(this.picked) : undefined\n root.selected = component ? [component] : []\n draft.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"]}
|
|
@@ -450,7 +450,7 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
|
|
|
450
450
|
/*
|
|
451
451
|
정밀 제작 도구.
|
|
452
452
|
|
|
453
|
-
|
|
453
|
+
도면 편집기는 자산을 배치하는 곳이라 대충 놓아도 되지만, 여기는 자산을 만드는
|
|
454
454
|
곳이다. 「몸통 한가운데」·「바닥에 붙이기」·「반대쪽에도 하나」를 손으로 계산하면
|
|
455
455
|
늘 조금씩 어긋나고, 기준 치수를 바꾸면 전부 다시 해야 한다.
|
|
456
456
|
*/
|
|
@@ -914,7 +914,7 @@ let FigureInspector = class FigureInspector extends localize(i18next)(LitElement
|
|
|
914
914
|
*
|
|
915
915
|
* ## 타입은 읽기만 한다
|
|
916
916
|
*
|
|
917
|
-
* 저장되는 식별자다. 배치된
|
|
917
|
+
* 저장되는 식별자다. 배치된 도면이 이 이름으로 타입을 찾으므로 만든 뒤에는 못 바꾼다.
|
|
918
918
|
*/
|
|
919
919
|
renderFigure(compact = false) {
|
|
920
920
|
const fields = html `
|