@things-factory/figure-ui 10.1.34 → 10.1.35

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,31 @@
1
+ import * as THREE from 'three';
2
+ /** Evaluated V3 boundary: graph values are resolved before reaching the renderer. */
3
+ export interface V3RenderPart {
4
+ id: string;
5
+ shape: {
6
+ provider: string;
7
+ dimensions: Record<string, number>;
8
+ profile?: Array<{
9
+ x: number;
10
+ y: number;
11
+ }>;
12
+ };
13
+ pose: {
14
+ r: number[][];
15
+ t: number[];
16
+ };
17
+ material: {
18
+ token: string;
19
+ preset?: string;
20
+ transparent?: boolean;
21
+ emissive?: {
22
+ token: string;
23
+ intensity: number;
24
+ on: boolean;
25
+ };
26
+ };
27
+ segments?: number;
28
+ }
29
+ export declare function disposeV3Meshes(group: THREE.Group): void;
30
+ /** Build first, then swap; failed rendering must not erase the previous scene. No old meshes survive a successful swap. */
31
+ export declare function replaceV3Meshes(group: THREE.Group, parts: readonly V3RenderPart[], palette: Record<string, string>): void;
@@ -0,0 +1,81 @@
1
+ import * as THREE from 'three';
2
+ function geometry(part) {
3
+ const d = part.shape.dimensions;
4
+ let g;
5
+ if (part.shape.provider === 'box')
6
+ g = new THREE.BoxGeometry(d.width, d.height, d.depth);
7
+ else if (part.shape.provider === 'cylinder')
8
+ g = new THREE.CylinderGeometry(d.radius, d.radius, d.length, part.segments ?? 12);
9
+ else if (part.shape.provider === 'roundedBox') {
10
+ const w = d.width / 2, z = d.depth / 2, r = d.round, s = new THREE.Shape();
11
+ s.moveTo(-w + r, -z);
12
+ s.lineTo(w - r, -z);
13
+ s.quadraticCurveTo(w, -z, w, -z + r);
14
+ s.lineTo(w, z - r);
15
+ s.quadraticCurveTo(w, z, w - r, z);
16
+ s.lineTo(-w + r, z);
17
+ s.quadraticCurveTo(-w, z, -w, z - r);
18
+ s.lineTo(-w, -z + r);
19
+ s.quadraticCurveTo(-w, -z, -w + r, -z);
20
+ g = new THREE.ExtrudeGeometry(s, { depth: d.height, bevelEnabled: false, steps: 1, curveSegments: 6 });
21
+ g.rotateX(Math.PI / 2);
22
+ g.translate(0, d.height / 2, 0);
23
+ }
24
+ else if (part.shape.provider === 'portal') {
25
+ if (!part.shape.profile?.length)
26
+ throw Error('Portal profile is missing');
27
+ const s = new THREE.Shape(part.shape.profile.map(p => new THREE.Vector2(p.x, p.y)));
28
+ g = new THREE.ExtrudeGeometry(s, { depth: d.depth, bevelEnabled: false, steps: 1 });
29
+ g.translate(0, 0, -d.depth / 2);
30
+ }
31
+ else
32
+ throw Error('Unsupported V3 provider: ' + part.shape.provider);
33
+ return g;
34
+ }
35
+ export function disposeV3Meshes(group) {
36
+ for (const child of [...group.children]) {
37
+ if (child instanceof THREE.Mesh) {
38
+ child.geometry.dispose();
39
+ const materials = Array.isArray(child.material) ? child.material : [child.material];
40
+ materials.forEach(m => m.dispose());
41
+ }
42
+ group.remove(child);
43
+ }
44
+ }
45
+ /** Build first, then swap; failed rendering must not erase the previous scene. No old meshes survive a successful swap. */
46
+ export function replaceV3Meshes(group, parts, palette) {
47
+ const staged = new THREE.Group(), ids = new Set();
48
+ try {
49
+ for (const part of parts) {
50
+ if (ids.has(part.id))
51
+ throw Error('Duplicate V3 geometry ID: ' + part.id);
52
+ ids.add(part.id);
53
+ const color = palette[part.material.token];
54
+ if (!color || !/^#[0-9a-f]{6}$/i.test(color))
55
+ throw Error('Unresolved material token: ' + part.material.token);
56
+ const { r, t } = part.pose;
57
+ if (r.length !== 3 || r.some(row => row.length !== 3 || row.some(x => !Number.isFinite(x))) || t.length !== 3 || t.some(x => !Number.isFinite(x)))
58
+ throw Error('Invalid V3 pose: ' + part.id);
59
+ const emissive = part.material.emissive;
60
+ if (emissive?.on && !/^#[0-9a-f]{6}$/i.test(palette[emissive.token] ?? ''))
61
+ throw Error('Unresolved emissive token');
62
+ const g = geometry(part);
63
+ const material = new THREE.MeshStandardMaterial({ color, metalness: part.material.preset === 'metal' ? 0.8 : 0, roughness: part.material.preset === 'metal' ? 0.3 : 0.65, transparent: !!part.material.transparent, opacity: part.material.transparent ? 0.35 : 1, emissive: emissive?.on ? palette[emissive.token] : 0, emissiveIntensity: emissive?.on ? emissive.intensity : 0 });
64
+ const mesh = new THREE.Mesh(g, material);
65
+ mesh.name = part.id;
66
+ mesh.userData.figurePartId = part.id;
67
+ mesh.matrixAutoUpdate = false;
68
+ mesh.matrix.set(r[0][0], r[0][1], r[0][2], t[0], r[1][0], r[1][1], r[1][2], t[1], r[2][0], r[2][1], r[2][2], t[2], 0, 0, 0, 1);
69
+ staged.add(mesh);
70
+ }
71
+ }
72
+ catch (error) {
73
+ disposeV3Meshes(staged);
74
+ throw error;
75
+ }
76
+ disposeV3Meshes(group);
77
+ for (const mesh of [...staged.children])
78
+ group.add(mesh);
79
+ group.updateMatrixWorld(true);
80
+ }
81
+ //# sourceMappingURL=v3-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v3-renderer.js","sourceRoot":"","sources":["../../client/modeller/v3-renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import * as THREE from 'three'\n\n/** Evaluated V3 boundary: graph values are resolved before reaching the renderer. */\nexport interface V3RenderPart {\n id: string\n shape: { provider: string; dimensions: Record<string, number>; profile?: Array<{x:number;y:number}> }\n pose: {r:number[][];t:number[]}\n material: {token:string;preset?:string;transparent?:boolean;emissive?:{token:string;intensity:number;on:boolean}}\n segments?:number\n}\n\nfunction geometry(part:V3RenderPart):THREE.BufferGeometry {\n const d=part.shape.dimensions\n let g:THREE.BufferGeometry\n if(part.shape.provider==='box')g=new THREE.BoxGeometry(d.width,d.height,d.depth)\n else if(part.shape.provider==='cylinder')g=new THREE.CylinderGeometry(d.radius,d.radius,d.length,part.segments??12)\n else if(part.shape.provider==='roundedBox'){\n const w=d.width/2,z=d.depth/2,r=d.round,s=new THREE.Shape()\n s.moveTo(-w+r,-z);s.lineTo(w-r,-z);s.quadraticCurveTo(w,-z,w,-z+r)\n s.lineTo(w,z-r);s.quadraticCurveTo(w,z,w-r,z);s.lineTo(-w+r,z)\n s.quadraticCurveTo(-w,z,-w,z-r);s.lineTo(-w,-z+r);s.quadraticCurveTo(-w,-z,-w+r,-z)\n g=new THREE.ExtrudeGeometry(s,{depth:d.height,bevelEnabled:false,steps:1,curveSegments:6})\n g.rotateX(Math.PI/2);g.translate(0,d.height/2,0)\n }else if(part.shape.provider==='portal'){\n if(!part.shape.profile?.length)throw Error('Portal profile is missing')\n const s=new THREE.Shape(part.shape.profile.map(p=>new THREE.Vector2(p.x,p.y)))\n g=new THREE.ExtrudeGeometry(s,{depth:d.depth,bevelEnabled:false,steps:1});g.translate(0,0,-d.depth/2)\n }else throw Error('Unsupported V3 provider: '+part.shape.provider)\n return g\n}\n\nexport function disposeV3Meshes(group:THREE.Group):void {\n for(const child of [...group.children]){\n if(child instanceof THREE.Mesh){child.geometry.dispose();const materials=Array.isArray(child.material)?child.material:[child.material];materials.forEach(m=>m.dispose())}\n group.remove(child)\n }\n}\n\n/** Build first, then swap; failed rendering must not erase the previous scene. No old meshes survive a successful swap. */\nexport function replaceV3Meshes(group:THREE.Group,parts:readonly V3RenderPart[],palette:Record<string,string>):void {\n const staged=new THREE.Group(),ids=new Set<string>()\n try{\n for(const part of parts){\n if(ids.has(part.id))throw Error('Duplicate V3 geometry ID: '+part.id);ids.add(part.id)\n const color=palette[part.material.token]\n if(!color||!/^#[0-9a-f]{6}$/i.test(color))throw Error('Unresolved material token: '+part.material.token)\n const {r,t}=part.pose\n if(r.length!==3||r.some(row=>row.length!==3||row.some(x=>!Number.isFinite(x)))||t.length!==3||t.some(x=>!Number.isFinite(x)))throw Error('Invalid V3 pose: '+part.id)\n const emissive=part.material.emissive\n if(emissive?.on&&!/^#[0-9a-f]{6}$/i.test(palette[emissive.token]??''))throw Error('Unresolved emissive token')\n const g=geometry(part)\n const material=new THREE.MeshStandardMaterial({color,metalness:part.material.preset==='metal'?0.8:0,roughness:part.material.preset==='metal'?0.3:0.65,transparent:!!part.material.transparent,opacity:part.material.transparent?0.35:1,emissive:emissive?.on?palette[emissive.token]:0,emissiveIntensity:emissive?.on?emissive.intensity:0})\n const mesh=new THREE.Mesh(g,material);mesh.name=part.id;mesh.userData.figurePartId=part.id\n mesh.matrixAutoUpdate=false;mesh.matrix.set(r[0][0],r[0][1],r[0][2],t[0],r[1][0],r[1][1],r[1][2],t[1],r[2][0],r[2][1],r[2][2],t[2],0,0,0,1)\n staged.add(mesh)\n }\n }catch(error){disposeV3Meshes(staged);throw error}\n disposeV3Meshes(group)\n for(const mesh of [...staged.children])group.add(mesh)\n group.updateMatrixWorld(true)\n}\n"]}
@@ -41,6 +41,7 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
41
41
  * 미저장 표시(`dirty`)는 그대로 남으므로 데이터를 잃지는 않았다. 잃은 것은 **사유**다.
42
42
  */
43
43
  private saveFailure;
44
+ private sourceLoadFailure;
44
45
  private showSaveModal;
45
46
  private saveModalType;
46
47
  private saveModalName;
@@ -217,6 +218,12 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
217
218
  * 그래도 저장은 저작자의 별도 행위다. 잘못 눌러도 DB 정본이나 발행판은 바뀌지 않는다.
218
219
  */
219
220
  private applySizingFix;
221
+ /**
222
+ * The release gate may need several anchors to keep a connected assembly together. It has already
223
+ * measured the complete set. Apply it directly to this unsaved work copy instead of making the
224
+ * author accept a sequence of opaque mini-proposals.
225
+ */
226
+ private applySizingFixPlan;
220
227
  /**
221
228
  * 고른 변경을 받아들인다.
222
229
  *
@@ -143,6 +143,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
143
143
  * 미저장 표시(`dirty`)는 그대로 남으므로 데이터를 잃지는 않았다. 잃은 것은 **사유**다.
144
144
  */
145
145
  this.saveFailure = '';
146
+ this.sourceLoadFailure = '';
146
147
  this.showSaveModal = false;
147
148
  this.saveModalType = '';
148
149
  this.saveModalName = '';
@@ -837,6 +838,11 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
837
838
  };
838
839
  }
