spytial-core 1.9.5 → 1.9.6
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.
|
@@ -847,7 +847,7 @@ Expecting `+M1.join(", ")+", got '"+(this.terminals_[to]||to)+"'":g1="Parse erro
|
|
|
847
847
|
</div>
|
|
848
848
|
</div>
|
|
849
849
|
<div id="error" style="display: none; color: red;"></div>
|
|
850
|
-
`;}initializeD3(){Co||(Co=window.d3),this.svg=Co.select(this.shadowRoot.querySelector("#svg")),this.container=this.svg.select(".zoomable"),Co.zoom?(this.zoomBehavior=Co.zoom().scaleExtent([.01,20]).on("start",()=>{Co.event.sourceEvent&&(this.userHasManuallyZoomed=true);}).on("zoom",()=>{this.container.attr("transform",Co.event.transform),this.updateZoomControlStates(),this.updateSmallNodeClasses();}),this.svg.call(this.zoomBehavior),this.initializeZoomControls()):console.warn("D3 zoom behavior not available. Ensure D3 v4+ is loaded.");}initializeZoomControls(){let n=this.shadowRoot.querySelector("#zoom-in"),c=this.shadowRoot.querySelector("#zoom-out"),s=this.shadowRoot.querySelector("#zoom-fit");n&&n.addEventListener("click",()=>{this.userHasManuallyZoomed=true,this.zoomIn();}),c&&c.addEventListener("click",()=>{this.userHasManuallyZoomed=true,this.zoomOut();}),s&&s.addEventListener("click",()=>{this.resetViewToFitContent();});let e=this.shadowRoot.querySelector("#routing-mode");if(e){let i=this.layoutFormat||"default";e.value=i,e.addEventListener("change",()=>{this.handleRoutingModeChange(e.value);});}this.updateZoomControlStates();}handleRoutingModeChange(n){this.setAttribute("layoutFormat",n),this.currentLayout&&this.colaLayout&&(n==="grid"?this.gridify(10,25,10):this.routeEdges(),this.dispatchEvent(new CustomEvent("routing-mode-changed",{detail:{mode:n}})));}updateRoutingModeDropdown(){let n=this.shadowRoot?.querySelector("#routing-mode");if(n){let c=this.layoutFormat||"default";n.value=c;}}initializeInputModeHandlers(){this.inputModeEnabled&&this.attachInputModeListeners();}attachInputModeListeners(){this.inputModeListenersAttached||(document.addEventListener("keydown",this.handleInputModeKeydown),document.addEventListener("keyup",this.handleInputModeKeyup),window.addEventListener("blur",this.handleInputModeBlur),this.inputModeListenersAttached=true);}detachInputModeListeners(){this.inputModeListenersAttached&&(document.removeEventListener("keydown",this.handleInputModeKeydown),document.removeEventListener("keyup",this.handleInputModeKeyup),window.removeEventListener("blur",this.handleInputModeBlur),this.inputModeListenersAttached=false);}activateInputMode(){this.isInputModeActive=true,this.svg&&this.svg.classed("input-mode",true),this.disableNodeDragging(),this.disableZoom(),this.updateEdgeEndpointMarkers(),this.dispatchEvent(new CustomEvent("input-mode-activated",{detail:{active:true}}));}deactivateInputMode(){this.isInputModeActive=false,this.svg&&this.svg.classed("input-mode",false),this.cleanupEdgeCreation(),this.enableNodeDragging(),this.enableZoom(),this.updateEdgeEndpointMarkers(),this.dispatchEvent(new CustomEvent("input-mode-deactivated",{detail:{active:false}}));}disableNodeDragging(){this.svgNodes&&this.colaLayout&&this.svgNodes.on(".drag",null);}enableNodeDragging(){if(this.svgNodes&&this.colaLayout&&this.colaLayout.drag){let n=this.colaLayout.drag();this.setupNodeDragHandlers(n),this.svgNodes.call(n);}}disableZoom(){this.svg&&this.zoomBehavior&&(this.storedTransform=Co.zoomTransform(this.svg.node()),this.svg.on(".zoom",null));}enableZoom(){this.svg&&this.zoomBehavior&&(this.svg.call(this.zoomBehavior),this.storedTransform&&this.svg.call(this.zoomBehavior.transform,this.storedTransform));}zoomIn(){this.svg&&this.zoomBehavior&&this.svg.transition().duration(200).call(this.zoomBehavior.scaleBy,1.5);}zoomOut(){this.svg&&this.zoomBehavior&&this.svg.transition().duration(200).call(this.zoomBehavior.scaleBy,1/1.5);}updateZoomControlStates(){if(!this.svg||!this.zoomBehavior)return;let c=Co.zoomTransform(this.svg.node()).k,[s,e]=this.zoomBehavior.scaleExtent(),i=this.shadowRoot.querySelector("#zoom-in"),o=this.shadowRoot.querySelector("#zoom-out");i&&(i.disabled=c>=e),o&&(o.disabled=c<=s);}cleanupEdgeCreation(){this.edgeCreationState.temporaryEdge&&this.edgeCreationState.temporaryEdge.remove(),this.edgeCreationState={isCreating:false,sourceNode:null,temporaryEdge:null};}setupNodeDragHandlers(n){n.on("start.cnd",c=>{this.userHasManuallyZoomed=true;let s={x:c.x,y:c.y};this.dragStartPositions.set(c.id,s),this.dispatchEvent(new CustomEvent("node-drag-start",{detail:{id:c.id,position:s}}));}).on("end.cnd",c=>{let s=this.dragStartPositions.get(c.id);this.dragStartPositions.delete(c.id);let e={id:c.id,previous:s,current:{x:c.x,y:c.y}};this.dispatchEvent(new CustomEvent("node-drag-end",{detail:e}));});}startEdgeCreation(n){this.isInputModeActive&&(this.cleanupEdgeCreation(),this.edgeCreationState.isCreating=true,this.edgeCreationState.sourceNode=n,this.edgeCreationState.temporaryEdge=this.container.append("line").attr("class","temporary-edge").attr("x1",n.x).attr("y1",n.y).attr("x2",n.x).attr("y2",n.y).attr("stroke","#007bff").attr("stroke-width",2).attr("stroke-dasharray","5,5").attr("opacity",.7),this.svg.on("mousemove.edgecreation",()=>{if(this.edgeCreationState.isCreating&&this.edgeCreationState.temporaryEdge){let[c,s]=Co.mouse(this.container.node());this.edgeCreationState.temporaryEdge.attr("x2",c).attr("y2",s);}}));}async finishEdgeCreation(n){if(!this.isInputModeActive||!this.edgeCreationState.isCreating||!this.edgeCreationState.sourceNode)return;let c=this.edgeCreationState.sourceNode;if(c.id===n.id&&!await this.showConfirmDialog(`Are you sure you want to create a self-loop edge on "${c.label||c.id}"?`)){this.cleanupEdgeCreation();return}this.svg.on("mousemove.edgecreation",null),await this.showEdgeLabelInput(c,n);}async showEdgeLabelInput(n,c){let s=await this.showPromptDialog(`Enter label for edge from "${n.label||n.id}" to "${c.label||c.id}":`,"");s!==null&&await this.createNewEdge(n,c,s||""),this.cleanupEdgeCreation();}async createNewEdge(n,c,s){if(!this.currentLayout)return;let e=this.currentLayout.nodes.findIndex(E=>E.id===n.id),i=this.currentLayout.nodes.findIndex(E=>E.id===c.id);if(e===-1||i===-1){console.error("Could not find node indices for edge creation");return}let p={id:`edge_${n.id}_${c.id}_${Date.now()}`,source:e,target:i,label:s,relName:s,color:"#333",isUserCreated:true};this.currentLayout.links.push(p),await this.updateExternalStateForNewEdge(n,c,s),this.dispatchEvent(new CustomEvent("edge-created",{detail:{edge:p,sourceNode:n,targetNode:c}})),this.rerenderGraph();}async updateExternalStateForNewEdge(n,c,s){if(s.trim())try{let e={atoms:[n.id,c.id],types:[n.type||"untyped",c.type||"untyped"]};console.log(`Dispatching edge creation request: ${s}(${n.id}, ${c.id})`);let i=new CustomEvent("edge-creation-requested",{detail:{relationId:s,sourceNodeId:n.id,targetNodeId:c.id,tuple:e},bubbles:!0});this.dispatchEvent(i);}catch(e){console.error("Failed to update external state for new edge:",e);}}rerenderGraph(){!this.currentLayout||!this.colaLayout||(this.colaLayout.links(this.currentLayout.links),this.container.selectAll(".link-group").remove(),this.renderLinks(this.currentLayout.links,this.colaLayout),this.colaLayout.start());}async editEdgeLabel(n){if(!this.isInputModeActive)return;let c=n.label||n.relName||"",s=await this.showEdgeEditDialog("Edit edge label:",c);if(s==="DELETE"){await this.deleteEdge(n);return}if(s!==null&&s!==c){let e=s,i=this.getNodeFromEdge(n,"source"),o=this.getNodeFromEdge(n,"target");await this.updateExternalStateForEdgeModification(i,o,c,e),n.label=e,n.relName=e,this.dispatchEvent(new CustomEvent("edge-modified",{detail:{edge:n,oldLabel:c,newLabel:e}})),this.rerenderGraph();}}getNodeFromEdge(n,c){if(!this.currentLayout)return null;let s=typeof n[c]=="number"?n[c]:n[c].index;return this.currentLayout.nodes[s]||null}async updateExternalStateForEdgeModification(n,c,s,e){if(!(!n||!c))try{let i={atoms:[n.id,c.id],types:[n.type||"untyped",c.type||"untyped"]};console.log(`Dispatching edge modification request: ${s} -> ${e}`);let o=new CustomEvent("edge-modification-requested",{detail:{oldRelationId:s,newRelationId:e,sourceNodeId:n.id,targetNodeId:c.id,tuple:i},bubbles:!0});this.dispatchEvent(o);}catch(i){console.error("Failed to update external state for edge modification:",i);}}async renderLayout(n,c){if(!NT(n))throw new Error("Invalid instance layout provided. Expected an InstanceLayout instance.");let s,e=false;if(c?.policy&&c.prevInstance&&c.currInstance){let p=c.priorPositions??this.getLayoutState();if(p&&p.positions.length>0){let E=this.getViewportBoundsInLayoutSpace(p.transform),b=c.policy.apply({priorState:p,prevInstance:c.prevInstance,currInstance:c.currInstance,spec:{constraints:{orientation:{relative:[],cyclic:[]},alignment:[],grouping:{groups:[],subgroups:[]}},directives:{sizes:[],hiddenAtoms:[],icons:[],projections:[],edgeStyles:[]}},viewportBounds:E});s=b.effectivePriorState,e=b.useReducedIterations;}}let i=s&&s.positions.length>0,o=i?{priorPositions:s}:void 0;if(i||(this.isInitialRender=true,this.userHasManuallyZoomed=false),this.svg&&this.zoomBehavior&&Co)try{if(i){let p=Co.zoomIdentity.translate(s.transform.x,s.transform.y).scale(s.transform.k);this.svg.call(this.zoomBehavior.transform,p);}else {let p=Co.zoomIdentity;this.svg.call(this.zoomBehavior.transform,p);}}catch(p){console.warn("Failed to set zoom transform:",p);}try{if(!Co)throw new Error("D3 library not available. Please ensure D3 v4 is loaded from CDN.");if(!Md){if(!window.cola)throw new Error("WebCola library not available. Please ensure vendor/cola.js is loaded.");Md=window.cola;}if((!this.container||!this.svg)&&this.initializeD3(),!this.container)throw new Error("Failed to initialize D3 container. SVG elements may not be available.");this.showLoading(),this.updateLoadingProgress("Translating layout...");let E=this.shadowRoot.querySelector("#svg-container").getBoundingClientRect(),b=E.width||800,y=E.height||600,a=await new exports.WebColaTranslator().translate(n,b,y,o);this.updateLoadingProgress(`Computing layout for ${a.nodes.length} nodes...`);let l=a.nodes.length,u=qi.INITIAL_UNCONSTRAINED_ITERATIONS,h=qi.INITIAL_USER_CONSTRAINT_ITERATIONS,d=qi.INITIAL_ALL_CONSTRAINTS_ITERATIONS;i&&e&&(u=0,h=Math.min(10,h),d=Math.min(20,d)),l>100?(u=Math.max(i?0:5,Math.floor(u*.5)),h=Math.max(25,Math.floor(h*.5)),d=Math.max(100,Math.floor(d*.5))):l>50&&(u=Math.max(i?0:8,Math.floor(u*.8)),h=Math.max(40,Math.floor(h*.8)),d=Math.max(150,Math.floor(d*.75)));let{scaledConstraints:g,linkLength:_,groupCompactness:S}=this.getScaledDetails(a.constraints,zF,a.nodes,a.groups,a.links);this.updateLoadingProgress("Applying constraints and initializing...");let v=i?.1:.001,T=Md.d3adaptor(Co).linkDistance(_).convergenceThreshold(v).avoidOverlaps(!0).handleDisconnected(!0).nodes(a.nodes).links(a.links).constraints(g).groups(a.groups).groupCompactness(S).size([a.FIG_WIDTH,a.FIG_HEIGHT]);this.currentLayout=a,this.colaLayout=T,this.container.selectAll("*").remove(),this.renderGroups(a.groups,T),this.renderLinks(a.links,T),this.renderNodes(a.nodes,T);let q=0,G=u+h+d;T.on("tick",()=>{if(q++,q%20===0){let j=Math.min(95,Math.round(q/G*100));this.updateLoadingProgress(`Computing layout... ${j}%`);}this.layoutFormat==="default"||!this.layoutFormat||this.layoutFormat===null?this.updatePositions():this.layoutFormat==="grid"?this.gridUpdatePositions():console.warn(`Unknown layout format: ${this.layoutFormat}. Skipping position updates.`);}).on("end",()=>{this.updateLoadingProgress("Finalizing..."),this.layoutFormat==="default"||!this.layoutFormat?this.routeEdges():this.layoutFormat==="grid"?this.gridify(10,25,10):console.warn(`Unknown layout format: ${this.layoutFormat}. Skipping edge routing.`),this.isUnsatCore&&this.showErrorIcon(),this.dispatchRelationsAvailableEvent(),this.dispatchEvent(new CustomEvent("layout-complete",{detail:{nodePositions:this.getNodePositions()}})),this.updateRoutingModeDropdown(),this.hideLoading();});try{T.start(u,h,d,qi.GRID_SNAP_ITERATIONS);}catch(j){console.warn("WebCola layout start encountered an error, trying alternative approach:",j);try{T.start();}catch(B){throw console.error("Both WebCola start methods failed:",B),new Error(`WebCola layout failed to start: ${B.message}`)}}}catch(p){console.error("Error rendering layout:",p),this.showError(`Layout rendering failed: ${p.message}`);}}clear(){if(this.colaLayout)try{this.colaLayout.stop?.();}catch{}this.container&&this.container.selectAll("*").remove(),this.currentLayout=null,this.colaLayout=null,this.svgNodes=null,this.svgLinks=null,this.svgGroups=null,this.edgeRoutingCache.edgesBetweenNodes.clear(),this.edgeRoutingCache.alignmentEdges.clear(),this.dragStartPositions.clear();}getNodePositions(){return this.currentLayout?.nodes?this.currentLayout.nodes.map(n=>({id:n.id,x:n.x,y:n.y})):[]}getCurrentTransform(){if(this.svg&&this.svg.node())try{let n=Co.zoomTransform(this.svg.node());return {k:n.k,x:n.x,y:n.y}}catch{return {k:1,x:0,y:0}}return {k:1,x:0,y:0}}getLayoutState(){return {positions:this.getNodePositions(),transform:this.getCurrentTransform()}}addToolbarControl(n){let c=this.shadowRoot?.querySelector("#graph-toolbar");c&&c.appendChild(n);}getToolbar(){return this.shadowRoot?.querySelector("#graph-toolbar")||null}renderGroups(n,c){if(!this.currentLayout.nodes||this.currentLayout.nodes.length===0){console.warn("Cannot render groups: nodes not available");return}this.svgGroups=this.setupGroups(n,this.currentLayout.nodes,c);}setupLinks(n,c){let s=this.container.selectAll(".link-group").data(n).enter().append("g").attr("class","link-group");return this.setupLinkPaths(s),this.setupLinkLabels(s),this.setupEdgeEndpointMarkers(s),s}setupLinkPaths(n){n.append("path").attr("class",c=>this.isAlignmentEdge(c)?"alignmentLink":this.isInferredEdge(c)?"inferredLink":"link").attr("data-link-id",c=>c.id||"").attr("stroke",c=>this.isAlignmentEdge(c)?"none":c.color).attr("fill","none").attr("opacity",c=>this.isAlignmentEdge(c)?0:null).style("stroke-width",c=>this.isAlignmentEdge(c)?"0":c.weight!=null?`${c.weight}px`:null).attr("stroke-dasharray",c=>this.isAlignmentEdge(c)?null:this.getEdgeDasharray(c.style)).attr("marker-end",c=>this.isAlignmentEdge(c)?"none":"url(#end-arrow)").attr("marker-start",c=>this.isAlignmentEdge(c)||!c.bidirectional?"none":"url(#start-arrow)").on("click.inputmode",c=>{this.isInputModeActive&&!this.isAlignmentEdge(c)&&(Co.event.stopPropagation(),this.editEdgeLabel(c).catch(s=>{console.error("Error editing edge label:",s);}));}).style("cursor",()=>this.isInputModeActive?"pointer":"default");}getEdgeDasharray(n){if(!n)return null;switch(n.toLowerCase()){case "dotted":return "1,4";case "dashed":return "6,4";case "solid":return null;default:return null}}setupLinkLabels(n){n.filter(c=>!this.isAlignmentEdge(c)&&(this.isInferredEdge(c)||c.showLabel!==false)).append("text").attr("class","linklabel").attr("text-anchor","middle").attr("dominant-baseline","middle").attr("font-family","system-ui").attr("pointer-events","none").text(c=>c.label||c.relName||"");}setupEdgeEndpointMarkers(n){n.filter(c=>!this.isAlignmentEdge(c)).append("circle").attr("class","edge-endpoint-marker target-marker").attr("r",8).attr("fill","#007bff").attr("stroke","white").attr("stroke-width",2).attr("opacity",0).attr("cursor","move").style("pointer-events","none").call(Co.drag().on("start",c=>this.startEdgeEndpointDrag(c,"target")).on("drag",c=>this.dragEdgeEndpoint(c,"target")).on("end",c=>this.endEdgeEndpointDrag(c,"target"))),n.filter(c=>!this.isAlignmentEdge(c)).append("circle").attr("class","edge-endpoint-marker source-marker").attr("r",8).attr("fill","#28a745").attr("stroke","white").attr("stroke-width",2).attr("opacity",0).attr("cursor","move").style("pointer-events","none").call(Co.drag().on("start",c=>this.startEdgeEndpointDrag(c,"source")).on("drag",c=>this.dragEdgeEndpoint(c,"source")).on("end",c=>this.endEdgeEndpointDrag(c,"source")));}startEdgeEndpointDrag(n,c){Co.event.sourceEvent.stopPropagation(),this.edgeDragState.isDragging=true,this.edgeDragState.edge=n,this.edgeDragState.endpoint=c,console.log(`\u{1F535} Started dragging ${c} endpoint of edge:`,n.id);}dragEdgeEndpoint(n,c){if(!this.edgeDragState.isDragging)return;let[s,e]=Co.mouse(this.container.node()),i=c==="target"?".target-marker":".source-marker";this.container.selectAll(".link-group").filter(o=>o.id===n.id).select(i).attr("cx",s).attr("cy",e);}async endEdgeEndpointDrag(n,c){if(!this.edgeDragState.isDragging)return;let[s,e]=Co.mouse(this.container.node()),i=this.findNodeAtPosition(s,e);i?(console.log(`\u{1F517} Reconnecting ${c} to node:`,i.id),await this.reconnectEdge(n,c,i)):(console.log("\u{1F5D1}\uFE0F No node found - deleting edge:",n.id),await this.deleteEdge(n)),this.edgeDragState={isDragging:false,edge:null,endpoint:null,dragMarker:null},this.rerenderGraph();}findNodeAtPosition(n,c){if(!this.currentLayout?.nodes)return null;for(let s of this.currentLayout.nodes){let e=(s.visualWidth??s.width??0)/2,i=(s.visualHeight??s.height??0)/2;if(n>=s.x-e&&n<=s.x+e&&c>=s.y-i&&c<=s.y+i)return s}return null}async reconnectEdge(n,c,s){let e=this.getNodeFromEdge(n,"source"),i=this.getNodeFromEdge(n,"target");if(!e||!i){console.error("Could not find source or target node");return}let o,p;if(c==="source"?(o=s,p=i):(o=e,p=s),o.id===e.id&&p.id===i.id){console.log("\u23ED\uFE0F Edge already connected to this node, no change needed");return}let E=n.label||n.relName||"";if(!E.trim()){console.warn("Edge has no relation name, cannot reconnect");return}let b={atoms:[e.id,i.id],types:[e.type||"untyped",i.type||"untyped"]},y={atoms:[o.id,p.id],types:[o.type||"untyped",p.type||"untyped"]};console.log(`\u{1F504} Reconnecting edge from ${e.id}->${i.id} to ${o.id}->${p.id}`);let m=new CustomEvent("edge-reconnection-requested",{detail:{relationId:E,oldTuple:b,newTuple:y,oldSourceNodeId:e.id,oldTargetNodeId:i.id,newSourceNodeId:o.id,newTargetNodeId:p.id},bubbles:true});this.dispatchEvent(m);let a=this.currentLayout.nodes.findIndex(u=>u.id===o.id),l=this.currentLayout.nodes.findIndex(u=>u.id===p.id);a!==-1&&l!==-1&&(n.source=a,n.target=l);}async deleteEdge(n){let c=this.getNodeFromEdge(n,"source"),s=this.getNodeFromEdge(n,"target");if(!c||!s){console.error("Could not find source or target node for edge deletion");return}let e=n.label||n.relName||"";if(!e.trim()){console.warn("Edge has no relation name, cannot delete from data instance"),this.removeEdgeFromLayout(n);return}let i={atoms:[c.id,s.id],types:[c.type||"untyped",s.type||"untyped"]};console.log(`\u{1F5D1}\uFE0F Deleting edge: ${e}(${c.id}, ${s.id})`);let o=new CustomEvent("edge-modification-requested",{detail:{oldRelationId:e,newRelationId:"",sourceNodeId:c.id,targetNodeId:s.id,tuple:i},bubbles:true});this.dispatchEvent(o),this.removeEdgeFromLayout(n);}removeEdgeFromLayout(n){if(!this.currentLayout?.links)return;let c=this.currentLayout.links.findIndex(s=>s.id===n.id);c!==-1&&(this.currentLayout.links.splice(c,1),console.log(`\u2705 Edge removed from layout: ${n.id}`));}setupGroups(n,c,s){let e=this.setupGroupRectangles(n,c,s);return this.svgGroupLabels=this.setupGroupLabels(n,s),e}setupGroupRectangles(n,c,s){return this.container.selectAll(".group").data(n).enter().append("rect").attr("class",i=>this.isDisconnectedGroup(i)?"disconnectedNode":this.isErrorGroup(i)?"error-group":"group").attr("rx",qi.GROUP_BORDER_RADIUS).attr("ry",qi.GROUP_BORDER_RADIUS).style("fill",i=>this.isDisconnectedGroup(i)?"transparent":c[i.keyNode]?.color||"#cccccc").attr("fill-opacity",qi.GROUP_FILL_OPACITY).attr("stroke",i=>this.isDisconnectedGroup(i)?"none":c[i.keyNode]?.color||"#999999").attr("stroke-width",1).call(s.drag)}setupGroupLabels(n,c){return this.container.selectAll(".groupLabel").data(n).enter().append("text").attr("class","groupLabel").attr("text-anchor","middle").attr("dominant-baseline","hanging").attr("font-family","system-ui").attr("font-size","12px").attr("font-weight","bold").attr("fill","#333").attr("pointer-events","none").text(s=>s.showLabel||false?(s.padding&&(s.padding=Math.max(s.padding,qi.GROUP_LABEL_PADDING)),s.name||""):"").call(c.drag)}renderLinks(n,c){this.svgLinkGroups=this.setupLinks(n,c);}setupNodes(n,c){let s=c.drag();this.setupNodeDragHandlers(s);let e=this.container.selectAll(".node").data(n).enter().append("g").attr("class",i=>{let o=this.isErrorNode(i)?"error-node":"node";return this.isErrorNode(i)&&this.isSmallNode(i)?o+" small-error-node":o}).call(s).on("mousedown.inputmode",i=>{this.isInputModeActive&&(Co.event.stopPropagation(),this.startEdgeCreation(i));}).on("mouseup.inputmode",i=>{this.isInputModeActive&&this.edgeCreationState.isCreating&&(Co.event.stopPropagation(),this.finishEdgeCreation(i).catch(o=>{console.error("Error finishing edge creation:",o);}));}).on("mouseover",function(i){Co.select(this).append("title").attr("class","node-tooltip").text(`ID: ${i.id}`);}).on("mouseout",function(){Co.select(this).select("title.node-tooltip").remove();});return this.setupNodeRectangles(e),this.setupNodeIcons(e),this.setupMostSpecificTypeLabels(e),this.setupNodeLabels(e),e}setupNodeRectangles(n){n.append("rect").attr("width",c=>c.visualWidth??c.width).attr("height",c=>c.visualHeight??c.height).attr("x",c=>-(c.visualWidth??c.width)/2).attr("y",c=>-(c.visualHeight??c.height)/2).attr("stroke",c=>c.color||"black").attr("rx",qi.NODE_BORDER_RADIUS).attr("ry",qi.NODE_BORDER_RADIUS).attr("stroke-width",qi.NODE_STROKE_WIDTH).attr("fill",c=>{let s=this.isHiddenNode(c),e=!!c.icon,i=c.showLabels;return s||e&&!i?"transparent":"white"});}setupNodeIcons(n){n.filter(c=>c.icon).append("image").attr("xlink:href",c=>c.icon).attr("width",c=>{let s=c.visualWidth??c.width;return c.showLabels?s*qi.SMALL_IMG_SCALE_FACTOR:s}).attr("height",c=>{let s=c.visualHeight??c.height;return c.showLabels?s*qi.SMALL_IMG_SCALE_FACTOR:s}).attr("x",c=>{let s=c.visualWidth??c.width;return c.showLabels?c.x+s-s*qi.SMALL_IMG_SCALE_FACTOR:c.x-s/2}).attr("y",c=>{let s=c.visualHeight??c.height;return c.y-s/2}).append("title").text(c=>c.label||c.name||c.id||"Node").on("error",function(c,s){Co.select(this).attr("xlink:href","img/default.png"),console.error(`Failed to load icon for node ${s.id}: ${s.icon}`);});}setupMostSpecificTypeLabels(n){n.append("text").attr("class","mostSpecificTypeLabel").style("fill",c=>c.color||"black").text(c=>c.mostSpecificType||"");}getTextMeasurementContext(){return this.textMeasurementCanvas||(this.textMeasurementCanvas=document.createElement("canvas")),this.textMeasurementCanvas.getContext("2d")}measureTextWidth(n,c,s="system-ui"){let e=this.getTextMeasurementContext();return e.font=`${c}px ${s}`,e.measureText(n).width}calculateOptimalFontSize(n,c,s,e="system-ui"){let i=qi.DEFAULT_FONT_SIZE;for(;i>qi.MIN_FONT_SIZE;){let o=this.measureTextWidth(n,i,e),p=i*qi.LINE_HEIGHT_RATIO;if(o<=c&&p<=s)break;i-=.5;}for(;i<qi.MAX_FONT_SIZE;){let o=i+.5,p=this.measureTextWidth(n,o,e),E=o*qi.LINE_HEIGHT_RATIO;if(p>c||E>s)break;i=o;}return Math.max(qi.MIN_FONT_SIZE,Math.min(i,qi.MAX_FONT_SIZE))}wrapText(n,c,s,e="system-ui"){let i=n.split(/\s+/),o=[],p="";for(let E of i){let b=p?`${p} ${E}`:E;this.measureTextWidth(b,s,e)<=c?p=b:p?(o.push(p),p=E):o.push(E);}return p&&o.push(p),o}setupNodeLabelsWithDynamicSizing(n){n.append("text").attr("class","label").attr("text-anchor","middle").attr("dominant-baseline","middle").attr("font-family","system-ui").attr("fill","black").each((c,s,e)=>{if(this.isHiddenNode(c)||!c.showLabels)return;let o=Co.select(e[s]),p=c.width||100,E=c.height||60,b=p-qi.TEXT_PADDING*2,y=E-qi.TEXT_PADDING*2,m=c.label||c.name||c.id||"Node",a=c.attributes||{},l=Object.entries(a).sort(([z],[tt])=>z.localeCompare(tt)),u=c.labels||{},h=Object.entries(u),d=h.length>0,g=l.length>0,_=d||g,S=_?y*.5:y,v=this.calculateOptimalFontSize(m,b,S,"system-ui");o.attr("font-size",`${v}px`);let T=v*qi.LINE_HEIGHT_RATIO,q=h.length+l.length,G=_?-q*T*.5:0;c._labelVerticalOffset=G,c._labelLineHeight=T,o.append("tspan").attr("x",0).attr("dy",`${G}px`).attr("class","main-label-tspan").style("font-weight","bold").style("font-size",`${v}px`).text(m);let j="";for(let[z,tt]of h){let pt=Array.isArray(tt)?tt.join(", "):String(tt);pt.length>j.length&&(j=pt);}for(let[z,tt]of l){let pt=`${z}: ${tt}`;pt.length>j.length&&(j=pt);}let B=v*.65,J=y-T,w=q>0?this.calculateOptimalFontSize(j||"SampleText",b,J/q,"system-ui"):v*.8,$=Math.max(w,B);if(d){let z="black";for(let[tt,pt]of h){let Z=Array.isArray(pt)?pt.join(", "):String(pt);o.append("tspan").attr("x",0).attr("dy",`${$*qi.LINE_HEIGHT_RATIO}px`).style("font-size",`${$}px`).style("fill",z).style("font-style","italic").text(Z);}}if(g)for(let z=0;z<l.length;z++){let[tt,pt]=l[z],Z=`${tt}: ${pt}`;o.append("tspan").attr("x",0).attr("dy",`${$*qi.LINE_HEIGHT_RATIO}px`).style("font-size",`${$}px`).text(Z);}});}setupNodeLabels(n){this.setupNodeLabelsWithDynamicSizing(n);}renderNodes(n,c){this.svgNodes=this.setupNodes(n,c);}resolveGroupEdgeEndpoints(n){if(!n.groupId)return {source:n.source,target:n.target};let c=this.currentLayout?.groups||[],s=c.find(p=>p.id===n.groupId);if(this._groupEdgeDebugLogged||(this._groupEdgeDebugLogged=new Set),this._groupEdgeDebugLogged.has(n.id)||(this._groupEdgeDebugLogged.add(n.id),console.log("[groupEdge DEBUG]",{edgeId:n.id,groupId:n.groupId,keyNodeId:n.keyNodeId,sourceId:n.source?.id,targetId:n.target?.id,sourceIsInt:typeof n.source=="number",targetIsInt:typeof n.target=="number",groupFound:!!s,groupId_on_group:s?.id,availableGroupIds:c.map(p=>p.id)})),!s)return {source:n.source,target:n.target};let e=n.source,i=n.target;n.source?.id===n.keyNodeId?i=s:n.target?.id===n.keyNodeId?e=s:console.warn("[groupEdge] keyNodeId matched neither side",{keyNodeId:n.keyNodeId,sourceId:n.source?.id,targetId:n.target?.id});let o=e===s?n.source:i===s?n.target:null;if(o&&!s.bounds&&o.x!=null&&Md?.Rectangle){let p=(o.visualWidth??o.width??50)/2,E=(o.visualHeight??o.height??30)/2;s.bounds=new Md.Rectangle(o.x-p,o.x+p,o.y-E,o.y+E);}return {source:e,target:i}}updatePositions(){this.svgGroups.attr("x",n=>n.bounds.x).attr("y",n=>n.bounds.y).attr("width",n=>n.bounds.width()).attr("height",n=>n.bounds.height()).lower(),this.svgNodes.select("rect").each(n=>{n.bounds&&(n.innerBounds=n.bounds.inflate(-1));}).attr("x",n=>n.x-(n.visualWidth??n.width)/2).attr("y",n=>n.y-(n.visualHeight??n.height)/2).attr("width",n=>n.visualWidth??n.width).attr("height",n=>n.visualHeight??n.height),this.svgNodes.select("image").attr("x",n=>{let c=n.visualWidth??n.width;return n.showLabels?n.x+c/2-c*qi.SMALL_IMG_SCALE_FACTOR:n.x-c/2}).attr("y",n=>{let c=n.visualHeight??n.height;return n.showLabels,n.y-c/2}),this.svgNodes.select(".mostSpecificTypeLabel").attr("x",n=>n.x-(n.visualWidth??n.width??0)/2+5).attr("y",n=>n.y-(n.visualHeight??n.height??0)/2+10).raise(),this.svgNodes.select(".label").attr("x",n=>n.x).attr("y",n=>n.y).each((n,c,s)=>{let i=n._labelVerticalOffset||0,o=n._labelLineHeight||12;Co.select(s[c]).selectAll("tspan").attr("x",n.x).attr("dy",(p,E)=>E===0?`${i}px`:`${o}px`);}).raise(),this.svgLinkGroups.select("path").attr("d",n=>{let{source:c,target:s}=this.resolveGroupEdgeEndpoints(n),e=this.getStableEdgePath(c,s);return this.lineFunction(e)}).attr("marker-end",n=>this.isAlignmentEdge(n)?"none":"url(#end-arrow)").attr("marker-start",n=>this.isAlignmentEdge(n)||!n.bidirectional?"none":"url(#start-arrow)").raise(),this.svgLinkGroups.select(".linklabel").attr("x",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);return c?this.calculateNewPosition(c,"x"):(n.source.x+n.target.x)/2}).attr("y",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);return c?this.calculateNewPosition(c,"y"):(n.source.y+n.target.y)/2}).style("font-size",()=>{let n=this.getCurrentZoomScale(),c=12,s=n<1?c/Math.sqrt(n):c;return `${Math.min(s,16)}px`}).raise(),this.updateEdgeEndpointMarkers(),this.svgGroupLabels.attr("x",n=>n.bounds?n.bounds.x+n.bounds.width()/2:0).attr("y",n=>n.bounds?n.bounds.y+5:0).attr("text-anchor","middle").lower(),this.svgLinkGroups.selectAll("marker").raise(),this.svgLinkGroups.selectAll(".linklabel").raise(),this.svgGroups.selectAll(".error-group").raise(),this.svgNodes.selectAll(".error-node").raise();}updateEdgeEndpointMarkers(){this.svgLinkGroups&&(this.svgLinkGroups.select(".target-marker").attr("cx",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);if(c){let s=c.getTotalLength();return c.getPointAtLength(s).x}return n.target.x||0}).attr("cy",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);if(c){let s=c.getTotalLength();return c.getPointAtLength(s).y}return n.target.y||0}).attr("opacity",this.isInputModeActive?.8:0).style("pointer-events",this.isInputModeActive?"all":"none").raise(),this.svgLinkGroups.select(".source-marker").attr("cx",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);return c?c.getPointAtLength(0).x:n.source.x||0}).attr("cy",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);return c?c.getPointAtLength(0).y:n.source.y||0}).attr("opacity",this.isInputModeActive?.8:0).style("pointer-events",this.isInputModeActive?"all":"none").raise());}gridUpdatePositions(){this.ensureNodeBounds(true);let n=this.container.selectAll(".node"),c=this.container.selectAll(".mostSpecificTypeLabel"),s=this.container.selectAll(".label"),e=this.container.selectAll(".group"),i=this.container.selectAll(".groupLabel");n.select("rect").each(function(p){p.innerBounds=p.bounds.inflate(-1);}).attr("x",function(p){return p.bounds.x}).attr("y",function(p){return p.bounds.y}).attr("width",function(p){return p.bounds.width()}).attr("height",function(p){return p.bounds.height()}),n.select("image").attr("x",function(p){let E=p.visualWidth??p.width;return p.showLabels?p.x+E/2-E*qi.SMALL_IMG_SCALE_FACTOR:p.bounds.x}).attr("y",function(p){let E=p.visualHeight??p.height;return p.showLabels?p.y-E/2:p.bounds.y}),c.attr("x",function(p){return p.bounds.x+5}).attr("y",function(p){return p.bounds.y+10}).raise(),s.attr("x",p=>p.x).attr("y",p=>p.y).each(function(p){var E=0;Co.select(this).selectAll("tspan").attr("x",p.x).attr("dy",function(){return E+=1,E===1?"0em":"1em"});}).raise(),e.attr("x",function(p){return p.bounds.x}).attr("y",function(p){return p.bounds.y}).attr("width",function(p){return p.bounds.width()}).attr("height",function(p){return p.bounds.height()}).lower(),i.attr("x",function(p){return p.bounds.x+p.bounds.width()/2}).attr("y",function(p){return p.bounds.y+12}).attr("text-anchor","middle").raise();let o=this.container.selectAll(".link-group");o.select("path").attr("d",p=>{if(p.source?.id===p.target?.id){let h=this.createSelfLoopRoute(p);return this.lineFunction(h)}let{source:E,target:b}=this.resolveGroupEdgeEndpoints(p),y=h=>h.bounds?typeof h.bounds.cx=="function"?{x:h.bounds.cx(),y:h.bounds.cy()}:{x:(h.bounds.x+h.bounds.X)/2,y:(h.bounds.y+h.bounds.Y)/2}:{x:h.x??0,y:h.y??0},m=y(E),a=y(b),l=a.x-m.x,u=a.y-m.y;if(Math.abs(l)>Math.abs(u)){let h=m.x+l/2;return this.gridLineFunction([{x:m.x,y:m.y},{x:h,y:m.y},{x:h,y:a.y},{x:a.x,y:a.y}])}else {let h=m.y+u/2;return this.gridLineFunction([{x:m.x,y:m.y},{x:m.x,y:h},{x:a.x,y:h},{x:a.x,y:a.y}])}}),o.select("text.linklabel").attr("x",p=>{let E=this.shadowRoot?.querySelector(`path[data-link-id="${p.id}"]`);if(E){let m=E.getTotalLength();return E.getPointAtLength(m/2).x}let b=p.source?.x??p.source?.bounds?.cx()??0,y=p.target?.x??p.target?.bounds?.cx()??0;return (b+y)/2}).attr("y",p=>{let E=this.shadowRoot?.querySelector(`path[data-link-id="${p.id}"]`);if(E){let m=E.getTotalLength();return E.getPointAtLength(m/2).y}let b=p.source?.y??p.source?.bounds?.cy()??0,y=p.target?.y??p.target?.bounds?.cy()??0;return (b+y)/2}).raise();}routeEdges(){try{this.ensureNodeBounds(!0),typeof this.colaLayout?.prepareEdgeRouting=="function"&&this.colaLayout.prepareEdgeRouting(qi.VIEWBOX_PADDING/qi.EDGE_ROUTE_MARGIN_DIVISOR),this.buildEdgeRoutingCaches(),this.routeLinkPaths(),this.updateLinkLabelsAfterRouting(),this.fitViewportToContent();}catch(n){console.error("Error in edge routing:",n),this.showError(`Edge routing failed: ${n.message}`);}}ensureNodeBounds(n=false){if(!(!this.currentLayout?.nodes||!Md?.Rectangle))for(let c of this.currentLayout.nodes){if(!n&&c.bounds&&typeof c.bounds.rayIntersection=="function"){let b=c.bounds.cx(),y=c.bounds.cy(),m=1;if(Math.abs(b-(c.x||0))<m&&Math.abs(y-(c.y||0))<m)continue}let s=(c.visualWidth??c.width??50)/2,e=(c.visualHeight??c.height??30)/2,i=(c.x||0)-s,o=(c.x||0)+s,p=(c.y||0)-e,E=(c.y||0)+e;c.bounds=new Md.Rectangle(i,o,p,E),c.innerBounds=c.bounds.inflate(-1);}}buildEdgeRoutingCaches(){this.edgeRoutingCache.edgesBetweenNodes.clear(),this.edgeRoutingCache.alignmentEdges.clear(),this.currentLayout?.links&&(this.currentLayout.links.forEach(n=>{n.id?.startsWith("_alignment_")&&this.edgeRoutingCache.alignmentEdges.add(n.id);}),this.currentLayout.links.forEach(n=>{if(this.isAlignmentEdge(n))return;let c=n.source.id,s=n.target.id,e=this.getNodePairKey(c,s);this.edgeRoutingCache.edgesBetweenNodes.has(e)||this.edgeRoutingCache.edgesBetweenNodes.set(e,[]),this.edgeRoutingCache.edgesBetweenNodes.get(e).push(n);}));}getNodePairKey(n,c){return n<c?`${n}:${c}`:`${c}:${n}`}route(n=[],c=[],s,e){n.forEach(o=>{let p=o.bounds||o.innerBounds||this.createFallbackBounds(o);o.routerNode={name:o.name,bounds:p};}),c.forEach(o=>{o.bounds||console.warn("Grid routing group missing bounds; routing may be degraded.",o),o.routerNode={bounds:o.bounds?.inflate(-e)??o.bounds,children:(typeof o.groups<"u"?o.groups.map(p=>n.length+p.id):[]).concat(typeof o.leaves<"u"?o.leaves.map(p=>p.index):[])};});let i=n.concat(c).map((o,p)=>o.routerNode?(o.routerNode.id=p,o.routerNode):null).filter(Boolean);return new Md.GridRouter(i,{getChildren:o=>o.children,getBounds:o=>o.bounds},s-e)}gridify(n,c,s){if(this.isGridifyingInProgress){console.warn("[gridify] Already in progress, skipping re-entrant call");return}this.isGridifyingInProgress=true;try{this.gridifyInternal(n,c,s);}catch(e){console.log("Error routing edges in GridRouter"),console.error(e);try{this.fallbackGridRouting(this.currentLayout?.links??[]);}catch(o){console.error("Fallback grid routing also failed:",o);}let i=document.getElementById("runtime_messages");if(i){let o=document.createElement("div");o.className="alert alert-danger alert-dismissible fade show",o.setAttribute("role","alert"),o.innerHTML="Runtime (WebCola) error when gridifying edges. You may have to click and drag these nodes slightly to un-stick layout.",i.querySelectorAll(".alert").forEach(E=>{E.innerHTML===o.innerHTML&&E.remove();}),i.appendChild(o);}}finally{this.isGridifyingInProgress=false;}}gridifyInternal(n,c,s){let e=this.currentLayout?.nodes??[],i=this.currentLayout?.groups??[],o=this.currentLayout?.links??[];if(e.length===0){console.warn("No nodes available for GridRouter; skipping gridify.");return}if(o.length===0){console.warn("No edges to route in GridRouter");return}console.log("[gridify] Node positions BEFORE ensureNodeBounds:"),e.slice(0,3).forEach(u=>{console.log(` ${u.id}: x=${u.x?.toFixed(2)}, y=${u.y?.toFixed(2)}, bounds.cx=${u.bounds?.cx?.()?.toFixed(2)}, bounds.x=${u.bounds?.x?.toFixed(2)}`);}),this.ensureNodeBounds(true);let p=e.filter(u=>!Number.isFinite(u.x)||!Number.isFinite(u.y));if(p.length>0){console.warn("[gridify] Found nodes with invalid positions, falling back to default routing:",p.map(u=>({id:u.id,x:u.x,y:u.y}))),this.fallbackGridRouting(o);return}console.log("[gridify] Node positions AFTER ensureNodeBounds:"),e.slice(0,3).forEach(u=>{console.log(` ${u.id}: x=${u.x?.toFixed(2)}, y=${u.y?.toFixed(2)}, bounds.cx=${u.bounds?.cx?.()?.toFixed(2)}, bounds.x=${u.bounds?.x?.toFixed(2)}`);});let E=this.route(e,i,c,s),b=[],y=o.filter(u=>{let h=u?.source?.routerNode&&u?.target?.routerNode,d=u?.source?.id===u?.target?.id;return h&&!d}),m=o.filter(u=>u?.source?.id===u?.target?.id);console.log("[gridify] Total edges:",o.length,"Routable:",y.length,"Self-loops:",m.length),y.length+m.length!==o.length&&o.filter(h=>(!h?.source?.routerNode||!h?.target?.routerNode)&&h?.source?.id!==h?.target?.id).forEach(h=>{console.warn("[gridify] Unroutable edge:",h.id,"source routerNode:",!!h?.source?.routerNode,"target routerNode:",!!h?.target?.routerNode,"source:",h?.source?.id,"x:",h?.source?.x,"y:",h?.source?.y,"target:",h?.target?.id,"x:",h?.target?.x,"y:",h?.target?.y);}),b=E.routeEdges(y,n,function(u){return u.source.routerNode.id},function(u){return u.target.routerNode.id});let a=new Map;y.forEach((u,h)=>{let d=b[h];u?.id&&d&&a.set(u.id,this.adjustGridRouteForEdge(u,d));}),console.log("[gridify] Routes generated:",a.size,"out of",y.length),this.container.selectAll(".link-group").data(o,u=>u.id??u).select("path").attr("d",u=>{if(u.source?.id===u.target?.id){let T=this.createSelfLoopRoute(u);return this.lineFunction(T)}let h=a.get(u.id);if(!h){let T=u.source?.x??u.source?.bounds?.cx()??0,q=u.source?.y??u.source?.bounds?.cy()??0,G=u.target?.x??u.target?.bounds?.cx()??0,j=u.target?.y??u.target?.bounds?.cy()??0;console.log("[gridify] Fallback path for edge:",u.id,"from",u.source?.id,"(",T,",",q,")","to",u.target?.id,"(",G,",",j,")");let B=G-T,J=j-q;if(Math.abs(B)>Math.abs(J)){let w=T+B/2;return this.gridLineFunction([{x:T,y:q},{x:w,y:q},{x:w,y:j},{x:G,y:j}])}else {let w=q+J/2;return this.gridLineFunction([{x:T,y:q},{x:T,y:w},{x:G,y:w},{x:G,y:j}])}}let S=Md.GridRouter.getRoutePath(h,5,3,7);return this.adjustGridRouteForArrowPositioning(u,S.routepath,h)||S.routepath}),this.gridUpdateLinkLabels(o,a),this.fitViewportToContent(),this.dispatchEvent(new Event("relationsAvailable"));}fallbackGridRouting(n){this.container.selectAll(".link-group").data(n,s=>s.id??s).select("path").attr("d",s=>{if(s.source?.id===s.target?.id){let y=this.createSelfLoopRoute(s);return this.lineFunction(y)}let e=s.source?.x??s.source?.bounds?.cx()??0,i=s.source?.y??s.source?.bounds?.cy()??0,o=s.target?.x??s.target?.bounds?.cx()??0,p=s.target?.y??s.target?.bounds?.cy()??0,E=o-e,b=p-i;if(Math.abs(E)>Math.abs(b)){let y=e+E/2;return this.gridLineFunction([{x:e,y:i},{x:y,y:i},{x:y,y:p},{x:o,y:p}])}else {let y=i+b/2;return this.gridLineFunction([{x:e,y:i},{x:e,y},{x:o,y},{x:o,y:p}])}}),this.fitViewportToContent();}gridUpdateLinkLabels(n,c){this.container.selectAll(".link-group").filter(e=>!this.isAlignmentEdge(e)).select("text.linklabel").attr("x",e=>{let i=this.shadowRoot?.querySelector(`path[data-link-id="${e.id}"]`);if(i)try{let p=i.getTotalLength();return i.getPointAtLength(p/2).x}catch{}return this.getGridRouteMidpoint(e,c)?.x??e.source?.x??e.source?.bounds?.cx()??0}).attr("y",e=>{let i=this.shadowRoot?.querySelector(`path[data-link-id="${e.id}"]`);if(i)try{let p=i.getTotalLength();return i.getPointAtLength(p/2).y}catch{}return this.getGridRouteMidpoint(e,c)?.y??e.source?.y??e.source?.bounds?.cy()??0}).attr("text-anchor","middle").attr("dominant-baseline","middle");}getGridRouteMidpoint(n,c){let s=c.get(n.id);if(!s){let y=n.source?.x??n.source?.bounds?.cx()??0,m=n.source?.y??n.source?.bounds?.cy()??0,a=n.target?.x??n.target?.bounds?.cx()??0,l=n.target?.y??n.target?.bounds?.cy()??0;return {x:(y+a)/2,y:(m+l)/2}}let e=[];if(s.forEach(y=>{e.length===0&&y.length>0&&e.push(y[0]),y.length>1&&e.push(y[1]);}),e.length<2)return null;let i=0,o=[];for(let y=0;y<e.length-1;y++){let m=e[y+1].x-e[y].x,a=e[y+1].y-e[y].y,l=Math.sqrt(m*m+a*a);o.push(l),i+=l;}let p=i/2,E=0;for(let y=0;y<o.length;y++){let m=o[y];if(E+m>=p){let a=p-E,l=m>0?a/m:0;return {x:e[y].x+l*(e[y+1].x-e[y].x),y:e[y].y+l*(e[y+1].y-e[y].y)}}E+=m;}let b=Math.floor(e.length/2);return e[b]}adjustGridRouteForEdge(n,c){if(!n?.id?.startsWith("_g_"))return c;let s=this.gridRouteToPoints(c);if(s.length<2)return c;let e=this.routeGroupEdge(n,s);return this.pointsToGridRoute(e)}adjustGridRouteForArrowPositioning(n,c,s){if(!c||!n.source||!n.target)return null;try{let e=this.gridRouteToPoints(s);if(e.length<2)return null;let i=n.source,o=n.target,p=i.bounds||{x:i.x-(i.width||0)/2,y:i.y-(i.height||0)/2,width:()=>i.width||0,height:()=>i.height||0},E=o.bounds||{x:o.x-(o.width||0)/2,y:o.y-(o.height||0)/2,width:()=>o.width||0,height:()=>o.height||0},y=this.getTouchDirection(p,E,5);if(y!=="none"){let{sourcePoint:h,targetPoint:d,middlePoints:g}=this.computePerpendicularRoute(p,E,y),_=[h,...g,d];return this.gridLineFunction(_)}let m=e.length>1?e[1]:e[0],a=this.getRectangleIntersection(p.x+p.width()/2,p.y+p.height()/2,m.x,m.y,p);a&&(e[0]=a);let l=e.length>1?e[e.length-2]:e[e.length-1],u=this.getRectangleIntersection(E.x+E.width()/2,E.y+E.height()/2,l.x,l.y,E);return u&&(e[e.length-1]=u),this.gridLineFunction(e)}catch(e){return console.warn("Error adjusting grid route for arrow positioning:",e),null}}getTouchDirection(n,c,s){let e=n.x,i=n.x+n.width(),o=n.y,p=n.y+n.height(),E=c.x,b=c.x+c.width(),y=c.y,m=c.y+c.height(),a=Math.max(0,Math.max(E-i,e-b)),l=Math.max(0,Math.max(y-p,o-m)),u=!(p<y||m<o),h=!(i<E||b<e);return a<=s&&u?"horizontal":l<=s&&h?"vertical":"none"}computePerpendicularRoute(n,c,s){let e=n.width(),i=n.height(),o=c.width(),p=c.height(),E=n.x+e/2,b=n.y+i/2,y=c.x+o/2,m=c.y+p/2,a=15;if(s==="horizontal")if(b<=m){let u=Math.min(n.y,c.y)-a;return {sourcePoint:{x:E,y:n.y},targetPoint:{x:y,y:c.y},middlePoints:[{x:E,y:u},{x:y,y:u}]}}else {let u=Math.max(n.y+i,c.y+p)+a;return {sourcePoint:{x:E,y:n.y+i},targetPoint:{x:y,y:c.y+p},middlePoints:[{x:E,y:u},{x:y,y:u}]}}else if(E<=y){let u=Math.min(n.x,c.x)-a;return {sourcePoint:{x:n.x,y:b},targetPoint:{x:c.x,y:m},middlePoints:[{x:u,y:b},{x:u,y:m}]}}else {let u=Math.max(n.x+e,c.x+o)+a;return {sourcePoint:{x:n.x+e,y:b},targetPoint:{x:c.x+o,y:m},middlePoints:[{x:u,y:b},{x:u,y:m}]}}}areBoundsNear(n,c,s){let e=n.x,i=n.x+n.width(),o=n.y,p=n.y+n.height(),E=c.x,b=c.x+c.width(),y=c.y,m=c.y+c.height(),a=Math.max(0,Math.max(E-i,e-b)),l=Math.max(0,Math.max(y-p,o-m));return Math.sqrt(a*a+l*l)<=s}chooseBoundaryPoint(n,c,s,e){let i=Math.max(1,s.width()),o=Math.max(1,s.height()),p=[{x:s.x,y:s.y+o/2},{x:s.x+i,y:s.y+o/2},{x:s.x+i/2,y:s.y},{x:s.x+i/2,y:s.y+o}],E=p[0],b=-1/0;for(let y of p){let m=y.x-e.x,a=y.y-e.y,l=Math.sqrt(m*m+a*a);l>b&&(b=l,E=y);}return E}normalizeNodeBounds(n){let c=n.visualWidth??n.width??50,s=n.visualHeight??n.height??30,e=n.bounds||{x:n.x-c/2,y:n.y-s/2,width:()=>c,height:()=>s};return {x:typeof e.x=="number"||e.X!==void 0?e.x:n.x-c/2,y:typeof e.y=="number"?e.y:n.y-s/2,width:()=>typeof e.width=="function"?e.width():e.X!==void 0?e.X-e.x:c,height:()=>typeof e.height=="function"?e.height():e.Y!==void 0?e.Y-e.y:s}}lineIntersectsRect(n,c,s){let e=s.x,i=s.x+s.width(),o=s.y,p=s.y+s.height(),E=Math.min(n.x,c.x),b=Math.max(n.x,c.x),y=Math.min(n.y,c.y),m=Math.max(n.y,c.y);if(b<e||E>i||m<o||y>p)return false;let a=n.x>=e&&n.x<=i&&n.y>=o&&n.y<=p,l=c.x>=e&&c.x<=i&&c.y>=o&&c.y<=p;if(a||l)return true;let u=c.x-n.x,h=c.y-n.y,d=(_,S,v)=>{if(h===0)return false;let T=(_-n.y)/h;if(T<0||T>1)return false;let q=n.x+T*u;return q>=S&&q<=v},g=(_,S,v)=>{if(u===0)return false;let T=(_-n.x)/u;if(T<0||T>1)return false;let q=n.y+T*h;return q>=S&&q<=v};return d(o,e,i)||d(p,e,i)||g(e,o,p)||g(i,o,p)}findBlockingNodes(n,c,s,e){if(!this.currentLayout?.nodes)return [];let i=this.normalizeNodeBounds(n),o=this.normalizeNodeBounds(c),p={x:i.x+i.width()/2,y:i.y+i.height()/2},E={x:o.x+o.width()/2,y:o.y+o.height()/2},b=[];for(let y of this.currentLayout.nodes){if(y.id===s||y.id===e)continue;let m=this.normalizeNodeBounds(y);if(this.lineIntersectsRect(p,E,m)){let a={x:m.x+m.width()/2,y:m.y+m.height()/2},l=Math.sqrt(Math.pow(a.x-p.x,2)+Math.pow(a.y-p.y,2));b.push({node:y,bounds:m,distance:l});}}return b.sort((y,m)=>y.distance-m.distance),b.map(y=>({node:y.node,bounds:y.bounds}))}computeRouteAroundBlockingNodes(n,c,s){let i=Math.min(n.x,c.x),o=Math.max(n.x+n.width(),c.x+c.width()),p=Math.min(n.y,c.y),E=Math.max(n.y+n.height(),c.y+c.height());for(let{bounds:h}of s)i=Math.min(i,h.x),o=Math.max(o,h.x+h.width()),p=Math.min(p,h.y),E=Math.max(E,h.y+h.height());let b=n.x+n.width()/2,y=n.y+n.height()/2,m=c.x+c.width()/2,a=c.y+c.height()/2,l=Math.abs(m-b);if(Math.abs(a-y)>l)if(b<=m){let d=i-15;return {sourcePoint:{x:n.x,y},targetPoint:{x:c.x,y:a},middlePoints:[{x:d,y},{x:d,y:a}]}}else {let d=o+15;return {sourcePoint:{x:n.x+n.width(),y},targetPoint:{x:c.x+c.width(),y:a},middlePoints:[{x:d,y},{x:d,y:a}]}}else if(y<=a){let d=p-15;return {sourcePoint:{x:b,y:n.y},targetPoint:{x:m,y:c.y},middlePoints:[{x:b,y:d},{x:m,y:d}]}}else {let d=E+15;return {sourcePoint:{x:b,y:n.y+n.height()},targetPoint:{x:m,y:c.y+c.height()},middlePoints:[{x:b,y:d},{x:m,y:d}]}}}getNearTouchPerpendicularRoute(n){if(!n.source||!n.target||n.source.id===n.target.id)return null;let c=n.source,s=n.target,e=this.normalizeNodeBounds(c),i=this.normalizeNodeBounds(s),p=this.getTouchDirection(e,i,5);if(p!=="none"){let{sourcePoint:b,targetPoint:y,middlePoints:m}=this.computePerpendicularRoute(e,i,p);return [b,...m,y]}let E=this.findBlockingNodes(c,s,c.id,s.id);if(E.length>0){let{sourcePoint:b,targetPoint:y,middlePoints:m}=this.computeRouteAroundBlockingNodes(e,i,E);return [b,...m,y]}return null}gridRouteToPoints(n){let c=[];return n.forEach((s,e)=>{e===0&&c.push({x:s[0].x,y:s[0].y}),c.push({x:s[1].x,y:s[1].y});}),c}pointsToGridRoute(n){let c=[];for(let s=0;s<n.length-1;s+=1)c.push([n[s],n[s+1]]);return c}createFallbackBounds(n){if(!Md?.Rectangle)return null;let c=((n.visualWidth??n.width)||50)/2,s=((n.visualHeight??n.height)||30)/2,e=(n.x||0)-c,i=(n.x||0)+c,o=(n.y||0)-s,p=(n.y||0)+s;return new Md.Rectangle(e,i,o,p)}getRectangleIntersection(n,c,s,e,i){let o=i.x,p=i.x+i.width(),E=i.y,b=i.y+i.height(),y=s-n,m=e-c;if(y===0&&m===0)return {x:n,y:c};let a=0,l=1;if(y!==0){let h=(o-n)/y,d=(p-n)/y;a=Math.max(a,Math.min(h,d)),l=Math.min(l,Math.max(h,d));}if(m!==0){let h=(E-c)/m,d=(b-c)/m;a=Math.max(a,Math.min(h,d)),l=Math.min(l,Math.max(h,d));}if(a>l)return null;let u=a>0?a:l;return {x:n+u*y,y:c+u*m}}routeLinkPaths(){this.container.selectAll(".link-group path").attr("d",n=>{try{return this.routeSingleEdge(n)}catch(c){return console.error(`Error routing edge ${n.id} from ${n.source.id} to ${n.target.id}:`,c),this.showRuntimeAlert(n.source.id,n.target.id),this.lineFunction([{x:n.source.x||0,y:n.source.y||0},{x:n.target.x||0,y:n.target.y||0}])}});}routeSingleEdge(n){if(this.isAlignmentEdge(n))return this.lineFunction([{x:n.source.x||0,y:n.source.y||0},{x:n.target.x||0,y:n.target.y||0}]);let c=[{x:n.source.x||0,y:n.source.y||0},{x:n.target.x||0,y:n.target.y||0}],s;if(typeof this.colaLayout?.routeEdge=="function")try{if(s=this.colaLayout.routeEdge(n),!s||!Array.isArray(s)||s.length<2||!s[0]||!s[1]||s[0].x===void 0||s[0].y===void 0)throw new Error(`WebCola failed to route edge ${n.id} from ${n.source.id} to ${n.target.id}`)}catch(i){return console.log("Error routing edge",n.id,`from ${n.source.id} to ${n.target.id}`),console.error(i),this.lineFunction(c)}else s=c;if(n.source.id===n.target.id)s=this.createSelfLoopRoute(n);else {if(n.id?.startsWith("_g_"))return s=this.routeGroupEdge(n,s),this.lineFunction(s);s=this.handleMultipleEdgeRouting(n,s);}let e=this.getNearTouchPerpendicularRoute(n);return e?this.lineFunction(e):this.lineFunction(s)}createSelfLoopRoute(n){let c=n.source,s=c.bounds;if(!s)return [{x:c.x,y:c.y},{x:c.x+20,y:c.y-20},{x:c.x,y:c.y}];let e=s.X-s.x,i=s.Y-s.y,o={x:s.x+e/2,y:s.y},p={x:s.X,y:s.y+i/2},b=1+(n.selfLoopIndex||0)*qi.SELF_LOOP_CURVATURE_SCALE,y={x:s.X+e/2*b,y:s.y-i/2*b};return [o,y,p]}routeGroupEdge(n,c){let s=n.groupId?(this.currentLayout?.groups||[]).find(e=>e.id===n.groupId):null;if(!s)console.warn("[routeGroupEdge] Group not found for edge:",n.id,"\u2014 groupId:",n.groupId);else {if(!s.bounds&&Md?.Rectangle){let e=n.source?.id===n.keyNodeId?c[c.length-1]:c[0];e&&(s.bounds=new Md.Rectangle(e.x-50,e.x+50,e.y-30,e.y+30));}if(s.bounds)if(n.source?.id===n.keyNodeId)c[c.length-1]=this.closestPointOnRect(s.bounds,c[0]);else if(n.target?.id===n.keyNodeId){let e=s.bounds.inflate?.(-1)??s.bounds;c[0]=this.closestPointOnRect(e,c[c.length-1]);}else console.warn("[routeGroupEdge] keyNodeId matched neither side",{keyNodeId:n.keyNodeId,sourceId:n.source?.id,targetId:n.target?.id});}return c.length>2&&c.splice(1,c.length-2),c}handleMultipleEdgeRouting(n,c){let s=this.getAllEdgesBetweenNodes(n.source.id,n.target.id);if(s.length<=1)return c;if(c.length===2){let b={x:(c[0].x+c[1].x)/2,y:(c[0].y+c[1].y)/2};c.splice(1,0,b);}let e=c[1].x-c[0].x,i=c[1].y-c[0].y,o=Math.atan2(i,e),p=this.getRouteLength(c),E=s.findIndex(b=>b.id===n.id);if(E!==-1){c=this.applyEdgeOffsetWithIndex(n,c,s,o,E,p);let b=this.calculateCurvatureWithIndex(s,n.id,E),y=this.clampCurvature(b);c=this.applyCurvatureToRoute(c,y,o,p);}return c}getAllEdgesBetweenNodes(n,c){if(!this.currentLayout?.links)return [];let s=this.getNodePairKey(n,c);return this.edgeRoutingCache.edgesBetweenNodes.has(s)?this.edgeRoutingCache.edgesBetweenNodes.get(s):this.currentLayout.links.filter(e=>!this.isAlignmentEdge(e)&&(e.source.id===n&&e.target.id===c||e.source.id===c&&e.target.id===n))}calculateCurvature(n,c,s,e){if(e.startsWith("_alignment_"))return 0;let i=n.length,o=n.findIndex(p=>p.id===e);return i<=1?0:(o%2===0?1:-1)*(Math.floor(o/2)+1)*qi.CURVATURE_BASE_MULTIPLIER*i}calculateCurvatureWithIndex(n,c,s){let e=n.length;return e<=1?0:(s%2===0?1:-1)*(Math.floor(s/2)+1)*qi.CURVATURE_BASE_MULTIPLIER*e}applyEdgeOffset(n,c,s,e){let i=s.findIndex(p=>p.id===n.id),o=this.getRouteLength(c);return this.applyEdgeOffsetWithIndex(n,c,s,e,i,o)}applyEdgeOffsetWithIndex(n,c,s,e,i,o){let p=(i%2===0?1:-1)*(Math.floor(i/2)+1)*qi.MIN_EDGE_DISTANCE,E=this.clampOffset(p,o),b=this.getDominantDirection(e);return b==="right"||b==="left"?(c[0].y+=E,c[c.length-1].y+=E):(b==="up"||b==="down")&&(c[0].x+=E,c[c.length-1].x+=E),n.source.innerBounds&&(c[0]=this.adjustPointToRectanglePerimeter(c[0],n.source.innerBounds)),n.target.innerBounds&&(c[c.length-1]=this.adjustPointToRectanglePerimeter(c[c.length-1],n.target.innerBounds)),c}clampOffset(n,c){let s=Math.max(qi.MIN_EDGE_DISTANCE,c*qi.MAX_EDGE_OFFSET_RATIO);return Math.max(-s,Math.min(s,n))}getRouteLength(n){return n.length<2?0:n.slice(1).reduce((c,s,e)=>{let i=n[e],o=s.x-i.x,p=s.y-i.y;return c+Math.sqrt(o*o+p*p)},0)}clampCurvature(n){return Math.max(-qi.MAX_EDGE_CURVATURE_RATIO,Math.min(qi.MAX_EDGE_CURVATURE_RATIO,n))}applyCurvatureToRoute(n,c,s,e){return c===0||n.forEach((i,o)=>{if(o>0&&o<n.length-1){let p=c*Math.abs(Math.sin(s))*e,E=c*Math.abs(Math.cos(s))*e;i.x+=p,i.y+=E;}}),n}getDominantDirection(n){return n=(n+Math.PI)%(2*Math.PI)-Math.PI,n>=-Math.PI/4&&n<=Math.PI/4?"right":n>Math.PI/4&&n<3*Math.PI/4?"up":n>=3*Math.PI/4||n<=-3*Math.PI/4?"left":n>-3*Math.PI/4&&n<-Math.PI/4?"down":null}closestPointOnRect(n,c){if(!n)return c;let{x:s,y:e,X:i,Y:o}=n,p=Math.max(s,Math.min(c.x,i)),E=Math.max(e,Math.min(c.y,o));return {x:p,y:E}}getStableEdgeAnchor(n,c){if(!n)return c;let s,e,i,o;if(typeof n.cx=="function")s=n.cx(),e=n.cy(),i=n.width()/2,o=n.height()/2;else if(n.x!==void 0&&n.X!==void 0)s=(n.x+n.X)/2,e=(n.y+n.Y)/2,i=(n.X-n.x)/2,o=(n.Y-n.y)/2;else return c;let p=c.x-s,E=c.y-e,b=Math.abs(p)/i,y=Math.abs(E)/o;return b>y?p>0?{x:s+i,y:e}:{x:s-i,y:e}:E>0?{x:s,y:e+o}:{x:s,y:e-o}}getStableEdgePath(n,c){let s;c.bounds&&typeof c.bounds.cx=="function"?s={x:c.bounds.cx(),y:c.bounds.cy()}:c.bounds?s={x:(c.bounds.x+c.bounds.X)/2,y:(c.bounds.y+c.bounds.Y)/2}:s={x:c.x||0,y:c.y||0};let e;n.bounds&&typeof n.bounds.cx=="function"?e={x:n.bounds.cx(),y:n.bounds.cy()}:n.bounds?e={x:(n.bounds.x+n.bounds.X)/2,y:(n.bounds.y+n.bounds.Y)/2}:e={x:n.x||0,y:n.y||0};let i=n.bounds||n.innerBounds?this.getStableEdgeAnchor(n.bounds||n.innerBounds,s):e,o=c.bounds||c.innerBounds?this.getStableEdgeAnchor(c.bounds||c.innerBounds,e):s;return [i,o]}adjustPointToRectanglePerimeter(n,c){return c?this.closestPointOnRect(c,n):n}updateLinkLabelsAfterRouting(){this.container.selectAll(".link-group .linklabel").attr("x",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);if(!c)return 0;let s=c.getTotalLength();return c.getPointAtLength(s/2).x}).attr("y",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);if(!c)return 0;let s=c.getTotalLength();return c.getPointAtLength(s/2).y}).attr("text-anchor","middle").each((n,c,s)=>{this.handleLabelOverlap(s[c]);}).raise();}handleLabelOverlap(n){let c=[];this.container.selectAll(".linklabel").each(function(){this!==n&&_ft(this,n)&&c.push(this);}),c.length>0&&this.minimizeOverlap(n,c);}minimizeOverlap(n,c){}fitViewportToContent(n=false){let c=this.svg?.node();if(!c||!this.zoomBehavior||this.userHasManuallyZoomed&&!this.isInitialRender&&!n)return;let s=this.calculateContentBounds();if(!s)return;let e=c.clientWidth||c.parentElement?.clientWidth||800,i=c.clientHeight||c.parentElement?.clientHeight||600,o=qi.VIEWBOX_PADDING*4,p=(e-o*2)/s.width,E=(i-o*2)/s.height,b=Math.min(p,E,1),[y,m]=this.zoomBehavior.scaleExtent(),a=Math.max(y,Math.min(m,b)),l=s.x+s.width/2,u=s.y+s.height/2,h=e/2-l*a,d=i/2-u*a,g=Co.zoomIdentity.translate(h,d).scale(a);this.isInitialRender?(this.svg.call(this.zoomBehavior.transform,g),this.isInitialRender=false):this.svg.transition().duration(300).ease(Co.easeCubicOut).call(this.zoomBehavior.transform,g),this.updateZoomControlStates();}resetViewToFitContent(){this.userHasManuallyZoomed=false,this.fitViewportToContent(true);}calculateContentBounds(){try{if(!this.currentLayout||!this.container)return null;let n=1/0,c=1/0,s=-1/0,e=-1/0,i=this.currentLayout.nodes;if(i&&i.length>0){i.forEach((a,l)=>{if(typeof a.x=="number"&&typeof a.y=="number"){let u=a.width||0,h=a.height||0,d=a.x,g=a.x+u,_=a.y,S=a.y+h,v=c,T=e;n=Math.min(n,d),s=Math.max(s,g),c=Math.min(c,_),e=Math.max(e,S);}});let m=i.reduce((a,l)=>{if(typeof l.x=="number"&&typeof l.y=="number"){let u=l.y+(l.height||0),h=a?a.y+(a.height||0):-1/0;return u>h?l:a}return a},null);}let o=this.container.selectAll(".link-group");o.empty()||o.each(function(){try{let m=this.getBBox();m.width>0&&m.height>0&&(n=Math.min(n,m.x),s=Math.max(s,m.x+m.width),c=Math.min(c,m.y),e=Math.max(e,m.y+m.height));}catch{}});let p=this.container.selectAll(".node, .error-node");p.empty()||p.each(function(){try{let m=this.getBBox();m.width>0&&m.height>0&&(n=Math.min(n,m.x),s=Math.max(s,m.x+m.width),c=Math.min(c,m.y),e=Math.max(e,m.y+m.height));}catch{}});let E=this.container.selectAll("text");if(!E.empty()){let m=0;E.each(function(){try{let a=this.getBBox();if(a.width>0&&a.height>0){m++;let l=5,u=a.y-l,h=a.y+a.height+l,d=e;n=Math.min(n,a.x-l),s=Math.max(s,a.x+a.width+l),c=Math.min(c,u),e=Math.max(e,h);}}catch{}});}let b=this.container.selectAll(".group");return b.empty()||b.each(function(){try{let m=this.getBBox();m.width>0&&m.height>0&&(n=Math.min(n,m.x),s=Math.max(s,m.x+m.width),c=Math.min(c,m.y),e=Math.max(e,m.y+m.height));}catch{}}),n===1/0||c===1/0||s===-1/0||e===-1/0?(console.warn("Could not calculate content bounds - no valid elements found"),null):{x:n,y:c,width:s-n,height:e-c}}catch(n){return console.error("Error calculating content bounds:",n),null}}dispatchRelationsAvailableEvent(){let n=this.getAllRelations(),c=new CustomEvent("relations-available",{detail:{relations:n,count:n.length,timestamp:Date.now(),graphId:this.id||"unknown"},bubbles:true,cancelable:true});this.dispatchEvent(c);}getAllRelations(){if(!this.currentLayout?.links)return [];let n=new Set(this.currentLayout.links.filter(c=>!this.isAlignmentEdge(c)).map(c=>c.relName).filter(Boolean));return Array.from(n)}highlightRelation(n){return this.currentLayout?.links?(this.svgLinkGroups.filter(c=>c.relName===n&&!this.isAlignmentEdge(c)).selectAll("path").classed("highlighted",true),true):false}clearHighlightRelation(n){return this.currentLayout?.links?(this.svgLinkGroups.filter(c=>c.relName===n&&!this.isAlignmentEdge(c)).selectAll("path").classed("highlighted",false),true):false}highlightNodes(n){if(!this.currentLayout?.nodes||!this.svgNodes||!n||n.length===0)return false;let c=new Set(n),s=false;return this.svgNodes.each((e,i,o)=>{c.has(e.id)&&(Co.select(o[i]).classed("highlighted",true),s=true);}),s}highlightNodePairs(n,c={}){if(!this.currentLayout?.nodes||!this.svgNodes||!n||n.length===0)return false;let{showBadges:s=false}=c,e=new Set,i=new Set;n.forEach((p,E)=>{if(!Array.isArray(p)){console.warn(`highlightNodePairs: Pair at index ${E} is not an array, skipping`);return}if(p.length!==2){console.warn(`highlightNodePairs: Pair at index ${E} has ${p.length} elements (expected 2), skipping`);return}let[b,y]=p;b&&e.add(b),y&&i.add(y);});let o=false;return this.svgNodes.each((p,E,b)=>{let y=Co.select(b[E]);e.has(p.id)&&(y.classed("highlighted-first",true),o=true,s&&this.addHighlightBadge(y,p,"1","#007aff")),i.has(p.id)&&(y.classed("highlighted-second",true),o=true,s&&(e.has(p.id)?this.addHighlightBadge(y,p,"1,2","#9B59B6"):this.addHighlightBadge(y,p,"2","#ff3b30")));}),o}clearNodeHighlights(){return this.svgNodes?(this.svgNodes.classed("highlighted",false).classed("highlighted-first",false).classed("highlighted-second",false).selectAll(".highlight-badge, .highlight-badge-bg").remove(),true):false}addHighlightBadge(n,c,s,e){n.selectAll(".highlight-badge, .highlight-badge-bg").remove();let i=16,o=4,p=(c.width||0)/2-i/2-o,E=-(c.height||0)/2+i/2+o;n.append("circle").attr("class","highlight-badge-bg").attr("cx",p).attr("cy",E).attr("r",i/2).attr("fill",e),n.append("text").attr("class","highlight-badge").attr("x",p).attr("y",E).attr("dy","0.35em").text(s);}showRuntimeAlert(n,c){console.warn(`Runtime (WebCola) error when laying out an edge from ${n} to ${c}. You may have to click and drag these nodes slightly to un-stick layout.`);}getCSS(){return `
|
|
850
|
+
`;}initializeD3(){Co||(Co=window.d3),this.svg=Co.select(this.shadowRoot.querySelector("#svg")),this.container=this.svg.select(".zoomable"),Co.zoom?(this.zoomBehavior=Co.zoom().scaleExtent([.01,20]).on("start",()=>{Co.event.sourceEvent&&(this.userHasManuallyZoomed=true);}).on("zoom",()=>{this.container.attr("transform",Co.event.transform),this.updateZoomControlStates(),this.updateSmallNodeClasses();}),this.svg.call(this.zoomBehavior),this.initializeZoomControls()):console.warn("D3 zoom behavior not available. Ensure D3 v4+ is loaded.");}initializeZoomControls(){let n=this.shadowRoot.querySelector("#zoom-in"),c=this.shadowRoot.querySelector("#zoom-out"),s=this.shadowRoot.querySelector("#zoom-fit");n&&n.addEventListener("click",()=>{this.userHasManuallyZoomed=true,this.zoomIn();}),c&&c.addEventListener("click",()=>{this.userHasManuallyZoomed=true,this.zoomOut();}),s&&s.addEventListener("click",()=>{this.resetViewToFitContent();});let e=this.shadowRoot.querySelector("#routing-mode");if(e){let i=this.layoutFormat||"default";e.value=i,e.addEventListener("change",()=>{this.handleRoutingModeChange(e.value);});}this.updateZoomControlStates();}handleRoutingModeChange(n){this.setAttribute("layoutFormat",n),this.currentLayout&&this.colaLayout&&(n==="grid"?this.gridify(10,25,10):this.routeEdges(),this.dispatchEvent(new CustomEvent("routing-mode-changed",{detail:{mode:n}})));}updateRoutingModeDropdown(){let n=this.shadowRoot?.querySelector("#routing-mode");if(n){let c=this.layoutFormat||"default";n.value=c;}}initializeInputModeHandlers(){this.inputModeEnabled&&this.attachInputModeListeners();}attachInputModeListeners(){this.inputModeListenersAttached||(document.addEventListener("keydown",this.handleInputModeKeydown),document.addEventListener("keyup",this.handleInputModeKeyup),window.addEventListener("blur",this.handleInputModeBlur),this.inputModeListenersAttached=true);}detachInputModeListeners(){this.inputModeListenersAttached&&(document.removeEventListener("keydown",this.handleInputModeKeydown),document.removeEventListener("keyup",this.handleInputModeKeyup),window.removeEventListener("blur",this.handleInputModeBlur),this.inputModeListenersAttached=false);}activateInputMode(){this.isInputModeActive=true,this.svg&&this.svg.classed("input-mode",true),this.disableNodeDragging(),this.disableZoom(),this.updateEdgeEndpointMarkers(),this.dispatchEvent(new CustomEvent("input-mode-activated",{detail:{active:true}}));}deactivateInputMode(){this.isInputModeActive=false,this.svg&&this.svg.classed("input-mode",false),this.cleanupEdgeCreation(),this.enableNodeDragging(),this.enableZoom(),this.updateEdgeEndpointMarkers(),this.dispatchEvent(new CustomEvent("input-mode-deactivated",{detail:{active:false}}));}disableNodeDragging(){this.svgNodes&&this.colaLayout&&this.svgNodes.on(".drag",null);}enableNodeDragging(){if(this.svgNodes&&this.colaLayout&&this.colaLayout.drag){let n=this.colaLayout.drag();this.setupNodeDragHandlers(n),this.svgNodes.call(n);}}disableZoom(){this.svg&&this.zoomBehavior&&(this.storedTransform=Co.zoomTransform(this.svg.node()),this.svg.on(".zoom",null));}enableZoom(){this.svg&&this.zoomBehavior&&(this.svg.call(this.zoomBehavior),this.storedTransform&&this.svg.call(this.zoomBehavior.transform,this.storedTransform));}zoomIn(){this.svg&&this.zoomBehavior&&this.svg.transition().duration(200).call(this.zoomBehavior.scaleBy,1.5);}zoomOut(){this.svg&&this.zoomBehavior&&this.svg.transition().duration(200).call(this.zoomBehavior.scaleBy,1/1.5);}updateZoomControlStates(){if(!this.svg||!this.zoomBehavior)return;let c=Co.zoomTransform(this.svg.node()).k,[s,e]=this.zoomBehavior.scaleExtent(),i=this.shadowRoot.querySelector("#zoom-in"),o=this.shadowRoot.querySelector("#zoom-out");i&&(i.disabled=c>=e),o&&(o.disabled=c<=s);}cleanupEdgeCreation(){this.edgeCreationState.temporaryEdge&&this.edgeCreationState.temporaryEdge.remove(),this.edgeCreationState={isCreating:false,sourceNode:null,temporaryEdge:null};}setupNodeDragHandlers(n){n.on("start.cnd",c=>{this.userHasManuallyZoomed=true;let s={x:c.x,y:c.y};this.dragStartPositions.set(c.id,s),this.dispatchEvent(new CustomEvent("node-drag-start",{detail:{id:c.id,position:s}}));}).on("end.cnd",c=>{let s=this.dragStartPositions.get(c.id);this.dragStartPositions.delete(c.id);let e={id:c.id,previous:s,current:{x:c.x,y:c.y}};this.dispatchEvent(new CustomEvent("node-drag-end",{detail:e}));});}startEdgeCreation(n){this.isInputModeActive&&(this.cleanupEdgeCreation(),this.edgeCreationState.isCreating=true,this.edgeCreationState.sourceNode=n,this.edgeCreationState.temporaryEdge=this.container.append("line").attr("class","temporary-edge").attr("x1",n.x).attr("y1",n.y).attr("x2",n.x).attr("y2",n.y).attr("stroke","#007bff").attr("stroke-width",2).attr("stroke-dasharray","5,5").attr("opacity",.7),this.svg.on("mousemove.edgecreation",()=>{if(this.edgeCreationState.isCreating&&this.edgeCreationState.temporaryEdge){let[c,s]=Co.mouse(this.container.node());this.edgeCreationState.temporaryEdge.attr("x2",c).attr("y2",s);}}));}async finishEdgeCreation(n){if(!this.isInputModeActive||!this.edgeCreationState.isCreating||!this.edgeCreationState.sourceNode)return;let c=this.edgeCreationState.sourceNode;if(c.id===n.id&&!await this.showConfirmDialog(`Are you sure you want to create a self-loop edge on "${c.label||c.id}"?`)){this.cleanupEdgeCreation();return}this.svg.on("mousemove.edgecreation",null),await this.showEdgeLabelInput(c,n);}async showEdgeLabelInput(n,c){let s=await this.showPromptDialog(`Enter label for edge from "${n.label||n.id}" to "${c.label||c.id}":`,"");s!==null&&await this.createNewEdge(n,c,s||""),this.cleanupEdgeCreation();}async createNewEdge(n,c,s){if(!this.currentLayout)return;let e=this.currentLayout.nodes.findIndex(E=>E.id===n.id),i=this.currentLayout.nodes.findIndex(E=>E.id===c.id);if(e===-1||i===-1){console.error("Could not find node indices for edge creation");return}let p={id:`edge_${n.id}_${c.id}_${Date.now()}`,source:e,target:i,label:s,relName:s,color:"#333",isUserCreated:true};this.currentLayout.links.push(p),await this.updateExternalStateForNewEdge(n,c,s),this.dispatchEvent(new CustomEvent("edge-created",{detail:{edge:p,sourceNode:n,targetNode:c}})),this.rerenderGraph();}async updateExternalStateForNewEdge(n,c,s){if(s.trim())try{let e={atoms:[n.id,c.id],types:[n.type||"untyped",c.type||"untyped"]};console.log(`Dispatching edge creation request: ${s}(${n.id}, ${c.id})`);let i=new CustomEvent("edge-creation-requested",{detail:{relationId:s,sourceNodeId:n.id,targetNodeId:c.id,tuple:e},bubbles:!0});this.dispatchEvent(i);}catch(e){console.error("Failed to update external state for new edge:",e);}}rerenderGraph(){!this.currentLayout||!this.colaLayout||(this.colaLayout.links(this.currentLayout.links),this.container.selectAll(".link-group").remove(),this.renderLinks(this.currentLayout.links,this.colaLayout),this.colaLayout.start());}async editEdgeLabel(n){if(!this.isInputModeActive)return;let c=n.label||n.relName||"",s=await this.showEdgeEditDialog("Edit edge label:",c);if(s==="DELETE"){await this.deleteEdge(n);return}if(s!==null&&s!==c){let e=s,i=this.getNodeFromEdge(n,"source"),o=this.getNodeFromEdge(n,"target");await this.updateExternalStateForEdgeModification(i,o,c,e),n.label=e,n.relName=e,this.dispatchEvent(new CustomEvent("edge-modified",{detail:{edge:n,oldLabel:c,newLabel:e}})),this.rerenderGraph();}}getNodeFromEdge(n,c){if(!this.currentLayout)return null;let s=typeof n[c]=="number"?n[c]:n[c].index;return this.currentLayout.nodes[s]||null}async updateExternalStateForEdgeModification(n,c,s,e){if(!(!n||!c))try{let i={atoms:[n.id,c.id],types:[n.type||"untyped",c.type||"untyped"]};console.log(`Dispatching edge modification request: ${s} -> ${e}`);let o=new CustomEvent("edge-modification-requested",{detail:{oldRelationId:s,newRelationId:e,sourceNodeId:n.id,targetNodeId:c.id,tuple:i},bubbles:!0});this.dispatchEvent(o);}catch(i){console.error("Failed to update external state for edge modification:",i);}}async renderLayout(n,c){if(!NT(n))throw new Error("Invalid instance layout provided. Expected an InstanceLayout instance.");let s,e=false;if(c?.policy&&c.prevInstance&&c.currInstance){let p=c.priorPositions??this.getLayoutState();if(p&&p.positions.length>0){let E=this.getViewportBoundsInLayoutSpace(p.transform),b=c.policy.apply({priorState:p,prevInstance:c.prevInstance,currInstance:c.currInstance,spec:{constraints:{orientation:{relative:[],cyclic:[]},alignment:[],grouping:{groups:[],subgroups:[]}},directives:{sizes:[],hiddenAtoms:[],icons:[],projections:[],edgeStyles:[]}},viewportBounds:E});s=b.effectivePriorState,e=b.useReducedIterations;}}let i=s&&s.positions.length>0,o=i?{priorPositions:s}:void 0;if(i||(this.isInitialRender=true,this.userHasManuallyZoomed=false),this.svg&&this.zoomBehavior&&Co)try{if(i){let p=Co.zoomIdentity.translate(s.transform.x,s.transform.y).scale(s.transform.k);this.svg.call(this.zoomBehavior.transform,p);}else {let p=Co.zoomIdentity;this.svg.call(this.zoomBehavior.transform,p);}}catch(p){console.warn("Failed to set zoom transform:",p);}try{if(!Co)throw new Error("D3 library not available. Please ensure D3 v4 is loaded from CDN.");if(!Md){if(!window.cola)throw new Error("WebCola library not available. Please ensure vendor/cola.js is loaded.");Md=window.cola;}if((!this.container||!this.svg)&&this.initializeD3(),!this.container)throw new Error("Failed to initialize D3 container. SVG elements may not be available.");this.showLoading(),this.updateLoadingProgress("Translating layout...");let E=this.shadowRoot.querySelector("#svg-container").getBoundingClientRect(),b=E.width||800,y=E.height||600,a=await new exports.WebColaTranslator().translate(n,b,y,o);this.updateLoadingProgress(`Computing layout for ${a.nodes.length} nodes...`);let l=a.nodes.length,u=qi.INITIAL_UNCONSTRAINED_ITERATIONS,h=qi.INITIAL_USER_CONSTRAINT_ITERATIONS,d=qi.INITIAL_ALL_CONSTRAINTS_ITERATIONS;i&&e&&(u=0,h=Math.min(10,h),d=Math.min(20,d)),l>100?(u=Math.max(i?0:5,Math.floor(u*.5)),h=Math.max(25,Math.floor(h*.5)),d=Math.max(100,Math.floor(d*.5))):l>50&&(u=Math.max(i?0:8,Math.floor(u*.8)),h=Math.max(40,Math.floor(h*.8)),d=Math.max(150,Math.floor(d*.75)));let{scaledConstraints:g,linkLength:_,groupCompactness:S}=this.getScaledDetails(a.constraints,zF,a.nodes,a.groups,a.links);this.updateLoadingProgress("Applying constraints and initializing...");let v=i?.1:.001,T=Md.d3adaptor(Co).linkDistance(_).convergenceThreshold(v).avoidOverlaps(!0).handleDisconnected(!0).nodes(a.nodes).links(a.links).constraints(g).groups(a.groups).groupCompactness(S).size([a.FIG_WIDTH,a.FIG_HEIGHT]);this.currentLayout=a,this.colaLayout=T,this.container.selectAll("*").remove(),this.renderGroups(a.groups,T),this.renderLinks(a.links,T),this.renderNodes(a.nodes,T);let q=0,G=u+h+d;T.on("tick",()=>{if(q++,q%20===0){let j=Math.min(95,Math.round(q/G*100));this.updateLoadingProgress(`Computing layout... ${j}%`);}this.layoutFormat==="default"||!this.layoutFormat||this.layoutFormat===null?this.updatePositions():this.layoutFormat==="grid"?this.gridUpdatePositions():console.warn(`Unknown layout format: ${this.layoutFormat}. Skipping position updates.`);}).on("end",()=>{this.updateLoadingProgress("Finalizing..."),this.layoutFormat==="default"||!this.layoutFormat?this.routeEdges():this.layoutFormat==="grid"?this.gridify(10,25,10):console.warn(`Unknown layout format: ${this.layoutFormat}. Skipping edge routing.`),this.isUnsatCore&&this.showErrorIcon(),this.dispatchRelationsAvailableEvent(),this.dispatchEvent(new CustomEvent("layout-complete",{detail:{nodePositions:this.getNodePositions()}})),this.updateRoutingModeDropdown(),this.hideLoading();});try{T.start(u,h,d,qi.GRID_SNAP_ITERATIONS);}catch(j){console.warn("WebCola layout start encountered an error, trying alternative approach:",j);try{T.start();}catch(B){throw console.error("Both WebCola start methods failed:",B),new Error(`WebCola layout failed to start: ${B.message}`)}}}catch(p){console.error("Error rendering layout:",p),this.showError(`Layout rendering failed: ${p.message}`);}}clear(){if(this.colaLayout)try{this.colaLayout.stop?.();}catch{}this.container&&this.container.selectAll("*").remove(),this.currentLayout=null,this.colaLayout=null,this.svgNodes=null,this.svgLinks=null,this.svgGroups=null,this.edgeRoutingCache.edgesBetweenNodes.clear(),this.edgeRoutingCache.alignmentEdges.clear(),this.dragStartPositions.clear();}getNodePositions(){return this.currentLayout?.nodes?this.currentLayout.nodes.map(n=>({id:n.id,x:n.x,y:n.y})):[]}getCurrentTransform(){if(this.svg&&this.svg.node())try{let n=Co.zoomTransform(this.svg.node());return {k:n.k,x:n.x,y:n.y}}catch{return {k:1,x:0,y:0}}return {k:1,x:0,y:0}}getLayoutState(){return {positions:this.getNodePositions(),transform:this.getCurrentTransform()}}addToolbarControl(n){let c=this.shadowRoot?.querySelector("#graph-toolbar");c&&c.appendChild(n);}getToolbar(){return this.shadowRoot?.querySelector("#graph-toolbar")||null}renderGroups(n,c){if(!this.currentLayout.nodes||this.currentLayout.nodes.length===0){console.warn("Cannot render groups: nodes not available");return}this.svgGroups=this.setupGroups(n,this.currentLayout.nodes,c);}setupLinks(n,c){let s=this.container.selectAll(".link-group").data(n).enter().append("g").attr("class","link-group");return this.setupLinkPaths(s),this.setupLinkLabels(s),this.setupEdgeEndpointMarkers(s),s}setupLinkPaths(n){n.append("path").attr("class",c=>this.isAlignmentEdge(c)?"alignmentLink":this.isInferredEdge(c)?"inferredLink":"link").attr("data-link-id",c=>c.id||"").attr("stroke",c=>this.isAlignmentEdge(c)?"none":c.color).attr("fill","none").attr("opacity",c=>this.isAlignmentEdge(c)?0:null).style("stroke-width",c=>this.isAlignmentEdge(c)?"0":c.weight!=null?`${c.weight}px`:null).attr("stroke-dasharray",c=>this.isAlignmentEdge(c)?null:this.getEdgeDasharray(c.style)).attr("marker-end",c=>this.isAlignmentEdge(c)?"none":"url(#end-arrow)").attr("marker-start",c=>this.isAlignmentEdge(c)||!c.bidirectional?"none":"url(#start-arrow)").on("click.inputmode",c=>{this.isInputModeActive&&!this.isAlignmentEdge(c)&&(Co.event.stopPropagation(),this.editEdgeLabel(c).catch(s=>{console.error("Error editing edge label:",s);}));}).style("cursor",()=>this.isInputModeActive?"pointer":"default");}getEdgeDasharray(n){if(!n)return null;switch(n.toLowerCase()){case "dotted":return "1,4";case "dashed":return "6,4";case "solid":return null;default:return null}}setupLinkLabels(n){n.filter(c=>!this.isAlignmentEdge(c)&&(this.isInferredEdge(c)||c.showLabel!==false)).append("text").attr("class","linklabel").attr("text-anchor","middle").attr("dominant-baseline","middle").attr("font-family","system-ui").attr("pointer-events","none").text(c=>c.label||c.relName||"");}setupEdgeEndpointMarkers(n){n.filter(c=>!this.isAlignmentEdge(c)).append("circle").attr("class","edge-endpoint-marker target-marker").attr("r",8).attr("fill","#007bff").attr("stroke","white").attr("stroke-width",2).attr("opacity",0).attr("cursor","move").style("pointer-events","none").call(Co.drag().on("start",c=>this.startEdgeEndpointDrag(c,"target")).on("drag",c=>this.dragEdgeEndpoint(c,"target")).on("end",c=>this.endEdgeEndpointDrag(c,"target"))),n.filter(c=>!this.isAlignmentEdge(c)).append("circle").attr("class","edge-endpoint-marker source-marker").attr("r",8).attr("fill","#28a745").attr("stroke","white").attr("stroke-width",2).attr("opacity",0).attr("cursor","move").style("pointer-events","none").call(Co.drag().on("start",c=>this.startEdgeEndpointDrag(c,"source")).on("drag",c=>this.dragEdgeEndpoint(c,"source")).on("end",c=>this.endEdgeEndpointDrag(c,"source")));}startEdgeEndpointDrag(n,c){Co.event.sourceEvent.stopPropagation(),this.edgeDragState.isDragging=true,this.edgeDragState.edge=n,this.edgeDragState.endpoint=c,console.log(`\u{1F535} Started dragging ${c} endpoint of edge:`,n.id);}dragEdgeEndpoint(n,c){if(!this.edgeDragState.isDragging)return;let[s,e]=Co.mouse(this.container.node()),i=c==="target"?".target-marker":".source-marker";this.container.selectAll(".link-group").filter(o=>o.id===n.id).select(i).attr("cx",s).attr("cy",e);}async endEdgeEndpointDrag(n,c){if(!this.edgeDragState.isDragging)return;let[s,e]=Co.mouse(this.container.node()),i=this.findNodeAtPosition(s,e);i?(console.log(`\u{1F517} Reconnecting ${c} to node:`,i.id),await this.reconnectEdge(n,c,i)):(console.log("\u{1F5D1}\uFE0F No node found - deleting edge:",n.id),await this.deleteEdge(n)),this.edgeDragState={isDragging:false,edge:null,endpoint:null,dragMarker:null},this.rerenderGraph();}findNodeAtPosition(n,c){if(!this.currentLayout?.nodes)return null;for(let s of this.currentLayout.nodes){let e=(s.visualWidth??s.width??0)/2,i=(s.visualHeight??s.height??0)/2;if(n>=s.x-e&&n<=s.x+e&&c>=s.y-i&&c<=s.y+i)return s}return null}async reconnectEdge(n,c,s){let e=this.getNodeFromEdge(n,"source"),i=this.getNodeFromEdge(n,"target");if(!e||!i){console.error("Could not find source or target node");return}let o,p;if(c==="source"?(o=s,p=i):(o=e,p=s),o.id===e.id&&p.id===i.id){console.log("\u23ED\uFE0F Edge already connected to this node, no change needed");return}let E=n.label||n.relName||"";if(!E.trim()){console.warn("Edge has no relation name, cannot reconnect");return}let b={atoms:[e.id,i.id],types:[e.type||"untyped",i.type||"untyped"]},y={atoms:[o.id,p.id],types:[o.type||"untyped",p.type||"untyped"]};console.log(`\u{1F504} Reconnecting edge from ${e.id}->${i.id} to ${o.id}->${p.id}`);let m=new CustomEvent("edge-reconnection-requested",{detail:{relationId:E,oldTuple:b,newTuple:y,oldSourceNodeId:e.id,oldTargetNodeId:i.id,newSourceNodeId:o.id,newTargetNodeId:p.id},bubbles:true});this.dispatchEvent(m);let a=this.currentLayout.nodes.findIndex(u=>u.id===o.id),l=this.currentLayout.nodes.findIndex(u=>u.id===p.id);a!==-1&&l!==-1&&(n.source=a,n.target=l);}async deleteEdge(n){let c=this.getNodeFromEdge(n,"source"),s=this.getNodeFromEdge(n,"target");if(!c||!s){console.error("Could not find source or target node for edge deletion");return}let e=n.label||n.relName||"";if(!e.trim()){console.warn("Edge has no relation name, cannot delete from data instance"),this.removeEdgeFromLayout(n);return}let i={atoms:[c.id,s.id],types:[c.type||"untyped",s.type||"untyped"]};console.log(`\u{1F5D1}\uFE0F Deleting edge: ${e}(${c.id}, ${s.id})`);let o=new CustomEvent("edge-modification-requested",{detail:{oldRelationId:e,newRelationId:"",sourceNodeId:c.id,targetNodeId:s.id,tuple:i},bubbles:true});this.dispatchEvent(o),this.removeEdgeFromLayout(n);}removeEdgeFromLayout(n){if(!this.currentLayout?.links)return;let c=this.currentLayout.links.findIndex(s=>s.id===n.id);c!==-1&&(this.currentLayout.links.splice(c,1),console.log(`\u2705 Edge removed from layout: ${n.id}`));}setupGroups(n,c,s){let e=this.setupGroupRectangles(n,c,s);return this.svgGroupLabels=this.setupGroupLabels(n,s),e}setupGroupRectangles(n,c,s){return this.container.selectAll(".group").data(n).enter().append("rect").attr("class",i=>this.isDisconnectedGroup(i)?"disconnectedNode":this.isErrorGroup(i)?"error-group":"group").attr("rx",qi.GROUP_BORDER_RADIUS).attr("ry",qi.GROUP_BORDER_RADIUS).style("fill",i=>this.isDisconnectedGroup(i)?"transparent":c[i.keyNode]?.color||"#cccccc").attr("fill-opacity",qi.GROUP_FILL_OPACITY).attr("stroke",i=>this.isDisconnectedGroup(i)?"none":c[i.keyNode]?.color||"#999999").attr("stroke-width",1).call(s.drag)}setupGroupLabels(n,c){return this.container.selectAll(".groupLabel").data(n).enter().append("text").attr("class","groupLabel").attr("text-anchor","middle").attr("dominant-baseline","hanging").attr("font-family","system-ui").attr("font-size","12px").attr("font-weight","bold").attr("fill","#333").attr("pointer-events","none").text(s=>s.showLabel||false?(s.padding&&(s.padding=Math.max(s.padding,qi.GROUP_LABEL_PADDING)),s.name||""):"").call(c.drag)}renderLinks(n,c){this.edgeRoutingCache.alignmentEdges.clear(),this.svgLinkGroups=this.setupLinks(n,c);}setupNodes(n,c){let s=c.drag();this.setupNodeDragHandlers(s);let e=this.container.selectAll(".node").data(n).enter().append("g").attr("class",i=>{let o=this.isErrorNode(i)?"error-node":"node";return this.isErrorNode(i)&&this.isSmallNode(i)?o+" small-error-node":o}).call(s).on("mousedown.inputmode",i=>{this.isInputModeActive&&(Co.event.stopPropagation(),this.startEdgeCreation(i));}).on("mouseup.inputmode",i=>{this.isInputModeActive&&this.edgeCreationState.isCreating&&(Co.event.stopPropagation(),this.finishEdgeCreation(i).catch(o=>{console.error("Error finishing edge creation:",o);}));}).on("mouseover",function(i){Co.select(this).append("title").attr("class","node-tooltip").text(`ID: ${i.id}`);}).on("mouseout",function(){Co.select(this).select("title.node-tooltip").remove();});return this.setupNodeRectangles(e),this.setupNodeIcons(e),this.setupMostSpecificTypeLabels(e),this.setupNodeLabels(e),e}setupNodeRectangles(n){n.append("rect").attr("width",c=>c.visualWidth??c.width).attr("height",c=>c.visualHeight??c.height).attr("x",c=>-(c.visualWidth??c.width)/2).attr("y",c=>-(c.visualHeight??c.height)/2).attr("stroke",c=>c.color||"black").attr("rx",qi.NODE_BORDER_RADIUS).attr("ry",qi.NODE_BORDER_RADIUS).attr("stroke-width",qi.NODE_STROKE_WIDTH).attr("fill",c=>{let s=this.isHiddenNode(c),e=!!c.icon,i=c.showLabels;return s||e&&!i?"transparent":"white"});}setupNodeIcons(n){n.filter(c=>c.icon).append("image").attr("xlink:href",c=>c.icon).attr("width",c=>{let s=c.visualWidth??c.width;return c.showLabels?s*qi.SMALL_IMG_SCALE_FACTOR:s}).attr("height",c=>{let s=c.visualHeight??c.height;return c.showLabels?s*qi.SMALL_IMG_SCALE_FACTOR:s}).attr("x",c=>{let s=c.visualWidth??c.width;return c.showLabels?c.x+s-s*qi.SMALL_IMG_SCALE_FACTOR:c.x-s/2}).attr("y",c=>{let s=c.visualHeight??c.height;return c.y-s/2}).append("title").text(c=>c.label||c.name||c.id||"Node").on("error",function(c,s){Co.select(this).attr("xlink:href","img/default.png"),console.error(`Failed to load icon for node ${s.id}: ${s.icon}`);});}setupMostSpecificTypeLabels(n){n.append("text").attr("class","mostSpecificTypeLabel").style("fill",c=>c.color||"black").text(c=>c.mostSpecificType||"");}getTextMeasurementContext(){return this.textMeasurementCanvas||(this.textMeasurementCanvas=document.createElement("canvas")),this.textMeasurementCanvas.getContext("2d")}measureTextWidth(n,c,s="system-ui"){let e=this.getTextMeasurementContext();return e.font=`${c}px ${s}`,e.measureText(n).width}calculateOptimalFontSize(n,c,s,e="system-ui"){let i=qi.DEFAULT_FONT_SIZE;for(;i>qi.MIN_FONT_SIZE;){let o=this.measureTextWidth(n,i,e),p=i*qi.LINE_HEIGHT_RATIO;if(o<=c&&p<=s)break;i-=.5;}for(;i<qi.MAX_FONT_SIZE;){let o=i+.5,p=this.measureTextWidth(n,o,e),E=o*qi.LINE_HEIGHT_RATIO;if(p>c||E>s)break;i=o;}return Math.max(qi.MIN_FONT_SIZE,Math.min(i,qi.MAX_FONT_SIZE))}wrapText(n,c,s,e="system-ui"){let i=n.split(/\s+/),o=[],p="";for(let E of i){let b=p?`${p} ${E}`:E;this.measureTextWidth(b,s,e)<=c?p=b:p?(o.push(p),p=E):o.push(E);}return p&&o.push(p),o}setupNodeLabelsWithDynamicSizing(n){n.append("text").attr("class","label").attr("text-anchor","middle").attr("dominant-baseline","middle").attr("font-family","system-ui").attr("fill","black").each((c,s,e)=>{if(this.isHiddenNode(c)||!c.showLabels)return;let o=Co.select(e[s]),p=c.width||100,E=c.height||60,b=p-qi.TEXT_PADDING*2,y=E-qi.TEXT_PADDING*2,m=c.label||c.name||c.id||"Node",a=c.attributes||{},l=Object.entries(a).sort(([z],[tt])=>z.localeCompare(tt)),u=c.labels||{},h=Object.entries(u),d=h.length>0,g=l.length>0,_=d||g,S=_?y*.5:y,v=this.calculateOptimalFontSize(m,b,S,"system-ui");o.attr("font-size",`${v}px`);let T=v*qi.LINE_HEIGHT_RATIO,q=h.length+l.length,G=_?-q*T*.5:0;c._labelVerticalOffset=G,c._labelLineHeight=T,o.append("tspan").attr("x",0).attr("dy",`${G}px`).attr("class","main-label-tspan").style("font-weight","bold").style("font-size",`${v}px`).text(m);let j="";for(let[z,tt]of h){let pt=Array.isArray(tt)?tt.join(", "):String(tt);pt.length>j.length&&(j=pt);}for(let[z,tt]of l){let pt=`${z}: ${tt}`;pt.length>j.length&&(j=pt);}let B=v*.65,J=y-T,w=q>0?this.calculateOptimalFontSize(j||"SampleText",b,J/q,"system-ui"):v*.8,$=Math.max(w,B);if(d){let z="black";for(let[tt,pt]of h){let Z=Array.isArray(pt)?pt.join(", "):String(pt);o.append("tspan").attr("x",0).attr("dy",`${$*qi.LINE_HEIGHT_RATIO}px`).style("font-size",`${$}px`).style("fill",z).style("font-style","italic").text(Z);}}if(g)for(let z=0;z<l.length;z++){let[tt,pt]=l[z],Z=`${tt}: ${pt}`;o.append("tspan").attr("x",0).attr("dy",`${$*qi.LINE_HEIGHT_RATIO}px`).style("font-size",`${$}px`).text(Z);}});}setupNodeLabels(n){this.setupNodeLabelsWithDynamicSizing(n);}renderNodes(n,c){this.svgNodes=this.setupNodes(n,c);}resolveGroupEdgeEndpoints(n){if(!n.groupId)return {source:n.source,target:n.target};let c=this.currentLayout?.groups||[],s=c.find(p=>p.id===n.groupId);if(this._groupEdgeDebugLogged||(this._groupEdgeDebugLogged=new Set),this._groupEdgeDebugLogged.has(n.id)||(this._groupEdgeDebugLogged.add(n.id),console.log("[groupEdge DEBUG]",{edgeId:n.id,groupId:n.groupId,keyNodeId:n.keyNodeId,sourceId:n.source?.id,targetId:n.target?.id,sourceIsInt:typeof n.source=="number",targetIsInt:typeof n.target=="number",groupFound:!!s,groupId_on_group:s?.id,availableGroupIds:c.map(p=>p.id)})),!s)return {source:n.source,target:n.target};let e=n.source,i=n.target;n.source?.id===n.keyNodeId?i=s:n.target?.id===n.keyNodeId?e=s:console.warn("[groupEdge] keyNodeId matched neither side",{keyNodeId:n.keyNodeId,sourceId:n.source?.id,targetId:n.target?.id});let o=e===s?n.source:i===s?n.target:null;if(o&&!s.bounds&&o.x!=null&&Md?.Rectangle){let p=(o.visualWidth??o.width??50)/2,E=(o.visualHeight??o.height??30)/2;s.bounds=new Md.Rectangle(o.x-p,o.x+p,o.y-E,o.y+E);}return {source:e,target:i}}updatePositions(){this.svgGroups.attr("x",n=>n.bounds.x).attr("y",n=>n.bounds.y).attr("width",n=>n.bounds.width()).attr("height",n=>n.bounds.height()).lower(),this.svgNodes.select("rect").each(n=>{n.bounds&&(n.innerBounds=n.bounds.inflate(-1));}).attr("x",n=>n.x-(n.visualWidth??n.width)/2).attr("y",n=>n.y-(n.visualHeight??n.height)/2).attr("width",n=>n.visualWidth??n.width).attr("height",n=>n.visualHeight??n.height),this.svgNodes.select("image").attr("x",n=>{let c=n.visualWidth??n.width;return n.showLabels?n.x+c/2-c*qi.SMALL_IMG_SCALE_FACTOR:n.x-c/2}).attr("y",n=>{let c=n.visualHeight??n.height;return n.showLabels,n.y-c/2}),this.svgNodes.select(".mostSpecificTypeLabel").attr("x",n=>n.x-(n.visualWidth??n.width??0)/2+5).attr("y",n=>n.y-(n.visualHeight??n.height??0)/2+10).raise(),this.svgNodes.select(".label").attr("x",n=>n.x).attr("y",n=>n.y).each((n,c,s)=>{let i=n._labelVerticalOffset||0,o=n._labelLineHeight||12;Co.select(s[c]).selectAll("tspan").attr("x",n.x).attr("dy",(p,E)=>E===0?`${i}px`:`${o}px`);}).raise(),this.svgLinkGroups.select("path").attr("d",n=>{let{source:c,target:s}=this.resolveGroupEdgeEndpoints(n),e=this.getStableEdgePath(c,s);return this.lineFunction(e)}).attr("marker-end",n=>this.isAlignmentEdge(n)?"none":"url(#end-arrow)").attr("marker-start",n=>this.isAlignmentEdge(n)||!n.bidirectional?"none":"url(#start-arrow)").raise(),this.svgLinkGroups.select(".linklabel").attr("x",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);return c?this.calculateNewPosition(c,"x"):(n.source.x+n.target.x)/2}).attr("y",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);return c?this.calculateNewPosition(c,"y"):(n.source.y+n.target.y)/2}).style("font-size",()=>{let n=this.getCurrentZoomScale(),c=12,s=n<1?c/Math.sqrt(n):c;return `${Math.min(s,16)}px`}).raise(),this.updateEdgeEndpointMarkers(),this.svgGroupLabels.attr("x",n=>n.bounds?n.bounds.x+n.bounds.width()/2:0).attr("y",n=>n.bounds?n.bounds.y+5:0).attr("text-anchor","middle").lower(),this.svgLinkGroups.selectAll("marker").raise(),this.svgLinkGroups.selectAll(".linklabel").raise(),this.svgGroups.selectAll(".error-group").raise(),this.svgNodes.selectAll(".error-node").raise();}updateEdgeEndpointMarkers(){this.svgLinkGroups&&(this.svgLinkGroups.select(".target-marker").attr("cx",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);if(c){let s=c.getTotalLength();return c.getPointAtLength(s).x}return n.target.x||0}).attr("cy",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);if(c){let s=c.getTotalLength();return c.getPointAtLength(s).y}return n.target.y||0}).attr("opacity",this.isInputModeActive?.8:0).style("pointer-events",this.isInputModeActive?"all":"none").raise(),this.svgLinkGroups.select(".source-marker").attr("cx",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);return c?c.getPointAtLength(0).x:n.source.x||0}).attr("cy",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);return c?c.getPointAtLength(0).y:n.source.y||0}).attr("opacity",this.isInputModeActive?.8:0).style("pointer-events",this.isInputModeActive?"all":"none").raise());}gridUpdatePositions(){this.ensureNodeBounds(true);let n=this.container.selectAll(".node"),c=this.container.selectAll(".mostSpecificTypeLabel"),s=this.container.selectAll(".label"),e=this.container.selectAll(".group"),i=this.container.selectAll(".groupLabel");n.select("rect").each(function(p){p.innerBounds=p.bounds.inflate(-1);}).attr("x",function(p){return p.bounds.x}).attr("y",function(p){return p.bounds.y}).attr("width",function(p){return p.bounds.width()}).attr("height",function(p){return p.bounds.height()}),n.select("image").attr("x",function(p){let E=p.visualWidth??p.width;return p.showLabels?p.x+E/2-E*qi.SMALL_IMG_SCALE_FACTOR:p.bounds.x}).attr("y",function(p){let E=p.visualHeight??p.height;return p.showLabels?p.y-E/2:p.bounds.y}),c.attr("x",function(p){return p.bounds.x+5}).attr("y",function(p){return p.bounds.y+10}).raise(),s.attr("x",p=>p.x).attr("y",p=>p.y).each(function(p){var E=0;Co.select(this).selectAll("tspan").attr("x",p.x).attr("dy",function(){return E+=1,E===1?"0em":"1em"});}).raise(),e.attr("x",function(p){return p.bounds.x}).attr("y",function(p){return p.bounds.y}).attr("width",function(p){return p.bounds.width()}).attr("height",function(p){return p.bounds.height()}).lower(),i.attr("x",function(p){return p.bounds.x+p.bounds.width()/2}).attr("y",function(p){return p.bounds.y+12}).attr("text-anchor","middle").raise();let o=this.container.selectAll(".link-group");o.select("path").attr("d",p=>{if(p.source?.id===p.target?.id){let h=this.createSelfLoopRoute(p);return this.lineFunction(h)}let{source:E,target:b}=this.resolveGroupEdgeEndpoints(p),y=h=>h.bounds?typeof h.bounds.cx=="function"?{x:h.bounds.cx(),y:h.bounds.cy()}:{x:(h.bounds.x+h.bounds.X)/2,y:(h.bounds.y+h.bounds.Y)/2}:{x:h.x??0,y:h.y??0},m=y(E),a=y(b),l=a.x-m.x,u=a.y-m.y;if(Math.abs(l)>Math.abs(u)){let h=m.x+l/2;return this.gridLineFunction([{x:m.x,y:m.y},{x:h,y:m.y},{x:h,y:a.y},{x:a.x,y:a.y}])}else {let h=m.y+u/2;return this.gridLineFunction([{x:m.x,y:m.y},{x:m.x,y:h},{x:a.x,y:h},{x:a.x,y:a.y}])}}),o.select("text.linklabel").attr("x",p=>{let E=this.shadowRoot?.querySelector(`path[data-link-id="${p.id}"]`);if(E){let m=E.getTotalLength();return E.getPointAtLength(m/2).x}let b=p.source?.x??p.source?.bounds?.cx()??0,y=p.target?.x??p.target?.bounds?.cx()??0;return (b+y)/2}).attr("y",p=>{let E=this.shadowRoot?.querySelector(`path[data-link-id="${p.id}"]`);if(E){let m=E.getTotalLength();return E.getPointAtLength(m/2).y}let b=p.source?.y??p.source?.bounds?.cy()??0,y=p.target?.y??p.target?.bounds?.cy()??0;return (b+y)/2}).raise();}routeEdges(){try{this.ensureNodeBounds(!0),typeof this.colaLayout?.prepareEdgeRouting=="function"&&this.colaLayout.prepareEdgeRouting(qi.VIEWBOX_PADDING/qi.EDGE_ROUTE_MARGIN_DIVISOR),this.buildEdgeRoutingCaches(),this.routeLinkPaths(),this.updateLinkLabelsAfterRouting(),this.fitViewportToContent();}catch(n){console.error("Error in edge routing:",n),this.showError(`Edge routing failed: ${n.message}`);}}ensureNodeBounds(n=false){if(!(!this.currentLayout?.nodes||!Md?.Rectangle))for(let c of this.currentLayout.nodes){if(!n&&c.bounds&&typeof c.bounds.rayIntersection=="function"){let b=c.bounds.cx(),y=c.bounds.cy(),m=1;if(Math.abs(b-(c.x||0))<m&&Math.abs(y-(c.y||0))<m)continue}let s=(c.visualWidth??c.width??50)/2,e=(c.visualHeight??c.height??30)/2,i=(c.x||0)-s,o=(c.x||0)+s,p=(c.y||0)-e,E=(c.y||0)+e;c.bounds=new Md.Rectangle(i,o,p,E),c.innerBounds=c.bounds.inflate(-1);}}buildEdgeRoutingCaches(){this.edgeRoutingCache.edgesBetweenNodes.clear(),this.edgeRoutingCache.alignmentEdges.clear(),this.currentLayout?.links&&(this.currentLayout.links.forEach(n=>{n.id?.startsWith("_alignment_")&&this.edgeRoutingCache.alignmentEdges.add(n.id);}),this.currentLayout.links.forEach(n=>{if(this.isAlignmentEdge(n))return;let c=n.source.id,s=n.target.id,e=this.getNodePairKey(c,s);this.edgeRoutingCache.edgesBetweenNodes.has(e)||this.edgeRoutingCache.edgesBetweenNodes.set(e,[]),this.edgeRoutingCache.edgesBetweenNodes.get(e).push(n);}));}getNodePairKey(n,c){return n<c?`${n}:${c}`:`${c}:${n}`}route(n=[],c=[],s,e){n.forEach(o=>{let p=o.bounds||o.innerBounds||this.createFallbackBounds(o);o.routerNode={name:o.name,bounds:p};}),c.forEach(o=>{o.bounds||console.warn("Grid routing group missing bounds; routing may be degraded.",o),o.routerNode={bounds:o.bounds?.inflate(-e)??o.bounds,children:(typeof o.groups<"u"?o.groups.map(p=>n.length+p.id):[]).concat(typeof o.leaves<"u"?o.leaves.map(p=>p.index):[])};});let i=n.concat(c).map((o,p)=>o.routerNode?(o.routerNode.id=p,o.routerNode):null).filter(Boolean);return new Md.GridRouter(i,{getChildren:o=>o.children,getBounds:o=>o.bounds},s-e)}gridify(n,c,s){if(this.isGridifyingInProgress){console.warn("[gridify] Already in progress, skipping re-entrant call");return}this.isGridifyingInProgress=true;try{this.gridifyInternal(n,c,s);}catch(e){console.log("Error routing edges in GridRouter"),console.error(e);try{this.fallbackGridRouting(this.currentLayout?.links??[]);}catch(o){console.error("Fallback grid routing also failed:",o);}let i=document.getElementById("runtime_messages");if(i){let o=document.createElement("div");o.className="alert alert-danger alert-dismissible fade show",o.setAttribute("role","alert"),o.innerHTML="Runtime (WebCola) error when gridifying edges. You may have to click and drag these nodes slightly to un-stick layout.",i.querySelectorAll(".alert").forEach(E=>{E.innerHTML===o.innerHTML&&E.remove();}),i.appendChild(o);}}finally{this.isGridifyingInProgress=false;}}gridifyInternal(n,c,s){let e=this.currentLayout?.nodes??[],i=this.currentLayout?.groups??[],o=this.currentLayout?.links??[];if(e.length===0){console.warn("No nodes available for GridRouter; skipping gridify.");return}if(o.length===0){console.warn("No edges to route in GridRouter");return}console.log("[gridify] Node positions BEFORE ensureNodeBounds:"),e.slice(0,3).forEach(u=>{console.log(` ${u.id}: x=${u.x?.toFixed(2)}, y=${u.y?.toFixed(2)}, bounds.cx=${u.bounds?.cx?.()?.toFixed(2)}, bounds.x=${u.bounds?.x?.toFixed(2)}`);}),this.ensureNodeBounds(true);let p=e.filter(u=>!Number.isFinite(u.x)||!Number.isFinite(u.y));if(p.length>0){console.warn("[gridify] Found nodes with invalid positions, falling back to default routing:",p.map(u=>({id:u.id,x:u.x,y:u.y}))),this.fallbackGridRouting(o);return}console.log("[gridify] Node positions AFTER ensureNodeBounds:"),e.slice(0,3).forEach(u=>{console.log(` ${u.id}: x=${u.x?.toFixed(2)}, y=${u.y?.toFixed(2)}, bounds.cx=${u.bounds?.cx?.()?.toFixed(2)}, bounds.x=${u.bounds?.x?.toFixed(2)}`);});let E=this.route(e,i,c,s),b=[],y=o.filter(u=>{let h=u?.source?.routerNode&&u?.target?.routerNode,d=u?.source?.id===u?.target?.id;return h&&!d}),m=o.filter(u=>u?.source?.id===u?.target?.id);console.log("[gridify] Total edges:",o.length,"Routable:",y.length,"Self-loops:",m.length),y.length+m.length!==o.length&&o.filter(h=>(!h?.source?.routerNode||!h?.target?.routerNode)&&h?.source?.id!==h?.target?.id).forEach(h=>{console.warn("[gridify] Unroutable edge:",h.id,"source routerNode:",!!h?.source?.routerNode,"target routerNode:",!!h?.target?.routerNode,"source:",h?.source?.id,"x:",h?.source?.x,"y:",h?.source?.y,"target:",h?.target?.id,"x:",h?.target?.x,"y:",h?.target?.y);}),b=E.routeEdges(y,n,function(u){return u.source.routerNode.id},function(u){return u.target.routerNode.id});let a=new Map;y.forEach((u,h)=>{let d=b[h];u?.id&&d&&a.set(u.id,this.adjustGridRouteForEdge(u,d));}),console.log("[gridify] Routes generated:",a.size,"out of",y.length),this.container.selectAll(".link-group").data(o,u=>u.id??u).select("path").attr("d",u=>{if(u.source?.id===u.target?.id){let T=this.createSelfLoopRoute(u);return this.lineFunction(T)}let h=a.get(u.id);if(!h){let T=u.source?.x??u.source?.bounds?.cx()??0,q=u.source?.y??u.source?.bounds?.cy()??0,G=u.target?.x??u.target?.bounds?.cx()??0,j=u.target?.y??u.target?.bounds?.cy()??0;console.log("[gridify] Fallback path for edge:",u.id,"from",u.source?.id,"(",T,",",q,")","to",u.target?.id,"(",G,",",j,")");let B=G-T,J=j-q;if(Math.abs(B)>Math.abs(J)){let w=T+B/2;return this.gridLineFunction([{x:T,y:q},{x:w,y:q},{x:w,y:j},{x:G,y:j}])}else {let w=q+J/2;return this.gridLineFunction([{x:T,y:q},{x:T,y:w},{x:G,y:w},{x:G,y:j}])}}let S=Md.GridRouter.getRoutePath(h,5,3,7);return this.adjustGridRouteForArrowPositioning(u,S.routepath,h)||S.routepath}),this.gridUpdateLinkLabels(o,a),this.fitViewportToContent(),this.dispatchEvent(new Event("relationsAvailable"));}fallbackGridRouting(n){this.container.selectAll(".link-group").data(n,s=>s.id??s).select("path").attr("d",s=>{if(s.source?.id===s.target?.id){let y=this.createSelfLoopRoute(s);return this.lineFunction(y)}let e=s.source?.x??s.source?.bounds?.cx()??0,i=s.source?.y??s.source?.bounds?.cy()??0,o=s.target?.x??s.target?.bounds?.cx()??0,p=s.target?.y??s.target?.bounds?.cy()??0,E=o-e,b=p-i;if(Math.abs(E)>Math.abs(b)){let y=e+E/2;return this.gridLineFunction([{x:e,y:i},{x:y,y:i},{x:y,y:p},{x:o,y:p}])}else {let y=i+b/2;return this.gridLineFunction([{x:e,y:i},{x:e,y},{x:o,y},{x:o,y:p}])}}),this.fitViewportToContent();}gridUpdateLinkLabels(n,c){this.container.selectAll(".link-group").filter(e=>!this.isAlignmentEdge(e)).select("text.linklabel").attr("x",e=>{let i=this.shadowRoot?.querySelector(`path[data-link-id="${e.id}"]`);if(i)try{let p=i.getTotalLength();return i.getPointAtLength(p/2).x}catch{}return this.getGridRouteMidpoint(e,c)?.x??e.source?.x??e.source?.bounds?.cx()??0}).attr("y",e=>{let i=this.shadowRoot?.querySelector(`path[data-link-id="${e.id}"]`);if(i)try{let p=i.getTotalLength();return i.getPointAtLength(p/2).y}catch{}return this.getGridRouteMidpoint(e,c)?.y??e.source?.y??e.source?.bounds?.cy()??0}).attr("text-anchor","middle").attr("dominant-baseline","middle");}getGridRouteMidpoint(n,c){let s=c.get(n.id);if(!s){let y=n.source?.x??n.source?.bounds?.cx()??0,m=n.source?.y??n.source?.bounds?.cy()??0,a=n.target?.x??n.target?.bounds?.cx()??0,l=n.target?.y??n.target?.bounds?.cy()??0;return {x:(y+a)/2,y:(m+l)/2}}let e=[];if(s.forEach(y=>{e.length===0&&y.length>0&&e.push(y[0]),y.length>1&&e.push(y[1]);}),e.length<2)return null;let i=0,o=[];for(let y=0;y<e.length-1;y++){let m=e[y+1].x-e[y].x,a=e[y+1].y-e[y].y,l=Math.sqrt(m*m+a*a);o.push(l),i+=l;}let p=i/2,E=0;for(let y=0;y<o.length;y++){let m=o[y];if(E+m>=p){let a=p-E,l=m>0?a/m:0;return {x:e[y].x+l*(e[y+1].x-e[y].x),y:e[y].y+l*(e[y+1].y-e[y].y)}}E+=m;}let b=Math.floor(e.length/2);return e[b]}adjustGridRouteForEdge(n,c){if(!n?.id?.startsWith("_g_"))return c;let s=this.gridRouteToPoints(c);if(s.length<2)return c;let e=this.routeGroupEdge(n,s);return this.pointsToGridRoute(e)}adjustGridRouteForArrowPositioning(n,c,s){if(!c||!n.source||!n.target)return null;try{let e=this.gridRouteToPoints(s);if(e.length<2)return null;let i=n.source,o=n.target,p=i.bounds||{x:i.x-(i.width||0)/2,y:i.y-(i.height||0)/2,width:()=>i.width||0,height:()=>i.height||0},E=o.bounds||{x:o.x-(o.width||0)/2,y:o.y-(o.height||0)/2,width:()=>o.width||0,height:()=>o.height||0},y=this.getTouchDirection(p,E,5);if(y!=="none"){let{sourcePoint:h,targetPoint:d,middlePoints:g}=this.computePerpendicularRoute(p,E,y),_=[h,...g,d];return this.gridLineFunction(_)}let m=e.length>1?e[1]:e[0],a=this.getRectangleIntersection(p.x+p.width()/2,p.y+p.height()/2,m.x,m.y,p);a&&(e[0]=a);let l=e.length>1?e[e.length-2]:e[e.length-1],u=this.getRectangleIntersection(E.x+E.width()/2,E.y+E.height()/2,l.x,l.y,E);return u&&(e[e.length-1]=u),this.gridLineFunction(e)}catch(e){return console.warn("Error adjusting grid route for arrow positioning:",e),null}}getTouchDirection(n,c,s){let e=n.x,i=n.x+n.width(),o=n.y,p=n.y+n.height(),E=c.x,b=c.x+c.width(),y=c.y,m=c.y+c.height(),a=Math.max(0,Math.max(E-i,e-b)),l=Math.max(0,Math.max(y-p,o-m)),u=!(p<y||m<o),h=!(i<E||b<e);return a<=s&&u?"horizontal":l<=s&&h?"vertical":"none"}computePerpendicularRoute(n,c,s){let e=n.width(),i=n.height(),o=c.width(),p=c.height(),E=n.x+e/2,b=n.y+i/2,y=c.x+o/2,m=c.y+p/2,a=15;if(s==="horizontal")if(b<=m){let u=Math.min(n.y,c.y)-a;return {sourcePoint:{x:E,y:n.y},targetPoint:{x:y,y:c.y},middlePoints:[{x:E,y:u},{x:y,y:u}]}}else {let u=Math.max(n.y+i,c.y+p)+a;return {sourcePoint:{x:E,y:n.y+i},targetPoint:{x:y,y:c.y+p},middlePoints:[{x:E,y:u},{x:y,y:u}]}}else if(E<=y){let u=Math.min(n.x,c.x)-a;return {sourcePoint:{x:n.x,y:b},targetPoint:{x:c.x,y:m},middlePoints:[{x:u,y:b},{x:u,y:m}]}}else {let u=Math.max(n.x+e,c.x+o)+a;return {sourcePoint:{x:n.x+e,y:b},targetPoint:{x:c.x+o,y:m},middlePoints:[{x:u,y:b},{x:u,y:m}]}}}areBoundsNear(n,c,s){let e=n.x,i=n.x+n.width(),o=n.y,p=n.y+n.height(),E=c.x,b=c.x+c.width(),y=c.y,m=c.y+c.height(),a=Math.max(0,Math.max(E-i,e-b)),l=Math.max(0,Math.max(y-p,o-m));return Math.sqrt(a*a+l*l)<=s}chooseBoundaryPoint(n,c,s,e){let i=Math.max(1,s.width()),o=Math.max(1,s.height()),p=[{x:s.x,y:s.y+o/2},{x:s.x+i,y:s.y+o/2},{x:s.x+i/2,y:s.y},{x:s.x+i/2,y:s.y+o}],E=p[0],b=-1/0;for(let y of p){let m=y.x-e.x,a=y.y-e.y,l=Math.sqrt(m*m+a*a);l>b&&(b=l,E=y);}return E}normalizeNodeBounds(n){let c=n.visualWidth??n.width??50,s=n.visualHeight??n.height??30,e=n.bounds||{x:n.x-c/2,y:n.y-s/2,width:()=>c,height:()=>s};return {x:typeof e.x=="number"||e.X!==void 0?e.x:n.x-c/2,y:typeof e.y=="number"?e.y:n.y-s/2,width:()=>typeof e.width=="function"?e.width():e.X!==void 0?e.X-e.x:c,height:()=>typeof e.height=="function"?e.height():e.Y!==void 0?e.Y-e.y:s}}lineIntersectsRect(n,c,s){let e=s.x,i=s.x+s.width(),o=s.y,p=s.y+s.height(),E=Math.min(n.x,c.x),b=Math.max(n.x,c.x),y=Math.min(n.y,c.y),m=Math.max(n.y,c.y);if(b<e||E>i||m<o||y>p)return false;let a=n.x>=e&&n.x<=i&&n.y>=o&&n.y<=p,l=c.x>=e&&c.x<=i&&c.y>=o&&c.y<=p;if(a||l)return true;let u=c.x-n.x,h=c.y-n.y,d=(_,S,v)=>{if(h===0)return false;let T=(_-n.y)/h;if(T<0||T>1)return false;let q=n.x+T*u;return q>=S&&q<=v},g=(_,S,v)=>{if(u===0)return false;let T=(_-n.x)/u;if(T<0||T>1)return false;let q=n.y+T*h;return q>=S&&q<=v};return d(o,e,i)||d(p,e,i)||g(e,o,p)||g(i,o,p)}findBlockingNodes(n,c,s,e){if(!this.currentLayout?.nodes)return [];let i=this.normalizeNodeBounds(n),o=this.normalizeNodeBounds(c),p={x:i.x+i.width()/2,y:i.y+i.height()/2},E={x:o.x+o.width()/2,y:o.y+o.height()/2},b=[];for(let y of this.currentLayout.nodes){if(y.id===s||y.id===e)continue;let m=this.normalizeNodeBounds(y);if(this.lineIntersectsRect(p,E,m)){let a={x:m.x+m.width()/2,y:m.y+m.height()/2},l=Math.sqrt(Math.pow(a.x-p.x,2)+Math.pow(a.y-p.y,2));b.push({node:y,bounds:m,distance:l});}}return b.sort((y,m)=>y.distance-m.distance),b.map(y=>({node:y.node,bounds:y.bounds}))}computeRouteAroundBlockingNodes(n,c,s){let i=Math.min(n.x,c.x),o=Math.max(n.x+n.width(),c.x+c.width()),p=Math.min(n.y,c.y),E=Math.max(n.y+n.height(),c.y+c.height());for(let{bounds:h}of s)i=Math.min(i,h.x),o=Math.max(o,h.x+h.width()),p=Math.min(p,h.y),E=Math.max(E,h.y+h.height());let b=n.x+n.width()/2,y=n.y+n.height()/2,m=c.x+c.width()/2,a=c.y+c.height()/2,l=Math.abs(m-b);if(Math.abs(a-y)>l)if(b<=m){let d=i-15;return {sourcePoint:{x:n.x,y},targetPoint:{x:c.x,y:a},middlePoints:[{x:d,y},{x:d,y:a}]}}else {let d=o+15;return {sourcePoint:{x:n.x+n.width(),y},targetPoint:{x:c.x+c.width(),y:a},middlePoints:[{x:d,y},{x:d,y:a}]}}else if(y<=a){let d=p-15;return {sourcePoint:{x:b,y:n.y},targetPoint:{x:m,y:c.y},middlePoints:[{x:b,y:d},{x:m,y:d}]}}else {let d=E+15;return {sourcePoint:{x:b,y:n.y+n.height()},targetPoint:{x:m,y:c.y+c.height()},middlePoints:[{x:b,y:d},{x:m,y:d}]}}}getNearTouchPerpendicularRoute(n){if(!n.source||!n.target||n.source.id===n.target.id)return null;let c=n.source,s=n.target,e=this.normalizeNodeBounds(c),i=this.normalizeNodeBounds(s),p=this.getTouchDirection(e,i,5);if(p!=="none"){let{sourcePoint:b,targetPoint:y,middlePoints:m}=this.computePerpendicularRoute(e,i,p);return [b,...m,y]}let E=this.findBlockingNodes(c,s,c.id,s.id);if(E.length>0){let{sourcePoint:b,targetPoint:y,middlePoints:m}=this.computeRouteAroundBlockingNodes(e,i,E);return [b,...m,y]}return null}gridRouteToPoints(n){let c=[];return n.forEach((s,e)=>{e===0&&c.push({x:s[0].x,y:s[0].y}),c.push({x:s[1].x,y:s[1].y});}),c}pointsToGridRoute(n){let c=[];for(let s=0;s<n.length-1;s+=1)c.push([n[s],n[s+1]]);return c}createFallbackBounds(n){if(!Md?.Rectangle)return null;let c=((n.visualWidth??n.width)||50)/2,s=((n.visualHeight??n.height)||30)/2,e=(n.x||0)-c,i=(n.x||0)+c,o=(n.y||0)-s,p=(n.y||0)+s;return new Md.Rectangle(e,i,o,p)}getRectangleIntersection(n,c,s,e,i){let o=i.x,p=i.x+i.width(),E=i.y,b=i.y+i.height(),y=s-n,m=e-c;if(y===0&&m===0)return {x:n,y:c};let a=0,l=1;if(y!==0){let h=(o-n)/y,d=(p-n)/y;a=Math.max(a,Math.min(h,d)),l=Math.min(l,Math.max(h,d));}if(m!==0){let h=(E-c)/m,d=(b-c)/m;a=Math.max(a,Math.min(h,d)),l=Math.min(l,Math.max(h,d));}if(a>l)return null;let u=a>0?a:l;return {x:n+u*y,y:c+u*m}}routeLinkPaths(){this.container.selectAll(".link-group path").attr("d",n=>{try{return this.routeSingleEdge(n)}catch(c){return console.error(`Error routing edge ${n.id} from ${n.source.id} to ${n.target.id}:`,c),this.showRuntimeAlert(n.source.id,n.target.id),this.lineFunction([{x:n.source.x||0,y:n.source.y||0},{x:n.target.x||0,y:n.target.y||0}])}});}routeSingleEdge(n){if(this.isAlignmentEdge(n))return this.lineFunction([{x:n.source.x||0,y:n.source.y||0},{x:n.target.x||0,y:n.target.y||0}]);let c=[{x:n.source.x||0,y:n.source.y||0},{x:n.target.x||0,y:n.target.y||0}],s;if(typeof this.colaLayout?.routeEdge=="function")try{if(s=this.colaLayout.routeEdge(n),!s||!Array.isArray(s)||s.length<2||!s[0]||!s[1]||s[0].x===void 0||s[0].y===void 0)throw new Error(`WebCola failed to route edge ${n.id} from ${n.source.id} to ${n.target.id}`)}catch(i){return console.log("Error routing edge",n.id,`from ${n.source.id} to ${n.target.id}`),console.error(i),this.lineFunction(c)}else s=c;if(n.source.id===n.target.id)s=this.createSelfLoopRoute(n);else {if(n.id?.startsWith("_g_"))return s=this.routeGroupEdge(n,s),this.lineFunction(s);s=this.handleMultipleEdgeRouting(n,s);}let e=this.getNearTouchPerpendicularRoute(n);return e?this.lineFunction(e):this.lineFunction(s)}createSelfLoopRoute(n){let c=n.source,s=c.bounds;if(!s)return [{x:c.x,y:c.y},{x:c.x+20,y:c.y-20},{x:c.x,y:c.y}];let e=s.X-s.x,i=s.Y-s.y,o={x:s.x+e/2,y:s.y},p={x:s.X,y:s.y+i/2},b=1+(n.selfLoopIndex||0)*qi.SELF_LOOP_CURVATURE_SCALE,y={x:s.X+e/2*b,y:s.y-i/2*b};return [o,y,p]}routeGroupEdge(n,c){let s=n.groupId?(this.currentLayout?.groups||[]).find(e=>e.id===n.groupId):null;if(!s)console.warn("[routeGroupEdge] Group not found for edge:",n.id,"\u2014 groupId:",n.groupId);else {if(!s.bounds&&Md?.Rectangle){let e=n.source?.id===n.keyNodeId?c[c.length-1]:c[0];e&&(s.bounds=new Md.Rectangle(e.x-50,e.x+50,e.y-30,e.y+30));}if(s.bounds)if(n.source?.id===n.keyNodeId)c[c.length-1]=this.closestPointOnRect(s.bounds,c[0]);else if(n.target?.id===n.keyNodeId){let e=s.bounds.inflate?.(-1)??s.bounds;c[0]=this.closestPointOnRect(e,c[c.length-1]);}else console.warn("[routeGroupEdge] keyNodeId matched neither side",{keyNodeId:n.keyNodeId,sourceId:n.source?.id,targetId:n.target?.id});}return c.length>2&&c.splice(1,c.length-2),c}handleMultipleEdgeRouting(n,c){let s=this.getAllEdgesBetweenNodes(n.source.id,n.target.id);if(s.length<=1)return c;if(c.length===2){let b={x:(c[0].x+c[1].x)/2,y:(c[0].y+c[1].y)/2};c.splice(1,0,b);}let e=c[1].x-c[0].x,i=c[1].y-c[0].y,o=Math.atan2(i,e),p=this.getRouteLength(c),E=s.findIndex(b=>b.id===n.id);if(E!==-1){c=this.applyEdgeOffsetWithIndex(n,c,s,o,E,p);let b=this.calculateCurvatureWithIndex(s,n.id,E),y=this.clampCurvature(b);c=this.applyCurvatureToRoute(c,y,o,p);}return c}getAllEdgesBetweenNodes(n,c){if(!this.currentLayout?.links)return [];let s=this.getNodePairKey(n,c);return this.edgeRoutingCache.edgesBetweenNodes.has(s)?this.edgeRoutingCache.edgesBetweenNodes.get(s):this.currentLayout.links.filter(e=>!this.isAlignmentEdge(e)&&(e.source.id===n&&e.target.id===c||e.source.id===c&&e.target.id===n))}calculateCurvature(n,c,s,e){if(e.startsWith("_alignment_"))return 0;let i=n.length,o=n.findIndex(p=>p.id===e);return i<=1?0:(o%2===0?1:-1)*(Math.floor(o/2)+1)*qi.CURVATURE_BASE_MULTIPLIER*i}calculateCurvatureWithIndex(n,c,s){let e=n.length;return e<=1?0:(s%2===0?1:-1)*(Math.floor(s/2)+1)*qi.CURVATURE_BASE_MULTIPLIER*e}applyEdgeOffset(n,c,s,e){let i=s.findIndex(p=>p.id===n.id),o=this.getRouteLength(c);return this.applyEdgeOffsetWithIndex(n,c,s,e,i,o)}applyEdgeOffsetWithIndex(n,c,s,e,i,o){let p=(i%2===0?1:-1)*(Math.floor(i/2)+1)*qi.MIN_EDGE_DISTANCE,E=this.clampOffset(p,o),b=this.getDominantDirection(e);return b==="right"||b==="left"?(c[0].y+=E,c[c.length-1].y+=E):(b==="up"||b==="down")&&(c[0].x+=E,c[c.length-1].x+=E),n.source.innerBounds&&(c[0]=this.adjustPointToRectanglePerimeter(c[0],n.source.innerBounds)),n.target.innerBounds&&(c[c.length-1]=this.adjustPointToRectanglePerimeter(c[c.length-1],n.target.innerBounds)),c}clampOffset(n,c){let s=Math.max(qi.MIN_EDGE_DISTANCE,c*qi.MAX_EDGE_OFFSET_RATIO);return Math.max(-s,Math.min(s,n))}getRouteLength(n){return n.length<2?0:n.slice(1).reduce((c,s,e)=>{let i=n[e],o=s.x-i.x,p=s.y-i.y;return c+Math.sqrt(o*o+p*p)},0)}clampCurvature(n){return Math.max(-qi.MAX_EDGE_CURVATURE_RATIO,Math.min(qi.MAX_EDGE_CURVATURE_RATIO,n))}applyCurvatureToRoute(n,c,s,e){return c===0||n.forEach((i,o)=>{if(o>0&&o<n.length-1){let p=c*Math.abs(Math.sin(s))*e,E=c*Math.abs(Math.cos(s))*e;i.x+=p,i.y+=E;}}),n}getDominantDirection(n){return n=(n+Math.PI)%(2*Math.PI)-Math.PI,n>=-Math.PI/4&&n<=Math.PI/4?"right":n>Math.PI/4&&n<3*Math.PI/4?"up":n>=3*Math.PI/4||n<=-3*Math.PI/4?"left":n>-3*Math.PI/4&&n<-Math.PI/4?"down":null}closestPointOnRect(n,c){if(!n)return c;let{x:s,y:e,X:i,Y:o}=n,p=Math.max(s,Math.min(c.x,i)),E=Math.max(e,Math.min(c.y,o));return {x:p,y:E}}getStableEdgeAnchor(n,c){if(!n)return c;let s,e,i,o;if(typeof n.cx=="function")s=n.cx(),e=n.cy(),i=n.width()/2,o=n.height()/2;else if(n.x!==void 0&&n.X!==void 0)s=(n.x+n.X)/2,e=(n.y+n.Y)/2,i=(n.X-n.x)/2,o=(n.Y-n.y)/2;else return c;let p=c.x-s,E=c.y-e,b=Math.abs(p)/i,y=Math.abs(E)/o;return b>y?p>0?{x:s+i,y:e}:{x:s-i,y:e}:E>0?{x:s,y:e+o}:{x:s,y:e-o}}getStableEdgePath(n,c){let s;c.bounds&&typeof c.bounds.cx=="function"?s={x:c.bounds.cx(),y:c.bounds.cy()}:c.bounds?s={x:(c.bounds.x+c.bounds.X)/2,y:(c.bounds.y+c.bounds.Y)/2}:s={x:c.x||0,y:c.y||0};let e;n.bounds&&typeof n.bounds.cx=="function"?e={x:n.bounds.cx(),y:n.bounds.cy()}:n.bounds?e={x:(n.bounds.x+n.bounds.X)/2,y:(n.bounds.y+n.bounds.Y)/2}:e={x:n.x||0,y:n.y||0};let i=n.bounds||n.innerBounds?this.getStableEdgeAnchor(n.bounds||n.innerBounds,s):e,o=c.bounds||c.innerBounds?this.getStableEdgeAnchor(c.bounds||c.innerBounds,e):s;return [i,o]}adjustPointToRectanglePerimeter(n,c){return c?this.closestPointOnRect(c,n):n}updateLinkLabelsAfterRouting(){this.container.selectAll(".link-group .linklabel").attr("x",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);if(!c)return 0;let s=c.getTotalLength();return c.getPointAtLength(s/2).x}).attr("y",n=>{let c=this.shadowRoot?.querySelector(`path[data-link-id="${n.id}"]`);if(!c)return 0;let s=c.getTotalLength();return c.getPointAtLength(s/2).y}).attr("text-anchor","middle").each((n,c,s)=>{this.handleLabelOverlap(s[c]);}).raise();}handleLabelOverlap(n){let c=[];this.container.selectAll(".linklabel").each(function(){this!==n&&_ft(this,n)&&c.push(this);}),c.length>0&&this.minimizeOverlap(n,c);}minimizeOverlap(n,c){}fitViewportToContent(n=false){let c=this.svg?.node();if(!c||!this.zoomBehavior||this.userHasManuallyZoomed&&!this.isInitialRender&&!n)return;let s=this.calculateContentBounds();if(!s)return;let e=c.clientWidth||c.parentElement?.clientWidth||800,i=c.clientHeight||c.parentElement?.clientHeight||600,o=qi.VIEWBOX_PADDING*4,p=(e-o*2)/s.width,E=(i-o*2)/s.height,b=Math.min(p,E,1),[y,m]=this.zoomBehavior.scaleExtent(),a=Math.max(y,Math.min(m,b)),l=s.x+s.width/2,u=s.y+s.height/2,h=e/2-l*a,d=i/2-u*a,g=Co.zoomIdentity.translate(h,d).scale(a);this.isInitialRender?(this.svg.call(this.zoomBehavior.transform,g),this.isInitialRender=false):this.svg.transition().duration(300).ease(Co.easeCubicOut).call(this.zoomBehavior.transform,g),this.updateZoomControlStates();}resetViewToFitContent(){this.userHasManuallyZoomed=false,this.fitViewportToContent(true);}calculateContentBounds(){try{if(!this.currentLayout||!this.container)return null;let n=1/0,c=1/0,s=-1/0,e=-1/0,i=this.currentLayout.nodes;if(i&&i.length>0){i.forEach((a,l)=>{if(typeof a.x=="number"&&typeof a.y=="number"){let u=a.width||0,h=a.height||0,d=a.x,g=a.x+u,_=a.y,S=a.y+h,v=c,T=e;n=Math.min(n,d),s=Math.max(s,g),c=Math.min(c,_),e=Math.max(e,S);}});let m=i.reduce((a,l)=>{if(typeof l.x=="number"&&typeof l.y=="number"){let u=l.y+(l.height||0),h=a?a.y+(a.height||0):-1/0;return u>h?l:a}return a},null);}let o=this.container.selectAll(".link-group");o.empty()||o.each(function(){try{let m=this.getBBox();m.width>0&&m.height>0&&(n=Math.min(n,m.x),s=Math.max(s,m.x+m.width),c=Math.min(c,m.y),e=Math.max(e,m.y+m.height));}catch{}});let p=this.container.selectAll(".node, .error-node");p.empty()||p.each(function(){try{let m=this.getBBox();m.width>0&&m.height>0&&(n=Math.min(n,m.x),s=Math.max(s,m.x+m.width),c=Math.min(c,m.y),e=Math.max(e,m.y+m.height));}catch{}});let E=this.container.selectAll("text");if(!E.empty()){let m=0;E.each(function(){try{let a=this.getBBox();if(a.width>0&&a.height>0){m++;let l=5,u=a.y-l,h=a.y+a.height+l,d=e;n=Math.min(n,a.x-l),s=Math.max(s,a.x+a.width+l),c=Math.min(c,u),e=Math.max(e,h);}}catch{}});}let b=this.container.selectAll(".group");return b.empty()||b.each(function(){try{let m=this.getBBox();m.width>0&&m.height>0&&(n=Math.min(n,m.x),s=Math.max(s,m.x+m.width),c=Math.min(c,m.y),e=Math.max(e,m.y+m.height));}catch{}}),n===1/0||c===1/0||s===-1/0||e===-1/0?(console.warn("Could not calculate content bounds - no valid elements found"),null):{x:n,y:c,width:s-n,height:e-c}}catch(n){return console.error("Error calculating content bounds:",n),null}}dispatchRelationsAvailableEvent(){let n=this.getAllRelations(),c=new CustomEvent("relations-available",{detail:{relations:n,count:n.length,timestamp:Date.now(),graphId:this.id||"unknown"},bubbles:true,cancelable:true});this.dispatchEvent(c);}getAllRelations(){if(!this.currentLayout?.links)return [];let n=new Set(this.currentLayout.links.filter(c=>!this.isAlignmentEdge(c)).map(c=>c.relName).filter(Boolean));return Array.from(n)}highlightRelation(n){return this.currentLayout?.links?(this.svgLinkGroups.filter(c=>c.relName===n&&!this.isAlignmentEdge(c)).selectAll("path").classed("highlighted",true),true):false}clearHighlightRelation(n){return this.currentLayout?.links?(this.svgLinkGroups.filter(c=>c.relName===n&&!this.isAlignmentEdge(c)).selectAll("path").classed("highlighted",false),true):false}highlightNodes(n){if(!this.currentLayout?.nodes||!this.svgNodes||!n||n.length===0)return false;let c=new Set(n),s=false;return this.svgNodes.each((e,i,o)=>{c.has(e.id)&&(Co.select(o[i]).classed("highlighted",true),s=true);}),s}highlightNodePairs(n,c={}){if(!this.currentLayout?.nodes||!this.svgNodes||!n||n.length===0)return false;let{showBadges:s=false}=c,e=new Set,i=new Set;n.forEach((p,E)=>{if(!Array.isArray(p)){console.warn(`highlightNodePairs: Pair at index ${E} is not an array, skipping`);return}if(p.length!==2){console.warn(`highlightNodePairs: Pair at index ${E} has ${p.length} elements (expected 2), skipping`);return}let[b,y]=p;b&&e.add(b),y&&i.add(y);});let o=false;return this.svgNodes.each((p,E,b)=>{let y=Co.select(b[E]);e.has(p.id)&&(y.classed("highlighted-first",true),o=true,s&&this.addHighlightBadge(y,p,"1","#007aff")),i.has(p.id)&&(y.classed("highlighted-second",true),o=true,s&&(e.has(p.id)?this.addHighlightBadge(y,p,"1,2","#9B59B6"):this.addHighlightBadge(y,p,"2","#ff3b30")));}),o}clearNodeHighlights(){return this.svgNodes?(this.svgNodes.classed("highlighted",false).classed("highlighted-first",false).classed("highlighted-second",false).selectAll(".highlight-badge, .highlight-badge-bg").remove(),true):false}addHighlightBadge(n,c,s,e){n.selectAll(".highlight-badge, .highlight-badge-bg").remove();let i=16,o=4,p=(c.width||0)/2-i/2-o,E=-(c.height||0)/2+i/2+o;n.append("circle").attr("class","highlight-badge-bg").attr("cx",p).attr("cy",E).attr("r",i/2).attr("fill",e),n.append("text").attr("class","highlight-badge").attr("x",p).attr("y",E).attr("dy","0.35em").text(s);}showRuntimeAlert(n,c){console.warn(`Runtime (WebCola) error when laying out an edge from ${n} to ${c}. You may have to click and drag these nodes slightly to un-stick layout.`);}getCSS(){return `
|
|
851
851
|
:host {
|
|
852
852
|
display: block;
|
|
853
853
|
width: 100%;
|