@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.
@@ -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,3 @@
1
+ import type { V3ProductStore } from '../modeller/v3-product-session.js';
2
+ /** V3 source is sent intact. No conversion into the old parts/base authoring representation. */
3
+ export declare const v3ProductStore: V3ProductStore;
@@ -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
+ }