@liquidcars/atlas 0.1.53 → 0.2.0
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/README.md +6 -0
- package/dist/liquidcars-atlas.js +10496 -4706
- package/dist/liquidcars-atlas.js.map +1 -1
- package/package.json +1 -1
- package/src/diagram-scene.js +125 -0
- package/src/diagram-svg.js +73 -90
- package/src/diagram-view.js +6 -1
- package/src/diagram-webgl.js +166 -0
- package/src/liquidcars-atlas.js +26 -17
package/package.json
CHANGED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Draws the already calculated diagram. No layout or SVG work occurs in a camera frame.
|
|
2
|
+
const isContainer=node=>['container','shell'].includes(node.type);
|
|
3
|
+
export class DiagramScene {
|
|
4
|
+
constructor(atlas,G){this.atlas=atlas;this.G=G;this.visuals=new Map();this.links=[];this.index=new Map();this.positions=new Map();this.batch=null;this.basic=new Map();}
|
|
5
|
+
material(source){
|
|
6
|
+
if(source.isLineBasicMaterial)return source;
|
|
7
|
+
if(this.basic.has(source))return this.basic.get(source);
|
|
8
|
+
const material=new this.G.MeshBasicMaterial({color:source.color||'#fff',map:source.map,side:source.side,alphaTest:source.alphaTest||0,transparent:true,opacity:source.userData?.atlasBaseOpacity??source.opacity??1,depthWrite:source.depthWrite,toneMapped:false});
|
|
9
|
+
material.userData.atlasBaseOpacity=material.opacity;this.basic.set(source,material);return material;
|
|
10
|
+
}
|
|
11
|
+
flattenMaterials(){
|
|
12
|
+
const originals=new Set();
|
|
13
|
+
for(const node of this.atlas.nodes.values()){
|
|
14
|
+
if(isContainer(node))continue;
|
|
15
|
+
node.mesh.traverse(mesh=>{if(!mesh.isMesh||mesh===node.pickMesh)return;const original=Array.isArray(mesh.material)?mesh.material:[mesh.material];const materials=original.map(source=>{originals.add(source);return this.material(source);});mesh.material=Array.isArray(mesh.material)?materials:materials[0];});
|
|
16
|
+
if(node.material)node.material=this.material(node.material);
|
|
17
|
+
if(node.appearanceMaterials)node.appearanceMaterials=node.appearanceMaterials.map(m=>this.material(m));
|
|
18
|
+
if(node.pickMesh)node.pickMesh.visible=false;
|
|
19
|
+
}
|
|
20
|
+
for(const material of originals)material.dispose();
|
|
21
|
+
}
|
|
22
|
+
caption(node,lines,fontSize,lineHeight,color,align){
|
|
23
|
+
const key=JSON.stringify([lines,fontSize,lineHeight,color,align]),visual=this.visuals.get(node.id);
|
|
24
|
+
if(visual?.captionKey===key)return visual.caption;
|
|
25
|
+
if(visual?.caption){node.group.remove(visual.caption);visual.caption.geometry.dispose();visual.caption.material.map.dispose();visual.caption.material.dispose();}
|
|
26
|
+
const canvas=this.atlas.ownerDocument.createElement('canvas'),ctx=canvas.getContext('2d');
|
|
27
|
+
ctx.font=`${fontSize}px system-ui`;
|
|
28
|
+
const width=Math.max(1,...lines.map(line=>ctx.measureText(line).width))+4,height=Math.max(lineHeight,lines.length*lineHeight);
|
|
29
|
+
const density=Math.min(4,4096/width,4096/height);
|
|
30
|
+
canvas.width=Math.ceil(width*density);canvas.height=Math.ceil(height*density);ctx.scale(density,density);ctx.font=`${fontSize}px system-ui`;ctx.fillStyle=color;ctx.textAlign=align;ctx.textBaseline='top';
|
|
31
|
+
lines.forEach((line,i)=>ctx.fillText(line,align==='left'?2:width/2,i*lineHeight));
|
|
32
|
+
const texture=new this.G.CanvasTexture(canvas);texture.colorSpace='srgb';
|
|
33
|
+
const mesh=new this.G.Mesh(new this.G.PlaneGeometry(width/this.scale,height/this.scale),new this.G.MeshBasicMaterial({map:texture,transparent:true,depthTest:false,depthWrite:false,toneMapped:false}));
|
|
34
|
+
mesh.renderOrder=100;mesh.userData.node=node;mesh.userData.size=[width/this.scale,height/this.scale];node.group.add(mesh);visual.captionKey=key;visual.caption=mesh;return mesh;
|
|
35
|
+
}
|
|
36
|
+
container(node,visual,width,height,color,depth){
|
|
37
|
+
if(!visual.fill){
|
|
38
|
+
visual.fill=new this.G.Mesh(new this.G.BufferGeometry(),new this.G.MeshBasicMaterial({color,depthTest:false,depthWrite:false,toneMapped:false}));
|
|
39
|
+
visual.border=new this.G.LineSegments(new this.G.BufferGeometry(),new this.G.LineBasicMaterial({color,transparent:true,opacity:.72,depthTest:false,depthWrite:false,toneMapped:false}));
|
|
40
|
+
node.group.add(visual.fill,visual.border);
|
|
41
|
+
}
|
|
42
|
+
const key=width+':'+height;
|
|
43
|
+
if(visual.rectangleKey!==key){
|
|
44
|
+
visual.rectangleKey=key;const radius=Math.min(12/this.scale,width/4,height/4),points=[],fill=[],border=[];
|
|
45
|
+
for(const [x,y,start] of [[width/2-radius,height/2-radius,0],[-width/2+radius,height/2-radius,90],[-width/2+radius,-height/2+radius,180],[width/2-radius,-height/2+radius,270]])
|
|
46
|
+
for(let i=0;i<=5;i++){const angle=(start+i*18)*Math.PI/180;points.push([x+Math.cos(angle)*radius,y+Math.sin(angle)*radius]);}
|
|
47
|
+
for(let i=0;i<points.length;i++){const a=points[i],b=points[(i+1)%points.length];fill.push(0,0,0,...a,0,...b,0);border.push(...a,0,...b,0);}
|
|
48
|
+
visual.fill.geometry.dispose();visual.border.geometry.dispose();visual.fill.geometry=new this.G.BufferGeometry().setAttribute('position',new this.G.Float32BufferAttribute(fill,3));visual.border.geometry=new this.G.BufferGeometry().setAttribute('position',new this.G.Float32BufferAttribute(border,3));
|
|
49
|
+
}
|
|
50
|
+
visual.fill.renderOrder=-200+depth;visual.border.renderOrder=-100+depth;visual.baseBackground=this.backgroundFor(node.parent);visual.fillTint=visual.baseBackground.clone().lerp(new this.G.Color(color),.14);
|
|
51
|
+
visual.fill.material.color.copy(visual.fillTint);visual.border.material.color.set(color).lerp(new this.G.Color(this.dark?'#e9f0f5':'#254253'),this.dark ? .65 : .48);
|
|
52
|
+
}
|
|
53
|
+
backgroundFor(id,seen=new Set()){
|
|
54
|
+
if(!id||!this.index.has(id)||seen.has(id))return new this.G.Color(this.config.background||(this.dark?'#2d2f31':'#f5f5f5'));
|
|
55
|
+
if(this.backgrounds.has(id))return this.backgrounds.get(id).clone();seen.add(id);
|
|
56
|
+
const node=this.atlas.nodes.get(id),color=this.atlas.modelData.palette[node.c]||node.c||'#5eaa9d',base=this.backgroundFor(node.parent,seen).lerp(new this.G.Color(color),.14);
|
|
57
|
+
this.backgrounds.set(id,base);return base.clone();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
update(layout,config){
|
|
61
|
+
this.layout=layout;this.scale=layout.scale;this.config=config;this.dark=config.theme==='dark';this.index=new Map();this.positions=new Map();this.backgrounds=new Map();
|
|
62
|
+
for(const entry of layout.items){const node=this.atlas.nodes.get(entry.id);if(!node)continue;this.index.set(entry.id,{...entry,kind:'item'});this.positions.set(entry.id,new this.G.Vector3(entry.x/this.scale,-entry.y/this.scale,10));}
|
|
63
|
+
for(const entry of layout.containers){this.index.set(entry.id,{...entry,kind:'container'});this.positions.set(entry.id,new this.G.Vector3((entry.minX+entry.maxX)/(2*this.scale),-(entry.minY+entry.maxY)/(2*this.scale),0));}
|
|
64
|
+
for(const [id,entry] of this.index){
|
|
65
|
+
const node=this.atlas.nodes.get(id);if(!node)continue;
|
|
66
|
+
if(!this.visuals.has(id))this.visuals.set(id,{});const visual=this.visuals.get(id);node.group.position.copy(this.positions.get(id));
|
|
67
|
+
const open=entry.kind==='container',box=entry.bounds;
|
|
68
|
+
const color=this.atlas.modelData.palette[node.c]||node.c||'#5eaa9d';
|
|
69
|
+
if(isContainer(node)){
|
|
70
|
+
const width=open?(entry.maxX-entry.minX)/this.scale:box.maxX-box.minX,height=open?(entry.maxY-entry.minY)/this.scale:box.maxY-box.minY;
|
|
71
|
+
this.container(node,visual,width,height,color,entry.depth||0);
|
|
72
|
+
}
|
|
73
|
+
const lines=open?[node.name||node.id]:entry.labelLines;
|
|
74
|
+
const font=open?layout.containerFontSize:entry.labelFontSize,lineHeight=open?font*1.3:entry.labelLineHeight;
|
|
75
|
+
const caption=this.caption(node,lines,font,lineHeight,this.dark?'#e9f0f5':'#172c3a',open?'left':'center'),[width,height]=caption.userData.size;
|
|
76
|
+
if(open)caption.position.set(-(entry.maxX-entry.minX)/(2*this.scale)+8/this.scale+width/2,(entry.maxY-entry.minY)/(2*this.scale)-6/this.scale-height/2,30);
|
|
77
|
+
else caption.position.set(0,box.minY-(entry.labelGap/this.scale)-height/2,30);
|
|
78
|
+
}
|
|
79
|
+
this.buildLinks(layout.relations);this.apply();
|
|
80
|
+
}
|
|
81
|
+
buildLinks(links){
|
|
82
|
+
this.links=links;this.atlas._clearRelationGroup();
|
|
83
|
+
const positions=[],colors=[],arrows=[],arrowColors=[],base=new this.G.Color(this.dark?'#b1c7d3':'#58788d'),background=new this.G.Color(this.dark?'#2d2f31':'#f5f5f5');
|
|
84
|
+
const segment=(a,b,color)=>{positions.push(a.x/this.scale,-a.y/this.scale,2,b.x/this.scale,-b.y/this.scale,2);colors.push(color.r,color.g,color.b,color.r,color.g,color.b);};
|
|
85
|
+
const arrow=(from,to,color)=>{
|
|
86
|
+
const dx=to.x-from.x,dy=to.y-from.y,length=Math.hypot(dx,dy)||1,s=Math.min(9,length*.25),x=to.x-dx/length*s,y=to.y-dy/length*s;
|
|
87
|
+
for(const point of [to,{x:x-dy/length*s*.42,y:y+dx/length*s*.42},{x:x+dy/length*s*.42,y:y-dx/length*s*.42}]){arrows.push(point.x/this.scale,-point.y/this.scale,2);arrowColors.push(color.r,color.g,color.b);}
|
|
88
|
+
};
|
|
89
|
+
for(const link of links){
|
|
90
|
+
const selected=link.index===this.atlas.selectedRelationIndex,color=link.mode==='broken'?new this.G.Color('#c43748'):selected?new this.G.Color('#008fcb'):base.clone();color.lerp(background,1-link.opacity);
|
|
91
|
+
if(link.mode==='broken'){for(let i=0;i<12;i+=2){const at=t=>({x:link.start.x+(link.end.x-link.start.x)*t,y:link.start.y+(link.end.y-link.start.y)*t});segment(at(i/12),at((i+1)/12),color);}}
|
|
92
|
+
else {segment(link.start,link.end,color);arrow(link.start,link.end,color);if(link.mode==='bidirectional')arrow(link.end,link.start,color);}
|
|
93
|
+
}
|
|
94
|
+
if(positions.length){const geometry=new this.G.BufferGeometry().setAttribute('position',new this.G.Float32BufferAttribute(positions,3)).setAttribute('color',new this.G.Float32BufferAttribute(colors,3));const mesh=new this.G.LineSegments(geometry,new this.G.LineBasicMaterial({vertexColors:true,transparent:true,depthTest:false,depthWrite:false,toneMapped:false}));mesh.renderOrder=-50;this.atlas.relationGroup.add(mesh);}
|
|
95
|
+
if(arrows.length){const geometry=new this.G.BufferGeometry().setAttribute('position',new this.G.Float32BufferAttribute(arrows,3)).setAttribute('color',new this.G.Float32BufferAttribute(arrowColors,3));const mesh=new this.G.Mesh(geometry,new this.G.MeshBasicMaterial({vertexColors:true,side:2,transparent:true,depthTest:false,depthWrite:false,toneMapped:false}));mesh.renderOrder=-49;this.atlas.relationGroup.add(mesh);}
|
|
96
|
+
}
|
|
97
|
+
apply(){
|
|
98
|
+
for(const node of this.atlas.nodes.values()){
|
|
99
|
+
const entry=this.index.get(node.id),visible=!!entry&&entry.visible;
|
|
100
|
+
node.group.visible=visible;node.label.visible=false;
|
|
101
|
+
if(!entry)continue;node.group.position.copy(this.positions.get(node.id));
|
|
102
|
+
if(isContainer(node)){node.mesh.visible=false;if(node.edge)node.edge.visible=false;const visual=this.visuals.get(node.id);visual.fill.visible=visible;visual.fill.material.color.copy(visual.fillTint).lerp(visual.baseBackground,1-entry.opacity);visual.border.visible=visible;visual.border.material.opacity=.72*entry.opacity;}
|
|
103
|
+
else{node.mesh.visible=visible;if(node.edge?.material)node.edge.material.opacity=(node===this.atlas.selected?1:.65)*entry.opacity;for(const material of node.appearanceMaterials||[node.material])if(material)material.opacity=(material.userData.atlasBaseOpacity??1)*entry.opacity;}
|
|
104
|
+
const visual=this.visuals.get(node.id);
|
|
105
|
+
if(node===this.atlas.selected&&!visual.selection){
|
|
106
|
+
const geometry=new this.G.BufferGeometry().setAttribute('position',new this.G.Float32BufferAttribute([-.5,-.5,0,.5,-.5,0,.5,-.5,0,.5,.5,0,.5,.5,0,-.5,.5,0,-.5,.5,0,-.5,-.5,0],3));
|
|
107
|
+
visual.selection=new this.G.LineSegments(geometry,new this.G.LineBasicMaterial({color:'#008fcb',depthTest:false,depthWrite:false,toneMapped:false}));visual.selection.renderOrder=90;node.group.add(visual.selection);
|
|
108
|
+
}
|
|
109
|
+
if(visual.selection){visual.selection.visible=visible&&node===this.atlas.selected;const box=entry.kind==='container'?{minX:-(entry.maxX-entry.minX)/(2*this.scale),maxX:(entry.maxX-entry.minX)/(2*this.scale),minY:-(entry.maxY-entry.minY)/(2*this.scale),maxY:(entry.maxY-entry.minY)/(2*this.scale)}:entry.bounds;visual.selection.scale.set(box.maxX-box.minX+.15,box.maxY-box.minY+.15,1);visual.selection.position.set((box.minX+box.maxX)/2,(box.minY+box.maxY)/2,20);}
|
|
110
|
+
if(visual?.caption){visual.caption.visible=visible&&(entry.kind==='container'||this.config.labels);visual.caption.material.opacity=entry.opacity;}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
pick(event,fallback){
|
|
114
|
+
const rect=this.atlas.canvas.getBoundingClientRect(),units=28/(Math.max(.001,this.atlas.zoom)*rect.height),point={x:this.atlas.focus.x+(event.clientX-rect.left-rect.width/2)*units,y:this.atlas.focus.y-(event.clientY-rect.top-rect.height/2)*units};
|
|
115
|
+
const captions=[...this.visuals.entries()].filter(([id])=>this.index.get(id)?.visible&&this.visuals.get(id).caption?.visible).map(([,v])=>v.caption);
|
|
116
|
+
const text=this.atlas.raycaster.intersectObjects(captions,false)[0];if(text)return {kind:'entity',node:text.object.userData.node};
|
|
117
|
+
if(fallback?.kind==='entity'&&!isContainer(fallback.node))return fallback;
|
|
118
|
+
const x=point.x*this.scale,y=-point.y*this.scale;let nearest=null,distance=5*units*this.scale;
|
|
119
|
+
for(const link of this.links){const a=link.start,b=link.end,dx=b.x-a.x,dy=b.y-a.y,t=Math.max(0,Math.min(1,((x-a.x)*dx+(y-a.y)*dy)/(dx*dx+dy*dy||1))),d=Math.hypot(x-a.x-t*dx,y-a.y-t*dy);if(d<distance){distance=d;nearest=link;}}
|
|
120
|
+
if(nearest)return {kind:'relation',index:nearest.index};
|
|
121
|
+
let chosen=null,area=Infinity;
|
|
122
|
+
for(const [id,entry] of this.index){if(!entry.visible||!isContainer(this.atlas.nodes.get(id)))continue;const box=entry.kind==='container'?entry:{minX:entry.x+entry.bounds.minX*this.scale,maxX:entry.x+entry.bounds.maxX*this.scale,minY:entry.y-entry.bounds.maxY*this.scale,maxY:entry.y-entry.bounds.minY*this.scale};const size=(box.maxX-box.minX)*(box.maxY-box.minY);if(x>=box.minX&&x<=box.maxX&&y>=box.minY&&y<=box.maxY&&size<area){chosen=id;area=size;}}
|
|
123
|
+
return chosen?{kind:'entity',node:this.atlas.nodes.get(chosen)}:null;
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/diagram-svg.js
CHANGED
|
@@ -185,31 +185,27 @@ function rayBoundary(item, towards) {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
function separateOverlaps(items, gap) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const sign = a.y <= b.y ? 1 : -1;
|
|
207
|
-
a.y -= sign * push;
|
|
208
|
-
b.y += sign * push;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (!moved) break;
|
|
188
|
+
const ordered = items.map(item => ({ item, box: itemBounds(item, item.labelLines.length > 0) }))
|
|
189
|
+
.sort((a,b) => a.box.minY-b.box.minY || a.box.minX-b.box.minX);
|
|
190
|
+
const cell = Math.max(1, ...ordered.map(({box}) => Math.max(box.maxX-box.minX,box.maxY-box.minY)+gap));
|
|
191
|
+
const cells = new Map();
|
|
192
|
+
const keys = box => {
|
|
193
|
+
const out=[];
|
|
194
|
+
for(let x=Math.floor(box.minX/cell);x<=Math.floor((box.maxX+gap)/cell);x++)
|
|
195
|
+
for(let y=Math.floor(box.minY/cell);y<=Math.floor((box.maxY+gap)/cell);y++)out.push(x+','+y);
|
|
196
|
+
return out;
|
|
197
|
+
};
|
|
198
|
+
for(const {item,box} of ordered){
|
|
199
|
+
for(;;){
|
|
200
|
+
const nearby=new Set(keys(box).flatMap(key=>cells.get(key)||[]));
|
|
201
|
+
let shift=0;
|
|
202
|
+
for(const other of nearby)if(box.minX<other.maxX+gap && box.maxX+gap>other.minX && box.minY<other.maxY+gap && box.maxY+gap>other.minY)
|
|
203
|
+
shift=Math.max(shift,other.maxY+gap-box.minY+.01);
|
|
204
|
+
if(!shift)break;
|
|
205
|
+
item.y+=shift;box.minY+=shift;box.maxY+=shift;
|
|
212
206
|
}
|
|
207
|
+
for(const key of keys(box)){if(!cells.has(key))cells.set(key,[]);cells.get(key).push(box);}
|
|
208
|
+
}
|
|
213
209
|
}
|
|
214
210
|
|
|
215
211
|
function labelLines(name) {
|
|
@@ -340,6 +336,7 @@ export function renderDiagramSvg(atlas, options = {}) {
|
|
|
340
336
|
const depth = Array.isArray(options.depth) ? options.depth : [-Infinity, Infinity];
|
|
341
337
|
let items = [];
|
|
342
338
|
const warnings = [];
|
|
339
|
+
const sharedFaces = new Map();
|
|
343
340
|
for (const node of atlas.nodes.values()) {
|
|
344
341
|
if (include && !include.has(node.id)) continue;
|
|
345
342
|
if (!isVisible(node) || (isContainer(node) && !isClosed(node))) continue;
|
|
@@ -348,7 +345,9 @@ export function renderDiagramSvg(atlas, options = {}) {
|
|
|
348
345
|
const distance = origin.dot(forward);
|
|
349
346
|
if (distance < depth[0] || distance > depth[1]) continue;
|
|
350
347
|
const faceCache = options.faceCache;
|
|
351
|
-
|
|
348
|
+
const faceKey = JSON.stringify([node.geometry || "box", node.type, node.c, node.s, node.radius, node.segments, node.geometryOptions, node.geometryPaletteOverrides, node.geometry ? null : node.mesh?.geometry?.uuid, node.c ? null : node.mesh?.material?.color?.getHexString(), independent ? null : node.material?.opacity, independent ? null : node.appearanceMaterials?.map(material => material.opacity)]);
|
|
349
|
+
let face = faceCache?.has(node) ? faceCache.get(node) : sharedFaces.get(faceKey);
|
|
350
|
+
if (!face) { face = frontFaces(node, independent); sharedFaces.set(faceKey, face); }
|
|
352
351
|
if (faceCache && !faceCache.has(node)) faceCache.set(node, face);
|
|
353
352
|
if (!face) { warnings.push(`${node.id}: no vector front face`); continue; }
|
|
354
353
|
// The 3D renderer can leave an opened shell's material translucent when
|
|
@@ -364,7 +363,7 @@ export function renderDiagramSvg(atlas, options = {}) {
|
|
|
364
363
|
const geometryHeight = (face.bounds.maxY - face.bounds.minY) * scale;
|
|
365
364
|
items.push({ id: node.id, name: node.name || "", x: origin.dot(right) * scale,
|
|
366
365
|
y: -origin.dot(up) * scale, scale, depth: distance, ...face, labelLines: lines,
|
|
367
|
-
labelFontSize, labelLineHeight, labelGap,
|
|
366
|
+
faceKey, labelFontSize, labelLineHeight, labelGap,
|
|
368
367
|
width: geometryWidth, height: geometryHeight });
|
|
369
368
|
}
|
|
370
369
|
if (options.cameraFrame && items.length) {
|
|
@@ -440,67 +439,33 @@ export function renderDiagramSvg(atlas, options = {}) {
|
|
|
440
439
|
return boxes;
|
|
441
440
|
};
|
|
442
441
|
let boxes = buildBoxes();
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (!moved) break;
|
|
471
|
-
}
|
|
472
|
-
const grouped = new Set([...rootMembers.values()].flat());
|
|
473
|
-
const blocks = rootGroups.map(root => ({ box: boxes.get(root.id), members: rootMembers.get(root.id) }));
|
|
474
|
-
for (const item of items) if (!grouped.has(item)) {
|
|
475
|
-
blocks.push({ box: itemBounds(item, options.labels !== false), members: [item] });
|
|
476
|
-
}
|
|
477
|
-
// Keep the canonical front's authored grouping. A turned camera can spread
|
|
478
|
-
// those same root groups across a very large page, so arrange the already
|
|
479
|
-
// resolved group rectangles into a compact reading surface.
|
|
480
|
-
const placement = forward.z < .97 ? compactRootBlocks(blocks, groupGap,
|
|
481
|
-
Math.max(.5, Math.min(3, Number(options.cameraFrame?.aspect) || 1.7))) : null;
|
|
482
|
-
if (placement) {
|
|
483
|
-
for (const block of blocks) {
|
|
484
|
-
const position = placement.get(block);
|
|
485
|
-
for (const item of block.members) {
|
|
486
|
-
item.x += position.x - block.box.minX;
|
|
487
|
-
item.y += position.y - block.box.minY;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
boxes = buildBoxes();
|
|
491
|
-
}
|
|
492
|
-
// Captured diagrams keep the user's chosen 2D arrangement independently of
|
|
493
|
-
// later changes to the packing heuristic or viewport aspect.
|
|
494
|
-
if (options.placements && typeof options.placements === "object") {
|
|
495
|
-
for (const item of items) {
|
|
496
|
-
const position = options.placements[item.id];
|
|
497
|
-
if (Array.isArray(position) && position.length === 2 && position.every(Number.isFinite)) {
|
|
498
|
-
item.x = position[0];
|
|
499
|
-
item.y = position[1];
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
boxes = buildBoxes();
|
|
503
|
-
}
|
|
442
|
+
const groupGap = labelScale ? 8 / labelScale : Math.max(8, gap);
|
|
443
|
+
// Resolve from the deepest siblings outwards. Each child container moves as a
|
|
444
|
+
// rigid subtree; parents are then measured again around their final children.
|
|
445
|
+
if (options.placements && typeof options.placements === "object") for(const item of items){
|
|
446
|
+
const at=options.placements[item.id];if(Array.isArray(at)&&at.length===2&&at.every(Number.isFinite)){item.x=at[0];item.y=at[1];}
|
|
447
|
+
}
|
|
448
|
+
boxes=buildBoxes();
|
|
449
|
+
const descendants=new Map(), parentOf=id=>{
|
|
450
|
+
let parent=atlas.nodes.get(id)?.parent,seen=new Set();
|
|
451
|
+
while(parent&&!seen.has(parent)){if(boxes.has(parent))return parent;seen.add(parent);parent=atlas.nodes.get(parent)?.parent;}
|
|
452
|
+
return null;
|
|
453
|
+
};
|
|
454
|
+
for(const item of items){let parent=parentOf(item.id),seen=new Set();while(parent&&!seen.has(parent)){seen.add(parent);if(!descendants.has(parent))descendants.set(parent,[]);descendants.get(parent).push(item);parent=parentOf(parent);}}
|
|
455
|
+
const parents=[...containers.filter(node=>boxes.has(node.id)).sort((a,b)=>containerDepth(b)-containerDepth(a)).map(node=>node.id),null];
|
|
456
|
+
for(const parent of parents){
|
|
457
|
+
const units=[...containers.filter(node=>boxes.has(node.id)&&parentOf(node.id)===parent).map(node=>({box:boxes.get(node.id),members:descendants.get(node.id)||[]})),...items.filter(item=>parentOf(item.id)===parent).map(item=>({box:itemBounds(item,options.labels!==!1),members:[item]}))];
|
|
458
|
+
let collision=false;
|
|
459
|
+
for(let i=0;i<units.length&&!collision;i++)for(let j=i+1;j<units.length;j++){const a=units[i].box,b=units[j].box;if(a.minX<b.maxX+groupGap&&a.maxX+groupGap>b.minX&&a.minY<b.maxY+groupGap&&a.maxY+groupGap>b.minY){collision=true;break;}}
|
|
460
|
+
if(!collision)continue;
|
|
461
|
+
const originX=Math.min(...units.map(u=>u.box.minX)),originY=Math.min(...units.map(u=>u.box.minY)),aspect=Math.max(.5,Math.min(3,Number(options.cameraFrame?.aspect)||1.7));
|
|
462
|
+
const area=units.reduce((sum,u)=>sum+(u.box.maxX-u.box.minX+groupGap)*(u.box.maxY-u.box.minY+groupGap),0),rowWidth=Math.max(Math.sqrt(area*aspect),...units.map(u=>u.box.maxX-u.box.minX));
|
|
463
|
+
units.sort((a,b)=>a.box.minY-b.box.minY||a.box.minX-b.box.minX);
|
|
464
|
+
let x=0,y=0,height=0;
|
|
465
|
+
for(const unit of units){const width=unit.box.maxX-unit.box.minX,h=unit.box.maxY-unit.box.minY;if(x&&x+width>rowWidth){x=0;y+=height+groupGap;height=0;}const dx=originX+x-unit.box.minX,dy=originY+y-unit.box.minY;for(const item of unit.members){item.x+=dx;item.y+=dy;}x+=width+groupGap;height=Math.max(height,h);}
|
|
466
|
+
boxes=buildBoxes();
|
|
467
|
+
}
|
|
468
|
+
|
|
504
469
|
const extent = [...items.map(item => itemBounds(item, options.labels !== false)), ...boxes.values()];
|
|
505
470
|
const left = (extent.length ? Math.min(...extent.map(box => box.minX)) : 0) - padding;
|
|
506
471
|
const top = (extent.length ? Math.min(...extent.map(box => box.minY)) : 0) - padding;
|
|
@@ -508,6 +473,12 @@ export function renderDiagramSvg(atlas, options = {}) {
|
|
|
508
473
|
const bottom = (extent.length ? Math.max(...extent.map(box => box.maxY)) : 0) + padding;
|
|
509
474
|
const width = Math.max(1, Math.ceil(rightEdge - left));
|
|
510
475
|
const height = Math.max(1, Math.ceil(bottom - top));
|
|
476
|
+
if (options.layoutOnly) return {
|
|
477
|
+
width, height, scale, left, top, labelFontSize, labelLineHeight, labelGap, containerFontSize,
|
|
478
|
+
items: items.map(item => ({ id:item.id, x:item.x-left, y:item.y-top, width:item.width, height:item.height, bounds:item.bounds, labelLines:item.labelLines, labelFontSize:item.labelFontSize, labelLineHeight:item.labelLineHeight, labelGap:item.labelGap, visible:!hiddenByFocus(item.id), opacity:dimmed(item.id) })),
|
|
479
|
+
containers: [...boxes.values()].map(box => ({ id:box.node.id, minX:box.minX-left, maxX:box.maxX-left, minY:box.minY-top, maxY:box.maxY-top, depth:containerDepth(box.node), visible:!hiddenByFocus(box.node.id), opacity:dimmed(box.node.id) })),
|
|
480
|
+
relations: visible.map(relation => { const from=byId.get(relation.from),to=byId.get(relation.to); return { id:relation.id, index:atlas.relations.indexOf(relation), from:relation.from, to:relation.to, start:rayBoundary(from,to), end:rayBoundary(to,from), mode:relationMode(relation), opacity:Math.min(dimmed(relation.from),dimmed(relation.to)) }; }).map(link => ({...link,start:{x:link.start.x-left,y:link.start.y-top},end:{x:link.end.x-left,y:link.end.y-top}})), warnings
|
|
481
|
+
};
|
|
511
482
|
if (options.measureOnly) return { width, height, warnings };
|
|
512
483
|
const fmt = p => `${round(p.x - left)} ${round(p.y - top)}`;
|
|
513
484
|
const relations = visible.map(relation => {
|
|
@@ -554,13 +525,22 @@ export function renderDiagramSvg(atlas, options = {}) {
|
|
|
554
525
|
? `<rect x="${x}" y="${y}" width="${round(box.maxX - box.minX)}" height="${round(box.maxY - box.minY)}" rx="12" fill="${parentBackdrop}"/>` : "";
|
|
555
526
|
return `${underlay}<g data-container="${escapeXml(box.node.id)}"${dimmed(box.node.id) < 1 ? ` opacity="${dimmed(box.node.id)}"` : ""}><title>${escapeXml(box.node.name || box.node.id)}</title><rect x="${x}" y="${y}" width="${round(box.maxX - box.minX)}" height="${round(box.maxY - box.minY)}" rx="12" ${fill} stroke="${ink}" stroke-opacity="0.72" stroke-width="2"/><g data-diagram-label="container" transform="translate(${x} ${y})"><text x="${labelX}" y="${labelY}" font-family="system-ui,sans-serif" font-size="${round(containerFontSize)}" font-weight="600" fill="${ink}">${escapeXml(box.node.name || box.node.id)}</text></g></g>`;
|
|
556
527
|
}).join("");
|
|
528
|
+
const svgSymbols = new Map(), svgDefs = [];
|
|
529
|
+
const svgGlyph = (item, make) => {
|
|
530
|
+
if (!svgSymbols.has(item.faceKey)) {
|
|
531
|
+
const id = 'atlas-geometry-' + svgSymbols.size;
|
|
532
|
+
svgDefs.push(`<symbol id="${id}" overflow="visible">${make()}</symbol>`);
|
|
533
|
+
svgSymbols.set(item.faceKey, `<use href="#${id}"/>`);
|
|
534
|
+
}
|
|
535
|
+
return svgSymbols.get(item.faceKey);
|
|
536
|
+
};
|
|
557
537
|
const entities = items.sort((a, b) => a.depth - b.depth).map(item => {
|
|
558
538
|
if (hiddenByFocus(item.id)) return "";
|
|
559
539
|
let parent = atlas.nodes.get(item.id)?.parent, guard = 0;
|
|
560
540
|
while (parent && !boxBackdrops.has(parent) && guard++ < atlas.nodes.size) parent = atlas.nodes.get(parent)?.parent;
|
|
561
541
|
const mask = options.maskGrid && !boxBackdrops.has(parent) && item.hull.length > 2
|
|
562
542
|
? `<polygon points="${item.hull.map(point => `${round(point.x * scale)},${round(-point.y * scale)}`).join(" ")}" fill="${escapeXml(canvasColor)}"/>` : "";
|
|
563
|
-
const paths = [...item.triangles, ...item.textured].sort((a, b) => a.depth - b.depth).map((triangle, index) => {
|
|
543
|
+
const paths = svgGlyph(item, () => [...item.triangles, ...item.textured].sort((a, b) => a.depth - b.depth).map((triangle, index) => {
|
|
564
544
|
if (triangle.image) {
|
|
565
545
|
const clipId = `atlas-clip-${escapeXml(item.id).replace(/[^a-zA-Z0-9_-]/g, "-")}-${index}`;
|
|
566
546
|
const hull = convexHull(triangle.points.map(point => ({ x: point.x, y: point.y })));
|
|
@@ -571,14 +551,14 @@ export function renderDiagramSvg(atlas, options = {}) {
|
|
|
571
551
|
const p = triangle.vertices.map(point => `${round(point.x * scale)},${round(-point.y * scale)}`).join(" ");
|
|
572
552
|
const stroke = triangle.opacity < 1 ? "" : ` stroke="${triangle.color}" stroke-width="0.8" stroke-linejoin="round"`;
|
|
573
553
|
return `<polygon points="${p}" fill="${triangle.color}"${stroke}${triangle.opacity < 1 ? ` opacity="${round(triangle.opacity)}"` : ""}/>`;
|
|
574
|
-
}).join("");
|
|
554
|
+
}).join(""));
|
|
575
555
|
const labelBottom = round(-item.bounds.minY * scale);
|
|
576
556
|
const label = options.labels === false || !item.labelLines.length ? "" : `<g data-diagram-label="entity" transform="translate(0 ${labelBottom})">${item.labelLines.map((line, i) => `<text x="0" y="${round(item.labelGap + item.labelFontSize + i * item.labelLineHeight)}" text-anchor="middle" font-family="system-ui,sans-serif" font-size="${round(item.labelFontSize)}" fill="${escapeXml(options.labelColor || "#172c3a")}">${escapeXml(line)}</text>`).join("")}</g>`;
|
|
577
557
|
const transform = `translate(${round(item.x - left)} ${round(item.y - top)})`;
|
|
578
558
|
return `${mask ? `<g transform="${transform}" pointer-events="none">${mask}</g>` : ""}<g data-entity="${escapeXml(item.id)}" transform="${transform}"${dimmed(item.id) < 1 ? ` opacity="${dimmed(item.id)}"` : ""}><title>${escapeXml(item.name || item.id)}</title>${paths}${label}</g>`;
|
|
579
559
|
}).join("");
|
|
580
560
|
const background = options.background ? `<rect width="100%" height="100%" fill="${escapeXml(options.background)}"/>` : "";
|
|
581
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}"><defs
|
|
561
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}"><defs>${svgDefs.join('')}<marker id="atlas-diagram-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10Z" fill="${escapeXml(options.connectionColor || "#58788d")}"/></marker></defs>${background}<g class="containers">${containersSvg}</g><g class="relations">${relations}</g><g class="entities">${entities}</g></svg>`;
|
|
582
562
|
return { svg, width, height, warnings, items: items.map(({ id, x, y, width: w, height: h }) => ({ id, x: round(x - left), y: round(y - top), width: round(w), height: round(h) })) };
|
|
583
563
|
}
|
|
584
564
|
|
|
@@ -601,3 +581,6 @@ export async function diagramSvgToPng(svg, ownerDocument = globalThis.document)
|
|
|
601
581
|
URL.revokeObjectURL(url);
|
|
602
582
|
}
|
|
603
583
|
}
|
|
584
|
+
|
|
585
|
+
/** Calculate the shared diagram without generating SVG or touching the DOM. */
|
|
586
|
+
export function computeDiagramLayout(atlas, options = {}) { return renderDiagramSvg(atlas, {...options, layoutOnly:true}); }
|
package/src/diagram-view.js
CHANGED
|
@@ -94,6 +94,7 @@ function diagramNodeVisible(atlas, node) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
function modelCenter(atlas) {
|
|
97
|
+
if (atlas._canonicalDiagramCenter) return atlas._canonicalDiagramCenter();
|
|
97
98
|
const points = [...atlas.nodes.values()].filter(node => diagramNodeVisible(atlas, node) && diagramItemNode(atlas, node)).map(node => node.group.position);
|
|
98
99
|
if (!points.length) return [0, 0, 0];
|
|
99
100
|
return ["x", "y", "z"].map(axis => (Math.min(...points.map(point => point[axis])) + Math.max(...points.map(point => point[axis]))) / 2);
|
|
@@ -322,6 +323,9 @@ export class DiagramView {
|
|
|
322
323
|
this.cube.hidden = !this.config.showOrientationCube;
|
|
323
324
|
this.cube.innerHTML = orientationCubeSvg(cameraAngles(this.config.cameraView));
|
|
324
325
|
toolbar.querySelector("[data-diagram-cube-visible]").checked = this.config.showOrientationCube;
|
|
326
|
+
const controlsKey = JSON.stringify([this.atlas._diagramRevision, [...this.atlas.open], this.config.includeIds, this.config.orbitId]);
|
|
327
|
+
if (this._itemsControlsKey !== controlsKey) {
|
|
328
|
+
this._itemsControlsKey = controlsKey;
|
|
325
329
|
const orbit = toolbar.querySelector("[data-diagram-orbit]");
|
|
326
330
|
const orbitIds = [...this.atlas.nodes.values()].filter(node => diagramNodeVisible(this.atlas, node) && diagramItemNode(this.atlas, node));
|
|
327
331
|
orbit.innerHTML = '<option value="">Model center</option>' + orbitIds.map(node =>
|
|
@@ -332,6 +336,7 @@ export class DiagramView {
|
|
|
332
336
|
const visible = new Set(this.config.includeIds || [...this.atlas.nodes.keys()]);
|
|
333
337
|
const nodes = [...this.atlas.nodes.values()].filter(node => diagramNodeVisible(this.atlas, node) && diagramItemNode(this.atlas, node));
|
|
334
338
|
items.innerHTML = nodes.map(node => `<button type="button" class="toolbar-option checkbox" data-diagram-item="${escapeHtml(node.id)}"><input type="checkbox" ${visible.has(node.id) ? "checked" : ""}><span>${escapeHtml(node.name || node.id)}</span></button>`).join("");
|
|
339
|
+
}
|
|
335
340
|
const groupIds = [...new Set(this.atlas.relations.map(relation => relation.group || "default"))];
|
|
336
341
|
const selectedGroups = new Set(this.config.relationGroups || groupIds);
|
|
337
342
|
for (const button of toolbar.querySelectorAll('[data-action="relation-group-toggle"]')) {
|
|
@@ -439,7 +444,7 @@ export class DiagramView {
|
|
|
439
444
|
this.atlas.toolbar.addEventListener("change", event => {
|
|
440
445
|
if (!event.target.matches("[data-diagram-orbit]")) return;
|
|
441
446
|
const orbitId = event.target.value;
|
|
442
|
-
const target = orbitId ? this.atlas.nodes.get(orbitId)?.group.position.toArray() : modelCenter(this.atlas);
|
|
447
|
+
const target = orbitId ? (this.atlas._diagramOrbitTarget?.(orbitId) || this.atlas.nodes.get(orbitId)?.group.position.toArray()) : modelCenter(this.atlas);
|
|
443
448
|
const angles = cameraAngles(this.config.cameraView);
|
|
444
449
|
this.setConfig({ orbitId, cameraView: cameraView(target || modelCenter(this.atlas), angles.yaw, angles.pitch) }, { layout: true });
|
|
445
450
|
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {DiagramScene} from './diagram-scene.js';
|
|
2
|
+
// Orthographic viewport over an independently calculated diagram; the domain/3D coordinates stay canonical.
|
|
3
|
+
const container=node=>['container','shell'].includes(node.type);
|
|
4
|
+
export function orthographicFit(bounds,aspect,halfHeight=14){
|
|
5
|
+
const width=Math.max(1,bounds.maxX-bounds.minX),height=Math.max(1,bounds.maxY-bounds.minY);
|
|
6
|
+
return Math.max(.001,Math.min(6,Math.min(halfHeight*2/height,halfHeight*2*aspect/width)/1.15));
|
|
7
|
+
}
|
|
8
|
+
export function diagramViewForModel(value,center,orbit){
|
|
9
|
+
const next={theme:'light',...value},valid=v=>Array.isArray(v)&&v.length===3&&v.every(Number.isFinite),view=next.cameraView;
|
|
10
|
+
if(!view||view.projection==='orthographic'||!valid(view.position)||!valid(view.target))next.cameraView={target:[...center],position:[center[0],center[1],center[2]+30]};
|
|
11
|
+
else if(orbit||valid(next.cameraModelCenter)){
|
|
12
|
+
const target=orbit||view.target.map((n,i)=>n+center[i]-next.cameraModelCenter[i]),delta=target.map((n,i)=>n-view.target[i]);
|
|
13
|
+
next.cameraView={target:[...target],position:view.position.map((n,i)=>n+delta[i])};
|
|
14
|
+
}
|
|
15
|
+
next.cameraModelCenter=[...center];return next;
|
|
16
|
+
}
|
|
17
|
+
export function installPlanarAtlas(Atlas,OrthographicCamera,computeLayout,G){
|
|
18
|
+
const proto=Atlas.prototype;if(proto._planarInstalled)return;proto._planarInstalled=true;
|
|
19
|
+
const original=Object.fromEntries(['_build','_worldPosition','getCameraView','setCameraView','_panByPixels','fit','frontView','resize','_positionJumpButton','_renderFrame','exportDiagramSvg','_rebuildRelations','_geometryContentUpdated','_pick','captureFrame','zoomIn','zoomOut','focusSelection'].map(key=>[key,proto[key]]));
|
|
20
|
+
proto._setupPlanar=function(){
|
|
21
|
+
if(this._planarSetup)return;this._planarSetup=true;
|
|
22
|
+
const style=this.ownerDocument.createElement('style');
|
|
23
|
+
style.textContent=`:host([data-planar="true"]) .viewport>canvas{visibility:visible!important;pointer-events:auto!important}
|
|
24
|
+
:host([data-planar="true"]) .diagram-view{display:none!important}
|
|
25
|
+
:host([data-planar="true"]) [data-toolbar-group="camera"]{display:none!important}
|
|
26
|
+
:host([data-planar="true"]) [data-toolbar-group="copy"]{display:flex!important}
|
|
27
|
+
.diagram-toolbar .toolbar-popover{max-height:min(420px,calc(100dvh - 160px),var(--lc-menu-height,420px));overflow-y:auto}
|
|
28
|
+
.diagram-appearance{padding:6px 9px;display:grid;gap:8px;color:#dbe7ec;font-size:12px}.diagram-appearance label{display:flex;gap:8px;align-items:center}`;
|
|
29
|
+
this.shadowRoot.append(style);
|
|
30
|
+
const diagram=this.diagramView,setState=diagram.setState.bind(diagram),action=diagram.action.bind(diagram);
|
|
31
|
+
diagram.setState=(value={})=>setState(diagramViewForModel(value,this._canonicalDiagramCenter(),value.orbitId?this._diagramOrbitTarget(value.orbitId):null));
|
|
32
|
+
diagram.refresh=()=>{if(!this._webglPlanar)return false;this._scheduleDiagram();return true;};
|
|
33
|
+
diagram.scheduleRefresh=()=>{if(this._webglPlanar)this._scheduleDiagram();};
|
|
34
|
+
diagram.focusSelection=()=>this.focusSelection();
|
|
35
|
+
diagram.action=(name,control)=>{
|
|
36
|
+
if(name==='diagram-plus'){this.zoomIn();return true;}if(name==='diagram-minus'){this.zoomOut();return true;}if(name==='diagram-fit'){this.fit();return true;}
|
|
37
|
+
return action(name,control);
|
|
38
|
+
};
|
|
39
|
+
const options=this.toolbar.querySelector('[data-toolbar-menu="diagram-options"] .toolbar-popover');
|
|
40
|
+
options?.insertAdjacentHTML('beforeend','<div class="diagram-appearance"><label><input type="checkbox" data-diagram-light checked>Fondo claro</label><label><input type="checkbox" data-diagram-grid checked>Cuadrícula de puntos</label></div>');
|
|
41
|
+
this.toolbar.addEventListener('change',event=>{
|
|
42
|
+
if(event.target.matches('[data-diagram-light]'))diagram.setConfig({theme:event.target.checked?'light':'dark'});
|
|
43
|
+
if(event.target.matches('[data-diagram-grid]'))diagram.setConfig({grid:event.target.checked});
|
|
44
|
+
});
|
|
45
|
+
this.addEventListener('lc-disconnect',()=>{cancelAnimationFrame(this._planarChangeFrame);cancelAnimationFrame(this._diagramFrame);});
|
|
46
|
+
};
|
|
47
|
+
proto._canonicalDiagramCenter=function(){
|
|
48
|
+
const points=[...this.nodes.values()].filter(n=>this._isVisible(n)&&(!container(n)||!this.open.has(n.id))).map(n=>this._canonicalPositions?.get(n.id)||original._worldPosition.call(this,n));
|
|
49
|
+
if(!points.length)return [0,0,0];return ['x','y','z'].map(axis=>(Math.min(...points.map(p=>p[axis]))+Math.max(...points.map(p=>p[axis])))/2);
|
|
50
|
+
};
|
|
51
|
+
proto._diagramOrbitTarget=function(id){return this._canonicalPositions?.get(id)?.toArray();};
|
|
52
|
+
proto._withCanonicalDiagram=function(run){
|
|
53
|
+
this._computingDiagram=true;
|
|
54
|
+
const visibility=new Map();for(const node of this.nodes.values()){node.group.position.copy(this._canonicalPositions.get(node.id));visibility.set(node.id,node.mesh.visible);node.mesh.visible=true;}
|
|
55
|
+
try{return run();}finally{for(const node of this.nodes.values())node.mesh.visible=visibility.get(node.id);this._computingDiagram=false;if(this._diagramScene)for(const [id,position] of this._diagramScene.positions)this.nodes.get(id)?.group.position.copy(position);}
|
|
56
|
+
};
|
|
57
|
+
proto._worldPosition=function(node,trail){
|
|
58
|
+
const position=this._webglPlanar&&!this._computingDiagram?this._diagramScene?.positions.get(node?.id):null;
|
|
59
|
+
return position?position.clone():original._worldPosition.call(this,node,trail);
|
|
60
|
+
};
|
|
61
|
+
proto._configurePlanarCamera=function(planar){
|
|
62
|
+
this._webglPlanar=planar;this.dataset.planar=String(planar);this.dataset.renderer=planar?'webgl-orthographic':'webgl-perspective';
|
|
63
|
+
if(planar){if(!this.camera.isOrthographicCamera){this._perspectiveCamera=this.camera;this.camera=new OrthographicCamera(-14,14,14,-14,.1,20000);this.camera.aspect=1;}this.yaw=0;this.pitch=0;}
|
|
64
|
+
else {if(this._perspectiveCamera){this.camera=this._perspectiveCamera;this._perspectiveCamera=null;}const viewport=this.shadowRoot.querySelector('.viewport');viewport.style.backgroundColor='';viewport.style.backgroundImage='';viewport.style.backgroundSize='';viewport.style.backgroundPosition='';this.shadowRoot.querySelector('.hint').style.color='';}
|
|
65
|
+
};
|
|
66
|
+
proto._build=function(options={}){
|
|
67
|
+
// Detached/headless callers may build geometry for vector export without a DOM renderer.
|
|
68
|
+
if(!this.ownerDocument)return original._build.call(this,options);
|
|
69
|
+
if(!this._ensureRenderer())return;
|
|
70
|
+
this._setupPlanar();this._configurePlanarCamera(this.viewMode==='2d');
|
|
71
|
+
cancelAnimationFrame(this._diagramFrame);this._diagramFrame=0;this._diagramScene=null;this._diagramLayout=null;this._diagramKey=null;this._diagramFaces=new Map();this._canonicalPositions=new Map();this._buildingDiagram=true;
|
|
72
|
+
try{original._build.call(this,options);}finally{this._buildingDiagram=false;}
|
|
73
|
+
for(const node of this.nodes.values())this._canonicalPositions.set(node.id,original._worldPosition.call(this,node));
|
|
74
|
+
this._diagramRevision=(this._diagramRevision||0)+1;
|
|
75
|
+
if(this._webglPlanar){this.diagramView.setState(this._diagramRevision===1?(this.modelData.render?.views?.['2d']||{}):this.diagramView.config);this._diagramScene=new DiagramScene(this,G);this._diagramScene.flattenMaterials();this._diagramFitted=true;this._runDiagram();}
|
|
76
|
+
};
|
|
77
|
+
proto._scheduleDiagram=function(){
|
|
78
|
+
if(this._buildingDiagram||this._diagramFrame||!this._webglPlanar)return;
|
|
79
|
+
this._diagramFrame=requestAnimationFrame(()=>{this._diagramFrame=0;if(this.isConnected&&this.ready&&this._webglPlanar)this._runDiagram();});
|
|
80
|
+
};
|
|
81
|
+
proto._diagramOptions=function(){
|
|
82
|
+
const config=this.diagramView.config,rect=this.canvas.getBoundingClientRect();
|
|
83
|
+
return {...this.diagramView.renderOptions(),cameraView:config.cameraView,cameraFrame:{zoom:config.cameraZoom,aspect:rect.width/Math.max(1,rect.height)},observerScale:1,faceCache:this._diagramFaces,background:null,maskGrid:true};
|
|
84
|
+
};
|
|
85
|
+
proto._runDiagram=function(){
|
|
86
|
+
if(!this._diagramScene||!this.nodes.size)return;
|
|
87
|
+
const start=performance.now(),options=this._diagramOptions(),key=JSON.stringify([this._diagramRevision,[...this.open],options,this.selectedRelationIndex]);
|
|
88
|
+
const cubeChanged=this._lastDiagramCube!==this.diagramView.config.showOrientationCube;this._lastDiagramCube=this.diagramView.config.showOrientationCube;
|
|
89
|
+
this.diagramView.cube.hidden=!this.diagramView.config.showOrientationCube;
|
|
90
|
+
if(key!==this._diagramKey){
|
|
91
|
+
const layout=this._withCanonicalDiagram(()=>computeLayout(this,options));
|
|
92
|
+
this._diagramScene.update(layout,this.diagramView.config);this._diagramLayout=layout;this._diagramKey=key;
|
|
93
|
+
this.dataset.layoutVersion=String((Number(this.dataset.layoutVersion)||0)+1);this.dataset.layoutMs=(performance.now()-start).toFixed(2);this.dataset.diagramItems=String(layout.items.filter(i=>i.visible).length);
|
|
94
|
+
if(this._diagramFitted)this.fit({animate:false});
|
|
95
|
+
}
|
|
96
|
+
if(this._diagramToolbarRevision!==this._diagramRevision){this._syncRelationToolbar();this._diagramToolbarRevision=this._diagramRevision;}
|
|
97
|
+
this.diagramView._syncControls();
|
|
98
|
+
this.toolbar.querySelector('[data-diagram-camera-zoom]').value=String(Math.round(this.diagramView.config.cameraZoom*100));
|
|
99
|
+
const light=this.toolbar.querySelector('[data-diagram-light]'),grid=this.toolbar.querySelector('[data-diagram-grid]');if(light)light.checked=this.diagramView.config.theme!=='dark';if(grid)grid.checked=this.diagramView.config.grid;
|
|
100
|
+
if(cubeChanged&&this._diagramFitted)this.fit({animate:false});
|
|
101
|
+
this._applyDiagramTheme();this.invalidate();
|
|
102
|
+
};
|
|
103
|
+
proto._applyDiagramTheme=function(){
|
|
104
|
+
const config=this.diagramView.config,dark=config.theme==='dark',viewport=this.shadowRoot.querySelector('.viewport');
|
|
105
|
+
viewport.style.backgroundColor=config.background||(dark?'#2d2f31':'#f5f5f5');viewport.style.backgroundImage=config.grid?`radial-gradient(circle,${dark?'#73777c':'#b3b3b3'} .8px,transparent 1px)`:'none';viewport.style.backgroundSize='16px 16px';
|
|
106
|
+
this.shadowRoot.querySelector('.hint').style.color=dark?'#b1c7d3':'#527286';
|
|
107
|
+
};
|
|
108
|
+
proto._applyDiagramPresentation=function(){this._diagramScene?.apply();};
|
|
109
|
+
proto.setViewMode=function(mode,options={}){
|
|
110
|
+
this._setupPlanar();if(!['2d','3d'].includes(mode))throw new TypeError('Atlas view mode must be 2d or 3d.');if(!this._ensureRenderer())return this;
|
|
111
|
+
const changed=this.viewMode!==mode;
|
|
112
|
+
if(changed&&this.ready){this._modeCameras??={};this._modeCameras[this.viewMode]=this.getCameraView();}
|
|
113
|
+
this.viewMode=mode;this._configurePlanarCamera(mode==='2d');this.dataset.viewMode=mode;this.diagramView.root.hidden=true;
|
|
114
|
+
this.canvas.setAttribute('aria-label',mode==='2d'?'Diagrama 2D de arquitectura':'Modelo 3D de arquitectura');
|
|
115
|
+
const button=this.toolbar.querySelector('[data-action="view-mode"]');button.textContent=mode==='2d'?'3D':'2D';button.setAttribute('aria-label',mode==='2d'?'Switch to 3D model':'Switch to 2D diagram');button.title=button.getAttribute('aria-label');
|
|
116
|
+
const facing=this.toolbar.querySelector('[data-action="item-facing"]');if(facing)facing.disabled=mode==='2d';
|
|
117
|
+
this.shadowRoot.querySelector('.hint').textContent=mode==='2d'?'Desplazar: arrastrar · Zoom: rueda o + / − · Orientación y filtros: barra y cubo':'Rotate: drag · Pan: ⇧+drag · Zoom: wheel · Select: click';
|
|
118
|
+
const complete=()=>{
|
|
119
|
+
if(!this.isConnected||!this.ready||this.viewMode!==mode)return;
|
|
120
|
+
if(this._projectedPlanar!==(mode==='2d'))this._build({preserveSelection:this.selectedId,preserveRelation:this.selectedRelationIndex});
|
|
121
|
+
this.resize();if(changed||options.force){const saved=this._modeCameras?.[mode];if(saved)this.setCameraView(saved,{animate:false});else this.fit({animate:false});}
|
|
122
|
+
if(mode==='2d')this._runDiagram();else {this._syncRelationToolbar();this._syncFocusToolbar();this._syncLabelsToolbar();}
|
|
123
|
+
this.invalidate();
|
|
124
|
+
};
|
|
125
|
+
this._projectedPlanar??=mode==='2d';
|
|
126
|
+
if(changed){cancelAnimationFrame(this._planarChangeFrame);this._planarChangeFrame=requestAnimationFrame(complete);}else complete();
|
|
127
|
+
if(changed&&options.emit!==false)this._emit('lc-view-mode-change',{mode,source:options.source||'api'});return this;
|
|
128
|
+
};
|
|
129
|
+
// Remember which representation is built (not just which mode the toolbar requested).
|
|
130
|
+
const build=proto._build;proto._build=function(options){build.call(this,options);this._projectedPlanar=this._webglPlanar;};
|
|
131
|
+
proto._rebuildRelations=function(){if(this._webglPlanar){if(!this._buildingDiagram)this._scheduleDiagram();return;}return original._rebuildRelations.call(this);};
|
|
132
|
+
proto._geometryContentUpdated=function(){if(this._webglPlanar){this._diagramFaces?.clear();this._diagramKey=null;this._scheduleDiagram();}else original._geometryContentUpdated.call(this);};
|
|
133
|
+
proto._updatePlanarCamera=function(){const camera=this.camera;camera.zoom=Math.max(.001,this.zoom);camera.position.set(this.focus.x,this.focus.y,1000);camera.lookAt(this.focus.x,this.focus.y,0);camera.updateProjectionMatrix();};
|
|
134
|
+
proto.resize=function(){
|
|
135
|
+
if(this._webglPlanar){const rect=this.canvas.getBoundingClientRect();this.camera.aspect=rect.width/Math.max(1,rect.height);this.camera.left=-14*this.camera.aspect;this.camera.right=14*this.camera.aspect;this.camera.top=14;this.camera.bottom=-14;}
|
|
136
|
+
return original.resize.call(this);
|
|
137
|
+
};
|
|
138
|
+
proto.getCameraView=function(){return this._webglPlanar?{position:[this.focus.x,this.focus.y,1000],target:[this.focus.x,this.focus.y,0],zoom:this.zoom,projection:'orthographic'}:original.getCameraView.call(this);};
|
|
139
|
+
proto.setCameraView=function(view={},options={}){
|
|
140
|
+
if(!this._webglPlanar)return original.setCameraView.call(this,view,options);
|
|
141
|
+
this._diagramFitted=false;
|
|
142
|
+
if(view.target?.length===3&&view.target.every(Number.isFinite))this.targetFocus.set(view.target[0],view.target[1],0);if(Number.isFinite(view.zoom)&&view.zoom>0)this.targetZoom=Math.max(.001,Math.min(6,view.zoom));
|
|
143
|
+
if(options.animate===false){this.focus.copy(this.targetFocus);this.zoom=this.targetZoom;}
|
|
144
|
+
this.invalidate();return this;
|
|
145
|
+
};
|
|
146
|
+
proto._panByPixels=function(dx,dy){if(!this._webglPlanar)return original._panByPixels.call(this,dx,dy);this._diagramFitted=false;const units=28/(Math.max(.001,this.zoom)*Math.max(1,this.canvas.getBoundingClientRect().height));this.targetFocus.x-=dx*units;this.targetFocus.y+=dy*units;this.invalidate();return this;};
|
|
147
|
+
proto.focusSelection=function(options={}){if(this._webglPlanar)this._diagramFitted=false;return original.focusSelection.call(this,options);};
|
|
148
|
+
proto.zoomIn=function(){if(this._webglPlanar)this._diagramFitted=false;return original.zoomIn.call(this);};
|
|
149
|
+
proto.zoomOut=function(){if(!this._webglPlanar)return original.zoomOut.call(this);this._diagramFitted=false;this.targetZoom=Math.max(.001,this.targetZoom/1.18);this.invalidate();return this;};
|
|
150
|
+
proto.fit=function(options={}){
|
|
151
|
+
if(!this._webglPlanar)return original.fit.call(this,options);if(!this._diagramLayout)return this;
|
|
152
|
+
this._diagramFitted=true;const layout=this._diagramLayout,scale=layout.scale,rect=this.canvas.getBoundingClientRect(),left=this.diagramView.config.showOrientationCube?132:16,right=16,top=34,bottom=this.toolbarVisible?this.toolbar.offsetHeight+44:16;
|
|
153
|
+
this.targetZoom=orthographicFit({minX:0,maxX:layout.width/scale,minY:0,maxY:layout.height/scale},this.camera.aspect||1)*Math.max(.1,Math.min((rect.width-left-right)/rect.width,(rect.height-top-bottom)/rect.height));
|
|
154
|
+
const units=28/(this.targetZoom*Math.max(1,rect.height));this.targetFocus.set(layout.width/(2*scale)-(left-right)*units/2,-layout.height/(2*scale)+(top-bottom)*units/2,0);
|
|
155
|
+
if(options.animate===false){this.focus.copy(this.targetFocus);this.zoom=this.targetZoom;}this.invalidate();return this;
|
|
156
|
+
};
|
|
157
|
+
proto.frontView=function(){if(!this._webglPlanar)return original.frontView.call(this);this.diagramView.action('diagram-front');return this;};
|
|
158
|
+
proto._pick=function(event){if(!this._webglPlanar)return original._pick.call(this,event);const mode=this.relationMode;let hit;this.relationMode='none';try{hit=original._pick.call(this,event);}finally{this.relationMode=mode;}return this._diagramScene?.pick(event,hit)||hit;};
|
|
159
|
+
proto._positionJumpButton=function(){if(!this._webglPlanar)return original._positionJumpButton.call(this);const mode=this.viewMode;this.viewMode='3d';try{return original._positionJumpButton.call(this);}finally{this.viewMode=mode;}};
|
|
160
|
+
proto.getDiagramView=function(){const state=this.diagramView.captureState();return {...state,cameraModelCenter:this._canonicalDiagramCenter(),viewport:this._webglPlanar?{...this.getCameraView(),fitted:this._diagramFitted}:state.viewport};};
|
|
161
|
+
proto.captureDiagramView=function(){return this.getDiagramView();};
|
|
162
|
+
proto.setDiagramView=function(state={}){this.diagramView.setState(state);if(this._webglPlanar&&state.viewport?.projection==='orthographic'){this._diagramFitted=false;this.setCameraView(state.viewport,{animate:false});this._diagramFitted=state.viewport.fitted===true;}return this;};
|
|
163
|
+
proto._renderFrame=function(time){const start=performance.now(),result=original._renderFrame.call(this,time);if(this._webglPlanar){this.dataset.frameMs=(performance.now()-start).toFixed(2);this.dataset.frames=String((Number(this.dataset.frames)||0)+1);this.dataset.drawCalls=String(this.renderer?.info.render.calls||0);}return result;};
|
|
164
|
+
proto.captureFrame=function(options={}){return original.captureFrame.call(this,this._webglPlanar&&options.background==='theme'?{...options,background:this.diagramView.config.background||(this.diagramView.config.theme==='dark'?'#2d2f31':'#f5f5f5')}:options);};
|
|
165
|
+
proto.exportDiagramSvg=function(options={}){this.dataset.svgExports=String((Number(this.dataset.svgExports)||0)+1);if(this._webglPlanar)return this._withCanonicalDiagram(()=>original.exportDiagramSvg.call(this,{...this._diagramOptions(),...options}));return original.exportDiagramSvg.call(this,options);};
|
|
166
|
+
}
|