@things-factory/figure-ui 10.1.46 → 10.1.48
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 +254 -262
- package/client/modeller/v3-clips.ts +89 -0
- package/client/modeller/v3-controls.ts +54 -0
- package/client/modeller/v3-editor.ts +566 -223
- package/client/modeller/v3-fields.ts +84 -0
- package/client/modeller/v3-help.ts +41 -0
- package/client/modeller/v3-joint-fan.ts +292 -0
- package/client/modeller/v3-keys.ts +34 -0
- package/client/modeller/v3-links.ts +101 -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/modeller/v3-thumbnail.ts +8 -4
- 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 +4 -1
- 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 +4 -1
- 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 +1 -1
- 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 +44 -37
- package/dist-client/modeller/v3-canvas.js +230 -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 +73 -1
- package/dist-client/modeller/v3-editor.js +593 -218
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-fields.d.ts +39 -0
- package/dist-client/modeller/v3-fields.js +69 -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-joint-fan.d.ts +9 -0
- package/dist-client/modeller/v3-joint-fan.js +264 -0
- package/dist-client/modeller/v3-joint-fan.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 +35 -1
- package/dist-client/modeller/v3-links.js +101 -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/modeller/v3-thumbnail.js +6 -3
- package/dist-client/modeller/v3-thumbnail.js.map +1 -1
- 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 +71 -0
- package/test/v3-gizmo.test.ts +3 -5
- package/test/v3-joint-fan.test.ts +210 -0
- package/test/v3-keys.test.ts +37 -0
- package/test/v3-layout.test.ts +78 -0
- package/test/v3-links.test.ts +89 -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-palette-icons.test.ts +22 -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-thumbnail.test.ts +29 -0
- package/test/v3-verdict.test.ts +12 -6
- package/test/v3-view.test.ts +20 -0
- package/translations/en.json +90 -6
- package/translations/ja.json +92 -8
- package/translations/ko.json +90 -6
- package/translations/ms.json +92 -8
- package/translations/zh.json +92 -8
|
@@ -1,305 +1,263 @@
|
|
|
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
|
-
import {
|
|
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
|
+
import { JOINT_FAN } from './v3-joint-fan.js';
|
|
9
|
+
/* The fan's colour when the figure's palette names no accent. */
|
|
10
|
+
const FAN_COLOUR = '#ff8a00';
|
|
11
|
+
/* The 2D side of the board, which the author never sees on this page but the scene paints. */
|
|
12
|
+
const PAPER = '#ffffff';
|
|
10
13
|
let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
11
14
|
constructor() {
|
|
12
15
|
super(...arguments);
|
|
13
16
|
this.parts = [];
|
|
14
17
|
this.palette = {};
|
|
18
|
+
/** Which figure is open. Another one stands the board up again. */
|
|
15
19
|
this.context = '';
|
|
16
|
-
/** The selected part, and what the gizmo may write for it (`gizmoReachOf`). Nothing reachable, no gizmo. */
|
|
17
20
|
this.selected = '';
|
|
21
|
+
/** What the gizmo may write for the selected part; a change beyond it is not applied and the part springs back. */
|
|
18
22
|
this.reach = { translate: false, rotate: false, scale: false };
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
this.
|
|
26
|
-
|
|
27
|
-
this.
|
|
28
|
-
this.
|
|
29
|
-
this.
|
|
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
|
-
};
|
|
23
|
+
this.view = DEFAULT_VIEW;
|
|
24
|
+
/**
|
|
25
|
+
* View only: nothing that happens on the board is told out. The card's picture is taken from a second canvas stood up
|
|
26
|
+
* off screen (`v3-thumbnail.ts`); the scene settles its components as it builds them, and a change told from there
|
|
27
|
+
* would reach the editor as an edit (V2's `viewOnly`, for the same reason).
|
|
28
|
+
*/
|
|
29
|
+
this.viewOnly = false;
|
|
30
|
+
/** Where the selected part turns and how far (`v3-joint-fan`): shown, never picked, never saved. */
|
|
31
|
+
this.fan = null;
|
|
32
|
+
/** Set while this canvas writes to components, so the scene's echo of it is not taken for a drag. */
|
|
33
|
+
this.writing = false;
|
|
87
34
|
}
|
|
88
35
|
static { this.styles = css `
|
|
89
36
|
:host {
|
|
90
|
-
display: block;
|
|
91
|
-
min-width: 0;
|
|
92
|
-
min-height: 0;
|
|
93
37
|
position: relative;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
*/
|
|
99
|
-
background: var(--md-sys-color-surface-container);
|
|
38
|
+
display: flex;
|
|
39
|
+
min-height: 0;
|
|
40
|
+
overflow: hidden;
|
|
41
|
+
background-color: ${unsafeCSS(WORKBENCH)};
|
|
100
42
|
}
|
|
101
|
-
|
|
102
|
-
|
|
43
|
+
/* The scene sets position: relative inline on its target; the size is given so it still fills the host. */
|
|
44
|
+
div[stage] {
|
|
45
|
+
position: absolute;
|
|
46
|
+
inset: 0;
|
|
103
47
|
width: 100%;
|
|
104
48
|
height: 100%;
|
|
105
49
|
}
|
|
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
50
|
p {
|
|
136
51
|
position: absolute;
|
|
137
52
|
inset: 45% 1rem auto;
|
|
53
|
+
margin: 0;
|
|
138
54
|
text-align: center;
|
|
139
55
|
pointer-events: none;
|
|
140
|
-
color:
|
|
56
|
+
color: #f4f4f4;
|
|
141
57
|
}
|
|
142
58
|
`; }
|
|
143
59
|
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>`}`;
|
|
60
|
+
return html `<div stage></div>${this.parts.length ? nothing : html `<p>${i18next.t('figure.text.add-a-shape-to-start')}</p>`}`;
|
|
158
61
|
}
|
|
159
62
|
firstUpdated() {
|
|
160
|
-
this.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
super.connectedCallback();
|
|
164
|
-
if (this.hasUpdated)
|
|
165
|
-
void this.updateComplete.then(() => this.initialize());
|
|
63
|
+
this.standUp();
|
|
64
|
+
this.watcher = new ResizeObserver(() => this.scene?.resize?.());
|
|
65
|
+
this.watcher.observe(this);
|
|
166
66
|
}
|
|
167
|
-
|
|
168
|
-
if (
|
|
67
|
+
updated(changed) {
|
|
68
|
+
if (!this.hasUpdated)
|
|
169
69
|
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);
|
|
70
|
+
if (changed.has('parts') || changed.has('palette') || changed.has('context') || changed.has('fan'))
|
|
71
|
+
this.standUp();
|
|
72
|
+
if (changed.has('view'))
|
|
73
|
+
this.applyView();
|
|
74
|
+
if (changed.has('selected'))
|
|
75
|
+
this.showSelected();
|
|
207
76
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
77
|
+
disconnectedCallback() {
|
|
78
|
+
super.disconnectedCallback();
|
|
79
|
+
this.watcher?.disconnect();
|
|
80
|
+
this.watcher = undefined;
|
|
81
|
+
this.teardown();
|
|
213
82
|
}
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
83
|
+
/**
|
|
84
|
+
* The board as a picture, for the card: the 3D view at this size, or nothing when it cannot be taken. The scene's
|
|
85
|
+
* `toDataURL` renders the 2D board (a white sheet, V2 found), and the 3D renderer keeps no drawing buffer, so the
|
|
86
|
+
* scene's own `snapshot3d` draws and reads in one task. A picture that cannot be taken is not an error and is not
|
|
87
|
+
* faked (V2's `snapshot`, lost when this canvas moved onto the scene -- no card had a picture since, user 2026-09-24).
|
|
88
|
+
*/
|
|
89
|
+
async snapshot(width = 480, height = 360) {
|
|
90
|
+
const layer = this.scene?.root;
|
|
91
|
+
if (!layer?.snapshot3d)
|
|
92
|
+
return undefined;
|
|
93
|
+
try {
|
|
94
|
+
/* webp: a card needs no alpha, and png is three or four times heavier in every list response. */
|
|
95
|
+
const url = await layer.snapshot3d(width, height, 'image/webp', 0.85);
|
|
96
|
+
return typeof url === 'string' && url.startsWith('data:') ? url : undefined;
|
|
225
97
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
this.draw();
|
|
230
|
-
return;
|
|
98
|
+
catch (e) {
|
|
99
|
+
console.warn(`[thumbnail] could not take the picture -- ${e.message}`);
|
|
100
|
+
return undefined;
|
|
231
101
|
}
|
|
232
|
-
const position = new THREE.Vector3(), quaternion = new THREE.Quaternion(), scale = new THREE.Vector3();
|
|
233
|
-
body.matrix.decompose(position, quaternion, scale);
|
|
234
|
-
this.proxy.position.copy(position);
|
|
235
|
-
this.proxy.quaternion.copy(quaternion);
|
|
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
102
|
}
|
|
244
|
-
|
|
103
|
+
/** The whole figure in view: the board's fit, then the 3D camera's own, as V2 did. */
|
|
104
|
+
fit() {
|
|
105
|
+
this.scene?.fit?.('ratio');
|
|
106
|
+
this.scene?.root?.fitCameraToModel?.();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Stand the board up for this figure, or bring the one standing up to date. A new board only for another figure or
|
|
110
|
+
* one that has outgrown it -- standing up again loses the selection and the camera.
|
|
111
|
+
*/
|
|
112
|
+
standUp() {
|
|
113
|
+
if (!this.stage)
|
|
114
|
+
return;
|
|
115
|
+
const needed = boardFor(this.parts, this.board);
|
|
116
|
+
const outgrown = !this.board || needed.width > this.board.width || needed.height > this.board.height || needed.depth > this.board.depth;
|
|
117
|
+
if (this.scene && this.standingFor === this.context && !outgrown) {
|
|
118
|
+
this.sync();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.teardown();
|
|
122
|
+
if (!this.parts.length)
|
|
123
|
+
return;
|
|
124
|
+
this.board = needed;
|
|
245
125
|
try {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
126
|
+
this.scene = create({
|
|
127
|
+
target: this.stage,
|
|
128
|
+
model: {
|
|
129
|
+
type: 'model-layer',
|
|
130
|
+
width: needed.width,
|
|
131
|
+
height: needed.height,
|
|
132
|
+
depth: needed.depth,
|
|
133
|
+
threed: true,
|
|
134
|
+
/* Two samples a pixel: MSAA alone leaves steps on the silhouettes of one asset shown large (V2). */
|
|
135
|
+
pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),
|
|
136
|
+
...sceneLook(this.view ?? DEFAULT_VIEW),
|
|
137
|
+
gridStep: niceGridStep(Math.max(needed.width, needed.height)),
|
|
138
|
+
sky: 'color',
|
|
139
|
+
fillStyle: PAPER,
|
|
140
|
+
components: this.components()
|
|
141
|
+
},
|
|
142
|
+
/* Editing mode: the camera view buttons, gizmo modes, help and keys V2's canvas had. */
|
|
143
|
+
mode: 1
|
|
144
|
+
});
|
|
145
|
+
this.standingFor = this.context;
|
|
146
|
+
this.watchScene();
|
|
147
|
+
this.scene.resize?.();
|
|
148
|
+
this.fit();
|
|
149
|
+
this.showSelected();
|
|
252
150
|
}
|
|
253
151
|
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
152
|
this.dispatchEvent(new CustomEvent('render-error', { detail: error, bubbles: true, composed: true }));
|
|
260
153
|
}
|
|
261
154
|
}
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
155
|
+
components() {
|
|
156
|
+
const parts = this.parts.map(part => componentOf(part, this.palette, this.board)).filter((c) => !!c);
|
|
157
|
+
/* The fan in the accent colour, translucent; `capturable: false` keeps clicks, the marquee and the gizmo off it. */
|
|
158
|
+
const fan = this.fan ? componentOf(this.fan, { ...this.palette, [JOINT_FAN]: this.palette['palette.accent'] ?? FAN_COLOUR }, this.board) : null;
|
|
159
|
+
return fan ? [...parts, { ...fan, capturable: false, locked: true }] : parts;
|
|
160
|
+
}
|
|
161
|
+
/** The components standing now, by name. Not held between calls: the scene stands its board up again at times. */
|
|
162
|
+
standing() {
|
|
163
|
+
const found = new Map();
|
|
164
|
+
for (const component of this.scene?.root?.components ?? []) {
|
|
165
|
+
const name = component.state?.name;
|
|
166
|
+
if (typeof name === 'string')
|
|
167
|
+
found.set(name, component);
|
|
267
168
|
}
|
|
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();
|
|
169
|
+
return found;
|
|
276
170
|
}
|
|
277
|
-
/**
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
171
|
+
/** Bring the components to the evaluation: remove the gone, write only what differs, add the new. */
|
|
172
|
+
sync() {
|
|
173
|
+
const layer = this.scene?.root;
|
|
174
|
+
if (!layer || !this.board)
|
|
175
|
+
return;
|
|
176
|
+
const wanted = new Map(this.components().map(c => [c.name, c]));
|
|
177
|
+
const standing = this.standing();
|
|
178
|
+
this.writing = true;
|
|
179
|
+
try {
|
|
180
|
+
for (const [name, component] of standing) {
|
|
181
|
+
if (wanted.has(name))
|
|
182
|
+
continue;
|
|
183
|
+
layer.removeComponent?.(component);
|
|
184
|
+
component.dispose?.();
|
|
185
|
+
}
|
|
186
|
+
for (const [name, state] of wanted) {
|
|
187
|
+
const found = standing.get(name);
|
|
188
|
+
if (found) {
|
|
189
|
+
const diff = changedKeys(found.state, state);
|
|
190
|
+
if (diff)
|
|
191
|
+
found.set(diff);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const made = Model.compile({ ...state }, layer.app);
|
|
195
|
+
if (made)
|
|
196
|
+
layer.addComponent?.(made);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
this.writing = false;
|
|
201
|
+
}
|
|
202
|
+
layer.invalidate?.();
|
|
203
|
+
}
|
|
204
|
+
/** Selection and drags, told out by the part's name -- never by its place in the list, which may change meanwhile. */
|
|
205
|
+
watchScene() {
|
|
206
|
+
const layer = this.scene?.root;
|
|
207
|
+
const root = layer?.root;
|
|
208
|
+
if (!layer || !root?.on || this.viewOnly)
|
|
209
|
+
return;
|
|
210
|
+
root.on('selected', (after) => {
|
|
211
|
+
const name = after?.[0]?.state?.name;
|
|
212
|
+
const id = typeof name === 'string' ? name : '';
|
|
213
|
+
if (id !== this.selected)
|
|
214
|
+
this.dispatchEvent(new CustomEvent('part-selected', { detail: id, bubbles: true, composed: true }));
|
|
215
|
+
});
|
|
216
|
+
/*
|
|
217
|
+
A change comes up to the board with the component in the last argument's `origin` (the scene appends
|
|
218
|
+
{ origin, name, deliverer } as it bubbles). Reading the first argument as the component does nothing (V2).
|
|
219
|
+
*/
|
|
220
|
+
layer.on?.('change', (...args) => {
|
|
221
|
+
if (this.writing)
|
|
222
|
+
return;
|
|
223
|
+
const origin = args[args.length - 1]?.origin;
|
|
224
|
+
const name = origin?.state?.name;
|
|
225
|
+
const part = typeof name === 'string' ? this.parts.find(p => p.id === name) : undefined;
|
|
226
|
+
if (!part || !this.board)
|
|
227
|
+
return;
|
|
228
|
+
const result = gizmoResultOf(part, origin.state, this.board);
|
|
229
|
+
if (!result.position && !result.rotation && !result.scale)
|
|
230
|
+
return;
|
|
231
|
+
this.dispatchEvent(new CustomEvent('part-transformed', { detail: { id: part.id, result }, bubbles: true, composed: true }));
|
|
232
|
+
/* After the editor has applied (or refused) it: a refused change springs back to where the inputs put it. */
|
|
233
|
+
setTimeout(() => this.sync());
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
showSelected() {
|
|
237
|
+
const layer = this.scene?.root;
|
|
238
|
+
const root = layer?.root;
|
|
239
|
+
if (!layer || !root)
|
|
240
|
+
return;
|
|
241
|
+
const component = this.selected ? this.standing().get(this.selected) : undefined;
|
|
242
|
+
const now = root.selected ?? [];
|
|
243
|
+
if ((now[0] ?? undefined) === component && now.length === (component ? 1 : 0))
|
|
244
|
+
return;
|
|
245
|
+
root.selected = component ? [component] : [];
|
|
246
|
+
layer.invalidate?.();
|
|
285
247
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
248
|
+
/** View settings go into the scene as they are: they are the board's own state names. Nothing is stood up again. */
|
|
249
|
+
applyView() {
|
|
250
|
+
const layer = this.scene?.root;
|
|
251
|
+
if (!layer)
|
|
252
|
+
return;
|
|
253
|
+
layer.set(sceneLook(this.view ?? DEFAULT_VIEW));
|
|
254
|
+
layer.invalidate?.();
|
|
289
255
|
}
|
|
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();
|
|
256
|
+
teardown() {
|
|
257
|
+
this.scene?.dispose?.();
|
|
258
|
+
this.scene = undefined;
|
|
259
|
+
this.standingFor = undefined;
|
|
260
|
+
this.board = undefined;
|
|
303
261
|
}
|
|
304
262
|
};
|
|
305
263
|
__decorate([
|
|
@@ -323,9 +281,21 @@ __decorate([
|
|
|
323
281
|
__metadata("design:type", Object)
|
|
324
282
|
], FigureV3Canvas.prototype, "reach", void 0);
|
|
325
283
|
__decorate([
|
|
326
|
-
property(),
|
|
327
|
-
__metadata("design:type",
|
|
328
|
-
], FigureV3Canvas.prototype, "
|
|
284
|
+
property({ attribute: false }),
|
|
285
|
+
__metadata("design:type", Object)
|
|
286
|
+
], FigureV3Canvas.prototype, "view", void 0);
|
|
287
|
+
__decorate([
|
|
288
|
+
property({ type: Boolean }),
|
|
289
|
+
__metadata("design:type", Object)
|
|
290
|
+
], FigureV3Canvas.prototype, "viewOnly", void 0);
|
|
291
|
+
__decorate([
|
|
292
|
+
property({ attribute: false }),
|
|
293
|
+
__metadata("design:type", Object)
|
|
294
|
+
], FigureV3Canvas.prototype, "fan", void 0);
|
|
295
|
+
__decorate([
|
|
296
|
+
query('div[stage]'),
|
|
297
|
+
__metadata("design:type", HTMLDivElement)
|
|
298
|
+
], FigureV3Canvas.prototype, "stage", void 0);
|
|
329
299
|
FigureV3Canvas = __decorate([
|
|
330
300
|
customElement('figure-v3-canvas')
|
|
331
301
|
], FigureV3Canvas);
|