@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.
Files changed (53) hide show
  1. package/client/graphql/index.ts +1 -0
  2. package/client/modeller/figure-v3-material.ts +248 -0
  3. package/client/modeller/figure-v3-parts.ts +16 -1
  4. package/client/modeller/modeller-route.ts +25 -0
  5. package/client/modeller/v3-align.ts +84 -0
  6. package/client/modeller/v3-canvas.ts +140 -2
  7. package/client/modeller/v3-editor.ts +170 -38
  8. package/client/modeller/v3-gizmo.ts +80 -0
  9. package/client/pages/figure-list-page.ts +5 -3
  10. package/client/pages/figure-modeller-page.ts +9 -0
  11. package/client/types.ts +2 -0
  12. package/dist-client/graphql/index.js +1 -0
  13. package/dist-client/graphql/index.js.map +1 -1
  14. package/dist-client/modeller/figure-v3-material.d.ts +36 -0
  15. package/dist-client/modeller/figure-v3-material.js +249 -0
  16. package/dist-client/modeller/figure-v3-material.js.map +1 -0
  17. package/dist-client/modeller/figure-v3-parts.js +16 -1
  18. package/dist-client/modeller/figure-v3-parts.js.map +1 -1
  19. package/dist-client/modeller/modeller-route.d.ts +16 -0
  20. package/dist-client/modeller/modeller-route.js +27 -0
  21. package/dist-client/modeller/modeller-route.js.map +1 -0
  22. package/dist-client/modeller/v3-align.d.ts +26 -0
  23. package/dist-client/modeller/v3-align.js +65 -0
  24. package/dist-client/modeller/v3-align.js.map +1 -0
  25. package/dist-client/modeller/v3-canvas.d.ts +18 -0
  26. package/dist-client/modeller/v3-canvas.js +154 -2
  27. package/dist-client/modeller/v3-canvas.js.map +1 -1
  28. package/dist-client/modeller/v3-editor.d.ts +24 -0
  29. package/dist-client/modeller/v3-editor.js +181 -35
  30. package/dist-client/modeller/v3-editor.js.map +1 -1
  31. package/dist-client/modeller/v3-gizmo.d.ts +48 -0
  32. package/dist-client/modeller/v3-gizmo.js +40 -0
  33. package/dist-client/modeller/v3-gizmo.js.map +1 -0
  34. package/dist-client/pages/figure-list-page.d.ts +1 -0
  35. package/dist-client/pages/figure-list-page.js +5 -3
  36. package/dist-client/pages/figure-list-page.js.map +1 -1
  37. package/dist-client/pages/figure-modeller-page.js +9 -0
  38. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  39. package/dist-client/route.d.ts +1 -1
  40. package/dist-client/tsconfig.tsbuildinfo +1 -1
  41. package/dist-client/types.d.ts +2 -0
  42. package/dist-client/types.js.map +1 -1
  43. package/package.json +4 -4
  44. package/test/modeller-route.test.ts +42 -0
  45. package/test/v3-align.test.ts +64 -0
  46. package/test/v3-gizmo.test.ts +97 -0
  47. package/test/v3-material.test.ts +68 -0
  48. package/test/v3-parts-wiring.test.ts +56 -0
  49. package/translations/en.json +5 -0
  50. package/translations/ja.json +5 -0
  51. package/translations/ko.json +5 -0
  52. package/translations/ms.json +5 -0
  53. 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
- return html `<canvas aria-label="Figure 3D 모델" @pointerup=${this.pick}></canvas
58
- >${this.parts.length ? null : html `<p>도형을 추가해 모델링을 시작하세요.</p>`}`;
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
- aside:first-child {
76
- border-right: 1px solid var(--md-sys-color-outline-variant);
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
- aside:first-child {
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
- <aside>
295
- <h3>도형 추가</h3>
296
- <div class="recipes">
297
- ${['box', 'cylinder', 'rounded-box', 'portal'].map((kind, i) => html `<button ?disabled=${unavailable} @click=${() => this.add(kind)}>${['상자', '원기둥', '둥근 상자', '문틀'][i]}</button>`)}
298
- </div>
299
- <h3>부품 ${view.geometry.length}</h3>
300
- <div class="parts">
301
- ${view.geometry.map(g => html `<button
302
- aria-pressed=${g.id === this.selected}
303
- @click=${() => {
304
- this.selected = g.id;
305
- }}
306
- >
307
- ${g.id}
308
- </button>`)}
309
- </div>
310
- </aside>
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 `<h3>재질</h3>
327
- <label
328
- ><span>색 역할</span
329
- ><select
330
- .value=${part.material.token}
331
- ?disabled=${unavailable}
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);