@things-factory/figure-ui 10.1.35 → 10.1.37
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/v3-store.ts +30 -0
- package/client/modeller/v3-canvas.ts +145 -0
- package/client/modeller/v3-editor.ts +317 -0
- package/client/modeller/v3-product-session.ts +141 -0
- package/client/pages/figure-v3-modeller-page.ts +117 -0
- package/dist-client/graphql/v3-store.d.ts +3 -0
- package/dist-client/graphql/v3-store.js +32 -0
- package/dist-client/graphql/v3-store.js.map +1 -0
- package/dist-client/modeller/v3-canvas.d.ts +28 -0
- package/dist-client/modeller/v3-canvas.js +157 -0
- package/dist-client/modeller/v3-canvas.js.map +1 -0
- package/dist-client/modeller/v3-editor.d.ts +19 -0
- package/dist-client/modeller/v3-editor.js +332 -0
- package/dist-client/modeller/v3-editor.js.map +1 -0
- package/dist-client/modeller/v3-product-session.d.ts +366 -0
- package/dist-client/modeller/v3-product-session.js +130 -0
- package/dist-client/modeller/v3-product-session.js.map +1 -0
- package/dist-client/pages/figure-v3-modeller-page.d.ts +26 -0
- package/dist-client/pages/figure-v3-modeller-page.js +140 -0
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/docs/v3-product-cutover.md +35 -0
- package/package.json +4 -4
- package/test/v3-product-session.test.ts +133 -0
- package/test/v3-product-write.test.ts +48 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { html, css } from 'lit'
|
|
2
|
+
import { customElement, state } from 'lit/decorators.js'
|
|
3
|
+
import { PageView } from '@operato/shell'
|
|
4
|
+
import { createV3ProductSession } from '../modeller/v3-product-session.js'
|
|
5
|
+
import { v3ProductStore } from '../graphql/v3-store.js'
|
|
6
|
+
import '../modeller/v3-editor.js'
|
|
7
|
+
|
|
8
|
+
/** The V3 product page has exactly one authoritative workspace for UI and AI input transactions. */
|
|
9
|
+
@customElement('figure-modeller-page')
|
|
10
|
+
export class FigureV3ModellerPage extends PageView {
|
|
11
|
+
static styles = css`
|
|
12
|
+
:host {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
height: 100%;
|
|
16
|
+
min-height: 0;
|
|
17
|
+
}
|
|
18
|
+
figure-v3-editor {
|
|
19
|
+
flex: 1;
|
|
20
|
+
min-height: 0;
|
|
21
|
+
}
|
|
22
|
+
.error {
|
|
23
|
+
padding: 16px;
|
|
24
|
+
color: #922716;
|
|
25
|
+
}
|
|
26
|
+
`
|
|
27
|
+
private readonly session = createV3ProductSession(v3ProductStore)
|
|
28
|
+
private routeId: string | undefined
|
|
29
|
+
private discardApproved = false
|
|
30
|
+
@state() private revision = 0
|
|
31
|
+
@state() private error = ''
|
|
32
|
+
private announce = () => {
|
|
33
|
+
const view = this.session.read()
|
|
34
|
+
if (!this.active || view.loading || view.saving || view.error || !view.context) return
|
|
35
|
+
window.dispatchEvent(
|
|
36
|
+
new CustomEvent('figure-ai-context', {
|
|
37
|
+
detail: {
|
|
38
|
+
figureId: view.id,
|
|
39
|
+
source: JSON.parse(this.session.source()),
|
|
40
|
+
context: view.context,
|
|
41
|
+
revision: view.revision,
|
|
42
|
+
format: 'v3'
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
private changed = () => {
|
|
48
|
+
this.revision++
|
|
49
|
+
if (!this.session.read().saving) this.announce()
|
|
50
|
+
}
|
|
51
|
+
/** The dock must explicitly use this checked transaction contract, never an unversioned overwrite. */
|
|
52
|
+
private applyAI = (event: Event) => {
|
|
53
|
+
const detail = (event as CustomEvent).detail
|
|
54
|
+
if (!this.active || !detail) return
|
|
55
|
+
try {
|
|
56
|
+
this.session.apply(detail.changes, 'ai', detail.revision, detail.context)
|
|
57
|
+
detail.applied = true
|
|
58
|
+
this.changed()
|
|
59
|
+
} catch (error) {
|
|
60
|
+
detail.applied = false
|
|
61
|
+
detail.error = error instanceof Error ? error.message : String(error)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
connectedCallback() {
|
|
65
|
+
super.connectedCallback()
|
|
66
|
+
window.addEventListener('figure-ai-context-request', this.announce)
|
|
67
|
+
window.addEventListener('figure-v3-apply', this.applyAI)
|
|
68
|
+
}
|
|
69
|
+
disconnectedCallback() {
|
|
70
|
+
window.removeEventListener('figure-ai-context-request', this.announce)
|
|
71
|
+
window.removeEventListener('figure-v3-apply', this.applyAI)
|
|
72
|
+
super.disconnectedCallback()
|
|
73
|
+
}
|
|
74
|
+
async canDeactivate() {
|
|
75
|
+
const view = this.session.read()
|
|
76
|
+
if (view.saving) return false
|
|
77
|
+
if (!view.dirty) return true
|
|
78
|
+
// The shell invokes this while the edited model is still visible, before navigating away.
|
|
79
|
+
this.discardApproved = window.confirm('저장하지 않은 변경을 버리고 이동하시겠습니까?')
|
|
80
|
+
return this.discardApproved
|
|
81
|
+
}
|
|
82
|
+
async pageUpdated(changes: Record<string, unknown>, lifecycle: { resourceId?: string }) {
|
|
83
|
+
if (!this.active) {
|
|
84
|
+
if (this.session.read().loading) this.session.close()
|
|
85
|
+
window.dispatchEvent(new CustomEvent('figure-ai-context', { detail: undefined }))
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
const id = lifecycle?.resourceId
|
|
89
|
+
if (this.session.read().context && id === this.routeId && !changes.active) return
|
|
90
|
+
try {
|
|
91
|
+
const discard = this.discardApproved
|
|
92
|
+
this.discardApproved = false
|
|
93
|
+
if (id) {
|
|
94
|
+
const opening = this.session.open(id, { discard })
|
|
95
|
+
this.changed()
|
|
96
|
+
await opening
|
|
97
|
+
} else this.session.startNew({ discard })
|
|
98
|
+
this.routeId = id
|
|
99
|
+
this.error = ''
|
|
100
|
+
this.changed()
|
|
101
|
+
} catch (error) {
|
|
102
|
+
this.error = error instanceof Error ? error.message : String(error)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
get context() {
|
|
106
|
+
return { title: this.session.read().name ?? 'Figure', actions: [] }
|
|
107
|
+
}
|
|
108
|
+
render() {
|
|
109
|
+
return this.error
|
|
110
|
+
? html`<p role="alert" class="error">${this.error}</p>`
|
|
111
|
+
: html`<figure-v3-editor
|
|
112
|
+
.session=${this.session}
|
|
113
|
+
.revision=${this.revision}
|
|
114
|
+
@v3-changed=${this.changed}
|
|
115
|
+
></figure-v3-editor>`
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import gql from 'graphql-tag';
|
|
2
|
+
import { client } from '@operato/graphql';
|
|
3
|
+
import { fetchFigure } from './index.js';
|
|
4
|
+
/** V3 source is sent intact. No conversion into the old parts/base authoring representation. */
|
|
5
|
+
export const v3ProductStore = {
|
|
6
|
+
async load(id) {
|
|
7
|
+
const record = await fetchFigure(id);
|
|
8
|
+
if (!record?.source)
|
|
9
|
+
throw Error('Figure 정본이 없습니다.');
|
|
10
|
+
return { id: record.id, source: record.source };
|
|
11
|
+
},
|
|
12
|
+
async save(request) {
|
|
13
|
+
const response = await client.mutate({
|
|
14
|
+
mutation: gql `
|
|
15
|
+
mutation SaveFigureV3($source: String!, $id: String, $expectedSource: String, $creationId: String) {
|
|
16
|
+
saveFigureV3(source: $source, id: $id, expectedSource: $expectedSource, creationId: $creationId) {
|
|
17
|
+
id
|
|
18
|
+
source
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`,
|
|
22
|
+
variables: request
|
|
23
|
+
});
|
|
24
|
+
if (response.errors?.length)
|
|
25
|
+
throw Error(response.errors.map(e => e.message).join('\n'));
|
|
26
|
+
const result = response.data?.saveFigureV3;
|
|
27
|
+
if (!result?.id || !result.source)
|
|
28
|
+
throw Error('서버가 저장 결과를 돌려주지 않았습니다.');
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=v3-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v3-store.js","sourceRoot":"","sources":["../../client/graphql/v3-store.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAGxC,gGAAgG;AAChG,MAAM,CAAC,MAAM,cAAc,GAAmB;IAC5C,KAAK,CAAC,IAAI,CAAC,EAAE;QACX,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,EAAE,CAAC,CAAA;QACpC,IAAI,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,KAAK,CAAC,kBAAkB,CAAC,CAAA;QACpD,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAA;IACjD,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,OAAO;QAChB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;YACnC,QAAQ,EAAE,GAAG,CAAA;;;;;;;OAOZ;YACD,SAAS,EAAE,OAAO;SACnB,CAAC,CAAA;QACF,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;QACxF,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,YAA2C,CAAA;QACzE,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,MAAM,KAAK,CAAC,wBAAwB,CAAC,CAAA;QACxE,OAAO,MAAM,CAAA;IACf,CAAC;CACF,CAAA","sourcesContent":["import gql from 'graphql-tag'\nimport { client } from '@operato/graphql'\nimport { fetchFigure } from './index.js'\nimport type { V3ProductStore, V3ProductRecord } from '../modeller/v3-product-session.js'\n\n/** V3 source is sent intact. No conversion into the old parts/base authoring representation. */\nexport const v3ProductStore: V3ProductStore = {\n async load(id) {\n const record = await fetchFigure(id)\n if (!record?.source) throw Error('Figure 정본이 없습니다.')\n return { id: record.id, source: record.source }\n },\n async save(request) {\n const response = await client.mutate({\n mutation: gql`\n mutation SaveFigureV3($source: String!, $id: String, $expectedSource: String, $creationId: String) {\n saveFigureV3(source: $source, id: $id, expectedSource: $expectedSource, creationId: $creationId) {\n id\n source\n }\n }\n `,\n variables: request\n })\n if (response.errors?.length) throw Error(response.errors.map(e => e.message).join('\\n'))\n const result = response.data?.saveFigureV3 as V3ProductRecord | undefined\n if (!result?.id || !result.source) throw Error('서버가 저장 결과를 돌려주지 않았습니다.')\n return result\n }\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { LitElement, type PropertyValues } from 'lit';
|
|
2
|
+
import { type V3RenderPart } from './v3-renderer.js';
|
|
3
|
+
/** Renders evaluated V3 geometry directly. There is no parts/base or things-scene conversion. */
|
|
4
|
+
export declare class FigureV3Canvas extends LitElement {
|
|
5
|
+
static styles: import("lit").CSSResult;
|
|
6
|
+
parts: V3RenderPart[];
|
|
7
|
+
palette: Record<string, string>;
|
|
8
|
+
context: string;
|
|
9
|
+
private renderer?;
|
|
10
|
+
private readonly scene;
|
|
11
|
+
private readonly group;
|
|
12
|
+
private readonly camera;
|
|
13
|
+
private controls?;
|
|
14
|
+
private observer?;
|
|
15
|
+
private initialized;
|
|
16
|
+
private contextAtRender;
|
|
17
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
18
|
+
protected firstUpdated(): void;
|
|
19
|
+
connectedCallback(): void;
|
|
20
|
+
private initialize;
|
|
21
|
+
protected updated(changed: PropertyValues): void;
|
|
22
|
+
private updateGeometry;
|
|
23
|
+
fit(): void;
|
|
24
|
+
private draw;
|
|
25
|
+
private pick;
|
|
26
|
+
thumbnail(): any;
|
|
27
|
+
disconnectedCallback(): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import { LitElement, html, css } from 'lit';
|
|
3
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
4
|
+
import * as THREE from 'three';
|
|
5
|
+
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
6
|
+
import { replaceV3Meshes, disposeV3Meshes } from './v3-renderer.js';
|
|
7
|
+
/** Renders evaluated V3 geometry directly. There is no parts/base or things-scene conversion. */
|
|
8
|
+
let FigureV3Canvas = class FigureV3Canvas extends LitElement {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
this.parts = [];
|
|
12
|
+
this.palette = {};
|
|
13
|
+
this.context = '';
|
|
14
|
+
this.scene = new THREE.Scene();
|
|
15
|
+
this.group = new THREE.Group();
|
|
16
|
+
this.camera = new THREE.PerspectiveCamera(40, 1, 0.1, 1000000);
|
|
17
|
+
this.initialized = false;
|
|
18
|
+
this.contextAtRender = '';
|
|
19
|
+
this.draw = () => {
|
|
20
|
+
if (this.renderer)
|
|
21
|
+
this.renderer.render(this.scene, this.camera);
|
|
22
|
+
};
|
|
23
|
+
this.pick = (event) => {
|
|
24
|
+
const r = event.currentTarget.getBoundingClientRect(), ray = new THREE.Raycaster();
|
|
25
|
+
ray.setFromCamera(new THREE.Vector2(((event.clientX - r.left) / r.width) * 2 - 1, (-(event.clientY - r.top) / r.height) * 2 + 1), this.camera);
|
|
26
|
+
const hit = ray.intersectObjects(this.group.children)[0];
|
|
27
|
+
this.dispatchEvent(new CustomEvent('part-selected', { detail: hit?.object.name ?? '', bubbles: true, composed: true }));
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
static { this.styles = css `
|
|
31
|
+
:host {
|
|
32
|
+
display: block;
|
|
33
|
+
min-width: 0;
|
|
34
|
+
min-height: 0;
|
|
35
|
+
position: relative;
|
|
36
|
+
background: #eef2f4;
|
|
37
|
+
}
|
|
38
|
+
canvas {
|
|
39
|
+
display: block;
|
|
40
|
+
width: 100%;
|
|
41
|
+
height: 100%;
|
|
42
|
+
}
|
|
43
|
+
p {
|
|
44
|
+
position: absolute;
|
|
45
|
+
inset: 45% 1rem auto;
|
|
46
|
+
text-align: center;
|
|
47
|
+
pointer-events: none;
|
|
48
|
+
color: #526773;
|
|
49
|
+
}
|
|
50
|
+
`; }
|
|
51
|
+
render() {
|
|
52
|
+
return html `<canvas aria-label="Figure 3D 모델" @pointerup=${this.pick}></canvas
|
|
53
|
+
>${this.parts.length ? null : html `<p>도형을 추가해 모델링을 시작하세요.</p>`}`;
|
|
54
|
+
}
|
|
55
|
+
firstUpdated() {
|
|
56
|
+
this.initialize();
|
|
57
|
+
}
|
|
58
|
+
connectedCallback() {
|
|
59
|
+
super.connectedCallback();
|
|
60
|
+
if (this.hasUpdated)
|
|
61
|
+
void this.updateComplete.then(() => this.initialize());
|
|
62
|
+
}
|
|
63
|
+
initialize() {
|
|
64
|
+
if (this.initialized || !this.isConnected)
|
|
65
|
+
return;
|
|
66
|
+
const canvas = this.renderRoot.querySelector('canvas');
|
|
67
|
+
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, preserveDrawingBuffer: true });
|
|
68
|
+
this.renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
|
69
|
+
this.scene.background = new THREE.Color('#eef2f4');
|
|
70
|
+
this.scene.add(this.group, new THREE.HemisphereLight(0xffffff, 0x62717d, 2.5));
|
|
71
|
+
const light = new THREE.DirectionalLight(0xffffff, 3);
|
|
72
|
+
light.position.set(1, 2, 3);
|
|
73
|
+
this.scene.add(light);
|
|
74
|
+
this.controls = new OrbitControls(this.camera, canvas);
|
|
75
|
+
this.controls.addEventListener('change', this.draw);
|
|
76
|
+
this.observer = new ResizeObserver(() => {
|
|
77
|
+
const r = this.getBoundingClientRect();
|
|
78
|
+
if (r.width && r.height) {
|
|
79
|
+
this.renderer.setSize(r.width, r.height, false);
|
|
80
|
+
this.camera.aspect = r.width / r.height;
|
|
81
|
+
this.camera.updateProjectionMatrix();
|
|
82
|
+
this.draw();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
this.observer.observe(this);
|
|
86
|
+
this.initialized = true;
|
|
87
|
+
this.updateGeometry(true);
|
|
88
|
+
}
|
|
89
|
+
updated(changed) {
|
|
90
|
+
if (this.initialized && (changed.has('parts') || changed.has('palette') || changed.has('context')))
|
|
91
|
+
this.updateGeometry(this.contextAtRender !== this.context);
|
|
92
|
+
}
|
|
93
|
+
updateGeometry(fit) {
|
|
94
|
+
try {
|
|
95
|
+
const wasEmpty = this.group.children.length === 0;
|
|
96
|
+
replaceV3Meshes(this.group, this.parts, this.palette);
|
|
97
|
+
this.contextAtRender = this.context;
|
|
98
|
+
if (fit || wasEmpty)
|
|
99
|
+
this.fit();
|
|
100
|
+
this.draw();
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// Do not leave another model's geometry visible after a failed context switch.
|
|
104
|
+
if (this.contextAtRender !== this.context) {
|
|
105
|
+
disposeV3Meshes(this.group);
|
|
106
|
+
this.draw();
|
|
107
|
+
}
|
|
108
|
+
this.dispatchEvent(new CustomEvent('render-error', { detail: error, bubbles: true, composed: true }));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
fit() {
|
|
112
|
+
const bounds = new THREE.Box3().setFromObject(this.group), center = new THREE.Vector3(), size = new THREE.Vector3();
|
|
113
|
+
if (!bounds.isEmpty()) {
|
|
114
|
+
bounds.getCenter(center);
|
|
115
|
+
bounds.getSize(size);
|
|
116
|
+
}
|
|
117
|
+
const radius = Math.max(size.length() / 2, 100), distance = (radius / Math.sin(THREE.MathUtils.degToRad(this.camera.fov / 2))) * 1.3;
|
|
118
|
+
this.camera.position.copy(center).add(new THREE.Vector3(1, 0.8, 1).normalize().multiplyScalar(distance));
|
|
119
|
+
this.camera.near = Math.max(0.01, distance / 10000);
|
|
120
|
+
this.camera.far = Math.max(10000, distance * 100);
|
|
121
|
+
this.camera.updateProjectionMatrix();
|
|
122
|
+
this.controls?.target.copy(center);
|
|
123
|
+
this.controls?.update();
|
|
124
|
+
this.draw();
|
|
125
|
+
}
|
|
126
|
+
thumbnail() {
|
|
127
|
+
this.draw();
|
|
128
|
+
return this.renderer?.domElement.toDataURL('image/png');
|
|
129
|
+
}
|
|
130
|
+
disconnectedCallback() {
|
|
131
|
+
this.observer?.disconnect();
|
|
132
|
+
this.controls?.dispose();
|
|
133
|
+
disposeV3Meshes(this.group);
|
|
134
|
+
this.renderer?.dispose();
|
|
135
|
+
this.renderer = undefined;
|
|
136
|
+
this.scene.clear();
|
|
137
|
+
this.initialized = false;
|
|
138
|
+
super.disconnectedCallback();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
__decorate([
|
|
142
|
+
property({ attribute: false }),
|
|
143
|
+
__metadata("design:type", Array)
|
|
144
|
+
], FigureV3Canvas.prototype, "parts", void 0);
|
|
145
|
+
__decorate([
|
|
146
|
+
property({ attribute: false }),
|
|
147
|
+
__metadata("design:type", Object)
|
|
148
|
+
], FigureV3Canvas.prototype, "palette", void 0);
|
|
149
|
+
__decorate([
|
|
150
|
+
property(),
|
|
151
|
+
__metadata("design:type", Object)
|
|
152
|
+
], FigureV3Canvas.prototype, "context", void 0);
|
|
153
|
+
FigureV3Canvas = __decorate([
|
|
154
|
+
customElement('figure-v3-canvas')
|
|
155
|
+
], FigureV3Canvas);
|
|
156
|
+
export { FigureV3Canvas };
|
|
157
|
+
//# sourceMappingURL=v3-canvas.js.map
|
|
@@ -0,0 +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 background: #eef2f4;\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: #526773;\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 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.scene.background = new THREE.Color('#eef2f4')\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 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.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"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { LitElement, type PropertyValues } from 'lit';
|
|
2
|
+
import type { V3ProductSession } from './v3-product-session.js';
|
|
3
|
+
import './v3-canvas.js';
|
|
4
|
+
/** Product editor surface. The injected session is shared by page persistence and AI commands. */
|
|
5
|
+
export declare class FigureV3Editor extends LitElement {
|
|
6
|
+
static styles: import("lit").CSSResult;
|
|
7
|
+
session?: V3ProductSession;
|
|
8
|
+
revision: number;
|
|
9
|
+
private selected;
|
|
10
|
+
private error;
|
|
11
|
+
private lastContext;
|
|
12
|
+
protected willUpdate(_changed: PropertyValues): void;
|
|
13
|
+
private run;
|
|
14
|
+
private changed;
|
|
15
|
+
private save;
|
|
16
|
+
private add;
|
|
17
|
+
private changeField;
|
|
18
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
19
|
+
}
|