<!DOCTYPE html>
<html>
<body>
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 550px"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gojs/release/go.js"></script>
<script src="https://cdn.jsdelivr.net/npm/create-gojs-kit/dist/extensions/GeometryReshapingTool.js"></script>
<script id="code">
myDiagram = new go.Diagram('myDiagramDiv', {
'undoManager.isEnabled': true // enable undo & redo
});
myDiagram.toolManager.mouseDownTools.insertAt(3, new GeometryReshapingTool({
isResegmenting: false,
// override this in order to update the "Segments" Adornment as well as the "GeometryReshaping" Adornment
updateAdornments: function(part) {
GeometryReshapingTool.prototype.updateAdornments.call(this, part);
// this "Segments" Adornment might be present and need to be updated even when the PART is not selected
updateHighlights(part);
}
}));
// returns { i, ax, ay, bx, by } describing segment closest to docpt, or else null
function findClosestSegment(shape, docpt) {
const geo = shape.geometry;
// assume Path Geometry
if (geo === null || geo.type !== go.GeometryType.Path) throw new Error("findClosestSegment got a Shape whose Geometry was not of type Path.");
// assume argument point is in document coordinates, transform into local coordinates for the Shape/Geometry
const pt = shape.getLocalPoint(docpt);
const px = pt.x;
const py = pt.y;
// find the segment that is closest to (PX,PY)
let bestdist = Infinity;
let bestidx = 0;
// now iterate over the PathSegments of the PathFigures
let i = 0;
let ax, ay, bx, by;
geo.figures.each(fig => {
// get the first point of the PathFigure
let sx = fig.startX;
let sy = fig.startY;
fig.segments.each(seg => {
let ex = seg.endX;
let ey = seg.endY;
// save some time by not calling Math.sqrt
let dist = 0.0;
if (seg.type === go.SegmentType.Line) {
dist = go.Point.distanceLineSegmentSquared(px, py, sx, sy, ex, ey);
} else if (seg.type === go.SegmentType.Move) {
sx = seg.endX;
sy = seg.endY;
return;
} else {
throw new Error("findClosestSegment got a PathSegment that is not of type Line.");
}
if (dist < bestdist) {
bestdist = dist;
bestidx = i;
ax = sx; ay = sy; bx = ex; by = ey;
}
sx = ex;
sy = ey;
i++;
if (seg.isClosed) {
ex = fig.startX;
ey = fig.startY;
dist = go.Point.distanceLineSegmentSquared(px, py, sx, sy, ex, ey);
if (dist < bestdist) {
bestdist = dist;
bestidx = i;
ax = sx; ay = sy; bx = ex; by = ey;
}
i++;
}
});
});
if (bestdist < 20 * 20) return { i: bestidx, ax, ay, bx, by };
return null;
}
function addHighlight(e, node) {
const shp = node.findObject("SHAPE");
if (!shp) return;
const info = findClosestSegment(shp, e.documentPoint);
if (!info) return;
const sw = 3; // strokeWidth for the highlight Shapes
let ad = node.findAdornment("Segments");
if (!ad) {
ad = new go.Adornment("Position")
.add(
new go.Placeholder({ position: new go.Point(sw/4, sw/4) }),
new go.Panel("Position")
);
ad.adornedObject = shp;
node.addAdornment("Segments", ad);
}
const shpPanel = ad.elt(1);
let highshp = null;
// find an existing Shape used to highlight this segment
const it = shpPanel.elements;
while (it.next()) {
const shape = it.value;
if (shape.segmentIndex === info.i) {
highshp = shape;
break;
}
}
if (!highshp) { // if not present, create it
// create the Shape showing the highlighted segment
// this could be of any stroke color or thickness
highshp = new go.Shape({
stroke: "red", strokeWidth: sw, fill: null,
isGeometryPositioned: true,
segmentIndex: info.i
});
shpPanel.add(highshp);
}
updateHighlights(node);
}
function updateHighlights(node) {
const shp = node.findObject("SHAPE");
if (!shp) return;
const geo = shp.geometry;
if (geo.type !== go.GeometryType.Path) return;
const ad = node.findAdornment("Segments");
if (!ad) return;
const shpPanel = ad.elt(1);
const fig = geo.figures.first(); //??? just do one PathFigure
shpPanel.elements.each(shape => {
let sx, sy, ex, ey;
const idx = shape.segmentIndex;
if (idx === 0) {
const firstseg = fig.segments.elt(0);
sx = fig.startX;
sy = fig.startY;
ex = firstseg.endX;
ey = firstseg.endY;
} else if (idx >= fig.segments.count) {
const lastseg = fig.segments.elt(fig.segments.count-1);
sx = lastseg.endX;
sy = lastseg.endY;
ex = fig.startX;
ey = fig.startY;
} else {
const prevseg = fig.segments.elt(idx-1);
const nextseg = fig.segments.elt(idx);
sx = prevseg.endX;
sy = prevseg.endY;
ex = nextseg.endX;
ey = nextseg.endY;
}
// update the geometry for the highlight segment
const geo = new go.Geometry(go.GeometryType.Line);
geo.startX = sx;
geo.startY = sy;
geo.endX = ex;
geo.endY = ey;
shape.geometry = geo;
});
}
myDiagram.nodeTemplate = new go.Node({
resizable: true,
resizeObjectName: 'SHAPE',
reshapable: true, // GeometryReshapingTool assumes nonexistent Part.reshapeObjectName would be "SHAPE"
rotatable: true,
rotationSpot: go.Spot.Center,
click: addHighlight,
contextMenu:
go.GraphObject.build("ContextMenu")
.add(
go.GraphObject.build("ContextMenuButton", {
click: (e, button) => {
const node = button.part.adornedPart;
node.removeAdornment("Segments");
}
})
.bindObject("visible", "adornedPart", node => node.findAdornment("Segments") !== null)
.add(new go.TextBlock("Discard Segments"))
)
})
.add(
new go.Shape({
name: 'SHAPE',
fill: 'lightgreen',
strokeWidth: 1.5
})
.bindTwoWay('geometryString', 'geo')
);
myDiagram.model = new go.GraphLinksModel([
{ geo: 'F M20 0 40 20 20 40 0 20z' },
{ geo: 'F M0 145 L75 8 100 20 120 40 131 87 160 70 180 50 195 0 L249 133z' }
]);
</script>
</body>
</html>