UNPKG

create-gojs-kit

Version:

A CLI for downloading GoJS samples, extensions, and docs

274 lines (250 loc) 9.76 kB
/* * Copyright 1998-2025 by Northwoods Software Corporation. All Rights Reserved. */ /* * This is an extension and not part of the main GoJS library. * The source code for this is at extensionsJSM/LinkLabelRouter.ts. * Note that the API for this class may change with any version, even point releases. * If you intend to use an extension in production, you should copy the code to your own source directory. * Extensions can be found in the GoJS kit under the extensions or extensionsJSM folders. * See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information. */ import * as go from 'gojs'; /** * A custom Router for reducing overlaps between label objects on links by moving them apart with a custom ForceDirectedLayout. * You can modify the properties of that Layout by setting {@link layoutProps} in the constructor. * * By default, this router considers a "link label" to be any GraphObject that is part of a {@link Link} which is not a path Shape * or an arrowhead. You can customize objects that the router operates on by overriding {@link LinkLabelRouter.isLabel}. * * This Router will override the {@link Spot.offsetX} and {@link Spot.offsetY} of the {@link GraphObject.alignmentFocus} value for all link labels. * * Typical setup: * ``` * myDiagram.routers.add(new LinkLabelRouter({ * layoutProps: { * defaultElectricalCharge: 100, * ... * } * })); * ``` * * If you want to experiment with this extension, try the <a href="../../samples/LinkLabelRouter.html">LinkLabelRouter</a> sample. * @category Router Extension */ export class LinkLabelRouter extends go.Router { /** @hidden */ private layout: LabelLayout; /** @hidden */ private _layoutProps: Partial<go.ForceDirectedLayout>; /** @hidden */ private _margin: go.Margin; constructor(init?: Partial<LinkLabelRouter>) { super(); this.name = 'LinkLabelRouter'; this.isRealtime = false; this._margin = new go.Margin(); if (init) Object.assign(this, init); if (init?.layoutProps) { this._layoutProps = init.layoutProps; this.layout = new LabelLayout(init.layoutProps); } else { this._layoutProps = {}; this.layout = new LabelLayout(); } this.layout.router = this; } /** * Properties of the underlying custom {@link ForceDirectedLayout} for this router. */ get layoutProps(): Partial<go.ForceDirectedLayout> { return this._layoutProps; } set layoutProps(value: Partial<go.ForceDirectedLayout>) { if (value !== this._layoutProps) { this._layoutProps = value; this.layout = new LabelLayout(this._layoutProps); this.layout.router = this; this.invalidateRouter(); } } /** * Margin that will be applied to each link label when checking for overlaps. * The default value is 0 on all sides. */ get margin(): go.MarginLike { return this._margin; } set margin(value: go.MarginLike) { const old = this._margin; if (typeof value === 'number') value = new go.Margin(value); else if (!(value instanceof go.Margin)) throw new Error('LinkLabelRouter.margin must be a Margin or a number, not ' + value); if (!old.equals(value)) { this._margin.set(value); this.invalidateRouter(); } } /** * Determines which GraphObjects in {@link Panel.elements} list of each link should be treated as labels. * By default this consists of all objects that are not a "main path" of the link, and are not fromArrows or toArrows. * * @param { go.GraphObject } obj * @returns */ isLabel(obj: go.GraphObject): boolean { if (!obj) return false; const link = obj.panel; if (link === null) return false; if (obj instanceof go.Shape && (obj.isPanelMain || link.findMainElement() === obj || obj.fromArrow !== 'None' || obj.toArrow !== 'None')) { return false; } else { return true; } } /** * Determine if the LinkLabelRouter should run on a given collection. * By default only run once on the whole Diagram, never on Groups * * @param { go.Diagram | go.Group } container * @returns */ override canRoute(container: go.Diagram | go.Group): boolean { if (container instanceof go.Group) return false; return super.canRoute(container); } /** * Attempt to move link label objects to avoid overlaps, if necessary. * * @param {go.Set<go.Link>} links * @param {*} container A Diagram or a Group * @returns */ override routeLinks(links: go.Set<go.Link>, container: go.Diagram | go.Group) { if (this.layout === null) return; if (container instanceof go.Group) return; this.layout.activeSet = links; if (container instanceof go.Diagram) this.layout.diagram = container; this.layout.doLayout(container.links); if (this.layout.network === null) return; for(const vertex of this.layout.network.vertexes) { if (!(vertex instanceof LabelVertex)) continue; if (vertex.isFixed) continue; const object = vertex.object; if (!object) continue; const x = isNaN(object.alignmentFocus.x) ? 0.5 : object.alignmentFocus.x; const y = isNaN(object.alignmentFocus.y) ? 0.5 : object.alignmentFocus.y; const dx = vertex.centerX - vertex.objectBounds!.centerX; const dy = vertex.centerY - vertex.objectBounds!.centerY; // moving alignmentFocus.offsetX/Y by some amount moves the node in the opposite direction, thus -dx and -dy object.alignmentFocus = new go.Spot(x, y, -dx, -dy); } } } /** @hidden @internal */ class LabelVertex extends go.ForceDirectedVertex { constructor(network: go.ForceDirectedNetwork) { super(network); } object: go.GraphObject | null = null; objectBounds: go.Rect | null = null; currentBounds: go.Rect | null = null; isDummy: boolean = false; } /** @hidden @internal */ class LabelLayout extends go.ForceDirectedLayout { constructor(init?: Partial<go.ForceDirectedLayout>) { super(); if (init) Object.assign(this, init); } /** @hidden */ router: LinkLabelRouter | null = null; /** @hidden */ activeSet: go.Set<go.Link> | null = null; /** * we should not ever do a prelayout on this virtual, "fake" force-directed network */ override needsPrelayout(): boolean { return false; } /** * Keep track of the current bounding box of the link label on each node when moving its associated LabelVertex. * * @param { LabelVertex } v */ override moveVertex(v: LabelVertex): number { const result = super.moveVertex(v); v.currentBounds!.offset( v.centerX - v.currentBounds!.centerX, v.centerY - v.currentBounds!.centerY ); return result; } /** * Only allow interaction between two nodes if their associated GraphObjects are currently intersecting. * * @param { LabelVertex } v1 * @param { LabelVertex } v2 */ override shouldInteract(v1: LabelVertex, v2: LabelVertex): boolean { if (v1.isDummy || v2.isDummy) return false; const b1 = v1.currentBounds!; const b2 = v2.currentBounds!; return b1.intersectsRect(b2); } override makeNetwork(coll: go.Diagram | go.Group | go.Iterable<go.Part>): go.ForceDirectedNetwork { const net = new go.ForceDirectedNetwork(this); let allparts: go.Iterable<go.Part>; if (coll instanceof go.Diagram) { allparts = coll.links; } else if (coll instanceof go.Group) { allparts = coll.memberParts; } else { allparts = coll; } for (const part of allparts) { if (!(part instanceof go.Link)) continue; part.ensureBounds(); for (const label of part.elements) { if (!this.router!.isLabel(label)) continue; const margin = this.router!.margin as go.Margin; const documentBounds = label.getDocumentBounds() .offset(label.alignmentFocus.offsetX, label.alignmentFocus.offsetY); // add margin to "real" document bounds documentBounds.addMargin(margin); if (this.activeSet?.has(part)) { // add vertex for label node const v1 = new LabelVertex(net); v1.centerX = documentBounds.centerX; v1.centerY = documentBounds.centerY; v1.width = documentBounds.width; v1.height = documentBounds.height; v1.object = label; v1.objectBounds = documentBounds.copy(); v1.currentBounds = documentBounds.copy(); net.addVertex(v1); // add vertex for fixed dummy node at the label's original position const v2 = new LabelVertex(net); v2.centerX = v1.centerX; v2.centerY = v1.centerY; v2.charge = 0; v2.isFixed = true; v2.isDummy = true; net.addVertex(v2); // add edge to incentivize the Label to stay near its original position const e = new go.ForceDirectedEdge(net); e.length = 0; e.fromVertex = v1; e.toVertex = v2; net.addEdge(e); } else { const v = new LabelVertex(net); v.centerX = documentBounds.centerX; v.centerY = documentBounds.centerY; v.width = documentBounds.width; v.height = documentBounds.height; v.object = label; v.objectBounds = documentBounds.copy(); v.currentBounds = documentBounds.copy(); v.isFixed = true; net.addVertex(v); } } } return net; } }