gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
935 lines (860 loc) • 90.2 kB
text/typescript
/*
* Copyright (C) 1998-2018 by Northwoods Software Corporation
* All Rights Reserved.
*
* Floorplan Class
* A Floorplan is a Diagram with special rules
*/
import * as go from "../../../release/go"
import WallBuildingTool = require("./WallBuildingTool")
import WallReshapingTool = require("./WallReshapingTool")
class Floorplan extends go.Diagram {
private _palettes: Array<go.Palette>;
private _pointNodes: go.Set<go.Node>;
private _dimensionLinks: go.Set<go.Link>;
private _angleNodes: go.Set<go.Node>;
constructor(div) {
super(div);
/*
* Floor Plan Setup:
* Initialize Floor Plan, Floor Plan Listeners, Floor Plan Overview
*/
// When a FloorplanPalette instance is made, it is automatically added to a Floorplan's "palettes" property
this._palettes = [];
// Point Nodes, Dimension Links, Angle Nodes on the Floorplan (never in model data)
this._pointNodes = new go.Set(/*go.Node*/);
this._dimensionLinks = new go.Set(/*go.Link*/);
this._angleNodes = new go.Set(/*go.Node*/);
var $ = go.GraphObject.make;
this.allowLink = false;
this.undoManager.isEnabled = true;
this.layout.isOngoing = false;
this.model = $(go.GraphLinksModel, {
modelData: {
"units": "centimeters",
"unitsAbbreviation": "cm",
"gridSize": 10,
"wallThickness": 5,
"preferences": {
showWallGuidelines: true,
showWallLengths: true,
showWallAngles: true,
showOnlySmallWallAngles: true,
showGrid: true,
gridSnap: true
}
}
});
this.grid = $(go.Panel, "Grid",
{ gridCellSize: new go.Size(this.model.modelData.gridSize, this.model.modelData.gridSize), visible: true },
$(go.Shape, "LineH", { stroke: "lightgray" }),
$(go.Shape, "LineV", { stroke: "lightgray" }));
this.contextMenu = makeContextMenu();
this.commandHandler.canGroupSelection = function () { return true; };
this.commandHandler.canUngroupSelection = function () { return true; };
this.commandHandler.archetypeGroupData = { isGroup: true };
/*
* Listeners
*/
// if a wall is copied, update its geometry
this.addDiagramListener("SelectionCopied", function (e) {
let fp: Floorplan = <Floorplan> e.diagram;
fp.selection.iterator.each(function(part){
if (part.category == "WallGroup") {
var w: go.Group = <go.Group>part;
fp.updateWall(w);
}
});
});
// If a node has been dropped onto the Floorplan from a Palette...
this.addDiagramListener("ExternalObjectsDropped", function (e) {
var garbage = [];
let fp: Floorplan = <Floorplan> e.diagram;
fp.selection.iterator.each(function(node){
// Event 1: handle a drag / drop of a wall node from the Palette (as opposed to wall construction via WallBuildingTool)
if (node.category === "PaletteWallNode") {
var paletteWallNode = node;
var endpoints = getWallPartEndpoints(paletteWallNode);
var data = { key: "wall", category: "WallGroup", caption: "Wall", startpoint: endpoints[0], endpoint: endpoints[1], thickness: parseFloat(e.diagram.model.modelData.wallThickness), isGroup: true, notes: "" };
e.diagram.model.addNodeData(data);
let wall: go.Group = <go.Group>e.diagram.findPartForKey(data.key);
fp.updateWall(wall);
garbage.push(paletteWallNode);
}
});
for (var i in garbage) {
e.diagram.remove(garbage[i]);
}
});
// When a wall is copied / pasted, update the wall geometry, angle, etc
this.addDiagramListener("ClipboardPasted", function (e) {
let fp: Floorplan = <Floorplan> e.diagram;
e.diagram.selection.iterator.each(function (node) {
if (node.category === "WallGroup") {
let w: go.Group = <go.Group>node;
fp.updateWall(w);
}
});
});
/*
* Node Templates
* Add Default Node, Multi-Purpose Node, Window Node, Palette Wall Node, and Door Node to the Node Template Map
*/
this.nodeTemplateMap.add("", makeDefaultNode()); // Default Node (furniture)
this.nodeTemplateMap.add("MultiPurposeNode", makeMultiPurposeNode()); // Multi-Purpose Node
this.nodeTemplateMap.add("WindowNode", makeWindowNode()); // Window Node
this.nodeTemplateMap.add("PaletteWallNode", makePaletteWallNode()); // Palette Wall Node
this.nodeTemplateMap.add("DoorNode", makeDoorNode()); // Door Node
/*
* Group Templates
* Add Default Group, Wall Group to Group Template Map
*/
this.groupTemplateMap.add("", makeDefaultGroup()); // Default Group
this.groupTemplateMap.add("WallGroup", makeWallGroup()); // Wall Group
/*
* Install Custom Tools
* Wall Building Tool, Wall Reshaping Tool
*/
var wallBuildingTool = new WallBuildingTool();
this.toolManager.mouseDownTools.insertAt(0, wallBuildingTool);
var wallReshapingTool = new WallReshapingTool();
this.toolManager.mouseDownTools.insertAt(3, wallReshapingTool);
wallBuildingTool.isEnabled = false;
/*
* Tool Overrides
*/
// If a wall was dragged to intersect another wall, update angle displays
this.toolManager.draggingTool.doMouseUp = function () {
go.DraggingTool.prototype.doMouseUp.call(this);
this.diagram.updateWallAngles();
this.isGridSnapEnabled = this.diagram.model.modelData.preferences.gridSnap;
}
// If user holds SHIFT while dragging, do not use grid snap
this.toolManager.draggingTool.doMouseMove = function () {
if (this.diagram.lastInput.shift) {
this.isGridSnapEnabled = false;
} else this.isGridSnapEnabled = this.diagram.model.modelData.preferences.gridSnap;
go.DraggingTool.prototype.doMouseMove.call(this);
}
// When resizing, constantly update the node info box with updated size info; constantly update Dimension Links
this.toolManager.resizingTool.doMouseMove = function () {
var floorplan = this.diagram;
var node = this.adornedObject;
this.diagram.updateWallDimensions();
go.ResizingTool.prototype.doMouseMove.call(this);
}
// When resizing a wallPart, do not allow it to be resized past the nearest wallPart / wall endpoints
this.toolManager.resizingTool.computeMaxSize = function () {
var tool = this;
var obj = tool.adornedObject.part;
var wall = this.diagram.findPartForKey(obj.data.group);
if ((obj.category === 'DoorNode' || obj.category === 'WindowNode') && wall !== null) {
var stationaryPt; var movingPt;
var resizeAdornment = null;
obj.adornments.iterator.each(function (adorn) { if (adorn.name === "WallPartResizeAdornment") resizeAdornment = adorn; });
resizeAdornment.elements.iterator.each(function (el) {
if (el instanceof go.Shape && el.alignment === tool.handle.alignment) movingPt = el.getDocumentPoint(go.Spot.Center);
if (el instanceof go.Shape && el.alignment !== tool.handle.alignment) stationaryPt = el.getDocumentPoint(go.Spot.Center);
});
// find the constrainingPt; that is, the endpoint (wallPart endpoint or wall endpoint) that is the one closest to movingPt but still farther from stationaryPt than movingPt
// this loop checks all other wallPart endpoints of the wall that the resizing wallPart is a part of
var constrainingPt; var closestDist = Number.MAX_VALUE;
wall.memberParts.iterator.each(function (part) {
if (part.data.key !== obj.data.key) {
var endpoints = getWallPartEndpoints(part);
for (var i = 0; i < endpoints.length; i++) {
var point = endpoints[i];
var distanceToMovingPt = Math.sqrt(point.distanceSquaredPoint(movingPt));
if (distanceToMovingPt < closestDist) {
var distanceToStationaryPt = Math.sqrt(point.distanceSquaredPoint(stationaryPt));
if (distanceToStationaryPt > distanceToMovingPt) {
closestDist = distanceToMovingPt;
constrainingPt = point;
}
}
}
}
});
// if we're not constrained by a wallPart endpoint, the constraint will come from a wall endpoint; figure out which one
if (constrainingPt === undefined || constrainingPt === null) {
if (wall.data.startpoint.distanceSquaredPoint(movingPt) > wall.data.startpoint.distanceSquaredPoint(stationaryPt)) constrainingPt = wall.data.endpoint;
else constrainingPt = wall.data.startpoint;
}
// set the new max size of the wallPart according to the constrainingPt
var maxLength = Math.sqrt(stationaryPt.distanceSquaredPoint(constrainingPt));
return new go.Size(maxLength, wall.data.thickness);
}
return go.ResizingTool.prototype.computeMaxSize.call(tool);
}
this.toolManager.draggingTool.isGridSnapEnabled = true;
} // end Floorplan constructor
/**
* Get / set array of all Palettes associated with this Floorplan
*/
get palettes(): Array<go.Palette> { return this._palettes }
set palettes(value: Array<go.Palette>) { this._palettes = value; }
/**
* Get / set pointNodes
*/
get pointNodes(): go.Set<go.Node> { return this._pointNodes }
set pointNodes(value: go.Set<go.Node>) { this._pointNodes = value; }
/**
* Get / set dimensionLinks
*/
get dimensionLinks(): go.Set<go.Link> { return this._dimensionLinks }
set dimensionLinks(value: go.Set<go.Link>) { this._dimensionLinks = value; }
/**
* Get / set angleNodes
*/
get angleNodes(): go.Set<go.Node> { return this._angleNodes }
set angleNodes(value: go.Set<go.Node>) { this._angleNodes = value; }
// Check what units are being used, convert to cm then multiply by 2, (1px = 2cm, change this if you want to use a different paradigm)
public convertPixelsToUnits = function (num: number) {
let units: string = this.model.modelData.units;
let factor: number = this.model.modelData.unitsConversionFactor;
if (units === 'meters') return (num / 100) * factor;
if (units === 'feet') return (num / 30.48) * factor;
if (units === 'inches') return (num / 2.54) * factor;
return num * factor;
}
// Take a number of units, convert to cm, then divide by 2, (1px = 2cm, change this if you want to use a different paradigm)
public convertUnitsToPixels = function (num: number) {
let units: string = this.model.modelData.units;
let factor: number = this.model.modelData.unitsConversionFactor;
if (units === 'meters') return (num * 100) / factor;
if (units === 'feet') return (num * 30.48) / factor;
if (units === 'inches') return (num * 2.54) / factor;
return num / factor;
}
/*
* Update the geometry, angle, and location of a given wall
* @param {Wall} wall A reference to a valid Wall Group (defined in Templates-Walls)
*/
public updateWall = function (wall: go.Group) {
if (wall.data.startpoint && wall.data.endpoint) {
let shape: go.Shape = <go.Shape>wall.findObject("SHAPE");
let geo: go.Geometry = new go.Geometry(go.Geometry.Line);
let sPt: go.Point = wall.data.startpoint;
let ePt: go.Point = wall.data.endpoint;
let mPt: go.Point = new go.Point((sPt.x + ePt.x) / 2, (sPt.y + ePt.y) / 2);
// define a wall's geometry as a simple horizontal line, then rotate it
geo.startX = 0;
geo.startY = 0;
geo.endX = Math.sqrt(sPt.distanceSquaredPoint(ePt));
geo.endY = 0;
shape.geometry = geo;
wall.location = mPt; // a wall's location is the midpoint between it's startpoint and endpoint
let angle: number = sPt.directionPoint(ePt);
wall.rotateObject.angle = angle;
this.updateWallDimensions();
}
}
/*
* Helper function for Build Dimension Link: get a to/from point for a Dimension Link
* @param {Wall} wall The Wall Group being given a Dimension Link
* @param {Number} angle The angle of "wall"
* @param {Number} wallOffset The distance the Dimension Link will be from wall (in pixels)
*/
public getAdjustedPoint = function (point: go.Point, wall: go.Group, angle: number, wallOffset: number): go.Point {
let oldPoint: go.Point = point.copy();
point.offset(0, -(wall.data.thickness * .5) - wallOffset);
point.offset(-oldPoint.x, -oldPoint.y).rotate(angle).offset(oldPoint.x, oldPoint.y);
return point;
}
/*
* Helper function for Update Wall Dimensions; used to build Dimension Links
* @param {Wall} wall The wall the Link runs along (either describing the wall itself or some wallPart on "wall")
* @param {Number} index A number appended to PointNode keys; used for finding PointNodes of Dimension Links later
* @param {Point} point1 The first point of the wallPart being described by the Link
* @param {Point} point2 The second point of the wallPart being described by the Link
* @param {Number} angle The angle of the wallPart
* @param {Number} wallOffset How far from the wall (in px) the Link should be
* @param {Boolean} soloWallFlag If this Link is the only Dimension Link for "wall" (no other wallParts on "wall" selected) this is true; else, false
* @param {Floorplan} floorplan A reference to a valid Floorplan
*/
public buildDimensionLink = function (wall: go.Group, index: number, point1: go.Point, point2: go.Point, angle: number, wallOffset: number, soloWallFlag: boolean, floorplan: Floorplan) {
point1 = floorplan.getAdjustedPoint(point1, wall, angle, wallOffset);
point2 = floorplan.getAdjustedPoint(point2, wall, angle, wallOffset);
let data1 = { key: wall.data.key + "PointNode" + index, category: "PointNode", loc: go.Point.stringify(point1) };
let data2 = { key: wall.data.key + "PointNode" + (index + 1), category: "PointNode", loc: go.Point.stringify(point2) };
let data3 = { key: wall.data.key + "DimensionLink", category: 'DimensionLink', from: data1.key, to: data2.key, stroke: 'gray', angle: angle, wall: wall.data.key, soloWallFlag: soloWallFlag };
let pointNode1: go.Node = makePointNode();
let pointNode2: go.Node = makePointNode();
let link: go.Link = makeDimensionLink();
floorplan.pointNodes.add(pointNode1);
floorplan.pointNodes.add(pointNode2);
floorplan.dimensionLinks.add(link);
floorplan.add(pointNode1);
floorplan.add(pointNode2);
floorplan.add(link);
pointNode1.data = data1;
pointNode2.data = data2;
link.data = data3;
link.fromNode = pointNode1;
link.toNode = pointNode2;
}
/*
* Update Dimension Links shown along walls, based on which walls and wallParts are selected
*/
public updateWallDimensions = function () {
const floorplan: Floorplan = this;
floorplan.skipsUndoManager = true;
floorplan.startTransaction("update wall dimensions");
// if showWallLengths === false, remove all pointNodes (used to build wall dimensions)
if (!floorplan.model.modelData.preferences.showWallLengths) {
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.commitTransaction("update wall dimensions");
floorplan.skipsUndoManager = false;
return;
}
// make visible all dimension links (zero-length dimension links are set to invisible at the end of the function)
floorplan.dimensionLinks.iterator.each(function (link) { link.visible = true; });
let selection: go.Set<go.Part> = floorplan.selection;
// gather all selected walls, including walls of selected DoorNodes and WindowNodes
let walls: go.Set<go.Group> = new go.Set(/*go.Group*/);
selection.iterator.each(function (part) {
if ((part.category === 'WindowNode' || part.category === 'DoorNode') && part.containingGroup !== null) walls.add(part.containingGroup);
if (part.category === 'WallGroup' && part.data && part.data.startpoint && part.data.endpoint) {
let wall: go.Group = <go.Group>part;
let soloWallLink = null;
floorplan.dimensionLinks.iterator.each(function (link) { if (link.data.soloWallFlag && link.data.wall === wall.data.key) soloWallLink = link; });
// if there's 1 Dimension Link for this wall (link has soloWallFlag), adjust to/from pointNodes of link, rather than deleting / redrawing
if (soloWallLink !== null) {
// since this is the only Dimension Link for this wall, keys of its pointNodes will be (wall.data.key) + 1 / (wall.data.key) + 2
let linkPoint1: go.Node = null; let linkPoint2: go.Node = null;
floorplan.pointNodes.iterator.each(function (node) {
if (node.data.key === wall.data.key + "PointNode1") linkPoint1 = node;
if (node.data.key === wall.data.key + "PointNode2") linkPoint2 = node;
});
let startpoint: go.Point = wall.data.startpoint; let endpoint: go.Point = wall.data.endpoint;
// adjust left/top-most / right/bottom-most wall endpoints so link angle is correct (else text appears on wrong side of Link)
let firstWallPt: go.Point = ((startpoint.x + startpoint.y) <= (endpoint.x + endpoint.y)) ? startpoint : endpoint;
let lastWallPt: go.Point = ((startpoint.x + startpoint.y) > (endpoint.x + endpoint.y)) ? startpoint : endpoint;
let newLoc1: go.Point = floorplan.getAdjustedPoint(firstWallPt.copy(), wall, wall.rotateObject.angle, 10);
let newLoc2: go.Point = floorplan.getAdjustedPoint(lastWallPt.copy(), wall, wall.rotateObject.angle, 10);
// cannot use model.setDataProperty, since pointNodes and dimensionLinks are not stored in the model
linkPoint1.data.loc = go.Point.stringify(newLoc1);
linkPoint2.data.loc = go.Point.stringify(newLoc2);
soloWallLink.data.angle = wall.rotateObject.angle;
linkPoint1.updateTargetBindings();
linkPoint2.updateTargetBindings();
soloWallLink.updateTargetBindings();
}
// else build a Dimension Link for this wall; this is removed / replaced if Dimension Links for wallParts this wall are built
else {
let startpoint: go.Point = wall.data.startpoint;
let endpoint: go.Point = wall.data.endpoint;
let firstWallPt: go.Point = ((startpoint.x + startpoint.y) <= (endpoint.x + endpoint.y)) ? startpoint : endpoint;
let lastWallPt: go.Point = ((startpoint.x + startpoint.y) > (endpoint.x + endpoint.y)) ? startpoint : endpoint;
floorplan.buildDimensionLink(wall, 1, firstWallPt.copy(), lastWallPt.copy(), wall.rotateObject.angle, 10, true, floorplan);
}
}
});
// create array of selected wall endpoints and selected wallPart endpoints along the wall that represent measured stretches
walls.iterator.each(function (wall) {
let startpoint: go.Point = wall.data.startpoint;
let endpoint: go.Point = wall.data.endpoint;
let firstWallPt: go.Point = ((startpoint.x + startpoint.y) <= (endpoint.x + endpoint.y)) ? startpoint : endpoint;
let lastWallPt: go.Point = ((startpoint.x + startpoint.y) > (endpoint.x + endpoint.y)) ? startpoint : endpoint;
// store all endpoints along with the part they correspond to (used later to either create DimensionLinks or simply adjust them)
let wallPartEndpoints: Array<go.Point> = [];
wall.memberParts.iterator.each(function (wallPart) {
if (wallPart.isSelected) {
var endpoints = getWallPartEndpoints(wallPart);
wallPartEndpoints.push(endpoints[0]);
wallPartEndpoints.push(endpoints[1]);
}
});
// sort all wallPartEndpoints by x coordinate left to right/ up to down
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;
});
wallPartEndpoints.unshift(firstWallPt);
wallPartEndpoints.push(lastWallPt);
let angle: number = wall.rotateObject.angle;
let k: number = 1; // k is a counter for the indices of PointNodes
// build / edit dimension links for each stretch, defined by pairs of points in wallPartEndpoints
for (let j: number = 0; j < wallPartEndpoints.length - 1; j++) {
let linkPoint1: go.Node = null; let linkPoint2: go.Node = null;
floorplan.pointNodes.iterator.each(function (node) {
if (node.data.key === wall.data.key + "PointNode" + k) linkPoint1 = node;
if (node.data.key === wall.data.key + "PointNode" + (k + 1)) linkPoint2 = node;
});
if (linkPoint1 !== null) {
let newLoc1: go.Point = floorplan.getAdjustedPoint(wallPartEndpoints[j].copy(), wall, angle, 5);
let newLoc2: go.Point = floorplan.getAdjustedPoint(wallPartEndpoints[j + 1].copy(), wall, angle, 5);
linkPoint1.data.loc = go.Point.stringify(newLoc1);
linkPoint2.data.loc = go.Point.stringify(newLoc2);
linkPoint1.updateTargetBindings();
linkPoint2.updateTargetBindings();
}
// only build new links if needed -- normally simply change pointNode locations
else floorplan.buildDimensionLink(wall, k, wallPartEndpoints[j].copy(), wallPartEndpoints[j + 1].copy(), angle, 5, false, floorplan);
k += 2;
}
// total wall Dimension Link constructed of a kth and k+1st pointNode
let totalWallDimensionLink = null;
floorplan.dimensionLinks.iterator.each(function (link) {
if ((link.fromNode.data.key === wall.data.key + "PointNode" + k) &&
(link.toNode.data.key === wall.data.key + "PointNode" + (k + 1))) totalWallDimensionLink = link;
});
// if a total wall Dimension Link already exists, adjust its constituent point nodes
if (totalWallDimensionLink !== null) {
let linkPoint1: go.Node = null; let linkPoint2: go.Node = null;
floorplan.pointNodes.iterator.each(function (node) {
if (node.data.key === wall.data.key + "PointNode" + k) linkPoint1 = node;
if (node.data.key === wall.data.key + "PointNode" + (k + 1)) linkPoint2 = node;
});
let newLoc1: go.Point = floorplan.getAdjustedPoint(wallPartEndpoints[0].copy(), wall, angle, 25);
let newLoc2: go.Point = floorplan.getAdjustedPoint(wallPartEndpoints[wallPartEndpoints.length - 1].copy(), wall, angle, 25);
linkPoint1.data.loc = go.Point.stringify(newLoc1);
linkPoint2.data.loc = go.Point.stringify(newLoc2);
linkPoint1.updateTargetBindings();
linkPoint2.updateTargetBindings();
}
// only build total wall Dimension Link (far out from wall to accomodate wallPart Dimension Links) if one does not already exist
else floorplan.buildDimensionLink(wall, k, wallPartEndpoints[0].copy(), wallPartEndpoints[wallPartEndpoints.length - 1].copy(), angle, 25, false, floorplan);
});
// Cleanup: hide zero-length Dimension Links, DimensionLinks with null wall points
floorplan.dimensionLinks.iterator.each(function (link) {
let canStay: boolean = false;
floorplan.pointNodes.iterator.each(function (node) {
if (node.data.key == link.data.to) canStay = true;
});
if (!canStay) floorplan.remove(link);
else {
let length: number = Math.sqrt(link.toNode.location.distanceSquaredPoint(link.fromNode.location));
if (length < 1 && !link.data.soloWallFlag) link.visible = false;
}
});
floorplan.commitTransaction("update wall dimensions");
floorplan.skipsUndoManager = false;
} // end updateWallDimensions()
/*
* Helper function for updateWallAngles(); returns the Point where two walls intersect; if they do not intersect, return null
* @param {Wall} wall1
* @param {Wall} wall2
*/
public getWallsIntersection = function (wall1: go.Group, wall2: go.Group): go.Point {
if (wall1 === null || wall2 === null) return null;
// treat walls as lines; get lines in formula of ax + by = c
let a1: number = wall1.data.endpoint.y - wall1.data.startpoint.y;
let b1: number = wall1.data.startpoint.x - wall1.data.endpoint.x;
let c1: number = (a1 * wall1.data.startpoint.x) + (b1 * wall1.data.startpoint.y);
let a2: number = wall2.data.endpoint.y - wall2.data.startpoint.y;
let b2: number = wall2.data.startpoint.x - wall2.data.endpoint.x;
let c2: number = (a2 * wall2.data.startpoint.x) + (b2 * wall2.data.startpoint.y);
// Solve the system of equations, finding where the lines (not segments) would intersect
/** Algebra Explanation:
Line 1: a1x + b1y = c1
Line 2: a2x + b2y = c2
Multiply Line1 equation by b2, Line2 equation by b1, get:
a1b1x + b1b2y = b2c1
a2b1x + b1b2y = b1c2
Subtract bottom from top:
a1b2x - a2b1x = b2c1 - b1c2
Divide both sides by a1b2 - a2b1, get equation for x. Equation for y is analogous
**/
let det: number = a1 * b2 - a2 * b1;
let x: number = null; var y: number = null;
// Edge Case: Lines are paralell
if (det === 0) {
// Edge Case: wall1 and wall2 have an endpoint to endpoint intersection (the only instance in which paralell walls could intersect at a specific point)
if (wall1.data.startpoint.equals(wall2.data.startpoint) || wall1.data.startpoint.equals(wall2.data.endpoint)) return wall1.data.startpoint;
if (wall1.data.endpoint.equals(wall2.data.startpoint) || wall1.data.endpoint.equals(wall2.data.endpoint)) return wall1.data.endpoint;
return null;
}
else {
x = (b2 * c1 - b1 * c2) / det;
y = (a1 * c2 - a2 * c1) / det;
}
// ensure proposed intersection is contained in both line segments (walls)
let inWall1: boolean = ((Math.min(wall1.data.startpoint.x, wall1.data.endpoint.x) <= x) && (Math.max(wall1.data.startpoint.x, wall1.data.endpoint.x) >= x)
&& (Math.min(wall1.data.startpoint.y, wall1.data.endpoint.y) <= y) && (Math.max(wall1.data.startpoint.y, wall1.data.endpoint.y) >= y));
let inWall2: boolean = ((Math.min(wall2.data.startpoint.x, wall2.data.endpoint.x) <= x) && (Math.max(wall2.data.startpoint.x, wall2.data.endpoint.x) >= x)
&& (Math.min(wall2.data.startpoint.y, wall2.data.endpoint.y) <= y) && (Math.max(wall2.data.startpoint.y, wall2.data.endpoint.y) >= y));
if (inWall1 && inWall2) return new go.Point(x, y);
else return null;
}
/*
* Update Angle Nodes shown along a wall, based on which wall(s) is/are selected
*/
public updateWallAngles = function () {
const floorplan: Floorplan = this;
floorplan.skipsUndoManager = true; // do not store displaying angles as a transaction
floorplan.startTransaction("display angles");
if (floorplan.model.modelData.preferences.showWallAngles) {
floorplan.angleNodes.iterator.each(function (node) { node.visible = true; });
let selectedWalls: Array<go.Group> = [];
floorplan.selection.iterator.each(function (part) {
if (part.category === "WallGroup") {
let w: go.Group = <go.Group>part;
selectedWalls.push(w);
}
});
for (let i: number = 0; i < selectedWalls.length; i++) {
let seen: go.Set<string> = new go.Set(/*"string"*/); // Set of all walls "seen" thus far for "wall"
let wall: go.Group = selectedWalls[i];
let possibleWalls: go.Iterator<go.Group> = <go.Iterator<go.Group>>floorplan.findNodesByExample({ category: "WallGroup" });
// go through all other walls; if the other wall intersects this wall, make angles
possibleWalls.iterator.each(function (otherWall) {
if (otherWall.data === null || wall.data === null || seen.contains(otherWall.data.key)) return;
if ((otherWall.data.key !== wall.data.key) && (floorplan.getWallsIntersection(wall, otherWall) !== null) && (!seen.contains(otherWall.data.key))) {
seen.add(otherWall.data.key);
// "otherWall" intersects "wall"; make or update angle nodes
let intersectionPoint: go.Point = floorplan.getWallsIntersection(wall, otherWall);
let wallsInvolved: go.Set<go.Group> = <go.Set<go.Group>>floorplan.findObjectsNear(intersectionPoint,
1,
function (x) { if (x.part !== null) return x.part; },
function (p) {
if (!(p instanceof go.Group)) return false;
else return p.category === "WallGroup";
},
false);
let endpoints: Array<any> = []; // store endpoints and their corresponding walls here
// gather endpoints of each wall in wallsInvolved; discard endpoints within a tolerance distance of intersectionPoint
wallsInvolved.iterator.each(function (w) {
let tolerance: number = (floorplan.model.modelData.gridSize >= 10) ? floorplan.model.modelData.gridSize : 10;
if (Math.sqrt(w.data.startpoint.distanceSquaredPoint(intersectionPoint)) > tolerance) endpoints.push({ point: w.data.startpoint, wall: w.data.key });
if (Math.sqrt(w.data.endpoint.distanceSquaredPoint(intersectionPoint)) > tolerance) endpoints.push({ point: w.data.endpoint, wall: w.data.key });
});
// find maxRadius (shortest distance from an involved wall's endpoint to intersectionPoint or 30, whichever is smaller)
let maxRadius: number = 30;
for (let i: number = 0; i < endpoints.length; i++) {
let distance: number = Math.sqrt(endpoints[i].point.distanceSquaredPoint(intersectionPoint));
if (distance < maxRadius) maxRadius = distance;
}
// sort endpoints in a clockwise fashion around the intersectionPoint
endpoints.sort(function (a, b) {
a = a.point; b = b.point;
if (a.x - intersectionPoint.x >= 0 && b.x - intersectionPoint.x < 0) return 1;
if (a.x - intersectionPoint.x < 0 && b.x - intersectionPoint.x >= 0) return -1;
if (a.x - intersectionPoint.x == 0 && b.x - intersectionPoint.x == 0) {
if (a.y - intersectionPoint.y >= 0 || b.y - intersectionPoint.y >= 0) return a.y > b.y ? 1 : -1;
return b.y > a.y ? 1 : -1;
}
// compute the cross product of vectors (center -> a) x (center -> b)
let det: number = (a.x - intersectionPoint.x) * (b.y - intersectionPoint.y) - (b.x - intersectionPoint.x) * (a.y - intersectionPoint.y);
if (det < 0) return 1;
if (det > 0) return -1;
// points a and b are on the same line from the center; check which point is closer to the center
let d1: number = (a.x - intersectionPoint.x) * (a.x - intersectionPoint.x) + (a.y - intersectionPoint.y) * (a.y - intersectionPoint.y);
let d2: number = (b.x - intersectionPoint.x) * (b.x - intersectionPoint.x) + (b.y - intersectionPoint.y) * (b.y - intersectionPoint.y);
return d1 > d2 ? 1 : -1;
}); // end endpoints sort
// for each pair of endpoints, construct or modify an angleNode
for (let i: number = 0; i < endpoints.length; i++) {
let p1: any = endpoints[i]; let p2: any;
if (endpoints[i + 1] != null) {
p2 = endpoints[i + 1];
}
else {
p2 = endpoints[0];
}
let a1: number = intersectionPoint.directionPoint(p1.point);
let a2: number = intersectionPoint.directionPoint(p2.point);
let sweep: number = Math.abs(a2 - a1 + 360) % 360;
let angle: number = a1;
/*
construct proper key for angleNode
proper angleNode key syntax is "wallWwallX...wallYangleNodeZ" such that W < Y < Y; angleNodes are sorted clockwise around the intersectionPoint by Z
*/
let keyArray: Array<go.Group> = []; // used to construct proper key
wallsInvolved.iterator.each(function (wall) { keyArray.push(wall); });
keyArray.sort(function (a, b) {
let aIndex = a.data.key.match(/\d+/g);
let bIndex = b.data.key.match(/\d+/g);
if (isNaN(aIndex)) return 1;
if (isNaN(bIndex)) return -1;
else return aIndex > bIndex ? 1 : -1;
});
let key: string = "";
for (let j: number = 0; j < keyArray.length; j++) key += keyArray[j].data.key;
key += "angle" + i;
// check if this angleNode already exists -- if it does, adjust data (instead of deleting/redrawing)
let angleNode: go.Node = null;
floorplan.angleNodes.iterator.each(function (aNode) { if (aNode.data.key === key) angleNode = aNode; });
if (angleNode !== null) {
angleNode.data.angle = angle;
angleNode.data.sweep = sweep;
angleNode.data.loc = go.Point.stringify(intersectionPoint);
angleNode.data.maxRadius = maxRadius;
angleNode.updateTargetBindings();
}
// if this angleNode does not already exist, create it and add it to the diagram
else {
let data = { key: key, category: "AngleNode", loc: go.Point.stringify(intersectionPoint), stroke: "dodgerblue", angle: angle, sweep: sweep, maxRadius: maxRadius };
let newAngleNode: go.Node = makeAngleNode();
newAngleNode.data = data;
floorplan.add(newAngleNode);
newAngleNode.updateTargetBindings();
floorplan.angleNodes.add(newAngleNode);
}
}
}
});
}
// garbage collection (angleNodes that should not exist any more)
let garbage: Array<go.Node> = [];
floorplan.angleNodes.iterator.each(function (node) {
let keyNums = node.data.key.match(/\d+/g); // values X for all wall keys involved, given key "wallX"
let numWalls: number = (node.data.key.match(/wall/g) || []).length; // # of walls involved in in "node"'s construction
let wallsInvolved: Array<string> = [];
// add all walls involved in angleNode's construction to wallsInvolved
for (let i: number = 0; i < keyNums.length - 1; i++) wallsInvolved.push("wall" + keyNums[i]);
// edge case: if the numWalls != keyNums.length, that means the wall with key "wall" (no number in key) is involved
if (numWalls !== keyNums.length - 1) wallsInvolved.push("wall");
// Case 1: if any wall pairs involved in this angleNode are no longer intersecting, add this angleNode to "garbage"
for (let i: number = 0; i < wallsInvolved.length - 1; i++) {
let wall1: go.Group = <go.Group>floorplan.findPartForKey(wallsInvolved[i]);
let wall2: go.Group = <go.Group>floorplan.findPartForKey(wallsInvolved[i + 1]);
let intersectionPoint: go.Point = floorplan.getWallsIntersection(wall1, wall2);
if (intersectionPoint === null) garbage.push(node);
}
// Case 2: if there are angleNode clusters with the same walls in their keys as "node" but different locations, destroy and rebuild
// collect all angleNodes with same walls in their construction as "node"
let possibleAngleNodes: go.Set<go.Node> = new go.Set(/*go.Node*/);
let allWalls = node.data.key.slice(0, node.data.key.indexOf("angle"));
floorplan.angleNodes.iterator.each(function (other) { if (other.data.key.indexOf(allWalls) !== -1) possibleAngleNodes.add(other); });
possibleAngleNodes.iterator.each(function (pNode) {
if (pNode.data.loc !== node.data.loc) {
garbage.push(pNode);
}
});
// Case 3: put any angleNodes with sweep === 0 in garbage
if (node.data.sweep === 0) garbage.push(node);
});
for (let i: number = 0; i < garbage.length; i++) {
floorplan.remove(garbage[i]); // remove garbage
floorplan.angleNodes.remove(garbage[i]);
}
}
// hide all angles > 180 if show only small angles == true in preferences
if (floorplan.model.modelData.preferences.showOnlySmallWallAngles) {
floorplan.angleNodes.iterator.each(function (node) { if (node.data.sweep >= 180) node.visible = false; });
}
// hide all angles if show wall angles == false in preferences
if (!floorplan.model.modelData.preferences.showWallAngles) {
floorplan.angleNodes.iterator.each(function (node) { node.visible = false; });
}
floorplan.commitTransaction("display angles");
floorplan.skipsUndoManager = false;
}
}
/*
* Copyright (C) 1998-2018 by Northwoods Software Corporation
* All Rights Reserved.
*
* FLOOR PLANNER CODE: TEMPLATES - GENERAL
* General GraphObject templates used in the Floor Planner sample
* Includes Context Menu, Diagram, Default Group, AngleNode, DimensionLink, PointNode
*/
/*
* Dependencies for Context Menu:
* Make Selection Group, Ungroup Selection, Clear Empty Groups
*/
// Make the selection a group
function makeSelectionGroup(floorplan) {
floorplan.startTransaction("group selection");
// ungroup all selected nodes; then group them; if one of the selected nodes is a group, ungroup all its nodes
var sel = floorplan.selection; var nodes = [];
sel.iterator.each(function (n) {
if (n instanceof go.Group) n.memberParts.iterator.each(function (part) { nodes.push(part); })
else nodes.push(n);
});
for (var i = 0; i < nodes.length; i++) nodes[i].isSelected = true;
ungroupSelection(floorplan);
floorplan.commandHandler.groupSelection();
var group = floorplan.selection.first(); // after grouping, the new group will be the only thing selected
floorplan.model.setDataProperty(group.data, "caption", "Group");
floorplan.model.setDataProperty(group.data, "notes", "");
clearEmptyGroups(floorplan);
// unselect / reselect group so data appears properly in Selection Info Window
floorplan.clearSelection();
floorplan.select(group);
floorplan.commitTransaction("group selection");
}
// Ungroup selected nodes; if the selection is a group, ungroup all it's memberParts
function ungroupSelection(floorplan) {
floorplan.startTransaction('ungroup selection');
// helper function to ungroup nodes
function ungroupNode(node) {
var group = node.containingGroup;
node.containingGroup = null;
if (group != null) {
if (group.memberParts.count === 0) floorplan.remove(group);
else if (group.memberParts.count === 1) group.memberParts.first().containingGroup = null;
}
}
// ungroup any selected nodes; remember groups that are selected
var sel = floorplan.selection; var groups = [];
sel.iterator.each(function (n) {
if (!(n instanceof go.Group)) ungroupNode(n);
else groups.push(n);
});
// go through selected groups, and ungroup their memberparts too
var nodes = [];
for (var i = 0; i < groups.length; i++) groups[i].memberParts.iterator.each(function (n) { nodes.push(n); });
for (var i = 0; i < nodes.length; i++) ungroupNode(nodes[i]);
clearEmptyGroups(floorplan);
floorplan.commitTransaction('ungroup selection');
}
// Clear all the groups that have no nodes
function clearEmptyGroups(floorplan) {
var nodes = floorplan.nodes; var arr = [];
nodes.iterator.each(function (node) { if (node instanceof go.Group && node.memberParts.count === 0 && node.category !== "WallGroup") { arr.push(node); } });
for (let i: number = 0; i < arr.length; i++) { floorplan.remove(arr[i]); }
}
/*
* General Group Dependencies:
* Group Tool Tip
*/
// Group Tool Tip
function makeGroupToolTip() {
var $ = go.GraphObject.make;
return $(go.Adornment, "Auto",
$(go.Shape, { fill: "#FFFFCC" }),
$(go.TextBlock, { margin: 4 },
new go.Binding("text", "", function (text, obj) {
var data = obj.part.adornedObject.data;
var name = (obj.part.adornedObject.category === "MultiPurposeNode") ? data.text : data.caption;
return "Name: " + name + "\nNotes: " + data.notes + '\nMembers: ' + obj.part.adornedObject.memberParts.count;
}).ofObject())
);
}
/*
* General Templates:
* Context Menu, Default Group
*/
// Context Menu -- referenced by Node, Diagram and Group Templates
function makeContextMenu() {
var $ = go.GraphObject.make
return $(go.Adornment, "Vertical",
// Make Selection Group Button
$("ContextMenuButton",
$(go.TextBlock, "Make Group"),
{ click: function (e, obj) { makeSelectionGroup(obj.part.diagram); } },
new go.Binding("visible", "visible", function (v, obj) {
var floorplan = obj.part.diagram;
if (floorplan.selection.count <= 1) return false;
var flag = true;
floorplan.selection.iterator.each(function (node) {
if (node.category === "WallGroup" || node.category === "WindowNode" || node.category === "DoorNode") flag = false;
});
return flag;
}).ofObject()
),
// Ungroup Selection Button
$("ContextMenuButton",
$(go.TextBlock, "Ungroup"),
{ click: function (e, obj) { ungroupSelection(obj.part.diagram); } },
new go.Binding("visible", "", function (v, obj) {
var floorplan = obj.part.diagram;
if (floorplan !== null) {
var node = floorplan.selection.first();
return ((node instanceof go.Node && node.containingGroup != null && node.containingGroup.category != 'WallGroup') ||
(node instanceof go.Group && node.category === ''));
} return false;
}).ofObject()
),
// Copy Button
$("ContextMenuButton",
$(go.TextBlock, "Copy"),
{ click: function (e, obj) { obj.part.diagram.commandHandler.copySelection() } },
new go.Binding("visible", "", function (v, obj) {
if (obj.part.diagram !== null) {
return obj.part.diagram.selection.count > 0;
} return false;
}).ofObject()
),
// Cut Button
$("ContextMenuButton",
$(go.TextBlock, "Cut"),
{ click: function (e, obj) { obj.part.diagram.commandHandler.cutSelection() } },
new go.Binding("visible", "", function (v, obj) {
if (obj.part.diagram !== null) {
return obj.part.diagram.selection.count > 0;
} return false;
}).ofObject()
),
// Delete Button
$("ContextMenuButton",
$(go.TextBlock, "Delete"),
{ click: function (e, obj) { obj.part.diagram.commandHandler.deleteSelection() } },
new go.Binding("visible", "", function (v, obj) {
if (obj.part.diagram !== null) {
return obj.part.diagram.selection.count > 0;
} return false;
}).ofObject()
),
// Paste Button
$("ContextMenuButton",
$(go.TextBlock, "Paste"),
{ click: function (e, obj) { obj.part.diagram.commandHandler.pasteSelection(obj.part.diagram.lastInput.documentPoint) } }
),
// Show Selection Info Button (only available when selection count > 0)
$("ContextMenuButton",
$(go.TextBlock, "Show Selection Info"),
{
click: function (e, obj) {
if (e.diagram.floorplanUI) {
var selectionInfoWindow = document.getElementById(e.diagram.floorplanUI.state.windows.selectionInfoWindow.id);
if (selectionInfoWindow.style.visibility !== 'visible') e.diagram.floorplanUI.hideShow('selectionInfoWindow');
}
}
},
new go.Binding("visible", "", function (v, obj) {
if (obj.part.diagram !== null) {
return obj.part.diagram.selection.count > 0;
} return false;
}).ofObject()
),
// Flip Dimension Side Button (only available when selection contains Wall Group(s))
$("ContextMenuButton",
$(go.TextBlock, "Flip Dimension Side"),
{
click: function (e, obj) {
var floorplan = obj.part.diagram;
if (floorplan !== null) {
floorplan.startTransaction("flip dimension link side");
var walls = [];
floorplan.selection.iterator.each(function (part) {
if (part.category === "WallGroup") walls.push(part);
});
for (var i = 0; i < walls.length; i++) {
var wall = walls[i];
var sPt = wall.data.startpoint.copy();
var ePt = wall.data.endpoint.copy();
floorplan.model.setDataProperty(wall.data, "startpoint", ePt);
floorplan.model.setDataProperty(wall.data, "endpoint", sPt);
floorplan.updateWall(wall);
}
floorplan.commitTransaction("flip dimension link side");
}
}
},
new go.Binding("visible", "", function (v, obj) {
if (obj.part.diagram !== null) {
var sel = obj.part.diagram.selection;
if (sel.count === 0) return false;
var flag = false;
sel.iterator.each(function (part) {
if (part.category === "WallGroup") flag = true;
});
return flag;
} return false;
}).ofObject()
)
);
}
// Default Group
function makeDefaultGroup() {
var $ = go.GraphObject.make;
return $(go.Group, "Vertical",
{
contextMenu: makeContextMenu(