Hi,
I am struggling with getting the right group template. My app has a Scene graph data model with entities having relative poses to parent entities. Also these entities can have additional poses relative to the entity; example:
[
{ id: "A", pose: {x: 0, y: 0, yaw: 0}, additionalPoses: [] },
{ id: "A.0", parentId: "A", pose: {x: 0, y: 0, yaw: 0}, additionalPoses: [] },
{ id: "A.1", parentId: "A", pose: {x: 100, y: 0, yaw: 0}, additionalPoses: [] },
{ id: "B", pose: {x: 200, y: 0, yaw: 0}, additionalPoses: [] },
{ id: "B.1", parentId: "B", pose: {x: 100, y: 0, yaw: 0}, additionalPoses: [
{ name: "B-i1", pose: {x: 0, y: 100, yaw: 0} },
{ name: "B-i2", position: {x: 0, y: 200, yaw: 0} }
]},
{ id: "C", pose: {x: 200, y: 200, yaw: 1.57}, additionalPoses: [
{ name: "C-i1", pose: {x: 100, y: 100, yaw: 0} },
{ name: "C-i2", pose: {x: 0, y: 100, yaw: 0} }
]}
]
Now I’m mapping this to GoJS as follows:
- Every node is a group (since it can have children)
- We pre-calculate the absolute
location
for every node since GoJS does not support relative positions for nodes w.r.t. their parent group - We treat the additionalPoses as an
itemArray
with relativeposition
w.r.t. the entity that holds the poses
This is what I came up with:
<!DOCTYPE html>
<html>
<head>
<title>Positions</title>
<meta charset="UTF-8">
</head>
<body onload="init()">
<div id="diagram" style="border: solid 1px black; width:100%; height:600px"></div>
<script src="go.js"></script>
<script id="code">
function init() {
var $ = go.GraphObject.make;
// start group-rotating-tool.js
/*
* Copyright (C) 1998-2017 by Northwoods Software Corporation. All Rights Reserved.
*/
/**
* @constructor
* @class
*/
function GroupRotatingTool() {
go.RotatingTool.call(this);
// Internal state
// ._initialInfo holds references to all selected non-Link Parts and
// their initial relative points and angles
this._initialInfo = null;
this._rotatePoint = new go.Point();
}
go.Diagram.inherit(GroupRotatingTool, go.RotatingTool);
GroupRotatingTool.prototype.doActivate = function () {
go.RotatingTool.prototype.doActivate.call(this);
var group = this.adornedObject.part;
if (group instanceof go.Group) {
if (group.placeholder !== null) throw new Error("GroupRotatingTool can't handle Placeholder in Group");
// assume rotation about the location point
this._rotatePoint = group.location;
// remember initial points for each Part
var infos = new go.Map(go.Part, MultiplePartInfo);
this.walkTree(group, infos);
this._initialInfo = infos;
}
}
GroupRotatingTool.prototype.walkTree = function (part, infos) {
if (part === null || part instanceof go.Link) return;
// saves original relative position and original angle
var loc = part.locationObject.getDocumentPoint(go.Spot.Center);
var locoffset = loc.copy().subtract(part.location);
var relloc = loc.subtract(this._rotatePoint);
infos.add(part, new MultiplePartInfo(relloc, locoffset, part.rotateObject.angle));
// recurse into Groups
if (part instanceof go.Group) {
var it = part.memberParts.iterator;
while (it.next()) this.walkTree(it.value, infos);
}
};
function MultiplePartInfo(relativeloc, locoffset, rotationAngle) {
this.relativeLocation = relativeloc;
this.centerLocationOffset = locoffset;
this.rotationAngle = rotationAngle; // in degrees
}
/**
* Rotate all members of a selected Group about the rotatePoint.
* @this {GroupRotatingTool}
* @param {number} newangle
*/
GroupRotatingTool.prototype.rotate = function (newangle) {
go.RotatingTool.prototype.rotate.call(this, newangle);
var group = this.adornedObject.part;
if (group instanceof go.Group) {
var ang = newangle - this.originalAngle;
var cp = this._rotatePoint;
var it = this._initialInfo.iterator;
while (it.next()) {
var part = it.key;
if (part instanceof go.Link) return; // only Nodes and simple Parts
var info = it.value;
part.rotateObject.angle = info.rotationAngle + ang;
var loc = cp.copy().add(info.relativeLocation);
var dir = cp.directionPoint(loc);
var newrad = (ang + dir) * (Math.PI / 180);
var locoffset = info.centerLocationOffset.copy();
locoffset.rotate(ang);
var dist = Math.sqrt(cp.distanceSquaredPoint(loc));
part.location = new go.Point(cp.x + dist * Math.cos(newrad),
cp.y + dist * Math.sin(newrad)).subtract(locoffset);
}
}
}
// end group-rotating-tool.js
diagram = $(go.Diagram, "diagram", // create a Diagram for the DIV HTML element
{
"undoManager.isEnabled": true,
"grid.visible": true
});
diagram.toolManager.rotatingTool = new GroupRotatingTool();
diagram.groupTemplate =
$(go.Group, "Position",
{ locationSpot: go.Spot.Center, locationObjectName: "ORIGIN", rotatable: true },
new go.Binding("location", "location"),
new go.Binding("angle", "angle").makeTwoWay(),
// $(go.Placeholder, { padding: 5}),
$(go.Shape, "RoundedRectangle", {width: 100, height: 30, position: new go.Point(-50, -15), fill: "white", stroke: "black"}),
$(go.Shape, "LineH", {width: 50, height: 5, position: new go.Point(2.5, 0)}, new go.Binding("stroke", "color")),
$(go.Shape, "Circle", {name: "ORIGIN", width: 5, height: 5}, new go.Binding("fill", "color")),
$(go.TextBlock, {margin: 5}, new go.Binding("text", "key"), new go.Binding("angle", "angle", (a) => -a)),
$(go.Panel, "Position", new go.Binding("itemArray", "itemArray"),
{
itemTemplate: $(
go.Panel,
{
fromLinkable: true,
toLinkable: true,
cursor: "pointer",
},
new go.Binding("position", "position"),
$(go.Shape, "LineH", {width: 50, height: 5, position: new go.Point(2.5, 0), stroke: "cyan"}),
$(go.Shape, "Circle", {width: 5, height: 5, fill: "cyan"}),
$(go.TextBlock, new go.Binding("text", "name"), { margin: 5 })
), // end of itemTemplate
})
);
diagram.linkTemplate =
$(go.Link,
{ curve: go.Link.Bezier, adjusting: go.Link.Stretch, reshapable: true },
$(go.Shape),
$(go.Shape, { toArrow: "Standard" })
);
diagram.model = new go.GraphLinksModel([
{ key: "A", isGroup: true, color: "red", location: new go.Point(0, 0), angle: 0, itemArray: [] },
{ key: "A.0", group: "A", isGroup: true, color: "red", location: new go.Point(0, 0), angle: 0, itemArray: [] },
{ key: "A.1", group: "A", isGroup: true, color: "red", location: new go.Point(100, 0), angle: 0, itemArray: [] },
{ key: "B", isGroup: true, color: "blue", location: new go.Point(200, 0), angle: 0, itemArray: [] },
{ key: "B.1", group: "B", isGroup: true, color: "blue", location: new go.Point(300, 0), angle: 0, itemArray: [
{ name: "B-i1", position: new go.Point(0, 100) },
{ name: "B-i2", position: new go.Point(0, 200) }
]},
{ key: "C", isGroup: true, color: "lightgreen", location: new go.Point(200, 200), angle: 90, itemArray: [
{ name: "C-i1", position: new go.Point(100, 100) },
{ name: "C-i2", position: new go.Point(0, 100) },
{ name: "C-i3", position: new go.Point(-100, -100) }
]},
]);
}
</script>
</body>
</html>
Now I have the following questions:
- Are the assumptions in my approach correct or would you approach the problem differently?
- How can I deal with group placeholders? When I comment out the placeholder line in the group template, the locations of the nodes seem incorrect.
- Not all shapes are correctly positioned; I would like to position my shapes always w.r.t. the specified location, how can I achieve this? I also don’t get the additional locations offset for all the shapes.
- How to position items with a negative position? Now it seems that only positive positions are allowed, see
C-i3
- For the text labels of the nodes I have a binding that ensures that the text is always places horizontally; is this also possible for the text in the item array?