839
840
  render() {
841
+ if (this.sourceLoadFailure)
842
+ return html `<section role="alert" style="padding:24px">
843
+ <h2>Figure를 열지 못했습니다</h2><p>${this.sourceLoadFailure}</p>
844
+ <button @click=${() => navigate('figure-list')}>Figure 목록으로</button>
845
+ </section>`;
840
846
  const source = this.folded;
841
847
  return html `
842
848
  <div
@@ -850,6 +856,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
850
856
  @figure-reverted=${() => this.figure?.id && this.load(this.figure.id)}
851
857
  @preview-sizing-fix=${(e) => this.previewSizingFix(e.detail)}
852
858
  @apply-sizing-fix=${(e) => this.applySizingFix(e.detail)}
859
+ @apply-sizing-fix-plan=${(e) => this.applySizingFixPlan(e.detail)}
853
860
  @restore-released-footprint=${(e) => this.restoreReleasedFootprint(e.detail)}
854
861
  @fit-base-to-parts=${() => this.fitBaseToParts()}
855
862
  @settle-parts-on-floor=${() => this.settlePartsOnFloor()}
@@ -1198,6 +1205,37 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1198
1205
  return;
1199
1206
  this.take();
1200
1207
  }
1208
+ /**
1209
+ * The release gate may need several anchors to keep a connected assembly together. It has already
1210
+ * measured the complete set. Apply it directly to this unsaved work copy instead of making the
1211
+ * author accept a sequence of opaque mini-proposals.
1212
+ */
1213
+ applySizingFixPlan(plan) {
1214
+ if (!plan.safe || !this.folded || plan.fixes.length === 0)
1215
+ return;
1216
+ const fixes = new Map(plan.fixes.map(fix => [`${fix.part}:${fix.axis}`, fix]));
1217
+ const next = {
1218
+ ...this.folded,
1219
+ parts: this.folded.parts.map(part => {
1220
+ const anchor = { ...(part.anchor ?? {}) };
1221
+ let changed = false;
1222
+ for (const axis of ['x', 'y', 'z']) {
1223
+ const fix = fixes.get(`${part.name}:${axis}`);
1224
+ if (!fix)
1225
+ continue;
1226
+ anchor[axis] = fix.to;
1227
+ changed = true;
1228
+ }
1229
+ return changed ? { ...part, anchor } : part;
1230
+ })
1231
+ };
1232
+ this.recordHistory();
1233
+ const { draft, parts } = fromFigureSource(next);
1234
+ this.draft = draft;
1235
+ this.parts = parts;
1236
+ this.selected = parts.length ? 0 : -1;
1237
+ this.dirty = true;
1238
+ }
1201
1239
  /**
1202
1240
  * 고른 변경을 받아들인다.
1203
1241
  *
@@ -1333,6 +1371,17 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1333
1371
  const figure = await fetchFigure(id);
1334
1372
  if (generation !== this.loadGeneration || !this.active)
1335
1373
  return;
1374
+ // Decode before assigning the target ID or generating a thumbnail. On failure the previous
1375
+ // editor snapshot remains internal, but is hidden and cannot be saved under the requested ID.
1376
+ let model;
1377
+ try {
1378
+ model = figure.source ? fromFigureSource(JSON.parse(figure.source)) : undefined;
1379
+ }
1380
+ catch (error) {
1381
+ this.sourceLoadFailure = error instanceof Error ? error.message : String(error);
1382
+ return;
1383
+ }
1384
+ this.sourceLoadFailure = '';
1336
1385
  this.proposalSession = undefined;
1337
1386
  this.proposalBaseSource = undefined;
1338
1387
  this.figure = figure;
@@ -1348,7 +1397,6 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1348
1397
  }
1349
1398
  // 서버가 준 것을 그대로 쓴다. 읽지 못하면 오류보고없이 빈 상태로 바꾸지 않는다.
1350
1399
  // 그러면 사용자가 저장을 선택해 원본을 덮어쓴다.
1351
- const model = figure.source ? fromFigureSource(JSON.parse(figure.source)) : undefined;
1352
1400
  this.draft = model?.draft;
1353
1401
  this.parts = model?.parts ?? [];
1354
1402
  /*
@@ -1390,6 +1438,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1390
1438
  }
1391
1439
  }
1392
1440
  startNew() {
1441
+ this.sourceLoadFailure = '';
1393
1442
  ++this.loadGeneration;
1394
1443
  this.proposalSession = undefined;
1395
1444
  this.proposalBaseSource = undefined;
@@ -1747,7 +1796,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
1747
1796
  }
1748
1797
  }
1749
1798
  save() {
1750
- if (!this.canSave || !this.folded || !this.figure) {
1799
+ if (this.sourceLoadFailure || !this.canSave || !this.folded || !this.figure) {
1751
1800
  return;
1752
1801
  }
1753
1802
  if (!this.figure.id) {
@@ -1943,6 +1992,10 @@ __decorate([
1943
1992
  state(),
1944
1993
  __metadata("design:type", Object)
1945
1994
  ], FigureModellerPage.prototype, "saveFailure", void 0);
1995
+ __decorate([
1996
+ state(),
1997
+ __metadata("design:type", Object)
1998
+ ], FigureModellerPage.prototype, "sourceLoadFailure", void 0);
1946
1999
  __decorate([
1947
2000
  state(),
1948
2001
  __metadata("design:type", Object)