create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
274 lines (250 loc) • 9.76 kB
text/typescript
/*
* 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;
}
}