gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
498 lines (466 loc) • 25.1 kB
JavaScript
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* FLOOR PLANNER CODE: TEMPLATES - WALLS
* GraphObject templates for Wall Groups, Wall Part Nodes (and their dependecies) used in the Floor Planner sample
* Includes Wall Group, Palette Wall Node, Window Node, Door Node
*/
/*
* Wall Group Dependencies:
* Snap Walls, Find Closest Loc on Wall, Add Wall Part, Wall Part Drag Over, Wall Part Drag Away
*/
/*
* Drag computation function to snap walls to the grid properly while dragging
* @param {Node} part A reference to dragged Part
* @param {Point} pt The Point describing the proposed location
* @param {Point} gridPt Snapped location
*/
var snapWalls = function (part, pt, gridPt) {
var floorplan = part.diagram;
floorplan.updateWallDimensions();
floorplan.updateWallAngles();
floorplan.updateWall(part);
var grid = part.diagram.grid;
var sPt = part.data.startpoint.copy();
var ePt = part.data.endpoint.copy();
var dx = pt.x - part.location.x;
var dy = pt.y - part.location.y;
var newSpt = sPt.offset(dx, dy);
var newEpt = ePt.offset(dx, dy);
if (floorplan.toolManager.draggingTool.isGridSnapEnabled) {
newSpt = newSpt.snapToGridPoint(grid.gridOrigin, grid.gridCellSize);
newEpt = newEpt.snapToGridPoint(grid.gridOrigin, grid.gridCellSize);
}
floorplan.model.setDataProperty(part.data, "startpoint", newSpt);
floorplan.model.setDataProperty(part.data, "endpoint", newEpt);
return new go.Point((newSpt.x + newEpt.x) / 2, (newSpt.y + newEpt.y) / 2);
}
/*
* Find closest loc (to mouse point) on wall a wallPart can be dropped onto without extending beyond wall endpoints or intruding into another wallPart
* @param {Group} wall A reference to a Wall Group
* @param {Node} part A reference to a Wall Part Node -- i.e. Door Node, Window Node
*/
function findClosestLocOnWall(wall, part) {
var orderedConstrainingPts = []; // wall endpoints and wallPart endpoints
var startpoint = wall.data.startpoint.copy();
var endpoint = wall.data.endpoint.copy();
// store all possible constraining endpoints (wall endpoints and wallPart endpoints) in the order in which they appear (left/top to right/bottom)
var firstWallPt = ((startpoint.x + startpoint.y) <= (endpoint.x + endpoint.y)) ? startpoint : endpoint;
var lastWallPt = ((startpoint.x + startpoint.y) > (endpoint.x + endpoint.y)) ? startpoint : endpoint;
var wallPartEndpoints = [];
wall.memberParts.iterator.each(function (wallPart) {
var endpoints = getWallPartEndpoints(wallPart);
wallPartEndpoints.push(endpoints[0]);
wallPartEndpoints.push(endpoints[1]);
});
// sort all wallPartEndpoints by x coordinate left to right
wallPartEndpoints.sort(function (a, b) {
if ((a.x + a.y) > (b.x + b.y)) return 1;
if ((a.x + a.y) < (b.x + b.y)) return -1;
else return 0;
});
orderedConstrainingPts.push(firstWallPt);
orderedConstrainingPts = orderedConstrainingPts.concat(wallPartEndpoints);
orderedConstrainingPts.push(lastWallPt);
// go through all constraining points; if there's a free stretch along the wall "part" could fit in, remember it
var possibleStretches = [];
for (var i = 0; i < orderedConstrainingPts.length; i += 2) {
var point1 = orderedConstrainingPts[i];
var point2 = orderedConstrainingPts[i + 1];
var distanceBetween = Math.sqrt(point1.distanceSquaredPoint(point2));
if (distanceBetween >= part.data.length) possibleStretches.push({ pt1: point1, pt2: point2 });
}
// go through all possible stretches along the wall the part *could* fit in; find the one closest to the part's current location
var closestDist = Number.MAX_VALUE; var closestStretch = null;
for (var i = 0; i < possibleStretches.length; i++) {
var testStretch = possibleStretches[i];
var testPoint1 = testStretch.pt1;
var testPoint2 = testStretch.pt2;
var testDistance1 = Math.sqrt(testPoint1.distanceSquaredPoint(part.location));
var testDistance2 = Math.sqrt(testPoint2.distanceSquaredPoint(part.location));
if (testDistance1 < closestDist) {
closestDist = testDistance1;
closestStretch = testStretch;
}
if (testDistance2 < closestDist) {
closestDist = testDistance2;
closestStretch = testStretch;
}
}
// Edge Case: If there's no space for the wallPart, return null
if (closestStretch === null) return null;
// using the closest free stretch along the wall, calculate endpoints that make the stretch's line segment, then project part.location onto the segment
var closestStretchLength = Math.sqrt(closestStretch.pt1.distanceSquaredPoint(closestStretch.pt2));
var offset = part.data.length / 2;
var point1 = new go.Point(closestStretch.pt1.x + ((offset / closestStretchLength) * (closestStretch.pt2.x - closestStretch.pt1.x)),
closestStretch.pt1.y + ((offset / closestStretchLength) * (closestStretch.pt2.y - closestStretch.pt1.y)));
var point2 = new go.Point(closestStretch.pt2.x + ((offset / closestStretchLength) * (closestStretch.pt1.x - closestStretch.pt2.x)),
closestStretch.pt2.y + ((offset / closestStretchLength) * (closestStretch.pt1.y - closestStretch.pt2.y)));
var newLoc = part.location.copy().projectOntoLineSegmentPoint(point1, point2);
return newLoc;
}
// MouseDrop event for wall groups; if a door or window is dropped on a wall, add it to the wall group
// Do not allow dropping wallParts that would extend beyond wall endpoints or intrude into another wallPart
var addWallPart = function (e, wall) {
var floorplan = e.diagram;
var wallPart = floorplan.selection.first();
if ((wallPart && (wallPart.category === "WindowNode" || wallPart.category === "DoorNode") && wallPart.containingGroup === null)) {
var newLoc = findClosestLocOnWall(wall, wallPart);
if (newLoc !== null) {
wall.findObject("SHAPE").stroke = "black";
floorplan.model.setDataProperty(wallPart.data, "group", wall.data.key);
wallPart.location = newLoc.projectOntoLineSegmentPoint(wall.data.startpoint, wall.data.endpoint);
wallPart.angle = wall.rotateObject.angle;
if (wallPart.category === "WindowNode") floorplan.model.setDataProperty(wallPart.data, "height", wall.data.thickness);
if (wallPart.category === "DoorNode") floorplan.model.setDataProperty(wallPart.data, "doorOpeningHeight", wall.data.thickness);
} else {
floorplan.remove(wallPart);
alert("There's not enough room on the wall!");
return;
}
}
if (floorplan.floorplanUI) floorplan.floorplanUI.setSelectionInfo(floorplan.selection.first(), floorplan);
floorplan.updateWallDimensions();
}
// MouseDragEnter event for walls; if a door or window is dragged over a wall, highlight the wall and change its angle
var wallPartDragOver = function (e, wall) {
var floorplan = e.diagram;
var parts = floorplan.toolManager.draggingTool.draggingParts;
parts.iterator.each(function (part) {
if ((part.category === "WindowNode" || part.category === "DoorNode") && part.containingGroup === null) {
wall.findObject("SHAPE").stroke = "lightblue";
part.angle = wall.rotateObject.angle;
}
});
}
// MouseDragLeave event for walls; if a wall part is dragged past a wall, unhighlight the wall and change back the wall part's angle to 0
var wallPartDragAway = function (e, wall) {
var floorplan = e.diagram;
wall.findObject("SHAPE").stroke = "black";
var parts = floorplan.toolManager.draggingTool.draggingParts;
parts.iterator.each(function (part) {
if ((part.category === "WindowNode" || part.category === "DoorNode") && part.containingGroup === null) part.angle = 0
});
}
/*
* Wall Group Template
*/
// Wall Group
function makeWallGroup() {
var $ = go.GraphObject.make;
return $(go.Group, "Spot",
{
contextMenu: makeContextMenu(),
toolTip: makeGroupToolTip(),
selectionObjectName: "SHAPE",
rotateObjectName: "SHAPE",
locationSpot: go.Spot.Center,
reshapable: true,
minSize: new go.Size(1, 1),
dragComputation: snapWalls,
selectionAdorned: false,
mouseDrop: addWallPart,
mouseDragEnter: wallPartDragOver,
mouseDragLeave: wallPartDragAway,
doubleClick: function (e) { if (e.diagram.floorplanUI) e.diagram.floorplanUI.hideShow("selectionInfoWindow"); }
},
$(go.Shape,
{
name: "SHAPE",
fill: "black",
},
new go.Binding("strokeWidth", "thickness"),
new go.Binding("stroke", "isSelected", function (s, obj) {
if (obj.part.containingGroup != null) {
var group = obj.part.containingGroup;
if (s) { group.data.isSelected = true; }
}
return s ? "dodgerblue" : "black";
}).ofObject()
))
}
/*
* Wall Part Node Dependencies:
* Get Wall Part Endpoints, Get Wall Part Stretch, Drag Wall Parts (Drag Computation Function),
* Wall Part Resize Adornment, Door Selection Adornment (Door Nodes only)
*/
/*
* Find and return an array of the endpoints of a given wallpart (window or door)
* @param {Node} wallPart A Wall Part Node -- i.e. Door Node, Window Node
*/
function getWallPartEndpoints(wallPart) {
var loc = wallPart.location;
var partLength = wallPart.data.length;
if (wallPart.containingGroup !== null) var angle = wallPart.containingGroup.rotateObject.angle;
else var angle = 180;
var point1 = new go.Point((loc.x + (partLength / 2)), loc.y);
var point2 = new go.Point((loc.x - (partLength / 2)), loc.y);
point1.offset(-loc.x, -loc.y).rotate(angle).offset(loc.x, loc.y);
point2.offset(-loc.x, -loc.y).rotate(angle).offset(loc.x, loc.y);
var arr = []; arr.push(point1); arr.push(point2);
return arr;
}
/*
* Returns a "stretch" (2 Points) that constrains a wallPart (door or window), comprised of "part"'s containing wall endpoints or other wallPart endpoints
* @param {Node} part A Wall Part Node -- i.e. Door Node, Window Node, that is attached to a wall
*/
function getWallPartStretch(part) {
var wall = part.containingGroup;
var startpoint = wall.data.startpoint.copy();
var endpoint = wall.data.endpoint.copy();
// sort all possible endpoints into either left/above or right/below
var leftOrAbove = new go.Set(/*go.Point*/); var rightOrBelow = new go.Set(/*go.Point*/);
wall.memberParts.iterator.each(function (wallPart) {
if (wallPart.data.key !== part.data.key) {
var endpoints = getWallPartEndpoints(wallPart);
for (var i = 0; i < endpoints.length; i++) {
if (endpoints[i].x < part.location.x || (endpoints[i].y > part.location.y && endpoints[i].x === part.location.x)) leftOrAbove.add(endpoints[i]);
else rightOrBelow.add(endpoints[i]);
}
}
});
// do the same with the startpoint and endpoint of the dragging part's wall
if (parseFloat(startpoint.x.toFixed(2)) < parseFloat(part.location.x.toFixed(2)) || (startpoint.y > part.location.y && parseFloat(startpoint.x.toFixed(2)) === parseFloat(part.location.x.toFixed(2)))) leftOrAbove.add(startpoint);
else rightOrBelow.add(startpoint);
if (parseFloat(endpoint.x.toFixed(2)) < parseFloat(part.location.x.toFixed(2)) || (endpoint.y > part.location.y && parseFloat(endpoint.x.toFixed(2)) === parseFloat(part.location.x.toFixed(2)))) leftOrAbove.add(endpoint);
else rightOrBelow.add(endpoint);
// of each set, find the closest point to the dragging part
var leftOrAbovePt; var closestDistLeftOrAbove = Number.MAX_VALUE;
leftOrAbove.iterator.each(function (point) {
var distance = Math.sqrt(point.distanceSquaredPoint(part.location));
if (distance < closestDistLeftOrAbove) {
closestDistLeftOrAbove = distance;
leftOrAbovePt = point;
}
});
var rightOrBelowPt; var closestDistRightOrBelow = Number.MAX_VALUE;
rightOrBelow.iterator.each(function (point) {
var distance = Math.sqrt(point.distanceSquaredPoint(part.location));
if (distance < closestDistRightOrBelow) {
closestDistRightOrBelow = distance;
rightOrBelowPt = point;
}
});
var stretch = { point1: leftOrAbovePt, point2: rightOrBelowPt };
return stretch;
}
/*
* Drag computation function for WindowNodes and DoorNodes; ensure wall parts stay in walls when dragged
* @param {Node} part A reference to dragged Part
* @param {Point} pt The Point describing the proposed location
* @param {Point} gridPt Snapped location
*/
var dragWallParts = function (part, pt, gridPt) {
if (part.containingGroup !== null && part.containingGroup.category === 'WallGroup') {
var floorplan = part.diagram;
// Edge Case: if part is not on its wall (due to incorrect load) snap part.loc onto its wall immediately; ideally this is never called
var wall = part.containingGroup;
var wStart = wall.data.startpoint;
var wEnd = wall.data.endpoint;
var dist1 = Math.sqrt(wStart.distanceSquaredPoint(part.location));
var dist2 = Math.sqrt(part.location.distanceSquaredPoint(wEnd));
var totalDist = Math.sqrt(wStart.distanceSquaredPoint(wEnd));
if (dist1 + dist2 !== totalDist) part.location = part.location.copy().projectOntoLineSegmentPoint(wStart, wEnd);
// main behavior
var stretch = getWallPartStretch(part);
var leftOrAbovePt = stretch.point1;
var rightOrBelowPt = stretch.point2;
// calc points along line created by the endpoints that are half the width of the moving window/door
var totalLength = Math.sqrt(leftOrAbovePt.distanceSquaredPoint(rightOrBelowPt));
var distance = (part.data.length / 2);
var point1 = new go.Point(leftOrAbovePt.x + ((distance / totalLength) * (rightOrBelowPt.x - leftOrAbovePt.x)),
leftOrAbovePt.y + ((distance / totalLength) * (rightOrBelowPt.y - leftOrAbovePt.y)));
var point2 = new go.Point(rightOrBelowPt.x + ((distance / totalLength) * (leftOrAbovePt.x - rightOrBelowPt.x)),
rightOrBelowPt.y + ((distance / totalLength) * (leftOrAbovePt.y - rightOrBelowPt.y)));
// calc distance from pt to line (part's wall) - use point to 2pt line segment distance formula
var distFromWall = Math.abs(((wEnd.y - wStart.y) * pt.x) - ((wEnd.x - wStart.x) * pt.y) + (wEnd.x * wStart.y) - (wEnd.y * wStart.x)) /
Math.sqrt(Math.pow((wEnd.y - wStart.y), 2) + Math.pow((wEnd.x - wStart.x), 2));
var tolerance = (20 * wall.data.thickness < 100) ? (20 * wall.data.thickness) : 100;
// if distance from pt to line > some tolerance, detach the wallPart from the wall
if (distFromWall > tolerance) {
part.containingGroup = null;
delete part.data.group;
part.angle = 0;
floorplan.pointNodes.iterator.each(function (node) { floorplan.remove(node) });
floorplan.dimensionLinks.iterator.each(function (link) { floorplan.remove(link) });
floorplan.pointNodes.clear();
floorplan.dimensionLinks.clear();
floorplan.updateWallDimensions();
if (floorplan.floorplanUI) floorplan.floorplanUI.setSelectionInfo(part);
}
// project the proposed location onto the line segment created by the new points (ensures wall parts are constrained properly when dragged)
pt = pt.copy().projectOntoLineSegmentPoint(point1, point2);
floorplan.skipsUndoManager = true;
floorplan.startTransaction("set loc");
floorplan.model.setDataProperty(part.data, "loc", go.Point.stringify(pt));
floorplan.commitTransaction("set loc");
floorplan.skipsUndoManager = false;
floorplan.updateWallDimensions(); // update the dimension links created by having this wall part selected
} return pt;
}
// Resize Adornment for Wall Part Nodes
function makeWallPartResizeAdornment() {
var $ = go.GraphObject.make;
return $(go.Adornment, "Spot",
{ name: "WallPartResizeAdornment" },
$(go.Placeholder),
$(go.Shape, { alignment: go.Spot.Left, cursor: "w-resize", figure: "Diamond", desiredSize: new go.Size(7, 7), fill: "#ffffff", stroke: "#808080" }),
$(go.Shape, { alignment: go.Spot.Right, cursor: "e-resize", figure: "Diamond", desiredSize: new go.Size(7, 7), fill: "#ffffff", stroke: "#808080" })
);
}
// Selection Adornment for Door Nodes
function makeDoorSelectionAdornment() {
var $ = go.GraphObject.make;
return $(go.Adornment, "Vertical",
{ name: "DoorSelectionAdornment" },
$(go.Panel, "Auto",
$(go.Shape, { fill: null, stroke: null }),
$(go.Placeholder)),
$(go.Panel, "Horizontal", { defaultStretch: go.GraphObject.Vertical },
$("Button",
$(go.Picture, { source: "icons/flipDoorOpeningLeft.png", column: 0, desiredSize: new go.Size(12, 12) },
new go.Binding("source", "", function (obj) {
if (obj.adornedPart === null) return "icons/flipDoorOpeningRight.png";
else if (obj.adornedPart.data.swing === "left") return "icons/flipDoorOpeningRight.png";
else return "icons/flipDoorOpeningLeft.png";
}).ofObject()
),
{
click: function (e, obj) {
var floorplan = obj.part.diagram;
floorplan.startTransaction("flip door");
var door = obj.part.adornedPart;
if (door.data.swing === "left") floorplan.model.setDataProperty(door.data, "swing", "right");
else floorplan.model.setDataProperty(door.data, "swing", "left");
floorplan.commitTransaction("flip door");
},
toolTip: $(go.Adornment, "Auto",
$(go.Shape, { fill: "#FFFFCC" }),
$(go.TextBlock, { margin: 4, text: "Flip Door Opening" }
))
},
new go.Binding("visible", "", function (obj) { return (obj.adornedPart === null) ? false : (obj.adornedPart.containingGroup !== null); }).ofObject()
),
$("Button",
$(go.Picture, { source: "icons/flipDoorSide.png", column: 0, desiredSize: new go.Size(12, 12) }),
{
click: function (e, obj) {
var floorplan = obj.part.diagram;
floorplan.startTransaction("rotate door");
var door = obj.part.adornedPart;
door.angle = (door.angle + 180) % 360;
floorplan.commitTransaction("rotate door");
},
toolTip: $(go.Adornment, "Auto",
$(go.Shape, { fill: "#FFFFCC" }),
$(go.TextBlock, { margin: 4, text: "Flip Door Side" }
))
}
),
new go.Binding("visible", "", function (obj) { return (obj.adornedPart === null) ? false : (obj.adornedPart.containingGroup !== null); }).ofObject()
)
);
}
/*
* Wall Part Nodes:
* Window Node, Door Node, Palette Wall Node
*/
// Window Node
function makeWindowNode() {
var $ = go.GraphObject.make;
return $(go.Node, "Spot",
{
contextMenu: makeContextMenu(),
selectionObjectName: "SHAPE",
selectionAdorned: false,
locationSpot: go.Spot.Center,
toolTip: makeNodeToolTip(),
minSize: new go.Size(5, 5),
resizable: true,
resizeAdornmentTemplate: makeWallPartResizeAdornment(),
resizeObjectName: "SHAPE",
rotatable: false,
doubleClick: function (e) { if (e.diagram.floorplanUI) e.diagram.floorplanUI.hideShow("selectionInfoWindow"); },
dragComputation: dragWallParts,
layerName: 'Foreground' // make sure windows are always in front of walls
},
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
new go.Binding("angle").makeTwoWay(),
$(go.Shape,
{ name: "SHAPE", fill: "white", strokeWidth: 0 },
new go.Binding("width", "length").makeTwoWay(),
new go.Binding("height").makeTwoWay(),
new go.Binding("stroke", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject(),
new go.Binding("fill", "isSelected", function (s, obj) { return s ? "lightgray" : "white"; }).ofObject()
),
$(go.Shape,
{ name: "LINESHAPE", fill: "darkgray", strokeWidth: 0, height: 10 },
new go.Binding("width", "length", function (width, obj) { return width - 10; }), // 5px padding each side
new go.Binding("height", "height", function (height, obj) { return (height / 5); }),
new go.Binding("stroke", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject()
)
);
}
// Door Node
function makeDoorNode() {
var $ = go.GraphObject.make;
return $(go.Node, "Spot",
{
contextMenu: makeContextMenu(),
selectionObjectName: "SHAPE",
selectionAdornmentTemplate: makeDoorSelectionAdornment(),
locationSpot: go.Spot.BottomCenter,
resizable: true,
resizeObjectName: "OPENING_SHAPE",
toolTip: makeNodeToolTip(),
minSize: new go.Size(10, 10),
doubleClick: function (e) { if (e.diagram.floorplanUI) e.diagram.floorplanUI.hideShow("selectionInfoWindow"); },
dragComputation: dragWallParts,
resizeAdornmentTemplate: makeWallPartResizeAdornment(),
layerName: 'Foreground' // make sure windows are always in front of walls
},
// remember location of the Node
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
new go.Binding("angle").makeTwoWay(),
// the door's locationSpot is affected by it's openingHeight, which is affected by the thickness of its containing wall
new go.Binding("locationSpot", "doorOpeningHeight", function (doh, obj) { return new go.Spot(0.5, 1, 0, -(doh / 2)); }),
// this is the shape that reprents the door itself and its swing
$(go.Shape,
{ name: "SHAPE" },
new go.Binding("width", "length"),
new go.Binding("height", "length").makeTwoWay(),
new go.Binding("stroke", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject(),
new go.Binding("fill", "color"),
new go.Binding("geometryString", "swing", function (swing) {
if (swing === "left") return "F1 M0,0 v-150 a150,150 0 0,1 150,150 ";
else return "F1 M275,175 v-150 a150,150 0 0,0 -150,150 ";
})
),
// door opening shape
$(go.Shape,
{
name: "OPENING_SHAPE", fill: "white",
strokeWidth: 0, height: 5, width: 40,
alignment: go.Spot.BottomCenter, alignmentFocus: go.Spot.Center
},
new go.Binding("height", "doorOpeningHeight").makeTwoWay(),
new go.Binding("stroke", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject(),
new go.Binding("fill", "isSelected", function (s, obj) { return s ? "lightgray" : "white"; }).ofObject(),
new go.Binding("width", "length").makeTwoWay()
)
);
}
// Palette Wall Node (becomes WallGroup when dropped from Palette onto diagram)
function makePaletteWallNode() {
var $ = go.GraphObject.make;
return $(go.Node, "Spot",
{ selectionAdorned: false },
$(go.Shape,
{ name: "SHAPE", fill: "black", strokeWidth: 0, height: 10, figure: "Rectangle" },
new go.Binding("width", "length").makeTwoWay(),
new go.Binding("height").makeTwoWay(),
new go.Binding("fill", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject(),
new go.Binding("stroke", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject())
);
}