@things-factory/figure-ui 10.1.40 → 10.1.42
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/graphql/index.ts +1 -0
- package/client/modeller/figure-v3-material.ts +248 -0
- package/client/modeller/figure-v3-parts.ts +16 -1
- package/client/modeller/modeller-route.ts +25 -0
- package/client/modeller/v3-align.ts +84 -0
- package/client/modeller/v3-canvas.ts +140 -2
- package/client/modeller/v3-editor.ts +170 -38
- package/client/modeller/v3-gizmo.ts +80 -0
- package/client/pages/figure-list-page.ts +5 -3
- package/client/pages/figure-modeller-page.ts +9 -0
- package/client/types.ts +2 -0
- package/dist-client/graphql/index.js +1 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +36 -0
- package/dist-client/modeller/figure-v3-material.js +249 -0
- package/dist-client/modeller/figure-v3-material.js.map +1 -0
- package/dist-client/modeller/figure-v3-parts.js +16 -1
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/modeller-route.d.ts +16 -0
- package/dist-client/modeller/modeller-route.js +27 -0
- package/dist-client/modeller/modeller-route.js.map +1 -0
- package/dist-client/modeller/v3-align.d.ts +26 -0
- package/dist-client/modeller/v3-align.js +65 -0
- package/dist-client/modeller/v3-align.js.map +1 -0
- package/dist-client/modeller/v3-canvas.d.ts +18 -0
- package/dist-client/modeller/v3-canvas.js +154 -2
- package/dist-client/modeller/v3-canvas.js.map +1 -1
- package/dist-client/modeller/v3-editor.d.ts +24 -0
- package/dist-client/modeller/v3-editor.js +181 -35
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-gizmo.d.ts +48 -0
- package/dist-client/modeller/v3-gizmo.js +40 -0
- package/dist-client/modeller/v3-gizmo.js.map +1 -0
- package/dist-client/pages/figure-list-page.d.ts +1 -0
- package/dist-client/pages/figure-list-page.js +5 -3
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.js +9 -0
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +2 -0
- package/dist-client/types.js.map +1 -1
- package/package.json +4 -4
- package/test/modeller-route.test.ts +42 -0
- package/test/v3-align.test.ts +64 -0
- package/test/v3-gizmo.test.ts +97 -0
- package/test/v3-material.test.ts +68 -0
- package/test/v3-parts-wiring.test.ts +56 -0
- package/translations/en.json +5 -0
- package/translations/ja.json +5 -0
- package/translations/ko.json +5 -0
- package/translations/ms.json +5 -0
- package/translations/zh.json +5 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
2
|
import { LitElement, html, css } from 'lit';
|
|
3
3
|
import { customElement, property } from 'lit/decorators.js';
|
|
4
|
+
import { i18next } from '@operato/i18n';
|
|
4
5
|
import * as THREE from 'three';
|
|
5
6
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
7
|
+
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
|
|
6
8
|
import { replaceV3Meshes, disposeV3Meshes } from './v3-renderer.js';
|
|
7
9
|
/** Renders evaluated V3 geometry directly. There is no parts/base or things-scene conversion. */
|
|
8
10
|
let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
@@ -11,16 +13,72 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
|
11
13
|
this.parts = [];
|
|
12
14
|
this.palette = {};
|
|
13
15
|
this.context = '';
|
|
16
|
+
/** The selected part, and what the gizmo may write for it (`gizmoReachOf`). Nothing reachable, no gizmo. */
|
|
17
|
+
this.selected = '';
|
|
18
|
+
this.reach = { translate: false, rotate: false, scale: false };
|
|
19
|
+
/** Move · turn · size — W · E · R, as in the V2 modeller. */
|
|
20
|
+
this.mode = 'translate';
|
|
21
|
+
/** What the gizmo holds: a stand-in at the body's origin, so the body's own matrix stays the evaluation's. */
|
|
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;
|
|
14
25
|
this.scene = new THREE.Scene();
|
|
15
26
|
this.group = new THREE.Group();
|
|
16
27
|
this.camera = new THREE.PerspectiveCamera(40, 1, 0.1, 1000000);
|
|
17
28
|
this.initialized = false;
|
|
18
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
|
+
};
|
|
19
75
|
this.draw = () => {
|
|
20
76
|
if (this.renderer)
|
|
21
77
|
this.renderer.render(this.scene, this.camera);
|
|
22
78
|
};
|
|
23
79
|
this.pick = (event) => {
|
|
80
|
+
if (this.justDragged || this.transform?.dragging)
|
|
81
|
+
return;
|
|
24
82
|
const r = event.currentTarget.getBoundingClientRect(), ray = new THREE.Raycaster();
|
|
25
83
|
ray.setFromCamera(new THREE.Vector2(((event.clientX - r.left) / r.width) * 2 - 1, (-(event.clientY - r.top) / r.height) * 2 + 1), this.camera);
|
|
26
84
|
const hit = ray.intersectObjects(this.group.children)[0];
|
|
@@ -45,6 +103,35 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
|
45
103
|
width: 100%;
|
|
46
104
|
height: 100%;
|
|
47
105
|
}
|
|
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
|
+
}
|
|
48
135
|
p {
|
|
49
136
|
position: absolute;
|
|
50
137
|
inset: 45% 1rem auto;
|
|
@@ -54,8 +141,20 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
|
54
141
|
}
|
|
55
142
|
`; }
|
|
56
143
|
render() {
|
|
57
|
-
|
|
58
|
-
|
|
144
|
+
const any = this.selected && (this.reach.translate || this.reach.rotate || this.reach.scale);
|
|
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>`}`;
|
|
59
158
|
}
|
|
60
159
|
firstUpdated() {
|
|
61
160
|
this.initialize();
|
|
@@ -87,6 +186,12 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
|
87
186
|
this.scene.add(light);
|
|
88
187
|
this.controls = new OrbitControls(this.camera, canvas);
|
|
89
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());
|
|
90
195
|
this.observer = new ResizeObserver(() => {
|
|
91
196
|
const r = this.getBoundingClientRect();
|
|
92
197
|
if (r.width && r.height) {
|
|
@@ -103,6 +208,38 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
|
103
208
|
updated(changed) {
|
|
104
209
|
if (this.initialized && (changed.has('parts') || changed.has('palette') || changed.has('context')))
|
|
105
210
|
this.updateGeometry(this.contextAtRender !== this.context);
|
|
211
|
+
if (this.initialized && ['parts', 'selected', 'reach', 'mode', 'context'].some(key => changed.has(key)))
|
|
212
|
+
this.placeGizmo();
|
|
213
|
+
}
|
|
214
|
+
/** Puts the gizmo on the selected body, in a mode it may write — or takes it away. */
|
|
215
|
+
placeGizmo() {
|
|
216
|
+
const transform = this.transform;
|
|
217
|
+
if (!transform || this.dragStart)
|
|
218
|
+
return;
|
|
219
|
+
if (!this.reach[this.mode]) {
|
|
220
|
+
const first = ['translate', 'rotate', 'scale'].find(mode => this.reach[mode]);
|
|
221
|
+
if (first && first !== this.mode) {
|
|
222
|
+
this.mode = first;
|
|
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();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
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();
|
|
106
243
|
}
|
|
107
244
|
updateGeometry(fit) {
|
|
108
245
|
try {
|
|
@@ -154,6 +291,9 @@ let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
|
154
291
|
this.observer?.disconnect();
|
|
155
292
|
this.themeObserver?.disconnect();
|
|
156
293
|
this.controls?.dispose();
|
|
294
|
+
this.transform?.detach();
|
|
295
|
+
this.transform?.dispose();
|
|
296
|
+
this.transform = undefined;
|
|
157
297
|
disposeV3Meshes(this.group);
|
|
158
298
|
this.renderer?.dispose();
|
|
159
299
|
this.renderer = undefined;
|
|
@@ -174,6 +314,18 @@ __decorate([
|
|
|
174
314
|
property(),
|
|
175
315
|
__metadata("design:type", Object)
|
|
176
316
|
], FigureV3Canvas.prototype, "context", void 0);
|
|
317
|
+
__decorate([
|
|
318
|
+
property(),
|
|
319
|
+
__metadata("design:type", Object)
|
|
320
|
+
], FigureV3Canvas.prototype, "selected", void 0);
|
|
321
|
+
__decorate([
|
|
322
|
+
property({ attribute: false }),
|
|
323
|
+
__metadata("design:type", Object)
|
|
324
|
+
], FigureV3Canvas.prototype, "reach", void 0);
|
|
325
|
+
__decorate([
|
|
326
|
+
property(),
|
|
327
|
+
__metadata("design:type", String)
|
|
328
|
+
], FigureV3Canvas.prototype, "mode", void 0);
|
|
177
329
|
FigureV3Canvas = __decorate([
|
|
178
330
|
customElement('figure-v3-canvas')
|
|
179
331
|
], FigureV3Canvas);
|
|
@@ -1 +1 @@
|
|
|
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, type PropertyValues } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport * as THREE from 'three'\nimport { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.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 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 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 return html`<canvas aria-label=\"Figure 3D 모델\" @pointerup=${this.pick}></canvas\n >${this.parts.length ? null : html`<p>도형을 추가해 모델링을 시작하세요.</p>`}`\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.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 }\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 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 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, 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,6 +1,8 @@
|
|
|
1
1
|
import { LitElement, type PropertyValues } from 'lit';
|
|
2
2
|
import type { V3ProductSession } from './v3-product-session.js';
|
|
3
3
|
import './v3-canvas.js';
|
|
4
|
+
import './figure-v3-parts.js';
|
|
5
|
+
import './figure-v3-material.js';
|
|
4
6
|
/** Product editor surface. The injected session is shared by page persistence and AI commands. */
|
|
5
7
|
export declare class FigureV3Editor extends LitElement {
|
|
6
8
|
static styles: import("lit").CSSResult;
|
|
@@ -8,12 +10,34 @@ export declare class FigureV3Editor extends LitElement {
|
|
|
8
10
|
revision: number;
|
|
9
11
|
private selected;
|
|
10
12
|
private error;
|
|
13
|
+
/**
|
|
14
|
+
* Parts hidden on the workbench — the author's view while working, not the asset's visibility. Not saved, as in
|
|
15
|
+
* the V2 modeller: a part hidden here is still in the figure, still checked, still drawn on a board.
|
|
16
|
+
*/
|
|
17
|
+
private hiddenParts;
|
|
11
18
|
private lastContext;
|
|
12
19
|
protected willUpdate(_changed: PropertyValues): void;
|
|
13
20
|
private run;
|
|
14
21
|
private changed;
|
|
15
22
|
private save;
|
|
16
23
|
private add;
|
|
24
|
+
/**
|
|
25
|
+
* The list's own operations, as V3 part edits — each one step of the session's history. The copy and the moved
|
|
26
|
+
* part stay selected, as they do in the V2 list.
|
|
27
|
+
*/
|
|
28
|
+
private duplicatePart;
|
|
29
|
+
private movePart;
|
|
30
|
+
private removePart;
|
|
31
|
+
private toggleHiddenPart;
|
|
32
|
+
/** What the gizmo may write for the selected part — read off the asset being edited. */
|
|
33
|
+
private reachOf;
|
|
34
|
+
/** A drag, written back as the part's own inputs — one step of the history, like typing the numbers. */
|
|
35
|
+
private transformed;
|
|
36
|
+
/** 맞추기 — one tool, one step of the history, through the same inputs as the gizmo. */
|
|
37
|
+
private align;
|
|
38
|
+
/** A mirrored copy: duplicate, then place the copy on the other side — two steps, as the V2 list makes it. */
|
|
39
|
+
private mirror;
|
|
40
|
+
private renderTools;
|
|
17
41
|
private changeField;
|
|
18
42
|
render(): import("lit-html").TemplateResult<1>;
|
|
19
43
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
2
|
import { LitElement, html, css } from 'lit';
|
|
3
|
+
import { i18next } from '@operato/i18n';
|
|
3
4
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
4
5
|
import { captureV3Thumbnail } from './v3-thumbnail.js';
|
|
5
6
|
import { drawableV3Parts } from './v3-renderer.js';
|
|
6
7
|
import './v3-canvas.js';
|
|
8
|
+
import './figure-v3-parts.js';
|
|
9
|
+
import './figure-v3-material.js';
|
|
10
|
+
import { gizmoChanges, gizmoReachOf } from './v3-gizmo.js';
|
|
11
|
+
import { alignChanges, canMirror, mirrorChanges } from './v3-align.js';
|
|
7
12
|
const names = {
|
|
8
13
|
position: '위치',
|
|
9
14
|
rotation: '회전',
|
|
@@ -36,6 +41,11 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
36
41
|
this.revision = 0;
|
|
37
42
|
this.selected = '';
|
|
38
43
|
this.error = '';
|
|
44
|
+
/**
|
|
45
|
+
* Parts hidden on the workbench — the author's view while working, not the asset's visibility. Not saved, as in
|
|
46
|
+
* the V2 modeller: a part hidden here is still in the figure, still checked, still drawn on a board.
|
|
47
|
+
*/
|
|
48
|
+
this.hiddenParts = new Set();
|
|
39
49
|
this.lastContext = null;
|
|
40
50
|
}
|
|
41
51
|
static { this.styles = css `
|
|
@@ -72,8 +82,8 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
72
82
|
overflow: auto;
|
|
73
83
|
background: var(--md-sys-color-surface, #fff);
|
|
74
84
|
}
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
figure-v3-parts {
|
|
86
|
+
min-height: 0;
|
|
77
87
|
}
|
|
78
88
|
aside:last-child {
|
|
79
89
|
border-left: 1px solid var(--md-sys-color-outline-variant);
|
|
@@ -110,6 +120,24 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
110
120
|
color: var(--md-sys-color-on-secondary-container);
|
|
111
121
|
border-color: var(--md-sys-color-primary);
|
|
112
122
|
}
|
|
123
|
+
div[tools] {
|
|
124
|
+
display: flex;
|
|
125
|
+
flex-wrap: wrap;
|
|
126
|
+
gap: 4px;
|
|
127
|
+
margin: 0 0 8px;
|
|
128
|
+
}
|
|
129
|
+
div[tools] button {
|
|
130
|
+
display: inline-flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
justify-content: center;
|
|
133
|
+
width: 34px;
|
|
134
|
+
padding: 0;
|
|
135
|
+
}
|
|
136
|
+
div[tools] md-icon {
|
|
137
|
+
font-size: 18px;
|
|
138
|
+
width: 18px;
|
|
139
|
+
height: 18px;
|
|
140
|
+
}
|
|
113
141
|
.parts {
|
|
114
142
|
display: grid;
|
|
115
143
|
gap: 6px;
|
|
@@ -165,7 +193,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
165
193
|
figure-v3-canvas {
|
|
166
194
|
order: -1;
|
|
167
195
|
}
|
|
168
|
-
|
|
196
|
+
figure-v3-parts {
|
|
169
197
|
border-right: none;
|
|
170
198
|
border-top: 1px solid var(--md-sys-color-outline-variant);
|
|
171
199
|
}
|
|
@@ -180,6 +208,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
180
208
|
this.lastContext = view?.context ?? null;
|
|
181
209
|
this.selected = '';
|
|
182
210
|
this.error = '';
|
|
211
|
+
this.hiddenParts = new Set();
|
|
183
212
|
}
|
|
184
213
|
if (view && !view.geometry.some(g => g.id === this.selected))
|
|
185
214
|
this.selected = '';
|
|
@@ -255,6 +284,122 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
255
284
|
this.selected = part.id;
|
|
256
285
|
});
|
|
257
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* The list's own operations, as V3 part edits — each one step of the session's history. The copy and the moved
|
|
289
|
+
* part stay selected, as they do in the V2 list.
|
|
290
|
+
*/
|
|
291
|
+
duplicatePart(id) {
|
|
292
|
+
this.run(() => {
|
|
293
|
+
const before = new Set(this.session.read().geometry.map(g => g.id));
|
|
294
|
+
this.session.edit({ kind: 'duplicate-part', target: id });
|
|
295
|
+
this.selected = this.session.read().geometry.find(g => !before.has(g.id))?.id ?? id;
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
movePart(id, to) {
|
|
299
|
+
this.run(() => this.session.edit({ kind: 'move-part', target: id, to }));
|
|
300
|
+
}
|
|
301
|
+
removePart(id) {
|
|
302
|
+
try {
|
|
303
|
+
this.session.edit({ kind: 'remove-part', target: id });
|
|
304
|
+
this.error = '';
|
|
305
|
+
if (this.selected === id)
|
|
306
|
+
this.selected = '';
|
|
307
|
+
this.changed();
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
/* The refusal a person meets most: another part stands on this one or follows its size. Said in their words. */
|
|
311
|
+
this.error =
|
|
312
|
+
error?.code === 'PART_REFERENCED'
|
|
313
|
+
? i18next.t('figure.text.part-is-referenced', { part: id })
|
|
314
|
+
: error instanceof Error
|
|
315
|
+
? error.message
|
|
316
|
+
: String(error);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
toggleHiddenPart(id) {
|
|
320
|
+
const next = new Set(this.hiddenParts);
|
|
321
|
+
if (next.has(id))
|
|
322
|
+
next.delete(id);
|
|
323
|
+
else
|
|
324
|
+
next.add(id);
|
|
325
|
+
this.hiddenParts = next;
|
|
326
|
+
}
|
|
327
|
+
/** What the gizmo may write for the selected part — read off the asset being edited. */
|
|
328
|
+
reachOf(id) {
|
|
329
|
+
const none = { translate: false, rotate: false, scale: false };
|
|
330
|
+
if (!id || !this.session)
|
|
331
|
+
return none;
|
|
332
|
+
try {
|
|
333
|
+
return gizmoReachOf(JSON.parse(this.session.source()), id);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return none;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/** A drag, written back as the part's own inputs — one step of the history, like typing the numbers. */
|
|
340
|
+
transformed(id, result) {
|
|
341
|
+
const view = this.session.read();
|
|
342
|
+
const sizes = Object.fromEntries(view.fields.map(field => [field.id, field.value]));
|
|
343
|
+
const changes = gizmoChanges(id, this.reachOf(id), result, sizes);
|
|
344
|
+
if (changes.length)
|
|
345
|
+
this.run(() => this.session.apply(changes));
|
|
346
|
+
else
|
|
347
|
+
this.requestUpdate();
|
|
348
|
+
}
|
|
349
|
+
/** 맞추기 — one tool, one step of the history, through the same inputs as the gizmo. */
|
|
350
|
+
align(tool) {
|
|
351
|
+
this.run(() => {
|
|
352
|
+
const asset = JSON.parse(this.session.source());
|
|
353
|
+
const changes = alignChanges(asset, this.selected, this.reachOf(this.selected), tool);
|
|
354
|
+
if (changes.length)
|
|
355
|
+
this.session.apply(changes);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
/** A mirrored copy: duplicate, then place the copy on the other side — two steps, as the V2 list makes it. */
|
|
359
|
+
mirror(axis) {
|
|
360
|
+
this.run(() => {
|
|
361
|
+
const id = this.selected;
|
|
362
|
+
const before = new Set(this.session.read().geometry.map(g => g.id));
|
|
363
|
+
this.session.edit({ kind: 'duplicate-part', target: id });
|
|
364
|
+
const copy = this.session.read().geometry.find(g => !before.has(g.id))?.id;
|
|
365
|
+
if (!copy)
|
|
366
|
+
return;
|
|
367
|
+
this.session.apply(mirrorChanges(JSON.parse(this.session.source()), id, copy, axis));
|
|
368
|
+
this.selected = copy;
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
renderTools(unavailable) {
|
|
372
|
+
const reach = this.reachOf(this.selected);
|
|
373
|
+
if (!this.selected || !reach.translate)
|
|
374
|
+
return null;
|
|
375
|
+
let asset;
|
|
376
|
+
try {
|
|
377
|
+
asset = JSON.parse(this.session.source());
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const tool = (name, icon, label, title) => html `<button ?disabled=${unavailable} aria-label=${i18next.t(label)} title=${i18next.t(title)} @click=${() => this.align(name)}>
|
|
383
|
+
<md-icon>${icon}</md-icon>
|
|
384
|
+
</button>`;
|
|
385
|
+
return html `<div tools title=${i18next.t('figure.text.tools-explained')}>
|
|
386
|
+
${tool('centre-x', 'horizontal_distribute', 'figure.button.center-x', 'figure.text.center-on-x')}
|
|
387
|
+
${tool('centre-z', 'vertical_align_center', 'figure.button.center-z', 'figure.text.center-on-z')}
|
|
388
|
+
${tool('floor', 'vertical_align_bottom', 'figure.button.to-floor', 'figure.text.put-on-floor')}
|
|
389
|
+
${['x', 'z'].map(axis => {
|
|
390
|
+
const able = canMirror(asset, this.selected, axis, reach);
|
|
391
|
+
return html `<button
|
|
392
|
+
?disabled=${unavailable || !able}
|
|
393
|
+
aria-label=${i18next.t('figure.button.mirror-' + axis)}
|
|
394
|
+
title=${i18next.t(able ? 'figure.text.mirror-explained' : 'figure.text.mirror-would-overlap')}
|
|
395
|
+
@click=${() => this.mirror(axis)}
|
|
396
|
+
>
|
|
397
|
+
<md-icon>${axis === 'x' ? 'flip' : 'flip_camera_android'}</md-icon>
|
|
398
|
+
</button>`;
|
|
399
|
+
})}
|
|
400
|
+
${tool('snap', 'grid_on', 'figure.button.snap', 'figure.text.snap-explained')}
|
|
401
|
+
</div>`;
|
|
402
|
+
}
|
|
258
403
|
changeField(field, event) {
|
|
259
404
|
const input = event.target, value = input.valueAsNumber;
|
|
260
405
|
this.run(() => this.session.apply([
|
|
@@ -291,27 +436,31 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
291
436
|
</header>
|
|
292
437
|
${this.error || view.error ? html `<div role="alert" class="error">${this.error || view.error}</div>` : null}
|
|
293
438
|
<main>
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
439
|
+
<figure-v3-parts
|
|
440
|
+
.parts=${view.geometry.map((g) => ({
|
|
441
|
+
id: g.id,
|
|
442
|
+
provider: g.shape?.provider ?? '',
|
|
443
|
+
hidden: this.hiddenParts.has(g.id),
|
|
444
|
+
invisible: g.visible === false
|
|
445
|
+
}))}
|
|
446
|
+
.selected=${this.selected}
|
|
447
|
+
?locked=${unavailable}
|
|
448
|
+
@add-part=${(e) => this.add(e.detail.kind)}
|
|
449
|
+
@select-part=${(e) => (this.selected = e.detail.id)}
|
|
450
|
+
@toggle-part-hidden=${(e) => this.toggleHiddenPart(e.detail.id)}
|
|
451
|
+
@set-all-parts-hidden=${(e) => (this.hiddenParts = e.detail.hidden ? new Set(view.geometry.map(g => g.id)) : new Set())}
|
|
452
|
+
@move-part-up=${(e) => this.movePart(e.detail.id, e.detail.to)}
|
|
453
|
+
@move-part-down=${(e) => this.movePart(e.detail.id, e.detail.to)}
|
|
454
|
+
@duplicate-part=${(e) => this.duplicatePart(e.detail.id)}
|
|
455
|
+
@remove-part=${(e) => this.removePart(e.detail.id)}
|
|
456
|
+
></figure-v3-parts>
|
|
311
457
|
<figure-v3-canvas
|
|
312
|
-
.parts=${drawableV3Parts(view.geometry ?? [])}
|
|
458
|
+
.parts=${drawableV3Parts(view.geometry ?? []).filter(body => !this.hiddenParts.has(body.id))}
|
|
313
459
|
.palette=${view.palette ?? {}}
|
|
314
460
|
.context=${view.context ?? ''}
|
|
461
|
+
.selected=${this.selected}
|
|
462
|
+
.reach=${unavailable ? { translate: false, rotate: false, scale: false } : this.reachOf(this.selected)}
|
|
463
|
+
@part-transformed=${(e) => this.transformed(e.detail.id, e.detail.result)}
|
|
315
464
|
@part-selected=${(e) => {
|
|
316
465
|
this.selected = e.detail;
|
|
317
466
|
}}
|
|
@@ -321,22 +470,15 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
321
470
|
></figure-v3-canvas>
|
|
322
471
|
<aside>
|
|
323
472
|
<h3>${this.selected || '모델 입력'}</h3>
|
|
473
|
+
${this.renderTools(unavailable)}
|
|
324
474
|
${fields.map(field => html `<label><span>${fieldLabel(field.id)} (${field.unit})${field.role === 'state' ? ' · 동작' : ''}</span><input aria-label=${field.id} type="number" step="any" .value=${String(field.value)} ?disabled=${unavailable} @change=${(e) => this.changeField(field, e)} /></label>`)}
|
|
325
475
|
${part
|
|
326
|
-
? html `<
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
@change=${(e) => this.run(() => this.session.edit({ kind: 'set-material', target: this.selected, material: { ...part.material, token: e.target.value } }))}
|
|
333
|
-
>
|
|
334
|
-
${Object.keys(view.palette ?? {}).map(token => html `<option value=${token}>${token === 'palette.primary' ? '기본 몸체' : token}</option>`)}
|
|
335
|
-
</select></label
|
|
336
|
-
>
|
|
337
|
-
<p class="hint">
|
|
338
|
-
도형과 위치를 결정하는 입력을 편집합니다. 다른 부품과 공유하는 입력은 함께 영향을 줄 수 있습니다.
|
|
339
|
-
</p>`
|
|
476
|
+
? html `<figure-v3-material
|
|
477
|
+
.body=${part}
|
|
478
|
+
.palette=${view.palette ?? {}}
|
|
479
|
+
?locked=${unavailable}
|
|
480
|
+
@v3-part-edit=${(e) => this.run(() => this.session.edit(e.detail.edit))}
|
|
481
|
+
></figure-v3-material>`
|
|
340
482
|
: null}
|
|
341
483
|
${view.bindings.length
|
|
342
484
|
? html `<h3>배치 연결점</h3>
|
|
@@ -362,6 +504,10 @@ __decorate([
|
|
|
362
504
|
state(),
|
|
363
505
|
__metadata("design:type", Object)
|
|
364
506
|
], FigureV3Editor.prototype, "error", void 0);
|
|
507
|
+
__decorate([
|
|
508
|
+
state(),
|
|
509
|
+
__metadata("design:type", Object)
|
|
510
|
+
], FigureV3Editor.prototype, "hiddenParts", void 0);
|
|
365
511
|
FigureV3Editor = __decorate([
|
|
366
512
|
customElement('figure-v3-editor')
|
|
367
513
|
], FigureV3Editor);
|