@things-factory/figure-ui 10.1.3
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/bootstrap.ts +16 -0
- package/client/graphql/index.ts +209 -0
- package/client/index.ts +2 -0
- package/client/modeller/figure-ask.ts +247 -0
- package/client/modeller/figure-canvas.ts +482 -0
- package/client/modeller/figure-inspector.ts +1054 -0
- package/client/modeller/figure-parts.ts +479 -0
- package/client/modeller/figure-preview.ts +381 -0
- package/client/modeller/figure-report.ts +895 -0
- package/client/modeller/figure-settings.ts +285 -0
- package/client/modeller/figure-side.ts +162 -0
- package/client/modeller/figure-source.test.ts +336 -0
- package/client/modeller/figure-source.ts +338 -0
- package/client/modeller/figure-view.ts +79 -0
- package/client/modeller/maturity.ts +288 -0
- package/client/modeller/part-edits.test.ts +252 -0
- package/client/modeller/part-edits.ts +246 -0
- package/client/modeller/proposal.ts +142 -0
- package/client/modeller/scene-budget.ts +102 -0
- package/client/pages/figure-list-page.ts +274 -0
- package/client/pages/figure-modeller-page.ts +1067 -0
- package/client/route.ts +11 -0
- package/client/themes/brand.css +51 -0
- package/client/themes/dark.css +64 -0
- package/client/themes/light.css +95 -0
- package/client/tsconfig.json +19 -0
- package/client/types.ts +96 -0
- package/client/viewparts/figure-card.ts +226 -0
- package/dist-client/bootstrap.d.ts +14 -0
- package/dist-client/bootstrap.js +17 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/graphql/index.d.ts +41 -0
- package/dist-client/graphql/index.js +173 -0
- package/dist-client/graphql/index.js.map +1 -0
- package/dist-client/index.d.ts +2 -0
- package/dist-client/index.js +3 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/modeller/figure-ask.d.ts +36 -0
- package/dist-client/modeller/figure-ask.js +256 -0
- package/dist-client/modeller/figure-ask.js.map +1 -0
- package/dist-client/modeller/figure-canvas.d.ts +128 -0
- package/dist-client/modeller/figure-canvas.js +431 -0
- package/dist-client/modeller/figure-canvas.js.map +1 -0
- package/dist-client/modeller/figure-inspector.d.ts +149 -0
- package/dist-client/modeller/figure-inspector.js +991 -0
- package/dist-client/modeller/figure-inspector.js.map +1 -0
- package/dist-client/modeller/figure-parts.d.ts +64 -0
- package/dist-client/modeller/figure-parts.js +478 -0
- package/dist-client/modeller/figure-parts.js.map +1 -0
- package/dist-client/modeller/figure-preview.d.ts +62 -0
- package/dist-client/modeller/figure-preview.js +360 -0
- package/dist-client/modeller/figure-preview.js.map +1 -0
- package/dist-client/modeller/figure-report.d.ts +134 -0
- package/dist-client/modeller/figure-report.js +881 -0
- package/dist-client/modeller/figure-report.js.map +1 -0
- package/dist-client/modeller/figure-settings.d.ts +39 -0
- package/dist-client/modeller/figure-settings.js +277 -0
- package/dist-client/modeller/figure-settings.js.map +1 -0
- package/dist-client/modeller/figure-side.d.ts +41 -0
- package/dist-client/modeller/figure-side.js +175 -0
- package/dist-client/modeller/figure-side.js.map +1 -0
- package/dist-client/modeller/figure-source.d.ts +196 -0
- package/dist-client/modeller/figure-source.js +153 -0
- package/dist-client/modeller/figure-source.js.map +1 -0
- package/dist-client/modeller/figure-view.d.ts +57 -0
- package/dist-client/modeller/figure-view.js +36 -0
- package/dist-client/modeller/figure-view.js.map +1 -0
- package/dist-client/modeller/maturity.d.ts +55 -0
- package/dist-client/modeller/maturity.js +212 -0
- package/dist-client/modeller/maturity.js.map +1 -0
- package/dist-client/modeller/part-edits.d.ts +64 -0
- package/dist-client/modeller/part-edits.js +218 -0
- package/dist-client/modeller/part-edits.js.map +1 -0
- package/dist-client/modeller/proposal.d.ts +45 -0
- package/dist-client/modeller/proposal.js +96 -0
- package/dist-client/modeller/proposal.js.map +1 -0
- package/dist-client/modeller/scene-budget.d.ts +78 -0
- package/dist-client/modeller/scene-budget.js +80 -0
- package/dist-client/modeller/scene-budget.js.map +1 -0
- package/dist-client/pages/figure-list-page.d.ts +40 -0
- package/dist-client/pages/figure-list-page.js +280 -0
- package/dist-client/pages/figure-list-page.js.map +1 -0
- package/dist-client/pages/figure-modeller-page.d.ts +219 -0
- package/dist-client/pages/figure-modeller-page.js +1045 -0
- package/dist-client/pages/figure-modeller-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +11 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/themes/brand.css +51 -0
- package/dist-client/themes/dark.css +64 -0
- package/dist-client/themes/light.css +95 -0
- package/dist-client/types.d.ts +93 -0
- package/dist-client/types.js +2 -0
- package/dist-client/types.js.map +1 -0
- package/dist-client/viewparts/figure-card.d.ts +24 -0
- package/dist-client/viewparts/figure-card.js +221 -0
- package/dist-client/viewparts/figure-card.js.map +1 -0
- package/dist-server/index.d.ts +7 -0
- package/dist-server/index.js +3 -0
- package/dist-server/index.js.map +1 -0
- package/package.json +41 -0
- package/server/index.ts +7 -0
- package/server/tsconfig.json +18 -0
- package/test/brand-palette.test.ts +188 -0
- package/test/heading-scale-guard.test.ts +144 -0
- package/test/resolve-ts-siblings.mjs +67 -0
- package/things-factory.config.js +11 -0
- package/translations/en.json +247 -0
- package/translations/ja.json +247 -0
- package/translations/ko.json +247 -0
- package/translations/ms.json +247 -0
- package/translations/zh.json +247 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import { css, html, LitElement, unsafeCSS } from 'lit';
|
|
3
|
+
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
4
|
+
import { i18next, localize } from '@operato/i18n';
|
|
5
|
+
import { create, Model } from '@hatiolab/things-scene';
|
|
6
|
+
import { gridStep } from './part-edits.js';
|
|
7
|
+
import { DEFAULT_VIEW, WORKBENCH } from './figure-view.js';
|
|
8
|
+
/**
|
|
9
|
+
* 판의 종이 색 — 거의 흰색.
|
|
10
|
+
*
|
|
11
|
+
* 2D 편집 도구(손잡이 · 안내선 · 선택 테)가 어두운 색으로 그려진다. 종이가 어두우면
|
|
12
|
+
* 그것들이 안 보여 편집을 할 수 없다. 순백이 아닌 것은 판의 가장자리가 작업면과
|
|
13
|
+
* 구별되게 하려는 것이다.
|
|
14
|
+
*/
|
|
15
|
+
const PAPER = '#f4f4f4';
|
|
16
|
+
/**
|
|
17
|
+
* 달라진 값만 골라 낸다. 같은 것뿐이면 `null`.
|
|
18
|
+
*
|
|
19
|
+
* 값 비교는 JSON 으로 한다 — 경로(`path`)처럼 배열인 값이 있어서 얕은 비교로는 늘
|
|
20
|
+
* 다르다고 나온다.
|
|
21
|
+
*/
|
|
22
|
+
function changedKeys(now, wanted) {
|
|
23
|
+
const diff = {};
|
|
24
|
+
let any = false;
|
|
25
|
+
for (const [key, value] of Object.entries(wanted)) {
|
|
26
|
+
const before = now[key];
|
|
27
|
+
const same = before === value || (before !== undefined && JSON.stringify(before) === JSON.stringify(value));
|
|
28
|
+
if (!same) {
|
|
29
|
+
diff[key] = value;
|
|
30
|
+
any = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return any ? diff : null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 편집 캔버스 — 부품들을 보드 위에 세우고, 여기서 고치게 한다.
|
|
37
|
+
*
|
|
38
|
+
* ## 미리보기가 아니다
|
|
39
|
+
*
|
|
40
|
+
* 한동안 이름이 figure-preview 였다. 원본을 컴파일해 컴포넌트 하나로 세우던 시절에는
|
|
41
|
+
* 정말 보기만 하는 화면이었다. 부품을 컴포넌트로 바꾸면서 편집이 여기서 일어나게 되었다.
|
|
42
|
+
* 부품을 고르고, 2D 로 바꿔 편집하고, 기즈모로 끄는 일이 전부 이 안에서 일어난다.
|
|
43
|
+
* 이름만 남아 있었다.
|
|
44
|
+
*
|
|
45
|
+
* ## 부품이 컴포넌트다
|
|
46
|
+
*
|
|
47
|
+
* 한동안 원본 데이터를 컴파일해 **컴포넌트 하나**로 놓고, 그 안에서 부품을 찾아 다뤘다.
|
|
48
|
+
* 배포용 형태를 편집 화면에 그대로 쓴 것이다. 그래서 어느 부품이 선택됐나 · 무엇을 감쌀까 ·
|
|
49
|
+
* 무엇을 끌까를 전부 따로 뚫어야 했다.
|
|
50
|
+
*
|
|
51
|
+
* 이제 부품 하나가 컴포넌트 하나다. 선택 · 기즈모 · 윤곽선 · 다중 선택 · 되돌리기가
|
|
52
|
+
* 씬의 기본 동작으로 그냥 된다.
|
|
53
|
+
*
|
|
54
|
+
* ## 보드가 곧 기준 상자다
|
|
55
|
+
*
|
|
56
|
+
* 보드의 `width` · `height` · `depth` 가 `FigureSource.base` 다. 부품 좌표를 변환할 때
|
|
57
|
+
* 보드 중심만 빼면 된다 — `figure-source.ts` 가 그 일을 한다.
|
|
58
|
+
*
|
|
59
|
+
* ## 다시 세우지 않는다
|
|
60
|
+
*
|
|
61
|
+
* 부품이 바뀌면 씬을 통째로 다시 세우지 않고 **바뀐 컴포넌트만 고친다.** 다시 세우면
|
|
62
|
+
* 고른 것과 카메라가 날아가서, 숫자 하나 고칠 때마다 화면이 처음으로 돌아간다.
|
|
63
|
+
* 판 크기가 바뀔 때만 다시 세운다 — 판은 씬을 만들 때 정해진다.
|
|
64
|
+
*/
|
|
65
|
+
let FigureCanvas = class FigureCanvas extends localize(i18next)(LitElement) {
|
|
66
|
+
constructor() {
|
|
67
|
+
super(...arguments);
|
|
68
|
+
/** 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다(`figure-view.ts`). */
|
|
69
|
+
this.view = DEFAULT_VIEW;
|
|
70
|
+
this.parts = [];
|
|
71
|
+
/**
|
|
72
|
+
* 보기 전용 — 씬에서 일어난 일을 밖에 알리지 않는다.
|
|
73
|
+
*
|
|
74
|
+
* AI 후보를 나란히 세울 때 쓴다. 후보는 아직 편집 대상이 아닌데, 씬은 컴포넌트를
|
|
75
|
+
* 만들면서 상태를 정리하고 그때 변경 이벤트를 낸다. 그 이벤트가 그대로 올라가면
|
|
76
|
+
* **후보의 값이 편집 중인 데이터를 덮어쓴다** — 실제로 그랬다. 받지 않기로 한 부품
|
|
77
|
+
* 위치가 오류보고없이 반영되어 있었고, 후보를 버려도 되돌아오지 않았다.
|
|
78
|
+
*/
|
|
79
|
+
this.viewOnly = false;
|
|
80
|
+
/** 세울 수 없는 사유. 없으면 세운 것이다. */
|
|
81
|
+
this.cannot = '';
|
|
82
|
+
}
|
|
83
|
+
static { this.styles = css `
|
|
84
|
+
/*
|
|
85
|
+
판 바깥을 우리가 칠한다.
|
|
86
|
+
|
|
87
|
+
무대에는 셋이 있다 — 판의 면(2D 는 종이, 3D 는 바닥판), 판 바깥, 좌표 바탕.
|
|
88
|
+
어느 것도 원본 데이터에 저장되지 않는다. 보려고 만든 것이므로 편집하기 좋게 잡는다.
|
|
89
|
+
|
|
90
|
+
2D 에는 바깥을 칠하는 사람이 없다. 씬의 2D 는 캔버스를 투명으로 지우고
|
|
91
|
+
끝이라(layer.ts 의 clearRect), 뒤에 있는 것이 그대로 비친다. 테마 토큰을
|
|
92
|
+
깔아 두었더니 어두운 데서 까맣게 나왔다.
|
|
93
|
+
|
|
94
|
+
3D 하늘과 같은 값을 쓴다. 갈리면 2D 로 갔을 때 바탕색이 바뀐다.
|
|
95
|
+
*/
|
|
96
|
+
:host {
|
|
97
|
+
position: relative;
|
|
98
|
+
display: flex;
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
background-color: ${unsafeCSS(WORKBENCH)};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/*
|
|
104
|
+
씬이 이 칸을 자기 것으로 삼으면서 인라인 스타일로 position: relative 를 박는다.
|
|
105
|
+
|
|
106
|
+
그러면 우리가 CSS 로 준 position: absolute 가 밀리고, 같이 준 inset: 0 도 함께 뜻을
|
|
107
|
+
잃는다. relative 인 블록은 상위 배치에서 크기를 받는데, 자식(캔버스)이 전부 절대 배치라
|
|
108
|
+
내용 폭이 0 이다. 그래서 폭 0 짜리 칸이 되고 캔버스도 폭 0 이 된다.
|
|
109
|
+
|
|
110
|
+
화면은 그냥 회색으로 남는다. 게다가 마우스 좌표를 NDC 로 바꿀 때 폭으로 나누므로
|
|
111
|
+
값이 무한대가 되어 기즈모를 집을 수 없다 — "기즈모가 안 잡힌다"의 실제 원인이
|
|
112
|
+
이것이었다.
|
|
113
|
+
|
|
114
|
+
그래서 width 와 height 를 직접 준다. 인라인으로 position 이 무엇으로 바뀌든 칸은
|
|
115
|
+
부모를 채운다.
|
|
116
|
+
*/
|
|
117
|
+
div[stage] {
|
|
118
|
+
position: absolute;
|
|
119
|
+
inset: 0;
|
|
120
|
+
width: 100%;
|
|
121
|
+
height: 100%;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* 세울 수 없는 상태를 빈 화면으로 두지 않는다 — 왜 안 보이는지 말한다 */
|
|
125
|
+
div[cannot] {
|
|
126
|
+
position: absolute;
|
|
127
|
+
inset: 0;
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
justify-content: center;
|
|
131
|
+
padding: var(--spacing-large, 12px);
|
|
132
|
+
text-align: center;
|
|
133
|
+
font: var(--label-font, inherit);
|
|
134
|
+
line-height: 1.6;
|
|
135
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
136
|
+
background-color: ${unsafeCSS(WORKBENCH)};
|
|
137
|
+
}
|
|
138
|
+
`; }
|
|
139
|
+
render() {
|
|
140
|
+
return html `
|
|
141
|
+
<div stage></div>
|
|
142
|
+
${this.cannot ? html `<div cannot>${this.cannot}</div>` : ''}
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
willUpdate(changed) {
|
|
146
|
+
if (!this.hasUpdated)
|
|
147
|
+
return;
|
|
148
|
+
if (changed.has('board') || changed.has('parts'))
|
|
149
|
+
this.standUp();
|
|
150
|
+
if (changed.has('view'))
|
|
151
|
+
this.applyView();
|
|
152
|
+
if (changed.has('picked'))
|
|
153
|
+
this.showPicked();
|
|
154
|
+
}
|
|
155
|
+
firstUpdated() {
|
|
156
|
+
this.standUp();
|
|
157
|
+
// 씬은 세울 때의 칸 크기를 읽는다. 저작면이 아직 배치되기 전이면 0x0 으로 굳어
|
|
158
|
+
// **캔버스는 붙었는데 아무것도 안 보이는** 상태가 된다 — 실제로 그렇게 났다.
|
|
159
|
+
this.watcher = new ResizeObserver(() => this.scene?.resize?.());
|
|
160
|
+
this.watcher.observe(this);
|
|
161
|
+
}
|
|
162
|
+
disconnectedCallback() {
|
|
163
|
+
super.disconnectedCallback();
|
|
164
|
+
this.watcher?.disconnect();
|
|
165
|
+
this.watcher = undefined;
|
|
166
|
+
this.teardown();
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 받은 것을 씬에 반영한다.
|
|
170
|
+
*
|
|
171
|
+
* **판이 그대로면 다시 세우지 않는다.** 다시 세우면 고른 것과 카메라가 날아가서,
|
|
172
|
+
* 숫자 하나 고칠 때마다 화면이 처음으로 돌아간다.
|
|
173
|
+
*/
|
|
174
|
+
standUp() {
|
|
175
|
+
const board = this.board;
|
|
176
|
+
if (!board) {
|
|
177
|
+
this.cannot = i18next.t('text.canvas-appears-once-a-part-exists');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const signature = `${board.width}x${board.height}x${board.depth}`;
|
|
181
|
+
if (this.scene && this.standing === signature) {
|
|
182
|
+
this.syncParts();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.teardown();
|
|
186
|
+
try {
|
|
187
|
+
this.scene = create({
|
|
188
|
+
target: this.stage,
|
|
189
|
+
model: {
|
|
190
|
+
type: 'model-layer',
|
|
191
|
+
width: board.width,
|
|
192
|
+
height: board.height,
|
|
193
|
+
depth: board.depth,
|
|
194
|
+
threed: true,
|
|
195
|
+
environment: 'studio',
|
|
196
|
+
/*
|
|
197
|
+
표본을 화면보다 더 쓴다. MSAA 만으로는 실루엣의 계단이 남는다 — 픽셀
|
|
198
|
+
비율만 올려 보니 눈에 띄게 매끄러워졌다. 저작면은 작은 칸에 자산 하나라
|
|
199
|
+
그릴 것이 적으니 표본을 더 써도 된다.
|
|
200
|
+
*/
|
|
201
|
+
pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),
|
|
202
|
+
/*
|
|
203
|
+
좌표 바탕을 씬에게 맡긴다. 우리가 만들어 얹었더니 2D 로 갔다 오면
|
|
204
|
+
사라졌다 — 그때 씬이 통째로 다시 서면서 새 THREE.Scene 을 만들기 때문이다.
|
|
205
|
+
보드는 이 값을 안 주므로 안 보인다.
|
|
206
|
+
*/
|
|
207
|
+
...this.sceneView(),
|
|
208
|
+
gridStep: gridStep(board),
|
|
209
|
+
sky: 'color',
|
|
210
|
+
/*
|
|
211
|
+
판의 면 — 2D 는 밝은 종이, 3D 는 없음.
|
|
212
|
+
|
|
213
|
+
종이는 밝아야 한다: 2D 편집 손잡이와 안내선이 어두운 색이다. 바닥판은
|
|
214
|
+
없어야 한다: 좌표 모눈과 겹쳐 싸우고 원점 아래쪽을 가린다.
|
|
215
|
+
*/
|
|
216
|
+
fillStyle: PAPER,
|
|
217
|
+
components: this.parts.map(part => ({ ...part }))
|
|
218
|
+
},
|
|
219
|
+
// **편집 모드**로 만든다. 보드의 3D 모델러가 그 모드에서 카메라 시점 버튼 ·
|
|
220
|
+
// 기즈모 모드 · 도움말 · 단축키를 띄운다.
|
|
221
|
+
mode: 1
|
|
222
|
+
});
|
|
223
|
+
this.standing = signature;
|
|
224
|
+
this.watchScene();
|
|
225
|
+
this.scene.resize?.();
|
|
226
|
+
/*
|
|
227
|
+
열자마자 모델 전체가 보이게 맞춘다.
|
|
228
|
+
|
|
229
|
+
카메라는 2D 뷰포트에서 만들어진다. 그런데 그 뷰포트의 처음 배율은 컨테이너
|
|
230
|
+
크기에서 오지 모델 크기에서 오지 않는다. 그래서 가로로 긴 모델(컨베이어가
|
|
231
|
+
그렇다)을 열면 화면 밖으로 나가 벨트 한 조각만 보였다.
|
|
232
|
+
|
|
233
|
+
**다시 세울 때만 맞춘다.** 창 크기가 바뀔 때마다 맞추면 사용자가 돌려 둔 시점과
|
|
234
|
+
확대가 매번 되돌아간다.
|
|
235
|
+
*/
|
|
236
|
+
this.scene.fit?.('ratio');
|
|
237
|
+
/*
|
|
238
|
+
3D 카메라는 2D 쪽 fit 으로 안 움직인다. 처음 3D 로 설 때 모델 크기를 보지 않기
|
|
239
|
+
때문이다. 씬이 내주는 자리로 따로 맞춘다.
|
|
240
|
+
|
|
241
|
+
`?.` 로 부르는 것은 배포된 판에 아직 없을 수 있어서다. 없으면 2D 만 맞고 3D 는
|
|
242
|
+
기본 시점으로 선다 — 여기서 카메라 계산을 흉내 내지 않는다.
|
|
243
|
+
*/
|
|
244
|
+
this.scene.root?.fitCameraToModel?.();
|
|
245
|
+
this.cannot = '';
|
|
246
|
+
}
|
|
247
|
+
catch (e) {
|
|
248
|
+
// 원인을 지어내지 않는다 — 던진 말을 그대로 보여 준다.
|
|
249
|
+
this.cannot = e.message;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* 지금 판 위에 있는 부품 컴포넌트를 이름으로 찾는다.
|
|
254
|
+
*
|
|
255
|
+
* **참조를 들고 있지 않는다.** 전에는 세울 때 한 번 색인해 두고 계속 그 Map 을 썼다.
|
|
256
|
+
* 그런데 씬은 우리 모르게 판을 다시 세울 때가 있다 — 2D 와 3D 를 오갈 때가 그렇다.
|
|
257
|
+
* 그러면 Map 안의 것들은 부모가 없는 죽은 객체가 되고, 그 뒤로는
|
|
258
|
+
*
|
|
259
|
+
* - 목록에서 골라도 선택이 안 된다. 루트의 `selected` 게터가 부모 없는 것을
|
|
260
|
+
* 걸러 내므로 빈 배열이 되고, 테두리도 기즈모도 붙지 않는다
|
|
261
|
+
* - 속성을 고쳐도 3D 가 안 따라온다. 죽은 객체에 값을 넣기 때문이다
|
|
262
|
+
*
|
|
263
|
+
* 둘 다 오류보고없이 일어난다. 실제로 화면에서 「기즈모가 안 잡힌다」로 나타났다.
|
|
264
|
+
*
|
|
265
|
+
* 부품은 많아야 20개다. 매번 순회하는 값이 참조를 갖고 있다가 틀리는 값보다 훨씬 싸다.
|
|
266
|
+
*/
|
|
267
|
+
partsOnBoard() {
|
|
268
|
+
const found = new Map();
|
|
269
|
+
for (const component of this.scene?.root?.components ?? []) {
|
|
270
|
+
const name = component.state?.name;
|
|
271
|
+
if (typeof name === 'string')
|
|
272
|
+
found.set(name, component);
|
|
273
|
+
}
|
|
274
|
+
return found;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* 보기 설정을 씬이 아는 이름으로 낸다.
|
|
278
|
+
*
|
|
279
|
+
* 옮겨 적는 표를 두지 않는다 — `ViewSettings` 의 이름이 곧 `model-layer` 의 상태
|
|
280
|
+
* 이름이다. 표를 두면 씬이 새 설정을 받아도 이쪽이 모른다.
|
|
281
|
+
*/
|
|
282
|
+
sceneView() {
|
|
283
|
+
const { grid, gridLabels, floor, environment, skyColor, ...light } = this.view ?? DEFAULT_VIEW;
|
|
284
|
+
return { grid, gridLabels, floor, environment, skyColor, ...light };
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* 설정이 바뀌면 **씬에 그대로 넣는다.** 다시 세우지 않는다.
|
|
288
|
+
*
|
|
289
|
+
* 씬이 이 값들에 스스로 반응한다(`three-capability` 의 `onchange`) — 조명 · 반사 ·
|
|
290
|
+
* 바닥 · 좌표 바탕이 그 자리에서 바뀐다. 다시 세우면 고른 것과 카메라가 날아가서,
|
|
291
|
+
* 밝기 하나 끄는 동안 화면이 계속 처음으로 돌아간다.
|
|
292
|
+
*/
|
|
293
|
+
applyView() {
|
|
294
|
+
const board = this.scene?.root;
|
|
295
|
+
if (!board)
|
|
296
|
+
return;
|
|
297
|
+
board.set?.(this.sceneView());
|
|
298
|
+
board.invalidate?.();
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* 부품 목록을 씬에 맞춘다. **다시 세우지 않고 바뀐 것만 고친다.**
|
|
302
|
+
*
|
|
303
|
+
* 다시 세우면 고른 것과 카메라가 날아가서, 숫자 하나 고칠 때마다 화면이 처음으로
|
|
304
|
+
* 돌아간다.
|
|
305
|
+
*/
|
|
306
|
+
syncParts() {
|
|
307
|
+
const root = this.scene?.root;
|
|
308
|
+
if (!root)
|
|
309
|
+
return;
|
|
310
|
+
const wanted = new Map(this.parts.map(part => [part.name, part]));
|
|
311
|
+
const standing = this.partsOnBoard();
|
|
312
|
+
// 없어진 것을 제거한다
|
|
313
|
+
for (const [name, component] of standing) {
|
|
314
|
+
if (!wanted.has(name))
|
|
315
|
+
component.remove?.();
|
|
316
|
+
}
|
|
317
|
+
for (const part of this.parts) {
|
|
318
|
+
const found = standing.get(part.name);
|
|
319
|
+
if (found) {
|
|
320
|
+
/*
|
|
321
|
+
**달라진 값만 넣는다.**
|
|
322
|
+
|
|
323
|
+
같은 값을 다시 넣어도 씬은 변경 이벤트를 낸다. 그 이벤트가 다시 편집 데이터를
|
|
324
|
+
만들고, 그것이 다시 여기로 돌아오면 끝없이 반복된다. 기즈모를 끄는 중이라면 그
|
|
325
|
+
되먹임이 끌기와 싸운다.
|
|
326
|
+
*/
|
|
327
|
+
const diff = changedKeys(found.state, part);
|
|
328
|
+
if (diff)
|
|
329
|
+
found.set(diff);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
// 새 부품. 씬의 공장으로 만들어 판에 붙인다.
|
|
333
|
+
const made = Model.compile({ ...part }, root.app);
|
|
334
|
+
if (made)
|
|
335
|
+
root.addComponent?.(made);
|
|
336
|
+
}
|
|
337
|
+
root.invalidate?.();
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* 씬에서 일어난 일을 밖에 알린다.
|
|
341
|
+
*
|
|
342
|
+
* **자리가 아니라 이름으로 알린다.** 목록의 몇 번째인지로 알리면, 알림이 오는 사이에
|
|
343
|
+
* 목록이 바뀌면 엉뚱한 부품을 가리킨다.
|
|
344
|
+
*/
|
|
345
|
+
watchScene() {
|
|
346
|
+
const board = this.scene?.root;
|
|
347
|
+
const root = board?.root;
|
|
348
|
+
if (!board || !root?.on || this.viewOnly)
|
|
349
|
+
return;
|
|
350
|
+
root.on('selected', (after) => {
|
|
351
|
+
const chosen = after?.[0];
|
|
352
|
+
const name = chosen?.state?.name;
|
|
353
|
+
if (typeof name === 'string')
|
|
354
|
+
this.tell('pick-part', { name });
|
|
355
|
+
});
|
|
356
|
+
/*
|
|
357
|
+
기즈모로 끌거나 2D 에서 옮기면 컴포넌트 상태가 바뀐다. 그것을 그대로 올려 보낸다 —
|
|
358
|
+
씬이 쓰는 용어 그대로다. 저장 형식으로 옮기는 일은 변환 함수가 한다.
|
|
359
|
+
|
|
360
|
+
**바뀜은 판으로 올라오고, 바뀐 컴포넌트는 끝 인자의 `origin` 에 있다.** 자식이
|
|
361
|
+
`trigger('change', after, before)` 를 하면 씬이 `{ origin, name, deliverer }` 를
|
|
362
|
+
뒤에 붙여 부모에게 넘긴다. 첫 인자를 컴포넌트로 읽으면 아무 일도 안 일어난다 —
|
|
363
|
+
실제로 그랬다.
|
|
364
|
+
*/
|
|
365
|
+
board.on?.('change', (...args) => {
|
|
366
|
+
const hint = args[args.length - 1];
|
|
367
|
+
const changed = hint?.origin;
|
|
368
|
+
const name = changed?.state?.name;
|
|
369
|
+
if (typeof name !== 'string')
|
|
370
|
+
return;
|
|
371
|
+
const was = this.parts.find(part => part.name === name);
|
|
372
|
+
if (!was)
|
|
373
|
+
return;
|
|
374
|
+
// 씬 모델 그대로 올려 보낸다. 모델러 페이지가 갖고 있는 것도 씬 모델이라 변환할 것이 없다.
|
|
375
|
+
this.tell('part-changed', { name, part: { ...was, ...changed.state } });
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
/** 목록에서 고른 것을 캔버스의 선택에 맞춘다. */
|
|
379
|
+
showPicked() {
|
|
380
|
+
const board = this.scene?.root;
|
|
381
|
+
const root = board?.root;
|
|
382
|
+
if (!board || !root)
|
|
383
|
+
return;
|
|
384
|
+
const component = this.picked ? this.partsOnBoard().get(this.picked) : undefined;
|
|
385
|
+
root.selected = component ? [component] : [];
|
|
386
|
+
board.invalidate?.();
|
|
387
|
+
}
|
|
388
|
+
tell(what, detail) {
|
|
389
|
+
this.dispatchEvent(new CustomEvent(what, { detail, bubbles: true, composed: true }));
|
|
390
|
+
}
|
|
391
|
+
teardown() {
|
|
392
|
+
this.scene?.dispose?.();
|
|
393
|
+
this.scene = undefined;
|
|
394
|
+
this.standing = undefined;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
__decorate([
|
|
398
|
+
property({ type: Object }),
|
|
399
|
+
__metadata("design:type", Object)
|
|
400
|
+
], FigureCanvas.prototype, "board", void 0);
|
|
401
|
+
__decorate([
|
|
402
|
+
property({ type: Object }),
|
|
403
|
+
__metadata("design:type", Object)
|
|
404
|
+
], FigureCanvas.prototype, "view", void 0);
|
|
405
|
+
__decorate([
|
|
406
|
+
property({ type: Array }),
|
|
407
|
+
__metadata("design:type", Array)
|
|
408
|
+
], FigureCanvas.prototype, "parts", void 0);
|
|
409
|
+
__decorate([
|
|
410
|
+
property({ type: String }),
|
|
411
|
+
__metadata("design:type", String)
|
|
412
|
+
], FigureCanvas.prototype, "picked", void 0);
|
|
413
|
+
__decorate([
|
|
414
|
+
property({ type: Boolean }),
|
|
415
|
+
__metadata("design:type", Object)
|
|
416
|
+
], FigureCanvas.prototype, "viewOnly", void 0);
|
|
417
|
+
__decorate([
|
|
418
|
+
query('div[stage]'),
|
|
419
|
+
__metadata("design:type", HTMLDivElement
|
|
420
|
+
/** 세울 수 없는 사유. 없으면 세운 것이다. */
|
|
421
|
+
)
|
|
422
|
+
], FigureCanvas.prototype, "stage", void 0);
|
|
423
|
+
__decorate([
|
|
424
|
+
state(),
|
|
425
|
+
__metadata("design:type", Object)
|
|
426
|
+
], FigureCanvas.prototype, "cannot", void 0);
|
|
427
|
+
FigureCanvas = __decorate([
|
|
428
|
+
customElement('figure-canvas')
|
|
429
|
+
], FigureCanvas);
|
|
430
|
+
export { FigureCanvas };
|
|
431
|
+
//# sourceMappingURL=figure-canvas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figure-canvas.js","sourceRoot":"","sources":["../../client/modeller/figure-canvas.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { create, Model } from '@hatiolab/things-scene'\n\nimport { gridStep } from './part-edits.js'\nimport { DEFAULT_VIEW, WORKBENCH } from './figure-view.js'\nimport type { ViewSettings } from './figure-view.js'\nimport type { BoardModel, PartModel } from './figure-source.js'\n\n/**\n * 판의 종이 색 — 거의 흰색.\n *\n * 2D 편집 도구(손잡이 · 안내선 · 선택 테)가 어두운 색으로 그려진다. 종이가 어두우면\n * 그것들이 안 보여 편집을 할 수 없다. 순백이 아닌 것은 판의 가장자리가 작업면과\n * 구별되게 하려는 것이다.\n */\nconst PAPER = '#f4f4f4'\n\n/**\n * 달라진 값만 골라 낸다. 같은 것뿐이면 `null`.\n *\n * 값 비교는 JSON 으로 한다 — 경로(`path`)처럼 배열인 값이 있어서 얕은 비교로는 늘\n * 다르다고 나온다.\n */\nfunction changedKeys(\n now: Record<string, unknown>,\n wanted: Record<string, unknown>\n): Record<string, unknown> | null {\n const diff: Record<string, unknown> = {}\n let any = false\n\n for (const [key, value] of Object.entries(wanted)) {\n const before = now[key]\n const same =\n before === value || (before !== undefined && JSON.stringify(before) === JSON.stringify(value))\n if (!same) {\n diff[key] = value\n any = true\n }\n }\n\n return any ? diff : null\n}\n\n/** 씬이 돌려주는 것 중 우리가 쓰는 부분. */\ninterface SceneHandle {\n dispose?: () => void\n resize?: () => void\n /** 모델 전체가 보이게 화면에 맞춘다. 'ratio' 는 비율을 지키며 맞춘다. */\n fit?: (type?: string) => void\n /**\n * **판**이다 — 루트가 아니다.\n *\n * `create()` 가 돌려주는 `root` 는 `model-layer` 이고, 그 위에 진짜 루트가 하나 더\n * 있다. 부품은 판에 붙고, 선택 알림은 루트가 낸다. 둘을 섞으면 알림이 안 온다 —\n * 실제로 그랬다.\n */\n root?: SceneBoard\n}\n\ninterface SceneBoard {\n invalidate?: () => void\n /** 보기 설정을 넣는 자리. 씬이 스스로 반응한다. */\n set?: (patch: Record<string, unknown>) => void\n /** 3D 카메라를 모델 전체가 보이게 맞춘다. 10.1.4 부터 있다. */\n fitCameraToModel?: (margin?: number) => void\n components?: SceneComponent[]\n selected?: SceneComponent[]\n app?: unknown\n addComponent?: (component: unknown) => void\n on?: (event: string, handler: (...args: unknown[]) => void) => void\n /** 판 위의 루트. 선택 알림이 여기서 난다. */\n root?: {\n selected?: SceneComponent[]\n on?: (event: string, handler: (...args: unknown[]) => void) => void\n }\n}\n\ninterface SceneComponent {\n state: Record<string, unknown>\n set: (props: Record<string, unknown> | string, value?: unknown) => void\n on?: (event: string, handler: (...args: unknown[]) => void) => void\n remove?: () => void\n}\n\n/**\n * 편집 캔버스 — 부품들을 보드 위에 세우고, 여기서 고치게 한다.\n *\n * ## 미리보기가 아니다\n *\n * 한동안 이름이 figure-preview 였다. 원본을 컴파일해 컴포넌트 하나로 세우던 시절에는\n * 정말 보기만 하는 화면이었다. 부품을 컴포넌트로 바꾸면서 편집이 여기서 일어나게 되었다.\n * 부품을 고르고, 2D 로 바꿔 편집하고, 기즈모로 끄는 일이 전부 이 안에서 일어난다.\n * 이름만 남아 있었다.\n *\n * ## 부품이 컴포넌트다\n *\n * 한동안 원본 데이터를 컴파일해 **컴포넌트 하나**로 놓고, 그 안에서 부품을 찾아 다뤘다.\n * 배포용 형태를 편집 화면에 그대로 쓴 것이다. 그래서 어느 부품이 선택됐나 · 무엇을 감쌀까 ·\n * 무엇을 끌까를 전부 따로 뚫어야 했다.\n *\n * 이제 부품 하나가 컴포넌트 하나다. 선택 · 기즈모 · 윤곽선 · 다중 선택 · 되돌리기가\n * 씬의 기본 동작으로 그냥 된다.\n *\n * ## 보드가 곧 기준 상자다\n *\n * 보드의 `width` · `height` · `depth` 가 `FigureSource.base` 다. 부품 좌표를 변환할 때\n * 보드 중심만 빼면 된다 — `figure-source.ts` 가 그 일을 한다.\n *\n * ## 다시 세우지 않는다\n *\n * 부품이 바뀌면 씬을 통째로 다시 세우지 않고 **바뀐 컴포넌트만 고친다.** 다시 세우면\n * 고른 것과 카메라가 날아가서, 숫자 하나 고칠 때마다 화면이 처음으로 돌아간다.\n * 판 크기가 바뀔 때만 다시 세운다 — 판은 씬을 만들 때 정해진다.\n */\n@customElement('figure-canvas')\nexport class FigureCanvas extends localize(i18next)(LitElement) {\n static styles = css`\n /*\n 판 바깥을 우리가 칠한다.\n\n 무대에는 셋이 있다 — 판의 면(2D 는 종이, 3D 는 바닥판), 판 바깥, 좌표 바탕.\n 어느 것도 원본 데이터에 저장되지 않는다. 보려고 만든 것이므로 편집하기 좋게 잡는다.\n\n 2D 에는 바깥을 칠하는 사람이 없다. 씬의 2D 는 캔버스를 투명으로 지우고\n 끝이라(layer.ts 의 clearRect), 뒤에 있는 것이 그대로 비친다. 테마 토큰을\n 깔아 두었더니 어두운 데서 까맣게 나왔다.\n\n 3D 하늘과 같은 값을 쓴다. 갈리면 2D 로 갔을 때 바탕색이 바뀐다.\n */\n :host {\n position: relative;\n display: flex;\n overflow: hidden;\n background-color: ${unsafeCSS(WORKBENCH)};\n }\n\n /*\n 씬이 이 칸을 자기 것으로 삼으면서 인라인 스타일로 position: relative 를 박는다.\n\n 그러면 우리가 CSS 로 준 position: absolute 가 밀리고, 같이 준 inset: 0 도 함께 뜻을\n 잃는다. relative 인 블록은 상위 배치에서 크기를 받는데, 자식(캔버스)이 전부 절대 배치라\n 내용 폭이 0 이다. 그래서 폭 0 짜리 칸이 되고 캔버스도 폭 0 이 된다.\n\n 화면은 그냥 회색으로 남는다. 게다가 마우스 좌표를 NDC 로 바꿀 때 폭으로 나누므로\n 값이 무한대가 되어 기즈모를 집을 수 없다 — \"기즈모가 안 잡힌다\"의 실제 원인이\n 이것이었다.\n\n 그래서 width 와 height 를 직접 준다. 인라인으로 position 이 무엇으로 바뀌든 칸은\n 부모를 채운다.\n */\n div[stage] {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n }\n\n /* 세울 수 없는 상태를 빈 화면으로 두지 않는다 — 왜 안 보이는지 말한다 */\n div[cannot] {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--spacing-large, 12px);\n text-align: center;\n font: var(--label-font, inherit);\n line-height: 1.6;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: ${unsafeCSS(WORKBENCH)};\n }\n `\n\n /**\n * 편집 중인 내용. **씬 모델 그대로 받는다.**\n *\n * 한동안 `FigureSource` 로 받아 여기서 변환했다. 그런데 모델러 페이지가 갖고 있는 것이\n * 이미 씬 모델이므로, 그러면 값을 하나 고칠 때마다 왕복 변환이 한 번씩 일어난다.\n * 무손실이긴 하지만 하는 일이 없다 — 보드가 곧 보드이고 부품이 곧 컴포넌트다.\n */\n @property({ type: Object }) board?: BoardModel\n /** 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다(`figure-view.ts`). */\n @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW\n @property({ type: Array }) parts: PartModel[] = []\n /** 지금 고른 부품 이름. 목록에서 고른 것을 캔버스에 알린다. */\n @property({ type: String }) picked?: string\n\n /**\n * 보기 전용 — 씬에서 일어난 일을 밖에 알리지 않는다.\n *\n * AI 후보를 나란히 세울 때 쓴다. 후보는 아직 편집 대상이 아닌데, 씬은 컴포넌트를\n * 만들면서 상태를 정리하고 그때 변경 이벤트를 낸다. 그 이벤트가 그대로 올라가면\n * **후보의 값이 편집 중인 데이터를 덮어쓴다** — 실제로 그랬다. 받지 않기로 한 부품\n * 위치가 오류보고없이 반영되어 있었고, 후보를 버려도 되돌아오지 않았다.\n */\n @property({ type: Boolean }) viewOnly = false\n\n @query('div[stage]') private stage!: HTMLDivElement\n\n /** 세울 수 없는 사유. 없으면 세운 것이다. */\n @state() private cannot = ''\n\n private scene?: SceneHandle\n /** 지금 씬이 서 있는 판. 이것이 달라지면 다시 세운다. */\n private standing?: string\n /** 칸 크기를 따라가게 지켜본다. 안 지켜보면 0x0 으로 고정된다. */\n private watcher?: ResizeObserver\n\n render() {\n return html`\n <div stage></div>\n ${this.cannot ? html`<div cannot>${this.cannot}</div>` : ''}\n `\n }\n\n willUpdate(changed: Map<string, unknown>) {\n if (!this.hasUpdated) return\n\n if (changed.has('board') || changed.has('parts')) this.standUp()\n if (changed.has('view')) this.applyView()\n if (changed.has('picked')) this.showPicked()\n }\n\n firstUpdated() {\n this.standUp()\n\n // 씬은 세울 때의 칸 크기를 읽는다. 저작면이 아직 배치되기 전이면 0x0 으로 굳어\n // **캔버스는 붙었는데 아무것도 안 보이는** 상태가 된다 — 실제로 그렇게 났다.\n this.watcher = new ResizeObserver(() => this.scene?.resize?.())\n this.watcher.observe(this)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.watcher?.disconnect()\n this.watcher = undefined\n this.teardown()\n }\n\n /**\n * 받은 것을 씬에 반영한다.\n *\n * **판이 그대로면 다시 세우지 않는다.** 다시 세우면 고른 것과 카메라가 날아가서,\n * 숫자 하나 고칠 때마다 화면이 처음으로 돌아간다.\n */\n private standUp() {\n const board = this.board\n if (!board) {\n this.cannot = i18next.t('text.canvas-appears-once-a-part-exists')\n return\n }\n\n const signature = `${board.width}x${board.height}x${board.depth}`\n if (this.scene && this.standing === signature) {\n this.syncParts()\n return\n }\n\n this.teardown()\n\n try {\n this.scene = create({\n target: this.stage,\n model: {\n type: 'model-layer',\n width: board.width,\n height: board.height,\n depth: board.depth,\n threed: true,\n environment: 'studio',\n /*\n 표본을 화면보다 더 쓴다. MSAA 만으로는 실루엣의 계단이 남는다 — 픽셀\n 비율만 올려 보니 눈에 띄게 매끄러워졌다. 저작면은 작은 칸에 자산 하나라\n 그릴 것이 적으니 표본을 더 써도 된다.\n */\n pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),\n /*\n 좌표 바탕을 씬에게 맡긴다. 우리가 만들어 얹었더니 2D 로 갔다 오면\n 사라졌다 — 그때 씬이 통째로 다시 서면서 새 THREE.Scene 을 만들기 때문이다.\n 보드는 이 값을 안 주므로 안 보인다.\n */\n ...this.sceneView(),\n gridStep: gridStep(board),\n sky: 'color',\n /*\n 판의 면 — 2D 는 밝은 종이, 3D 는 없음.\n\n 종이는 밝아야 한다: 2D 편집 손잡이와 안내선이 어두운 색이다. 바닥판은\n 없어야 한다: 좌표 모눈과 겹쳐 싸우고 원점 아래쪽을 가린다.\n */\n fillStyle: PAPER,\n components: this.parts.map(part => ({ ...part }))\n },\n // **편집 모드**로 만든다. 보드의 3D 모델러가 그 모드에서 카메라 시점 버튼 ·\n // 기즈모 모드 · 도움말 · 단축키를 띄운다.\n mode: 1\n }) as SceneHandle\n\n this.standing = signature\n this.watchScene()\n this.scene.resize?.()\n /*\n 열자마자 모델 전체가 보이게 맞춘다.\n\n 카메라는 2D 뷰포트에서 만들어진다. 그런데 그 뷰포트의 처음 배율은 컨테이너\n 크기에서 오지 모델 크기에서 오지 않는다. 그래서 가로로 긴 모델(컨베이어가\n 그렇다)을 열면 화면 밖으로 나가 벨트 한 조각만 보였다.\n\n **다시 세울 때만 맞춘다.** 창 크기가 바뀔 때마다 맞추면 사용자가 돌려 둔 시점과\n 확대가 매번 되돌아간다.\n */\n this.scene.fit?.('ratio')\n /*\n 3D 카메라는 2D 쪽 fit 으로 안 움직인다. 처음 3D 로 설 때 모델 크기를 보지 않기\n 때문이다. 씬이 내주는 자리로 따로 맞춘다.\n\n `?.` 로 부르는 것은 배포된 판에 아직 없을 수 있어서다. 없으면 2D 만 맞고 3D 는\n 기본 시점으로 선다 — 여기서 카메라 계산을 흉내 내지 않는다.\n */\n this.scene.root?.fitCameraToModel?.()\n this.cannot = ''\n } catch (e) {\n // 원인을 지어내지 않는다 — 던진 말을 그대로 보여 준다.\n this.cannot = (e as Error).message\n }\n }\n\n /**\n * 지금 판 위에 있는 부품 컴포넌트를 이름으로 찾는다.\n *\n * **참조를 들고 있지 않는다.** 전에는 세울 때 한 번 색인해 두고 계속 그 Map 을 썼다.\n * 그런데 씬은 우리 모르게 판을 다시 세울 때가 있다 — 2D 와 3D 를 오갈 때가 그렇다.\n * 그러면 Map 안의 것들은 부모가 없는 죽은 객체가 되고, 그 뒤로는\n *\n * - 목록에서 골라도 선택이 안 된다. 루트의 `selected` 게터가 부모 없는 것을\n * 걸러 내므로 빈 배열이 되고, 테두리도 기즈모도 붙지 않는다\n * - 속성을 고쳐도 3D 가 안 따라온다. 죽은 객체에 값을 넣기 때문이다\n *\n * 둘 다 오류보고없이 일어난다. 실제로 화면에서 「기즈모가 안 잡힌다」로 나타났다.\n *\n * 부품은 많아야 20개다. 매번 순회하는 값이 참조를 갖고 있다가 틀리는 값보다 훨씬 싸다.\n */\n private partsOnBoard(): Map<string, SceneComponent> {\n const found = new Map<string, SceneComponent>()\n for (const component of this.scene?.root?.components ?? []) {\n const name = component.state?.name\n if (typeof name === 'string') found.set(name, component)\n }\n return found\n }\n\n /**\n * 보기 설정을 씬이 아는 이름으로 낸다.\n *\n * 옮겨 적는 표를 두지 않는다 — `ViewSettings` 의 이름이 곧 `model-layer` 의 상태\n * 이름이다. 표를 두면 씬이 새 설정을 받아도 이쪽이 모른다.\n */\n private sceneView(): Record<string, unknown> {\n const { grid, gridLabels, floor, environment, skyColor, ...light } = this.view ?? DEFAULT_VIEW\n\n return { grid, gridLabels, floor, environment, skyColor, ...light }\n }\n\n /**\n * 설정이 바뀌면 **씬에 그대로 넣는다.** 다시 세우지 않는다.\n *\n * 씬이 이 값들에 스스로 반응한다(`three-capability` 의 `onchange`) — 조명 · 반사 ·\n * 바닥 · 좌표 바탕이 그 자리에서 바뀐다. 다시 세우면 고른 것과 카메라가 날아가서,\n * 밝기 하나 끄는 동안 화면이 계속 처음으로 돌아간다.\n */\n private applyView(): void {\n const board = this.scene?.root\n if (!board) return\n\n board.set?.(this.sceneView())\n board.invalidate?.()\n }\n\n /**\n * 부품 목록을 씬에 맞춘다. **다시 세우지 않고 바뀐 것만 고친다.**\n *\n * 다시 세우면 고른 것과 카메라가 날아가서, 숫자 하나 고칠 때마다 화면이 처음으로\n * 돌아간다.\n */\n private syncParts() {\n const root = this.scene?.root\n if (!root) return\n\n const wanted = new Map(this.parts.map(part => [part.name, part]))\n const standing = this.partsOnBoard()\n\n // 없어진 것을 제거한다\n for (const [name, component] of standing) {\n if (!wanted.has(name)) component.remove?.()\n }\n\n for (const part of this.parts) {\n const found = standing.get(part.name)\n if (found) {\n /*\n **달라진 값만 넣는다.**\n\n 같은 값을 다시 넣어도 씬은 변경 이벤트를 낸다. 그 이벤트가 다시 편집 데이터를\n 만들고, 그것이 다시 여기로 돌아오면 끝없이 반복된다. 기즈모를 끄는 중이라면 그\n 되먹임이 끌기와 싸운다.\n */\n const diff = changedKeys(found.state, part as unknown as Record<string, unknown>)\n if (diff) found.set(diff)\n continue\n }\n // 새 부품. 씬의 공장으로 만들어 판에 붙인다.\n const made = Model.compile({ ...part }, root.app as never) as SceneComponent | null\n if (made) root.addComponent?.(made)\n }\n\n root.invalidate?.()\n }\n\n /**\n * 씬에서 일어난 일을 밖에 알린다.\n *\n * **자리가 아니라 이름으로 알린다.** 목록의 몇 번째인지로 알리면, 알림이 오는 사이에\n * 목록이 바뀌면 엉뚱한 부품을 가리킨다.\n */\n private watchScene() {\n const board = this.scene?.root\n const root = board?.root\n if (!board || !root?.on || this.viewOnly) return\n\n root.on('selected', (after: unknown) => {\n const chosen = (after as SceneComponent[] | undefined)?.[0]\n const name = chosen?.state?.name\n if (typeof name === 'string') this.tell('pick-part', { name })\n })\n\n /*\n 기즈모로 끌거나 2D 에서 옮기면 컴포넌트 상태가 바뀐다. 그것을 그대로 올려 보낸다 —\n 씬이 쓰는 용어 그대로다. 저장 형식으로 옮기는 일은 변환 함수가 한다.\n\n **바뀜은 판으로 올라오고, 바뀐 컴포넌트는 끝 인자의 `origin` 에 있다.** 자식이\n `trigger('change', after, before)` 를 하면 씬이 `{ origin, name, deliverer }` 를\n 뒤에 붙여 부모에게 넘긴다. 첫 인자를 컴포넌트로 읽으면 아무 일도 안 일어난다 —\n 실제로 그랬다.\n */\n board.on?.('change', (...args: unknown[]) => {\n const hint = args[args.length - 1] as { origin?: SceneComponent } | undefined\n const changed = hint?.origin\n const name = changed?.state?.name\n if (typeof name !== 'string') return\n\n const was = this.parts.find(part => part.name === name)\n if (!was) return\n\n // 씬 모델 그대로 올려 보낸다. 모델러 페이지가 갖고 있는 것도 씬 모델이라 변환할 것이 없다.\n this.tell('part-changed', { name, part: { ...was, ...changed!.state } as PartModel })\n })\n }\n\n /** 목록에서 고른 것을 캔버스의 선택에 맞춘다. */\n private showPicked() {\n const board = this.scene?.root\n const root = board?.root\n if (!board || !root) return\n\n const component = this.picked ? this.partsOnBoard().get(this.picked) : undefined\n root.selected = component ? [component] : []\n board.invalidate?.()\n }\n\n private tell(what: string, detail: object) {\n this.dispatchEvent(new CustomEvent(what, { detail, bubbles: true, composed: true }))\n }\n\n private teardown() {\n this.scene?.dispose?.()\n this.scene = undefined\n this.standing = undefined\n }\n}\n"]}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import type { BoardModel, PartModel } from './figure-source.js';
|
|
3
|
+
declare const FigureInspector_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
4
|
+
/**
|
|
5
|
+
* 고른 부품의 속성.
|
|
6
|
+
*
|
|
7
|
+
* ## 여기가 사용자 용어로 보여 주는 자리다
|
|
8
|
+
*
|
|
9
|
+
* 모델러 페이지가 갖고 있는 것은 **씬 모델**이다 — 보드 위에 부품 컴포넌트가 놓인 상태.
|
|
10
|
+
* 그런데 사용자에게 보여야 하는 것은 씬 속성 이름(`left`·`top`·`zPos`)이 아니라
|
|
11
|
+
* **X·Y·Z** 다. 다른 3D 도구가 모두 그렇게 부르고, 위치도 부품의 **중심**으로 읽는다.
|
|
12
|
+
*
|
|
13
|
+
* 그래서 이 패널은 받은 부품을 FigureSource 형태로 **변환해서 보여 주고, 수정한 것을
|
|
14
|
+
* 다시 씬 모델로 되돌려 알린다.** 변환은 `figure-source.ts` 가 한다 — 여기서 축을 다시 계산하지
|
|
15
|
+
* 않는다.
|
|
16
|
+
*
|
|
17
|
+
* ## 있는 것만 보여 준다
|
|
18
|
+
*
|
|
19
|
+
* 분할 수는 곡면이 있는 도형에만, 모서리 굴림은 단면을 뽑는 도형에만 뜻이 있다.
|
|
20
|
+
* 뜻 없는 칸을 회색으로 늘어놓으면 저작자가 「왜 안 먹지」를 고민하게 된다 —
|
|
21
|
+
* 아예 내지 않는다.
|
|
22
|
+
*
|
|
23
|
+
* ## 이름은 저장되는 식별자다
|
|
24
|
+
*
|
|
25
|
+
* `FigurePart.name` 은 바인딩·애니메이션·슬롯이 가리키는 이름이다. 고치면 배치된
|
|
26
|
+
* 인스턴스의 바인딩이 **조용히** 끊긴다. 그래서 이름 칸에만 경고를 붙인다.
|
|
27
|
+
*
|
|
28
|
+
* ## 「상태 색이 붙는 자리」를 걷었다
|
|
29
|
+
*
|
|
30
|
+
* `FigurePart.materialSlot`(당시 이름은 `slot`) 을 고르는 칸이 있었는데 **아무 일도 하지 않았다.** things-scene 의
|
|
31
|
+
* figure 런타임이 `slot` 을 읽지 않는다 — 선언 한 줄이 전부다. 되는 줄 알고 만들었고,
|
|
32
|
+
* 확인하지 않은 것이 잘못이다.
|
|
33
|
+
*
|
|
34
|
+
* 그리고 되게 만들더라도 이 모양이 아니다. 상태 색은 특별한 것이 아니라 **속성 하나**이고,
|
|
35
|
+
* 툴팁·라벨·표시 여부와 같은 자리에서 다뤄져야 한다. 그 설계가 정해지면(figure-model#2)
|
|
36
|
+
* 이 자리에 **속성 선언과 부품 바인딩** 편집기가 들어온다.
|
|
37
|
+
*
|
|
38
|
+
* 그때까지 빈 약속을 두지 않는다. 아무 일도 안 하는 칸은 없느니만 못하다.
|
|
39
|
+
*
|
|
40
|
+
* ## 고치지 않는다
|
|
41
|
+
*
|
|
42
|
+
* 정본 한 벌은 저작면이 갖는다. 여기서는 바뀐 부품을 알릴 뿐이다.
|
|
43
|
+
*/
|
|
44
|
+
export declare class FigureInspector extends FigureInspector_base {
|
|
45
|
+
static styles: import("lit").CSSResult[];
|
|
46
|
+
/** 보드. 위치를 보드 중심 기준으로 바꾸려면 이것이 있어야 한다. */
|
|
47
|
+
board?: BoardModel;
|
|
48
|
+
/** 보드 위의 부품들. 씬 모델 그대로다. */
|
|
49
|
+
parts: PartModel[];
|
|
50
|
+
selected: number;
|
|
51
|
+
/** `part` 는 HTMLElement 의 속성(DOMTokenList)이다 — 가리면 형이 어긋난다. */
|
|
52
|
+
private get chosen();
|
|
53
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
54
|
+
private renderIdentity;
|
|
55
|
+
private renderPlacement;
|
|
56
|
+
/** 분할 수와 모서리 굴림 — 해당하는 도형에만 낸다. */
|
|
57
|
+
private renderShape;
|
|
58
|
+
/**
|
|
59
|
+
* 민 단면 — 모서리와 **속을 파는 두께.**
|
|
60
|
+
*
|
|
61
|
+
* 벽 두께를 0 으로 두면 통짜다. 0 보다 크면 그만큼 안으로 들어온 테가 생기고, 그것이
|
|
62
|
+
* 트레이 · 토트 · 통 · 케이스가 되는 자리다. 바닥은 비워 두면 벽과 같고, 0 으로 두면
|
|
63
|
+
* 위아래가 다 뚫린 테두리가 된다.
|
|
64
|
+
*/
|
|
65
|
+
private renderExtruded;
|
|
66
|
+
/**
|
|
67
|
+
* 분할 수 — 모양과 무게를 함께 고른다.
|
|
68
|
+
*
|
|
69
|
+
* 삼각형 수는 화면이 셈하지 않는다. `trianglesOf` 에 그 분할 수를 가진 부품을 주고
|
|
70
|
+
* 물어본다 — 화면이 제 나름의 공식을 들면 figure-model 이 규칙을 바꿔도 화면만
|
|
71
|
+
* 옛 답을 계속 낸다.
|
|
72
|
+
*/
|
|
73
|
+
/**
|
|
74
|
+
* 원통을 원통으로 둘까 — **고르는 값이고, 값을 치른다.**
|
|
75
|
+
*
|
|
76
|
+
* 인스턴스가 축마다 다른 배율을 받으면 원기둥이 타원 기둥이 된다. 지키려면 제 변환이
|
|
77
|
+
* 있어야 하고, 합친 형상은 변환을 하나만 받으므로 지키는 곡면 부품 하나가 draw call
|
|
78
|
+
* 하나다. 저작자가 그 대가를 모르고 고르면 안 되므로 그 자리에 적는다.
|
|
79
|
+
*/
|
|
80
|
+
private renderKeepRound;
|
|
81
|
+
private renderSegments;
|
|
82
|
+
/**
|
|
83
|
+
* 분할 수의 실루엣.
|
|
84
|
+
*
|
|
85
|
+
* 곡면 프리미티브를 위에서 본 윤곽이 정확히 정n각형이다 — 원기둥의 단면도, 구의
|
|
86
|
+
* 적도도. 그림을 지어내는 것이 아니라 실제로 만들어지는 모양이다.
|
|
87
|
+
*/
|
|
88
|
+
private silhouette;
|
|
89
|
+
/** 견본 한 줄. 무리마다 부른다. */
|
|
90
|
+
private swatches;
|
|
91
|
+
private renderMaterial;
|
|
92
|
+
/**
|
|
93
|
+
* 인스턴스 크기가 바뀔 때 이 부품이 어떻게 변하나.
|
|
94
|
+
*
|
|
95
|
+
* ## 늘어나는 축은 아직 못 보여 준다
|
|
96
|
+
*
|
|
97
|
+
* `stretch` 는 형식에 축을 적는 자리가 없어서 그리는 쪽이 **가장 길게 뻗은 축**으로
|
|
98
|
+
* 정한다. 그 답을 화면에 적어 주려면 규칙을 가진 쪽에 물어야 하는데, 그 함수
|
|
99
|
+
* (`stretchAxisOf` · `stretchAxis`)가 아직 배포된 판에 없다.
|
|
100
|
+
*
|
|
101
|
+
* 화면이 제 나름대로 계산하지 않는다. 규칙이 두 벌이 되면 보이는 축과 실제로 늘어나는
|
|
102
|
+
* 축이 갈리고, 그 어긋남은 화면을 믿게 만든 다음에 드러난다. 배포되면 잇는다.
|
|
103
|
+
*
|
|
104
|
+
* ## 값이 있는 것을 밝힌다
|
|
105
|
+
*
|
|
106
|
+
* `scale` 이 아닌 부품은 합쳐 그릴 수 없어 draw call 을 하나 더 쓴다. 그 비용이
|
|
107
|
+
* 성숙도의 묶음 수에 그대로 잡힌다. 고르는 자리에서 미리 말해 준다.
|
|
108
|
+
*/
|
|
109
|
+
private renderBehaviour;
|
|
110
|
+
/**
|
|
111
|
+
* 숫자 칸.
|
|
112
|
+
*
|
|
113
|
+
* 빈 칸이나 글자를 넣었을 때 0 으로 바꿔치지 않는다 — 저작자가 지우는 중일 수 있고,
|
|
114
|
+
* 조용히 0 이 되면 부품이 사라진 것처럼 보인다. 숫자가 아니면 그냥 안 받는다.
|
|
115
|
+
*/
|
|
116
|
+
private number;
|
|
117
|
+
/**
|
|
118
|
+
* 도형을 바꾼다.
|
|
119
|
+
*
|
|
120
|
+
* 도형이 바뀌면 그 도형에 뜻 없는 값이 남는다. 남겨 두면 검증이 「무시됨」 위반을
|
|
121
|
+
* 내므로 여기서 걷는다.
|
|
122
|
+
*/
|
|
123
|
+
private changePrimitive;
|
|
124
|
+
private put;
|
|
125
|
+
private putTransform;
|
|
126
|
+
/**
|
|
127
|
+
* 발광 세기를 고친다. **0 이면 블록을 통째로 지운다.**
|
|
128
|
+
*
|
|
129
|
+
* 세기 0 인 발광을 남겨 두면 「안 빛나는데 램프인 것」이 되어, 재질 키가 갈려 묶음이
|
|
130
|
+
* 헛되이 하나 는다. 0 은 「램프가 아니다」로 읽는 것이 맞다.
|
|
131
|
+
*
|
|
132
|
+
* `Number(value) || undefined` 를 쓰면 안 된다 — 0 이 falsy 라 변경으로 안 잡혀 옛
|
|
133
|
+
* 값이 그대로 남는다. 실제로 램프가 안 꺼졌다.
|
|
134
|
+
*/
|
|
135
|
+
private putEmissive;
|
|
136
|
+
private putMaterial;
|
|
137
|
+
/**
|
|
138
|
+
* 수정한 것을 씬 모델로 되돌려 알린다.
|
|
139
|
+
*
|
|
140
|
+
* 사용자는 X·Y·Z 로 수정하고, 모델러 페이지는 씬 모델을 갖는다. 그 사이를 여기서
|
|
141
|
+
* 잇는다 — 변환해서 수정하고 다시 되돌린다. **왕복 변환이 무손실이라야** 위치 하나
|
|
142
|
+
* 고쳤을 뿐인데 회전값이 조금씩 밀리는 일이 없다(`fold.test.ts` 가 이를 확인한다).
|
|
143
|
+
*/
|
|
144
|
+
private change;
|
|
145
|
+
/** 정밀 도구를 시킨다. 정본은 저작면이 갖는다 — 여기서는 알리기만 한다. */
|
|
146
|
+
private ask;
|
|
147
|
+
private emit;
|
|
148
|
+
}
|
|
149
|
+
export {};
|