@things-factory/figure-ui 10.1.46 → 10.1.47
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/angle-dial.ts +70 -0
- package/client/modeller/figure-settings.ts +328 -0
- package/client/modeller/figure-v3-capability.ts +137 -0
- package/client/modeller/figure-v3-clips.ts +253 -0
- package/client/modeller/figure-v3-controls.ts +212 -0
- package/client/modeller/figure-v3-links.ts +61 -81
- package/client/modeller/figure-v3-material.ts +108 -35
- package/client/modeller/figure-v3-parts.ts +96 -3
- package/client/modeller/figure-v3-preview.ts +326 -0
- package/client/modeller/figure-v3-properties.ts +29 -57
- package/client/modeller/figure-v3-verdict.ts +598 -60
- package/client/modeller/figure-view.ts +152 -0
- package/client/modeller/side-panel-styles.ts +326 -0
- package/client/modeller/v3-canvas.ts +221 -263
- package/client/modeller/v3-clips.ts +89 -0
- package/client/modeller/v3-controls.ts +54 -0
- package/client/modeller/v3-editor.ts +531 -223
- package/client/modeller/v3-fields.ts +62 -0
- package/client/modeller/v3-help.ts +41 -0
- package/client/modeller/v3-keys.ts +34 -0
- package/client/modeller/v3-links.ts +93 -9
- package/client/modeller/v3-live.ts +79 -0
- package/client/modeller/v3-maturity.ts +415 -0
- package/client/modeller/v3-palette.ts +45 -0
- package/client/modeller/v3-preview-model.ts +54 -0
- package/client/modeller/v3-stage-model.ts +159 -0
- package/client/pages/figure-list-page.ts +4 -4
- package/client/pages/figure-v3-modeller-page.ts +66 -134
- package/dist-client/modeller/angle-dial.d.ts +32 -0
- package/dist-client/modeller/angle-dial.js +41 -0
- package/dist-client/modeller/angle-dial.js.map +1 -0
- package/dist-client/modeller/figure-settings.d.ts +39 -0
- package/dist-client/modeller/figure-settings.js +317 -0
- package/dist-client/modeller/figure-settings.js.map +1 -0
- package/dist-client/modeller/figure-v3-capability.d.ts +22 -0
- package/dist-client/modeller/figure-v3-capability.js +141 -0
- package/dist-client/modeller/figure-v3-capability.js.map +1 -0
- package/dist-client/modeller/figure-v3-clips.d.ts +31 -0
- package/dist-client/modeller/figure-v3-clips.js +268 -0
- package/dist-client/modeller/figure-v3-clips.js.map +1 -0
- package/dist-client/modeller/figure-v3-controls.d.ts +36 -0
- package/dist-client/modeller/figure-v3-controls.js +226 -0
- package/dist-client/modeller/figure-v3-controls.js.map +1 -0
- package/dist-client/modeller/figure-v3-links.d.ts +5 -2
- package/dist-client/modeller/figure-v3-links.js +64 -82
- package/dist-client/modeller/figure-v3-links.js.map +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +5 -2
- package/dist-client/modeller/figure-v3-material.js +109 -35
- package/dist-client/modeller/figure-v3-material.js.map +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +10 -0
- package/dist-client/modeller/figure-v3-parts.js +88 -3
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-preview.d.ts +34 -0
- package/dist-client/modeller/figure-v3-preview.js +322 -0
- package/dist-client/modeller/figure-v3-preview.js.map +1 -0
- package/dist-client/modeller/figure-v3-properties.d.ts +2 -2
- package/dist-client/modeller/figure-v3-properties.js +29 -57
- package/dist-client/modeller/figure-v3-properties.js.map +1 -1
- package/dist-client/modeller/figure-v3-verdict.d.ts +31 -17
- package/dist-client/modeller/figure-v3-verdict.js +579 -59
- package/dist-client/modeller/figure-v3-verdict.js.map +1 -1
- package/dist-client/modeller/figure-view.d.ts +118 -0
- package/dist-client/modeller/figure-view.js +78 -0
- package/dist-client/modeller/figure-view.js.map +1 -0
- package/dist-client/modeller/side-panel-styles.d.ts +23 -0
- package/dist-client/modeller/side-panel-styles.js +326 -0
- package/dist-client/modeller/side-panel-styles.js.map +1 -0
- package/dist-client/modeller/v3-canvas.d.ts +29 -37
- package/dist-client/modeller/v3-canvas.js +188 -260
- package/dist-client/modeller/v3-canvas.js.map +1 -1
- package/dist-client/modeller/v3-clips.d.ts +39 -0
- package/dist-client/modeller/v3-clips.js +58 -0
- package/dist-client/modeller/v3-clips.js.map +1 -0
- package/dist-client/modeller/v3-controls.d.ts +28 -0
- package/dist-client/modeller/v3-controls.js +28 -0
- package/dist-client/modeller/v3-controls.js.map +1 -0
- package/dist-client/modeller/v3-editor.d.ts +58 -1
- package/dist-client/modeller/v3-editor.js +555 -218
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-fields.d.ts +30 -0
- package/dist-client/modeller/v3-fields.js +46 -0
- package/dist-client/modeller/v3-fields.js.map +1 -0
- package/dist-client/modeller/v3-help.d.ts +18 -0
- package/dist-client/modeller/v3-help.js +37 -0
- package/dist-client/modeller/v3-help.js.map +1 -0
- package/dist-client/modeller/v3-keys.d.ts +14 -0
- package/dist-client/modeller/v3-keys.js +23 -0
- package/dist-client/modeller/v3-keys.js.map +1 -0
- package/dist-client/modeller/v3-links.d.ts +30 -1
- package/dist-client/modeller/v3-links.js +94 -8
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-live.d.ts +31 -0
- package/dist-client/modeller/v3-live.js +58 -0
- package/dist-client/modeller/v3-live.js.map +1 -0
- package/dist-client/modeller/v3-maturity.d.ts +120 -0
- package/dist-client/modeller/v3-maturity.js +317 -0
- package/dist-client/modeller/v3-maturity.js.map +1 -0
- package/dist-client/modeller/v3-palette.d.ts +7 -0
- package/dist-client/modeller/v3-palette.js +42 -0
- package/dist-client/modeller/v3-palette.js.map +1 -0
- package/dist-client/modeller/v3-preview-model.d.ts +32 -0
- package/dist-client/modeller/v3-preview-model.js +48 -0
- package/dist-client/modeller/v3-preview-model.js.map +1 -0
- package/dist-client/modeller/v3-stage-model.d.ts +31 -0
- package/dist-client/modeller/v3-stage-model.js +121 -0
- package/dist-client/modeller/v3-stage-model.js.map +1 -0
- package/dist-client/pages/figure-list-page.js +4 -4
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-v3-modeller-page.d.ts +2 -6
- package/dist-client/pages/figure-v3-modeller-page.js +64 -136
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.chk.tsbuildinfo +1 -0
- package/dist-client/tsconfig.scratch-fm.tsbuildinfo +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/v3-clips.test.ts +70 -0
- package/test/v3-controls.test.ts +68 -0
- package/test/v3-fields.test.ts +62 -0
- package/test/v3-gizmo.test.ts +3 -5
- package/test/v3-keys.test.ts +37 -0
- package/test/v3-layout.test.ts +78 -0
- package/test/v3-links.test.ts +77 -6
- package/test/v3-live.test.ts +99 -0
- package/test/v3-material.test.ts +6 -0
- package/test/v3-maturity.test.ts +268 -0
- package/test/v3-parts-wiring.test.ts +3 -1
- package/test/v3-preview.test.ts +42 -0
- package/test/v3-properties.test.ts +4 -2
- package/test/v3-scene-canvas.test.ts +85 -0
- package/test/v3-stage-model.test.ts +25 -0
- package/test/v3-theme.test.ts +16 -25
- package/test/v3-verdict.test.ts +12 -6
- package/test/v3-view.test.ts +20 -0
- package/translations/en.json +84 -5
- package/translations/ja.json +86 -7
- package/translations/ko.json +84 -5
- package/translations/ms.json +86 -7
- package/translations/zh.json +86 -7
|
@@ -1,305 +1,229 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
|
-
import { LitElement, html, css } from 'lit';
|
|
3
|
-
import { customElement, property } from 'lit/decorators.js';
|
|
2
|
+
import { LitElement, html, css, nothing, unsafeCSS } from 'lit';
|
|
3
|
+
import { customElement, property, query } from 'lit/decorators.js';
|
|
4
4
|
import { i18next } from '@operato/i18n';
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
import { create, Model, niceGridStep } from '@hatiolab/things-scene';
|
|
6
|
+
import { DEFAULT_VIEW, sceneLook, WORKBENCH } from './figure-view.js';
|
|
7
|
+
import { boardFor, changedKeys, componentOf, gizmoResultOf } from './v3-stage-model.js';
|
|
8
|
+
/* The 2D side of the board, which the author never sees on this page but the scene paints. */
|
|
9
|
+
const PAPER = '#ffffff';
|
|
10
10
|
let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
11
11
|
constructor() {
|
|
12
12
|
super(...arguments);
|
|
13
13
|
this.parts = [];
|
|
14
14
|
this.palette = {};
|
|
15
|
+
/** Which figure is open. Another one stands the board up again. */
|
|
15
16
|
this.context = '';
|
|
16
|
-
/** The selected part, and what the gizmo may write for it (`gizmoReachOf`). Nothing reachable, no gizmo. */
|
|
17
17
|
this.selected = '';
|
|
18
|
+
/** What the gizmo may write for the selected part; a change beyond it is not applied and the part springs back. */
|
|
18
19
|
this.reach = { translate: false, rotate: false, scale: false };
|
|
19
|
-
|
|
20
|
-
this
|
|
21
|
-
|
|
22
|
-
this.proxy = new THREE.Object3D();
|
|
23
|
-
/** The pointerup that ends a drag is not a pick — releasing off the body would otherwise deselect it. */
|
|
24
|
-
this.justDragged = false;
|
|
25
|
-
this.scene = new THREE.Scene();
|
|
26
|
-
this.group = new THREE.Group();
|
|
27
|
-
this.camera = new THREE.PerspectiveCamera(40, 1, 0.1, 1000000);
|
|
28
|
-
this.initialized = false;
|
|
29
|
-
this.contextAtRender = '';
|
|
30
|
-
this.key = (event) => {
|
|
31
|
-
const mode = { w: 'translate', e: 'rotate', r: 'scale' }[event.key.toLowerCase()];
|
|
32
|
-
if (mode && this.reach[mode])
|
|
33
|
-
this.mode = mode;
|
|
34
|
-
};
|
|
35
|
-
this.dragging = (event) => {
|
|
36
|
-
const on = !!event.value;
|
|
37
|
-
if (this.controls)
|
|
38
|
-
this.controls.enabled = !on;
|
|
39
|
-
const body = this.group.children.find(child => child.name === this.selected);
|
|
40
|
-
if (on && body) {
|
|
41
|
-
this.proxy.updateMatrix();
|
|
42
|
-
this.dragStart = { proxy: this.proxy.matrix.clone(), mesh: body.matrix.clone(), body };
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const start = this.dragStart;
|
|
46
|
-
this.dragStart = undefined;
|
|
47
|
-
if (!start)
|
|
48
|
-
return;
|
|
49
|
-
/* Either listener may see the release first; the flag lasts past both, then clears. */
|
|
50
|
-
this.justDragged = true;
|
|
51
|
-
setTimeout(() => (this.justDragged = false));
|
|
52
|
-
const result = {};
|
|
53
|
-
if (this.mode === 'translate')
|
|
54
|
-
result.position = this.proxy.position.toArray();
|
|
55
|
-
if (this.mode === 'rotate') {
|
|
56
|
-
/* rigid@1 composes Rx·Ry·Rz — three's 'XYZ' order — so the angles come back as the inputs mean them. */
|
|
57
|
-
const euler = new THREE.Euler().setFromQuaternion(this.proxy.quaternion, 'XYZ');
|
|
58
|
-
result.rotation = [euler.x, euler.y, euler.z].map(THREE.MathUtils.radToDeg);
|
|
59
|
-
}
|
|
60
|
-
if (this.mode === 'scale')
|
|
61
|
-
result.scale = this.proxy.scale.toArray();
|
|
62
|
-
this.dispatchEvent(new CustomEvent('part-transformed', { detail: { id: this.selected, result }, bubbles: true, composed: true }));
|
|
63
|
-
};
|
|
64
|
-
/** While dragging, the body follows the stand-in; the next evaluation replaces it with where the inputs put it. */
|
|
65
|
-
this.follow = () => {
|
|
66
|
-
const start = this.dragStart;
|
|
67
|
-
if (!start)
|
|
68
|
-
return;
|
|
69
|
-
this.proxy.updateMatrix();
|
|
70
|
-
const delta = this.proxy.matrix.clone().multiply(start.proxy.clone().invert());
|
|
71
|
-
start.body.matrix.copy(delta.multiply(start.mesh));
|
|
72
|
-
start.body.matrixWorldNeedsUpdate = true;
|
|
73
|
-
this.group.updateMatrixWorld(true);
|
|
74
|
-
};
|
|
75
|
-
this.draw = () => {
|
|
76
|
-
if (this.renderer)
|
|
77
|
-
this.renderer.render(this.scene, this.camera);
|
|
78
|
-
};
|
|
79
|
-
this.pick = (event) => {
|
|
80
|
-
if (this.justDragged || this.transform?.dragging)
|
|
81
|
-
return;
|
|
82
|
-
const r = event.currentTarget.getBoundingClientRect(), ray = new THREE.Raycaster();
|
|
83
|
-
ray.setFromCamera(new THREE.Vector2(((event.clientX - r.left) / r.width) * 2 - 1, (-(event.clientY - r.top) / r.height) * 2 + 1), this.camera);
|
|
84
|
-
const hit = ray.intersectObjects(this.group.children)[0];
|
|
85
|
-
this.dispatchEvent(new CustomEvent('part-selected', { detail: hit?.object.name ?? '', bubbles: true, composed: true }));
|
|
86
|
-
};
|
|
20
|
+
this.view = DEFAULT_VIEW;
|
|
21
|
+
/** Set while this canvas writes to components, so the scene's echo of it is not taken for a drag. */
|
|
22
|
+
this.writing = false;
|
|
87
23
|
}
|
|
88
24
|
static { this.styles = css `
|
|
89
25
|
:host {
|
|
90
|
-
display: block;
|
|
91
|
-
min-width: 0;
|
|
92
|
-
min-height: 0;
|
|
93
26
|
position: relative;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
*/
|
|
99
|
-
background: var(--md-sys-color-surface-container);
|
|
27
|
+
display: flex;
|
|
28
|
+
min-height: 0;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
background-color: ${unsafeCSS(WORKBENCH)};
|
|
100
31
|
}
|
|
101
|
-
|
|
102
|
-
|
|
32
|
+
/* The scene sets position: relative inline on its target; the size is given so it still fills the host. */
|
|
33
|
+
div[stage] {
|
|
34
|
+
position: absolute;
|
|
35
|
+
inset: 0;
|
|
103
36
|
width: 100%;
|
|
104
37
|
height: 100%;
|
|
105
38
|
}
|
|
106
|
-
div[gizmo] {
|
|
107
|
-
position: absolute;
|
|
108
|
-
top: 8px;
|
|
109
|
-
left: 8px;
|
|
110
|
-
display: flex;
|
|
111
|
-
gap: 2px;
|
|
112
|
-
padding: 2px;
|
|
113
|
-
border-radius: 6px;
|
|
114
|
-
background: var(--md-sys-color-surface);
|
|
115
|
-
border: 1px solid var(--md-sys-color-outline-variant);
|
|
116
|
-
}
|
|
117
|
-
div[gizmo] button {
|
|
118
|
-
font: inherit;
|
|
119
|
-
font-size: 12px;
|
|
120
|
-
padding: 3px 8px;
|
|
121
|
-
border: none;
|
|
122
|
-
border-radius: 4px;
|
|
123
|
-
background: none;
|
|
124
|
-
color: var(--md-sys-color-on-surface);
|
|
125
|
-
cursor: pointer;
|
|
126
|
-
}
|
|
127
|
-
div[gizmo] button[on] {
|
|
128
|
-
background: var(--md-sys-color-secondary-container);
|
|
129
|
-
color: var(--md-sys-color-on-secondary-container);
|
|
130
|
-
}
|
|
131
|
-
div[gizmo] button:disabled {
|
|
132
|
-
opacity: 0.4;
|
|
133
|
-
cursor: default;
|
|
134
|
-
}
|
|
135
39
|
p {
|
|
136
40
|
position: absolute;
|
|
137
41
|
inset: 45% 1rem auto;
|
|
42
|
+
margin: 0;
|
|
138
43
|
text-align: center;
|
|
139
44
|
pointer-events: none;
|
|
140
|
-
color:
|
|
45
|
+
color: #f4f4f4;
|
|
141
46
|
}
|
|
142
47
|
`; }
|
|
143
48
|
render() {
|
|
144
|
-
|
|
145
|
-
return html `<canvas aria-label="Figure 3D 모델" tabindex="0" @pointerup=${this.pick} @keydown=${this.key}></canvas
|
|
146
|
-
>${any
|
|
147
|
-
? html `<div gizmo>
|
|
148
|
-
${['translate', 'rotate', 'scale'].map((mode, i) => html `<button
|
|
149
|
-
?on=${this.mode === mode}
|
|
150
|
-
?disabled=${!this.reach[mode]}
|
|
151
|
-
title=${`${i18next.t('figure.label.gizmo-' + mode)} (${'WER'[i]})`}
|
|
152
|
-
@click=${() => (this.mode = mode)}
|
|
153
|
-
>
|
|
154
|
-
${i18next.t('figure.label.gizmo-' + mode)}
|
|
155
|
-
</button>`)}
|
|
156
|
-
</div>`
|
|
157
|
-
: null}${this.parts.length ? null : html `<p>도형을 추가해 모델링을 시작하세요.</p>`}`;
|
|
49
|
+
return html `<div stage></div>${this.parts.length ? nothing : html `<p>${i18next.t('figure.text.add-a-shape-to-start')}</p>`}`;
|
|
158
50
|
}
|
|
159
51
|
firstUpdated() {
|
|
160
|
-
this.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
super.connectedCallback();
|
|
164
|
-
if (this.hasUpdated)
|
|
165
|
-
void this.updateComplete.then(() => this.initialize());
|
|
52
|
+
this.standUp();
|
|
53
|
+
this.watcher = new ResizeObserver(() => this.scene?.resize?.());
|
|
54
|
+
this.watcher.observe(this);
|
|
166
55
|
}
|
|
167
|
-
|
|
168
|
-
if (
|
|
56
|
+
updated(changed) {
|
|
57
|
+
if (!this.hasUpdated)
|
|
169
58
|
return;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
board in the middle of a dark screen after every switch (seen 2026-09-24), so it is read again then.
|
|
177
|
-
*/
|
|
178
|
-
this.themeObserver = new MutationObserver(() => {
|
|
179
|
-
this.paintBackground();
|
|
180
|
-
this.draw();
|
|
181
|
-
});
|
|
182
|
-
this.themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
|
183
|
-
this.scene.add(this.group, new THREE.HemisphereLight(0xffffff, 0x62717d, 2.5));
|
|
184
|
-
const light = new THREE.DirectionalLight(0xffffff, 3);
|
|
185
|
-
light.position.set(1, 2, 3);
|
|
186
|
-
this.scene.add(light);
|
|
187
|
-
this.controls = new OrbitControls(this.camera, canvas);
|
|
188
|
-
this.controls.addEventListener('change', this.draw);
|
|
189
|
-
this.transform = new TransformControls(this.camera, canvas);
|
|
190
|
-
this.transform.setSpace('world');
|
|
191
|
-
this.transform.addEventListener('change', this.draw);
|
|
192
|
-
this.transform.addEventListener('dragging-changed', this.dragging);
|
|
193
|
-
this.transform.addEventListener('objectChange', this.follow);
|
|
194
|
-
this.scene.add(this.proxy, this.transform.getHelper());
|
|
195
|
-
this.observer = new ResizeObserver(() => {
|
|
196
|
-
const r = this.getBoundingClientRect();
|
|
197
|
-
if (r.width && r.height) {
|
|
198
|
-
this.renderer.setSize(r.width, r.height, false);
|
|
199
|
-
this.camera.aspect = r.width / r.height;
|
|
200
|
-
this.camera.updateProjectionMatrix();
|
|
201
|
-
this.draw();
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
this.observer.observe(this);
|
|
205
|
-
this.initialized = true;
|
|
206
|
-
this.updateGeometry(true);
|
|
59
|
+
if (changed.has('parts') || changed.has('palette') || changed.has('context'))
|
|
60
|
+
this.standUp();
|
|
61
|
+
if (changed.has('view'))
|
|
62
|
+
this.applyView();
|
|
63
|
+
if (changed.has('selected'))
|
|
64
|
+
this.showSelected();
|
|
207
65
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
66
|
+
disconnectedCallback() {
|
|
67
|
+
super.disconnectedCallback();
|
|
68
|
+
this.watcher?.disconnect();
|
|
69
|
+
this.watcher = undefined;
|
|
70
|
+
this.teardown();
|
|
71
|
+
}
|
|
72
|
+
/** The whole figure in view: the board's fit, then the 3D camera's own, as V2 did. */
|
|
73
|
+
fit() {
|
|
74
|
+
this.scene?.fit?.('ratio');
|
|
75
|
+
this.scene?.root?.fitCameraToModel?.();
|
|
213
76
|
}
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Stand the board up for this figure, or bring the one standing up to date. A new board only for another figure or
|
|
79
|
+
* one that has outgrown it -- standing up again loses the selection and the camera.
|
|
80
|
+
*/
|
|
81
|
+
standUp() {
|
|
82
|
+
if (!this.stage)
|
|
218
83
|
return;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
const body = this.group.children.find(child => child.name === this.selected);
|
|
227
|
-
if (!body || !this.reach[this.mode]) {
|
|
228
|
-
transform.detach();
|
|
229
|
-
this.draw();
|
|
84
|
+
const needed = boardFor(this.parts, this.board);
|
|
85
|
+
const outgrown = !this.board || needed.width > this.board.width || needed.height > this.board.height || needed.depth > this.board.depth;
|
|
86
|
+
if (this.scene && this.standingFor === this.context && !outgrown) {
|
|
87
|
+
this.sync();
|
|
230
88
|
return;
|
|
231
89
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
this.
|
|
236
|
-
this.proxy.scale.set(1, 1, 1);
|
|
237
|
-
this.proxy.updateMatrixWorld(true);
|
|
238
|
-
transform.setMode(this.mode);
|
|
239
|
-
/* Size follows the body's own axes; moving and turning read in the asset's axes, which the inputs are in. */
|
|
240
|
-
transform.setSpace(this.mode === 'scale' ? 'local' : 'world');
|
|
241
|
-
transform.attach(this.proxy);
|
|
242
|
-
this.draw();
|
|
243
|
-
}
|
|
244
|
-
updateGeometry(fit) {
|
|
90
|
+
this.teardown();
|
|
91
|
+
if (!this.parts.length)
|
|
92
|
+
return;
|
|
93
|
+
this.board = needed;
|
|
245
94
|
try {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
95
|
+
this.scene = create({
|
|
96
|
+
target: this.stage,
|
|
97
|
+
model: {
|
|
98
|
+
type: 'model-layer',
|
|
99
|
+
width: needed.width,
|
|
100
|
+
height: needed.height,
|
|
101
|
+
depth: needed.depth,
|
|
102
|
+
threed: true,
|
|
103
|
+
/* Two samples a pixel: MSAA alone leaves steps on the silhouettes of one asset shown large (V2). */
|
|
104
|
+
pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),
|
|
105
|
+
...sceneLook(this.view ?? DEFAULT_VIEW),
|
|
106
|
+
gridStep: niceGridStep(Math.max(needed.width, needed.height)),
|
|
107
|
+
sky: 'color',
|
|
108
|
+
fillStyle: PAPER,
|
|
109
|
+
components: this.components()
|
|
110
|
+
},
|
|
111
|
+
/* Editing mode: the camera view buttons, gizmo modes, help and keys V2's canvas had. */
|
|
112
|
+
mode: 1
|
|
113
|
+
});
|
|
114
|
+
this.standingFor = this.context;
|
|
115
|
+
this.watchScene();
|
|
116
|
+
this.scene.resize?.();
|
|
117
|
+
this.fit();
|
|
118
|
+
this.showSelected();
|
|
252
119
|
}
|
|
253
120
|
catch (error) {
|
|
254
|
-
// Do not leave another model's geometry visible after a failed context switch.
|
|
255
|
-
if (this.contextAtRender !== this.context) {
|
|
256
|
-
disposeV3Meshes(this.group);
|
|
257
|
-
this.draw();
|
|
258
|
-
}
|
|
259
121
|
this.dispatchEvent(new CustomEvent('render-error', { detail: error, bubbles: true, composed: true }));
|
|
260
122
|
}
|
|
261
123
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
124
|
+
components() {
|
|
125
|
+
return this.parts.map(part => componentOf(part, this.palette, this.board)).filter((c) => !!c);
|
|
126
|
+
}
|
|
127
|
+
/** The components standing now, by name. Not held between calls: the scene stands its board up again at times. */
|
|
128
|
+
standing() {
|
|
129
|
+
const found = new Map();
|
|
130
|
+
for (const component of this.scene?.root?.components ?? []) {
|
|
131
|
+
const name = component.state?.name;
|
|
132
|
+
if (typeof name === 'string')
|
|
133
|
+
found.set(name, component);
|
|
267
134
|
}
|
|
268
|
-
|
|
269
|
-
this.camera.position.copy(center).add(new THREE.Vector3(1, 0.8, 1).normalize().multiplyScalar(distance));
|
|
270
|
-
this.camera.near = Math.max(0.01, distance / 10000);
|
|
271
|
-
this.camera.far = Math.max(10000, distance * 100);
|
|
272
|
-
this.camera.updateProjectionMatrix();
|
|
273
|
-
this.controls?.target.copy(center);
|
|
274
|
-
this.controls?.update();
|
|
275
|
-
this.draw();
|
|
135
|
+
return found;
|
|
276
136
|
}
|
|
277
|
-
/**
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
137
|
+
/** Bring the components to the evaluation: remove the gone, write only what differs, add the new. */
|
|
138
|
+
sync() {
|
|
139
|
+
const layer = this.scene?.root;
|
|
140
|
+
if (!layer || !this.board)
|
|
141
|
+
return;
|
|
142
|
+
const wanted = new Map(this.components().map(c => [c.name, c]));
|
|
143
|
+
const standing = this.standing();
|
|
144
|
+
this.writing = true;
|
|
145
|
+
try {
|
|
146
|
+
for (const [name, component] of standing) {
|
|
147
|
+
if (wanted.has(name))
|
|
148
|
+
continue;
|
|
149
|
+
layer.removeComponent?.(component);
|
|
150
|
+
component.dispose?.();
|
|
151
|
+
}
|
|
152
|
+
for (const [name, state] of wanted) {
|
|
153
|
+
const found = standing.get(name);
|
|
154
|
+
if (found) {
|
|
155
|
+
const diff = changedKeys(found.state, state);
|
|
156
|
+
if (diff)
|
|
157
|
+
found.set(diff);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const made = Model.compile({ ...state }, layer.app);
|
|
161
|
+
if (made)
|
|
162
|
+
layer.addComponent?.(made);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
this.writing = false;
|
|
167
|
+
}
|
|
168
|
+
layer.invalidate?.();
|
|
169
|
+
}
|
|
170
|
+
/** Selection and drags, told out by the part's name -- never by its place in the list, which may change meanwhile. */
|
|
171
|
+
watchScene() {
|
|
172
|
+
const layer = this.scene?.root;
|
|
173
|
+
const root = layer?.root;
|
|
174
|
+
if (!layer || !root?.on)
|
|
175
|
+
return;
|
|
176
|
+
root.on('selected', (after) => {
|
|
177
|
+
const name = after?.[0]?.state?.name;
|
|
178
|
+
const id = typeof name === 'string' ? name : '';
|
|
179
|
+
if (id !== this.selected)
|
|
180
|
+
this.dispatchEvent(new CustomEvent('part-selected', { detail: id, bubbles: true, composed: true }));
|
|
181
|
+
});
|
|
182
|
+
/*
|
|
183
|
+
A change comes up to the board with the component in the last argument's `origin` (the scene appends
|
|
184
|
+
{ origin, name, deliverer } as it bubbles). Reading the first argument as the component does nothing (V2).
|
|
185
|
+
*/
|
|
186
|
+
layer.on?.('change', (...args) => {
|
|
187
|
+
if (this.writing)
|
|
188
|
+
return;
|
|
189
|
+
const origin = args[args.length - 1]?.origin;
|
|
190
|
+
const name = origin?.state?.name;
|
|
191
|
+
const part = typeof name === 'string' ? this.parts.find(p => p.id === name) : undefined;
|
|
192
|
+
if (!part || !this.board)
|
|
193
|
+
return;
|
|
194
|
+
const result = gizmoResultOf(part, origin.state, this.board);
|
|
195
|
+
if (!result.position && !result.rotation && !result.scale)
|
|
196
|
+
return;
|
|
197
|
+
this.dispatchEvent(new CustomEvent('part-transformed', { detail: { id: part.id, result }, bubbles: true, composed: true }));
|
|
198
|
+
/* After the editor has applied (or refused) it: a refused change springs back to where the inputs put it. */
|
|
199
|
+
setTimeout(() => this.sync());
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
showSelected() {
|
|
203
|
+
const layer = this.scene?.root;
|
|
204
|
+
const root = layer?.root;
|
|
205
|
+
if (!layer || !root)
|
|
206
|
+
return;
|
|
207
|
+
const component = this.selected ? this.standing().get(this.selected) : undefined;
|
|
208
|
+
const now = root.selected ?? [];
|
|
209
|
+
if ((now[0] ?? undefined) === component && now.length === (component ? 1 : 0))
|
|
210
|
+
return;
|
|
211
|
+
root.selected = component ? [component] : [];
|
|
212
|
+
layer.invalidate?.();
|
|
285
213
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
214
|
+
/** View settings go into the scene as they are: they are the board's own state names. Nothing is stood up again. */
|
|
215
|
+
applyView() {
|
|
216
|
+
const layer = this.scene?.root;
|
|
217
|
+
if (!layer)
|
|
218
|
+
return;
|
|
219
|
+
layer.set(sceneLook(this.view ?? DEFAULT_VIEW));
|
|
220
|
+
layer.invalidate?.();
|
|
289
221
|
}
|
|
290
|
-
|
|
291
|
-
this.
|
|
292
|
-
this.
|
|
293
|
-
this.
|
|
294
|
-
this.
|
|
295
|
-
this.transform?.dispose();
|
|
296
|
-
this.transform = undefined;
|
|
297
|
-
disposeV3Meshes(this.group);
|
|
298
|
-
this.renderer?.dispose();
|
|
299
|
-
this.renderer = undefined;
|
|
300
|
-
this.scene.clear();
|
|
301
|
-
this.initialized = false;
|
|
302
|
-
super.disconnectedCallback();
|
|
222
|
+
teardown() {
|
|
223
|
+
this.scene?.dispose?.();
|
|
224
|
+
this.scene = undefined;
|
|
225
|
+
this.standingFor = undefined;
|
|
226
|
+
this.board = undefined;
|
|
303
227
|
}
|
|
304
228
|
};
|
|
305
229
|
__decorate([
|
|
@@ -323,9 +247,13 @@ __decorate([
|
|
|
323
247
|
__metadata("design:type", Object)
|
|
324
248
|
], FigureV3Canvas.prototype, "reach", void 0);
|
|
325
249
|
__decorate([
|
|
326
|
-
property(),
|
|
327
|
-
__metadata("design:type",
|
|
328
|
-
], FigureV3Canvas.prototype, "
|
|
250
|
+
property({ attribute: false }),
|
|
251
|
+
__metadata("design:type", Object)
|
|
252
|
+
], FigureV3Canvas.prototype, "view", void 0);
|
|
253
|
+
__decorate([
|
|
254
|
+
query('div[stage]'),
|
|
255
|
+
__metadata("design:type", HTMLDivElement)
|
|
256
|
+
], FigureV3Canvas.prototype, "stage", void 0);
|
|
329
257
|
FigureV3Canvas = __decorate([
|
|
330
258
|
customElement('figure-v3-canvas')
|
|
331
259
|
], FigureV3Canvas);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v3-canvas.js","sourceRoot":"","sources":["../../client/modeller/v3-canvas.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,EAAuB,MAAM,KAAK,CAAA;AAChE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,8CAA8C,CAAA;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,kDAAkD,CAAA;AAEpF,OAAO,EAAE,eAAe,EAAE,eAAe,EAAqB,MAAM,kBAAkB,CAAA;AAEtF,iGAAiG;AAE1F,IAAM,cAAc,GAApB,MAAM,cAAe,SAAQ,UAAU;IAAvC;;QAwD2B,UAAK,GAAmB,EAAE,CAAA;QAC1B,YAAO,GAA2B,EAAE,CAAA;QACxD,YAAO,GAAG,EAAE,CAAA;QACxB,4GAA4G;QAChG,aAAQ,GAAG,EAAE,CAAA;QACO,UAAK,GAAe,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;QACrG,6DAA6D;QACjD,SAAI,GAAqC,WAAW,CAAA;QAEhE,8GAA8G;QAC7F,UAAK,GAAG,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAA;QAE7C,yGAAyG;QACjG,gBAAW,GAAG,KAAK,CAAA;QAEV,UAAK,GAAG,IAAI,KAAK,CAAC,KAAK,EAAE,CAAA;QACzB,UAAK,GAAG,IAAI,KAAK,CAAC,KAAK,EAAE,CAAA;QACzB,WAAM,GAAG,IAAI,KAAK,CAAC,iBAAiB,CAAC,EAAE,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,CAAA;QAIlE,gBAAW,GAAG,KAAK,CAAA;QACnB,oBAAe,GAAG,EAAE,CAAA;QAmBpB,QAAG,GAAG,CAAC,KAAoB,EAAE,EAAE;YACrC,MAAM,IAAI,GAAI,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAY,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,EAAqB,CAAC,CAAA;YAC/G,IAAI,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChD,CAAC,CAAA;QAoFO,aAAQ,GAAG,CAAC,KAAyB,EAAE,EAAE;YAC/C,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,CAAA;YACxB,IAAI,IAAI,CAAC,QAAQ;gBAAE,IAAI,CAAC,QAAQ,CAAC,OAAO,GAAG,CAAC,EAAE,CAAA;YAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,QAAQ,CAA2B,CAAA;YACtG,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC;gBACf,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,CAAA;gBACzB,IAAI,CAAC,SAAS,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,CAAA;gBACtF,OAAM;YACR,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAA;YAC5B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;YAC1B,IAAI,CAAC,KAAK;gBAAE,OAAM;YAClB,uFAAuF;YACvF,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;YACvB,UAAU,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,CAAC,CAAA;YAC5C,MAAM,MAAM,GAAgB,EAAE,CAAA;YAC9B,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW;gBAAE,MAAM,CAAC,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,EAA8B,CAAA;YAC1G,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC3B,wGAAwG;gBACxG,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;gBAC/E,MAAM,CAAC,QAAQ,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,CAA6B,CAAA;YACzG,CAAC;YACD,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;gBAAE,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAA8B,CAAA;YAChG,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QACnI,CAAC,CAAA;QACD,mHAAmH;QAC3G,WAAM,GAAG,GAAG,EAAE;YACpB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAA;YAC5B,IAAI,CAAC,KAAK;gBAAE,OAAM;YAClB,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,CAAA;YACzB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC,CAAA;YAC9E,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;YAClD,KAAK,CAAC,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAA;YACxC,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAA;QACpC,CAAC,CAAA;QA2CO,SAAI,GAAG,GAAG,EAAE;YAClB,IAAI,IAAI,CAAC,QAAQ;gBAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;QAClE,CAAC,CAAA;QACO,SAAI,GAAG,CAAC,KAAmB,EAAE,EAAE;YACrC,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,SAAS,EAAE,QAAQ;gBAAE,OAAM;YACxD,MAAM,CAAC,GAAI,KAAK,CAAC,aAAmC,CAAC,qBAAqB,EAAE,EAC1E,GAAG,GAAG,IAAI,KAAK,CAAC,SAAS,EAAE,CAAA;YAC7B,GAAG,CAAC,aAAa,CACf,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAC9G,IAAI,CAAC,MAAM,CACZ,CAAA;YACD,MAAM,GAAG,GAAG,GAAG,CAAC,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;YACxD,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CACpG,CAAA;QACH,CAAC,CAAA;IAmBH,CAAC;aAtSQ,WAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDlB,AAtDY,CAsDZ;IAwBD,MAAM;QACJ,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QAC5F,OAAO,IAAI,CAAA,6DAA6D,IAAI,CAAC,IAAI,aAAa,IAAI,CAAC,GAAG;SACjG,GAAG;YACJ,CAAC,CAAC,IAAI,CAAA;cACC,CAAC,WAAW,EAAE,QAAQ,EAAE,OAAO,CAAW,CAAC,GAAG,CAC/C,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAA;sBACT,IAAI,CAAC,IAAI,KAAK,IAAI;4BACZ,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;wBACrB,GAAG,OAAO,CAAC,CAAC,CAAC,qBAAqB,GAAG,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,GAAG;yBACzD,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;;kBAE/B,OAAO,CAAC,CAAC,CAAC,qBAAqB,GAAG,IAAI,CAAC;wBACjC,CACX;iBACI;YACT,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA,4BAA4B,EAAE,CAAA;IAC5E,CAAC;IAKS,YAAY;QACpB,IAAI,CAAC,UAAU,EAAE,CAAA;IACnB,CAAC;IACD,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAA;QACzB,IAAI,IAAI,CAAC,UAAU;YAAE,KAAK,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAA;IAC7E,CAAC;IACO,UAAU;QAChB,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC,IAAI,CAAC,WAAW;YAAE,OAAM;QACjD,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,QAAQ,CAAE,CAAA;QACvD,IAAI,CAAC,QAAQ,GAAG,IAAI,KAAK,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,qBAAqB,EAAE,IAAI,EAAE,CAAC,CAAA;QACjG,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,CAAA;QAC1D,IAAI,CAAC,eAAe,EAAE,CAAA;QACtB;;;UAGE;QACF,IAAI,CAAC,aAAa,GAAG,IAAI,gBAAgB,CAAC,GAAG,EAAE;YAC7C,IAAI,CAAC,eAAe,EAAE,CAAA;YACtB,IAAI,CAAC,IAAI,EAAE,CAAA;QACb,CAAC,CAAC,CAAA;QACF,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QAC3F,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,KAAK,CAAC,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAA;QAC9E,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;QACrD,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;QAC3B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QACrB,IAAI,CAAC,QAAQ,GAAG,IAAI,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QACtD,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;QACnD,IAAI,CAAC,SAAS,GAAG,IAAI,iBAAiB,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC3D,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;QAChC,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;QACpD,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;QAClE,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;QAC5D,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,CAAC,CAAA;QACtD,IAAI,CAAC,QAAQ,GAAG,IAAI,cAAc,CAAC,GAAG,EAAE;YACtC,MAAM,CAAC,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAA;YACtC,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;gBACxB,IAAI,CAAC,QAAS,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;gBAChD,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,MAAM,CAAA;gBACvC,IAAI,CAAC,MAAM,CAAC,sBAAsB,EAAE,CAAA;gBACpC,IAAI,CAAC,IAAI,EAAE,CAAA;YACb,CAAC;QACH,CAAC,CAAC,CAAA;QACF,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QAC3B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;QACvB,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAA;IAC3B,CAAC;IACS,OAAO,CAAC,OAAuB;QACvC,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAChG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,eAAe,KAAK,IAAI,CAAC,OAAO,CAAC,CAAA;QAC5D,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAAE,IAAI,CAAC,UAAU,EAAE,CAAA;IAC5H,CAAC;IACD,sFAAsF;IAC9E,UAAU;QAChB,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAA;QAChC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS;YAAE,OAAM;QACxC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAI,CAAC,WAAW,EAAE,QAAQ,EAAE,OAAO,CAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;YACxF,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;gBACjC,IAAI,CAAC,IAAI,GAAG,KAAK,CAAA;gBACjB,OAAM;YACR,CAAC;QACH,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,QAAQ,CAA2B,CAAA;QACtG,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACpC,SAAS,CAAC,MAAM,EAAE,CAAA;YAClB,IAAI,CAAC,IAAI,EAAE,CAAA;YACX,OAAM;QACR,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,EAClC,UAAU,GAAG,IAAI,KAAK,CAAC,UAAU,EAAE,EACnC,KAAK,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAA;QAC7B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,UAAU,EAAE,KAAK,CAAC,CAAA;QAClD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QAClC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QACtC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;QAC7B,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAA;QAClC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC5B,6GAA6G;QAC7G,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;QAC7D,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC5B,IAAI,CAAC,IAAI,EAAE,CAAA;IACb,CAAC;IAoCO,cAAc,CAAC,GAAY;QACjC,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAA;YACjD,eAAe,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;YACrD,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,OAAO,CAAA;YACnC,IAAI,GAAG,IAAI,QAAQ;gBAAE,IAAI,CAAC,GAAG,EAAE,CAAA;YAC/B,IAAI,CAAC,IAAI,EAAE,CAAA;QACb,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,+EAA+E;YAC/E,IAAI,IAAI,CAAC,eAAe,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;gBAC1C,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;gBAC3B,IAAI,CAAC,IAAI,EAAE,CAAA;YACb,CAAC;YACD,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QACvG,CAAC;IACH,CAAC;IACD,GAAG;QACD,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,EACvD,MAAM,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,EAC5B,IAAI,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAA;QAC5B,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;YACtB,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;YACxB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QACtB,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC,EAC7C,QAAQ,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAA;QACrF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAA;QACxG,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,GAAG,KAAK,CAAC,CAAA;QACnD,IAAI,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,GAAG,GAAG,CAAC,CAAA;QACjD,IAAI,CAAC,MAAM,CAAC,sBAAsB,EAAE,CAAA;QACpC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAClC,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;QACvB,IAAI,CAAC,IAAI,EAAE,CAAA;IACb,CAAC;IACD;;;OAGG;IACK,eAAe;QACrB,MAAM,MAAM,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC,eAAe,CAAA;QACrD,IAAI,MAAM,IAAI,MAAM,KAAK,kBAAkB,IAAI,MAAM,KAAK,aAAa;YAAE,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IAC1H,CAAC;IAiBD,SAAS;QACP,IAAI,CAAC,IAAI,EAAE,CAAA;QACX,OAAO,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,SAAS,CAAC,WAAW,CAAC,CAAA;IACzD,CAAC;IACD,oBAAoB;QAClB,IAAI,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAA;QAC3B,IAAI,CAAC,aAAa,EAAE,UAAU,EAAE,CAAA;QAChC,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;QACxB,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAA;QACxB,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,CAAA;QACzB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;QAC1B,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC3B,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;QACxB,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAA;QACzB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;QAClB,IAAI,CAAC,WAAW,GAAG,KAAK,CAAA;QACxB,KAAK,CAAC,oBAAoB,EAAE,CAAA;IAC9B,CAAC;;AA9O+B;IAA/B,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;;6CAA2B;AAC1B;IAA/B,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;;+CAAqC;AACxD;IAAX,QAAQ,EAAE;;+CAAa;AAEZ;IAAX,QAAQ,EAAE;;gDAAc;AACO;IAA/B,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;;6CAAsE;AAEzF;IAAX,QAAQ,EAAE;;4CAAqD;AA/DrD,cAAc;IAD1B,aAAa,CAAC,kBAAkB,CAAC;GACrB,cAAc,CAuS1B","sourcesContent":["import { LitElement, html, css, type PropertyValues } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\nimport * as THREE from 'three'\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'\nimport { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'\nimport type { GizmoReach, GizmoResult } from './v3-gizmo.js'\nimport { replaceV3Meshes, disposeV3Meshes, type V3RenderPart } from './v3-renderer.js'\n\n/** Renders evaluated V3 geometry directly. There is no parts/base or things-scene conversion. */\n@customElement('figure-v3-canvas')\nexport class FigureV3Canvas extends LitElement {\n static styles = css`\n :host {\n display: block;\n min-width: 0;\n min-height: 0;\n position: relative;\n /*\n The working board is one step above the surface in both themes (UX, 2026-09-24): surface-container is 238\n in light and 31 in dark. The 3D background is read from this same value (paintBackground), so the\n first paint and the scene agree and there is no flash between them.\n */\n background: var(--md-sys-color-surface-container);\n }\n canvas {\n display: block;\n width: 100%;\n height: 100%;\n }\n div[gizmo] {\n position: absolute;\n top: 8px;\n left: 8px;\n display: flex;\n gap: 2px;\n padding: 2px;\n border-radius: 6px;\n background: var(--md-sys-color-surface);\n border: 1px solid var(--md-sys-color-outline-variant);\n }\n div[gizmo] button {\n font: inherit;\n font-size: 12px;\n padding: 3px 8px;\n border: none;\n border-radius: 4px;\n background: none;\n color: var(--md-sys-color-on-surface);\n cursor: pointer;\n }\n div[gizmo] button[on] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n div[gizmo] button:disabled {\n opacity: 0.4;\n cursor: default;\n }\n p {\n position: absolute;\n inset: 45% 1rem auto;\n text-align: center;\n pointer-events: none;\n color: var(--md-sys-color-on-surface-variant);\n }\n `\n @property({ attribute: false }) parts: V3RenderPart[] = []\n @property({ attribute: false }) palette: Record<string, string> = {}\n @property() context = ''\n /** The selected part, and what the gizmo may write for it (`gizmoReachOf`). Nothing reachable, no gizmo. */\n @property() selected = ''\n @property({ attribute: false }) reach: GizmoReach = { translate: false, rotate: false, scale: false }\n /** Move · turn · size — W · E · R, as in the V2 modeller. */\n @property() mode: 'translate' | 'rotate' | 'scale' = 'translate'\n private transform?: TransformControls\n /** What the gizmo holds: a stand-in at the body's origin, so the body's own matrix stays the evaluation's. */\n private readonly proxy = new THREE.Object3D()\n private dragStart?: { proxy: THREE.Matrix4; mesh: THREE.Matrix4; body: THREE.Mesh }\n /** The pointerup that ends a drag is not a pick — releasing off the body would otherwise deselect it. */\n private justDragged = false\n private renderer?: THREE.WebGLRenderer\n private readonly scene = new THREE.Scene()\n private readonly group = new THREE.Group()\n private readonly camera = new THREE.PerspectiveCamera(40, 1, 0.1, 1000000)\n private controls?: OrbitControls\n private observer?: ResizeObserver\n private themeObserver?: MutationObserver\n private initialized = false\n private contextAtRender = ''\n render() {\n const any = this.selected && (this.reach.translate || this.reach.rotate || this.reach.scale)\n return html`<canvas aria-label=\"Figure 3D 모델\" tabindex=\"0\" @pointerup=${this.pick} @keydown=${this.key}></canvas\n >${any\n ? html`<div gizmo>\n ${(['translate', 'rotate', 'scale'] as const).map(\n (mode, i) => html`<button\n ?on=${this.mode === mode}\n ?disabled=${!this.reach[mode]}\n title=${`${i18next.t('figure.label.gizmo-' + mode)} (${'WER'[i]})`}\n @click=${() => (this.mode = mode)}\n >\n ${i18next.t('figure.label.gizmo-' + mode)}\n </button>`\n )}\n </div>`\n : null}${this.parts.length ? null : html`<p>도형을 추가해 모델링을 시작하세요.</p>`}`\n }\n private key = (event: KeyboardEvent) => {\n const mode = ({ w: 'translate', e: 'rotate', r: 'scale' } as const)[event.key.toLowerCase() as 'w' | 'e' | 'r']\n if (mode && this.reach[mode]) this.mode = mode\n }\n protected firstUpdated() {\n this.initialize()\n }\n connectedCallback() {\n super.connectedCallback()\n if (this.hasUpdated) void this.updateComplete.then(() => this.initialize())\n }\n private initialize() {\n if (this.initialized || !this.isConnected) return\n const canvas = this.renderRoot.querySelector('canvas')!\n this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, preserveDrawingBuffer: true })\n this.renderer.setPixelRatio(Math.min(devicePixelRatio, 2))\n this.paintBackground()\n /*\n The app switches theme by the body's class (light · dark). Reading the colour once at start left a light\n board in the middle of a dark screen after every switch (seen 2026-09-24), so it is read again then.\n */\n this.themeObserver = new MutationObserver(() => {\n this.paintBackground()\n this.draw()\n })\n this.themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] })\n this.scene.add(this.group, new THREE.HemisphereLight(0xffffff, 0x62717d, 2.5))\n const light = new THREE.DirectionalLight(0xffffff, 3)\n light.position.set(1, 2, 3)\n this.scene.add(light)\n this.controls = new OrbitControls(this.camera, canvas)\n this.controls.addEventListener('change', this.draw)\n this.transform = new TransformControls(this.camera, canvas)\n this.transform.setSpace('world')\n this.transform.addEventListener('change', this.draw)\n this.transform.addEventListener('dragging-changed', this.dragging)\n this.transform.addEventListener('objectChange', this.follow)\n this.scene.add(this.proxy, this.transform.getHelper())\n this.observer = new ResizeObserver(() => {\n const r = this.getBoundingClientRect()\n if (r.width && r.height) {\n this.renderer!.setSize(r.width, r.height, false)\n this.camera.aspect = r.width / r.height\n this.camera.updateProjectionMatrix()\n this.draw()\n }\n })\n this.observer.observe(this)\n this.initialized = true\n this.updateGeometry(true)\n }\n protected updated(changed: PropertyValues) {\n if (this.initialized && (changed.has('parts') || changed.has('palette') || changed.has('context')))\n this.updateGeometry(this.contextAtRender !== this.context)\n if (this.initialized && ['parts', 'selected', 'reach', 'mode', 'context'].some(key => changed.has(key))) this.placeGizmo()\n }\n /** Puts the gizmo on the selected body, in a mode it may write — or takes it away. */\n private placeGizmo() {\n const transform = this.transform\n if (!transform || this.dragStart) return\n if (!this.reach[this.mode]) {\n const first = (['translate', 'rotate', 'scale'] as const).find(mode => this.reach[mode])\n if (first && first !== this.mode) {\n this.mode = first\n return\n }\n }\n const body = this.group.children.find(child => child.name === this.selected) as THREE.Mesh | undefined\n if (!body || !this.reach[this.mode]) {\n transform.detach()\n this.draw()\n return\n }\n const position = new THREE.Vector3(),\n quaternion = new THREE.Quaternion(),\n scale = new THREE.Vector3()\n body.matrix.decompose(position, quaternion, scale)\n this.proxy.position.copy(position)\n this.proxy.quaternion.copy(quaternion)\n this.proxy.scale.set(1, 1, 1)\n this.proxy.updateMatrixWorld(true)\n transform.setMode(this.mode)\n /* Size follows the body's own axes; moving and turning read in the asset's axes, which the inputs are in. */\n transform.setSpace(this.mode === 'scale' ? 'local' : 'world')\n transform.attach(this.proxy)\n this.draw()\n }\n private dragging = (event: { value: unknown }) => {\n const on = !!event.value\n if (this.controls) this.controls.enabled = !on\n const body = this.group.children.find(child => child.name === this.selected) as THREE.Mesh | undefined\n if (on && body) {\n this.proxy.updateMatrix()\n this.dragStart = { proxy: this.proxy.matrix.clone(), mesh: body.matrix.clone(), body }\n return\n }\n const start = this.dragStart\n this.dragStart = undefined\n if (!start) return\n /* Either listener may see the release first; the flag lasts past both, then clears. */\n this.justDragged = true\n setTimeout(() => (this.justDragged = false))\n const result: GizmoResult = {}\n if (this.mode === 'translate') result.position = this.proxy.position.toArray() as [number, number, number]\n if (this.mode === 'rotate') {\n /* rigid@1 composes Rx·Ry·Rz — three's 'XYZ' order — so the angles come back as the inputs mean them. */\n const euler = new THREE.Euler().setFromQuaternion(this.proxy.quaternion, 'XYZ')\n result.rotation = [euler.x, euler.y, euler.z].map(THREE.MathUtils.radToDeg) as [number, number, number]\n }\n if (this.mode === 'scale') result.scale = this.proxy.scale.toArray() as [number, number, number]\n this.dispatchEvent(new CustomEvent('part-transformed', { detail: { id: this.selected, result }, bubbles: true, composed: true }))\n }\n /** While dragging, the body follows the stand-in; the next evaluation replaces it with where the inputs put it. */\n private follow = () => {\n const start = this.dragStart\n if (!start) return\n this.proxy.updateMatrix()\n const delta = this.proxy.matrix.clone().multiply(start.proxy.clone().invert())\n start.body.matrix.copy(delta.multiply(start.mesh))\n start.body.matrixWorldNeedsUpdate = true\n this.group.updateMatrixWorld(true)\n }\n private updateGeometry(fit: boolean) {\n try {\n const wasEmpty = this.group.children.length === 0\n replaceV3Meshes(this.group, this.parts, this.palette)\n this.contextAtRender = this.context\n if (fit || wasEmpty) this.fit()\n this.draw()\n } catch (error) {\n // Do not leave another model's geometry visible after a failed context switch.\n if (this.contextAtRender !== this.context) {\n disposeV3Meshes(this.group)\n this.draw()\n }\n this.dispatchEvent(new CustomEvent('render-error', { detail: error, bubbles: true, composed: true }))\n }\n }\n fit() {\n const bounds = new THREE.Box3().setFromObject(this.group),\n center = new THREE.Vector3(),\n size = new THREE.Vector3()\n if (!bounds.isEmpty()) {\n bounds.getCenter(center)\n bounds.getSize(size)\n }\n const radius = Math.max(size.length() / 2, 100),\n distance = (radius / Math.sin(THREE.MathUtils.degToRad(this.camera.fov / 2))) * 1.3\n this.camera.position.copy(center).add(new THREE.Vector3(1, 0.8, 1).normalize().multiplyScalar(distance))\n this.camera.near = Math.max(0.01, distance / 10000)\n this.camera.far = Math.max(10000, distance * 100)\n this.camera.updateProjectionMatrix()\n this.controls?.target.copy(center)\n this.controls?.update()\n this.draw()\n }\n /**\n * The scene's background, taken from the host's computed background — the token resolved and normalised to\n * `rgb(r, g, b)`, which THREE.Color reads. The token's own text may use the space-separated form it does not.\n */\n private paintBackground() {\n const colour = getComputedStyle(this).backgroundColor\n if (colour && colour !== 'rgba(0, 0, 0, 0)' && colour !== 'transparent') this.scene.background = new THREE.Color(colour)\n }\n private draw = () => {\n if (this.renderer) this.renderer.render(this.scene, this.camera)\n }\n private pick = (event: PointerEvent) => {\n if (this.justDragged || this.transform?.dragging) return\n const r = (event.currentTarget as HTMLCanvasElement).getBoundingClientRect(),\n ray = new THREE.Raycaster()\n ray.setFromCamera(\n new THREE.Vector2(((event.clientX - r.left) / r.width) * 2 - 1, (-(event.clientY - r.top) / r.height) * 2 + 1),\n this.camera\n )\n const hit = ray.intersectObjects(this.group.children)[0]\n this.dispatchEvent(\n new CustomEvent('part-selected', { detail: hit?.object.name ?? '', bubbles: true, composed: true })\n )\n }\n thumbnail() {\n this.draw()\n return this.renderer?.domElement.toDataURL('image/png')\n }\n disconnectedCallback() {\n this.observer?.disconnect()\n this.themeObserver?.disconnect()\n this.controls?.dispose()\n this.transform?.detach()\n this.transform?.dispose()\n this.transform = undefined\n disposeV3Meshes(this.group)\n this.renderer?.dispose()\n this.renderer = undefined\n this.scene.clear()\n this.initialized = false\n super.disconnectedCallback()\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"v3-canvas.js","sourceRoot":"","sources":["../../client/modeller/v3-canvas.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css, nothing, unsafeCSS, type PropertyValues } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\nimport { create, Model, niceGridStep } from '@hatiolab/things-scene'\n\nimport type { GizmoReach } from './v3-gizmo.js'\nimport type { V3RenderPart } from './v3-renderer.js'\nimport { DEFAULT_VIEW, sceneLook, WORKBENCH, type ViewSettings } from './figure-view.js'\nimport { boardFor, changedKeys, componentOf, gizmoResultOf, type Board } from './v3-stage-model.js'\n\n/*\n * The V3 modeller's canvas -- a things-scene board in editing mode, as the V2 modeller's was.\n *\n * Each body figure-model evaluates stands on the board as a `figure-v3-part` component, so everything the V2 canvas\n * had comes from the scene, not from code of our own: picking and the orange outline, the gizmo with its snapping and\n * live readout, box and shift selection, the camera (damping, zoom toward the cursor, double-click to fit, the six\n * view presets and their keys), the studio environment, the coordinate ground, lights and shadows, supersampling.\n * An earlier canvas drew the bodies with three.js directly and lost all of that (user, 2026-09-24: \"why nerf it?\").\n *\n * What the gizmo writes back to a component comes out as `part-transformed` in the inputs' terms (`v3-stage-model`),\n * and the editor turns it into one step of the history; the next evaluation puts the component where the inputs say,\n * so a change a part cannot take (a fastened part moved) springs back.\n */\n\ninterface SceneComponent {\n state: Record<string, unknown>\n set: (props: Record<string, unknown>) => void\n dispose?: () => void\n}\ninterface SceneLayer extends SceneComponent {\n components?: SceneComponent[]\n root?: { on?: (event: string, handler: (...args: unknown[]) => void) => void; selected?: SceneComponent[] }\n on?: (event: string, handler: (...args: unknown[]) => void) => void\n addComponent?: (component: SceneComponent) => void\n removeComponent?: (component: SceneComponent) => void\n invalidate?: () => void\n fitCameraToModel?: () => void\n app?: unknown\n}\ninterface SceneHandle {\n dispose?: () => void\n resize?: () => void\n fit?: (type?: string) => void\n root?: SceneLayer\n}\n\n/* The 2D side of the board, which the author never sees on this page but the scene paints. */\nconst PAPER = '#ffffff'\n\n@customElement('figure-v3-canvas')\nexport class FigureV3Canvas extends LitElement {\n static styles = css`\n :host {\n position: relative;\n display: flex;\n min-height: 0;\n overflow: hidden;\n background-color: ${unsafeCSS(WORKBENCH)};\n }\n /* The scene sets position: relative inline on its target; the size is given so it still fills the host. */\n div[stage] {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n }\n p {\n position: absolute;\n inset: 45% 1rem auto;\n margin: 0;\n text-align: center;\n pointer-events: none;\n color: #f4f4f4;\n }\n `\n @property({ attribute: false }) parts: V3RenderPart[] = []\n @property({ attribute: false }) palette: Record<string, string> = {}\n /** Which figure is open. Another one stands the board up again. */\n @property() context = ''\n @property() selected = ''\n /** What the gizmo may write for the selected part; a change beyond it is not applied and the part springs back. */\n @property({ attribute: false }) reach: GizmoReach = { translate: false, rotate: false, scale: false }\n @property({ attribute: false }) view: ViewSettings = DEFAULT_VIEW\n\n @query('div[stage]') private stage!: HTMLDivElement\n\n private scene?: SceneHandle\n private board?: Board & { depth: number }\n private standingFor?: string\n private watcher?: ResizeObserver\n /** Set while this canvas writes to components, so the scene's echo of it is not taken for a drag. */\n private writing = false\n\n render() {\n return html`<div stage></div>${this.parts.length ? nothing : html`<p>${i18next.t('figure.text.add-a-shape-to-start')}</p>`}`\n }\n\n firstUpdated() {\n this.standUp()\n this.watcher = new ResizeObserver(() => this.scene?.resize?.())\n this.watcher.observe(this)\n }\n\n protected updated(changed: PropertyValues) {\n if (!this.hasUpdated) return\n if (changed.has('parts') || changed.has('palette') || changed.has('context')) this.standUp()\n if (changed.has('view')) this.applyView()\n if (changed.has('selected')) this.showSelected()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.watcher?.disconnect()\n this.watcher = undefined\n this.teardown()\n }\n\n /** The whole figure in view: the board's fit, then the 3D camera's own, as V2 did. */\n fit() {\n this.scene?.fit?.('ratio')\n this.scene?.root?.fitCameraToModel?.()\n }\n\n /**\n * Stand the board up for this figure, or bring the one standing up to date. A new board only for another figure or\n * one that has outgrown it -- standing up again loses the selection and the camera.\n */\n private standUp() {\n if (!this.stage) return\n const needed = boardFor(this.parts, this.board)\n const outgrown = !this.board || needed.width > this.board.width || needed.height > this.board.height || needed.depth > this.board.depth\n if (this.scene && this.standingFor === this.context && !outgrown) {\n this.sync()\n return\n }\n this.teardown()\n if (!this.parts.length) return\n this.board = needed\n try {\n this.scene = create({\n target: this.stage,\n model: {\n type: 'model-layer',\n width: needed.width,\n height: needed.height,\n depth: needed.depth,\n threed: true,\n /* Two samples a pixel: MSAA alone leaves steps on the silhouettes of one asset shown large (V2). */\n pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),\n ...sceneLook(this.view ?? DEFAULT_VIEW),\n gridStep: niceGridStep(Math.max(needed.width, needed.height)),\n sky: 'color',\n fillStyle: PAPER,\n components: this.components()\n },\n /* Editing mode: the camera view buttons, gizmo modes, help and keys V2's canvas had. */\n mode: 1\n }) as SceneHandle\n this.standingFor = this.context\n this.watchScene()\n this.scene.resize?.()\n this.fit()\n this.showSelected()\n } catch (error) {\n this.dispatchEvent(new CustomEvent('render-error', { detail: error, bubbles: true, composed: true }))\n }\n }\n\n private components(): Record<string, unknown>[] {\n return this.parts.map(part => componentOf(part, this.palette, this.board!)).filter((c): c is Record<string, unknown> => !!c)\n }\n\n /** The components standing now, by name. Not held between calls: the scene stands its board up again at times. */\n private standing(): 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 /** Bring the components to the evaluation: remove the gone, write only what differs, add the new. */\n private sync() {\n const layer = this.scene?.root\n if (!layer || !this.board) return\n const wanted = new Map(this.components().map(c => [c.name as string, c]))\n const standing = this.standing()\n this.writing = true\n try {\n for (const [name, component] of standing) {\n if (wanted.has(name)) continue\n layer.removeComponent?.(component)\n component.dispose?.()\n }\n for (const [name, state] of wanted) {\n const found = standing.get(name)\n if (found) {\n const diff = changedKeys(found.state, state)\n if (diff) found.set(diff)\n continue\n }\n const made = Model.compile({ ...state }, layer.app as never) as SceneComponent | null\n if (made) layer.addComponent?.(made)\n }\n } finally {\n this.writing = false\n }\n layer.invalidate?.()\n }\n\n /** Selection and drags, told out by the part's name -- never by its place in the list, which may change meanwhile. */\n private watchScene() {\n const layer = this.scene?.root\n const root = layer?.root\n if (!layer || !root?.on) return\n root.on('selected', (after: unknown) => {\n const name = (after as SceneComponent[] | undefined)?.[0]?.state?.name\n const id = typeof name === 'string' ? name : ''\n if (id !== this.selected) this.dispatchEvent(new CustomEvent('part-selected', { detail: id, bubbles: true, composed: true }))\n })\n /*\n A change comes up to the board with the component in the last argument's `origin` (the scene appends\n { origin, name, deliverer } as it bubbles). Reading the first argument as the component does nothing (V2).\n */\n layer.on?.('change', (...args: unknown[]) => {\n if (this.writing) return\n const origin = (args[args.length - 1] as { origin?: SceneComponent } | undefined)?.origin\n const name = origin?.state?.name\n const part = typeof name === 'string' ? this.parts.find(p => p.id === name) : undefined\n if (!part || !this.board) return\n const result = gizmoResultOf(part, origin!.state, this.board)\n if (!result.position && !result.rotation && !result.scale) return\n this.dispatchEvent(new CustomEvent('part-transformed', { detail: { id: part.id, result }, bubbles: true, composed: true }))\n /* After the editor has applied (or refused) it: a refused change springs back to where the inputs put it. */\n setTimeout(() => this.sync())\n })\n }\n\n private showSelected() {\n const layer = this.scene?.root\n const root = layer?.root\n if (!layer || !root) return\n const component = this.selected ? this.standing().get(this.selected) : undefined\n const now = root.selected ?? []\n if ((now[0] ?? undefined) === component && now.length === (component ? 1 : 0)) return\n root.selected = component ? [component] : []\n layer.invalidate?.()\n }\n\n /** View settings go into the scene as they are: they are the board's own state names. Nothing is stood up again. */\n private applyView() {\n const layer = this.scene?.root\n if (!layer) return\n layer.set(sceneLook(this.view ?? DEFAULT_VIEW))\n layer.invalidate?.()\n }\n\n private teardown() {\n this.scene?.dispose?.()\n this.scene = undefined\n this.standingFor = undefined\n this.board = undefined\n }\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { V3Asset, V3InputChange } from '@hatiolab/figure-model';
|
|
2
|
+
export interface ClipKey {
|
|
3
|
+
seconds: number;
|
|
4
|
+
value: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ClipTrack {
|
|
7
|
+
state: string;
|
|
8
|
+
label: string;
|
|
9
|
+
unit: string;
|
|
10
|
+
min: number;
|
|
11
|
+
max: number;
|
|
12
|
+
keys: ClipKey[];
|
|
13
|
+
interpolation: 'linear' | 'step';
|
|
14
|
+
loop: 'wrap' | 'hold';
|
|
15
|
+
accumulates: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface Clip {
|
|
18
|
+
name: string;
|
|
19
|
+
duration: number;
|
|
20
|
+
tracks: ClipTrack[];
|
|
21
|
+
}
|
|
22
|
+
export declare function clipsOf(asset: V3Asset): Clip[];
|
|
23
|
+
/** The controls no clip plays yet -- the ones a new track can take. One driver per control. */
|
|
24
|
+
export declare function undrivenOf(asset: V3Asset): string[];
|
|
25
|
+
/** The pose a clip gives at a moment, as one batch of state changes -- one step of the history. */
|
|
26
|
+
export declare function poseAt(asset: V3Asset, clip: string, seconds: number): V3InputChange[];
|
|
27
|
+
/** A key added at a moment, replacing one already there; the keys stay in time order. */
|
|
28
|
+
export declare function withKey(keys: readonly ClipKey[], key: ClipKey): ClipKey[];
|
|
29
|
+
/** A track's keys without one; a track keeps at least its two ends, as the format asks. */
|
|
30
|
+
export declare function withoutKey(keys: readonly ClipKey[], index: number): ClipKey[] | null;
|
|
31
|
+
/** The edit that writes a track back. */
|
|
32
|
+
export declare const trackEdit: (clip: Clip, track: ClipTrack, patch?: Partial<Pick<ClipTrack, "keys" | "loop" | "interpolation">>) => {
|
|
33
|
+
kind: "set-driver";
|
|
34
|
+
state: string;
|
|
35
|
+
clip: string;
|
|
36
|
+
keys: ClipKey[];
|
|
37
|
+
loop: "wrap" | "hold";
|
|
38
|
+
interpolation: "linear" | "step";
|
|
39
|
+
};
|