UNPKG

gojs

Version:

Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams

618 lines (552 loc) 27.9 kB
/* * Copyright (C) 1998-2018 by Northwoods Software Corporation * All Rights Reserved. * * FLOOR PLANNER: WALL RESHAPING TOOL * Used to reshape walls via their endpoints in a Floorplan */ import * as go from "../../../release/go" import Floorplan = require("./Floorplan"); class WallReshapingTool extends go.Tool { private _handleArchetype: go.Shape; private _handle: go.GraphObject; private _adornedShape: go.Shape; private _angle: number; private _length: number; private _reshapeObjectName: string; private _isBuilding: boolean; private _returnData: any; private _returnPoint: go.Point; constructor() { super(); let h: go.Shape = new go.Shape(); h.figure = "Diamond"; h.desiredSize = new go.Size(7, 7); h.fill = "lightblue"; h.stroke = "dodgerblue"; h.cursor = "move"; this._handleArchetype = h; this._handle = null; this._adornedShape = null; this._reshapeObjectName = 'SHAPE'; this._angle = 0; this._length; this._isBuilding = false; // only true when a wall is first being constructed, set in WallBuildingTool's doMouseUp function this._returnPoint = null; // used if reshape is cancelled; return reshaping wall endpoint to its previous location this._returnData = null; // used if reshape is cancelled; return all windows/doors of a reshaped wall to their old place } // Get the archetype for the handle (a Shape) get handleArchetype() { return this._handleArchetype } // Get / set current handle being used to reshape the wall get handle() { return this._handle; } set handle(value: go.GraphObject) { this._handle = value; } // Get / set adorned shape (shape of the Wall Group being reshaped) get adornedShape() { return this._adornedShape; } set adornedShape(value: go.Shape) { this._adornedShape = value; } // Get / set current angle get angle() { return this._angle; } set angle(value: number) { this._angle = value; } // Get / set length of the wall being reshaped (used only with SHIFT + drag) get length() { return this._length; } set length(value: number) { this._length = value; } // Get / set the name of the object being reshaped get reshapeObjectName() { return this._reshapeObjectName; } set reshapeObjectName(value: string) { this._reshapeObjectName = value; } // Get / set flag telling tool whether it's reshaping a new wall (isBuilding = true) or reshaping an old wall (isBuilding = false) get isBuilding() { return this._isBuilding; } set isBuilding(value: boolean) { this._isBuilding = value; } // Get set loc data for wallParts to return to if reshape is cancelled get returnData() { return this._returnData; } set returnData(value: any) { this._returnData = value; } // Get / set the point to return the reshaping wall endpoint to if reshape is cancelled get returnPoint() { return this._returnPoint; } set returnPoint(value: go.Point) { this._returnPoint = value; } /* * Places reshape handles on either end of a wall node * @param {part} The wall to adorn */ public updateAdornments(part: go.Part): void { if (part === null || part instanceof go.Link) return; if (part.isSelected && !this.diagram.isReadOnly) { let selelt_go: go.GraphObject = part.findObject(this.reshapeObjectName); if (selelt_go.part.data.category === "WallGroup") { let selelt: go.Shape = <go.Shape>selelt_go; let adornment: go.Adornment = part.findAdornment(this.name); if (adornment === null) { adornment = this.makeAdornment(selelt); } if (adornment !== null && selelt.geometry != null) { // update the position/alignment of each handle let geo: go.Geometry = selelt.geometry; let b: go.Rect = geo.bounds; // update the size of the adornment adornment.findObject("BODY").desiredSize = b.size; adornment.elements.each(function (h) { if (h.name === undefined) return; let x: number = 0; let y: number = 0; switch (h.name) { case 'sPt': x = geo.startX; y = geo.startY; break; case 'ePt': x = geo.endX; y = geo.endY; break; } let xCheck: number = Math.min((x - b.x) / b.width, 1); let yCheck: number = Math.min((y - b.y) / b.height, 1); if (xCheck < 0) xCheck = 0; if (yCheck < 0) yCheck = 0; if (xCheck > 1) xCheck = 1; if (yCheck > 1) yCheck = 1; if (isNaN(xCheck)) xCheck = 0; if (isNaN(yCheck)) yCheck = 0; h.alignment = new go.Spot(Math.max(0, xCheck), Math.max(0, yCheck)); }); part.addAdornment(this.name, adornment); adornment.location = selelt.getDocumentPoint(go.Spot.Center); adornment.angle = selelt.getDocumentAngle(); return; } } } part.removeAdornment(this.name); } // If the user has clicked down at a visible handle on a wall node, then the tool may start public canStart(): boolean { if (!this.isEnabled) return false; const diagram: go.Diagram = this.diagram; if (diagram === null || diagram.isReadOnly) return false; if (!diagram.allowReshape) return false; if (!diagram.lastInput.left) return false; let h: go.GraphObject = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name); return (h !== null || this.isBuilding); } // Start a new transaction for the wall reshaping public doActivate(): void { const diagram: go.Diagram = this.diagram; if (diagram === null) return; if (this.isBuilding) { // this.adornedShape has already been set in WallBuildingTool's doMouseDown function let wall: go.Group = <go.Group>this.adornedShape.part; this.handle = this.findToolHandleAt(wall.data.endpoint, this.name); } else { this.handle = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name); if (this.handle === null) return; let adorn: go.Adornment = <go.Adornment>this.handle.part; let shape: go.Shape = <go.Shape>adorn.adornedObject; let wall: go.Group = <go.Group>shape.part; if (!shape) return; this.adornedShape = shape; // store pre-reshape location of wall's reshaping endpoint this.returnPoint = this.snapPointToGrid(diagram.firstInput.documentPoint); // store pre-reshape locations of all wall's members (windows / doors) let wallParts: go.Iterator<go.Part> = wall.memberParts; if (wallParts.count != 0) { let locationsMap: go.Map<string, go.Point> = new go.Map(/*"string", go.Point*/); wallParts.iterator.each(function (wallPart) { locationsMap.add(wallPart.data.key, wallPart.location); }); this.returnData = locationsMap; } } diagram.isMouseCaptured = true; this.startTransaction(this.name); this.isActive = true; } // Adjust the handle's coordinates, along with the wall's points public doMouseMove(): void { const diagram: go.Diagram = this.diagram; const tool: WallReshapingTool = this; let adorn: go.Adornment = <go.Adornment>tool.handle.part; let wall: go.Group = <go.Group>adorn.adornedPart; if (tool.isActive && diagram !== null) { let mousePt: go.Point = diagram.lastInput.documentPoint; tool.calcAngleAndLengthFromHandle(mousePt); // sets this.angle and this.length (useful for when SHIFT is held) let newpt: go.Point = diagram.lastInput.documentPoint; tool.reshape(newpt); } let fp: Floorplan = <Floorplan>diagram; fp.updateWallAngles(); } // Does one final reshape, commits the transaction, then stops the tool public doMouseUp(): void { const diagram: go.Diagram = this.diagram; if (this.isActive && diagram !== null) { let newpt: go.Point = diagram.lastInput.documentPoint; this.reshape(newpt); this.transactionResult = this.name; // success } this.stopTool(); } // End the wall reshaping transaction public doDeactivate(): void { const diagram: go.Diagram = this.diagram; let fp: Floorplan = <Floorplan>diagram; let returnData: any = this.returnData; // if a wall reshaped to length < 1 px, remove it let adorn: go.Adornment = <go.Adornment>this.handle.part; let wall: go.Group = <go.Group>adorn.adornedPart; let sPt: go.Point = wall.data.startpoint; let ePt: go.Point = wall.data.endpoint; let length: number = Math.sqrt(sPt.distanceSquared(ePt.x, ePt.y)); if (length < 1) { diagram.remove(wall); // remove wall wall.memberParts.iterator.each(function (member) { diagram.remove(member); }) // remove wall's parts let wallDimensionLinkPointNodes: Array<go.Node> = []; fp.pointNodes.iterator.each(function (node) { if (node.data.key.indexOf(wall.data.key) !== -1) wallDimensionLinkPointNodes.push(node); }); diagram.remove(wallDimensionLinkPointNodes[0]); diagram.remove(wallDimensionLinkPointNodes[1]); } // remove wall's dimension links if tool cancelled via esc key if (diagram.lastInput.key === "Esc" && !this.isBuilding) { diagram.skipsUndoManager = true; diagram.startTransaction("reset to old data"); if (this.handle.name === "sPt") wall.data.startpoint = this.returnPoint; else wall.data.endpoint = this.returnPoint; fp.updateWall(wall); if (this.returnData) { this.returnData.iterator.each(function (kvp) { let key: string = kvp.key; let loc: go.Point = kvp.value; let wallPart: go.Node = <go.Node>diagram.findPartForKey(key); wallPart.location = loc; wallPart.rotateObject.angle = wall.rotateObject.angle; }); } diagram.commitTransaction("reset to old data"); diagram.skipsUndoManager = false; } // remove guide line point nodes let glPoints: go.Iterator<go.Node> = this.diagram.findNodesByExample({ category: 'GLPointNode' }); diagram.removeParts(glPoints, true); fp.updateWallDimensions(); // commit transaction, deactivate tool diagram.commitTransaction(this.name); this.isActive = false; } /* * Creates an adornment with 2 handles * @param {Shape} The adorned wall's Shape element */ public makeAdornment = function (selelt: go.Shape): go.Adornment { let adornment: go.Adornment = new go.Adornment; adornment.type = go.Panel.Spot; adornment.locationObjectName = "BODY"; adornment.locationSpot = go.Spot.Center; let h: go.Shape = new go.Shape(); h.name = "BODY" h.fill = null; h.stroke = null; h.strokeWidth = 0; adornment.add(h); h = this.makeHandle(); h.name = 'sPt'; adornment.add(h); h = this.makeHandle(); h.name = 'ePt'; adornment.add(h); adornment.category = this.name; adornment.adornedObject = selelt; return adornment; } // Creates a basic handle archetype public makeHandle = function (): go.Shape { let h: go.Shape = this.handleArchetype; return h.copy(); } /* * Calculate the angle and length made from the mousepoint and the non-moving handle; used to reshape wall when holding SHIFT * @param {Point} mousePt The mouse cursors coordinate position */ public calcAngleAndLengthFromHandle = function (mousePt: go.Point) { const tool: WallReshapingTool = this; const diagram: go.Diagram = this.diagram; let h: go.GraphObject = this.handle; let otherH: go.GraphObject; let node: go.Node = this.adornedShape.part; let adornments: go.Iterator<go.Adornment> = node.adornments.iterator; let adornment: go.Adornment; adornments.each(function (a) { if (a.category === tool.name) adornment = a; }) adornment.elements.each(function (e) { if (e.name != undefined && e.name != h.name) { otherH = e; } }); // calc angle from otherH against the horizontal let otherHandlePt: go.Point = otherH.getDocumentPoint(go.Spot.Center); let deltaY: number = mousePt.y - otherHandlePt.y let deltaX: number = mousePt.x - otherHandlePt.x let angle: number = Math.atan2(deltaY, deltaX) * (180 / Math.PI); // because atan2 goes from -180 to +180 and we want it to be 0-360 // so -90 becomes 270, etc. if (angle < 0) angle += 360; tool.angle = angle; let distanceBetween: number = Math.sqrt(mousePt.distanceSquared(otherHandlePt.x, otherHandlePt.y)); tool.length = distanceBetween; } /* * Takes a point -- returns a new point that is closest to the original point that conforms to the grid snap * @param {Point} point The point to snap to grid */ public snapPointToGrid = function (point: go.Point): go.Point { const diagram: go.Diagram = this.diagram; let newx: number = diagram.model.modelData.gridSize * Math.round(point.x / diagram.model.modelData.gridSize); let newy: number = diagram.model.modelData.gridSize * Math.round(point.y / diagram.model.modelData.gridSize); let newPt: go.Point = new go.Point(newx, newy); return newPt; } /* * Reshapes the shape's geometry, updates model data * @param {Point} newPoint The point to move the reshaping wall's reshaping endpoint to */ public reshape = function (newPoint: go.Point) { const diagram: go.Diagram = this.diagram; const tool: WallReshapingTool = this; let shape: go.Shape = this.adornedShape; let node: go.Group = <go.Group>shape.part; // if user holds SHIFT, make angle between startPoint / endPoint and the horizontal line a multiple of 45 if (this.diagram.lastInput.shift) { let sPt: go.Point; // the stationary point -- the point at the handle that is not being adjusted if (tool.handle.name === 'sPt') sPt = node.data.endpoint; else sPt = node.data.startpoint; let oldGridSize: number = diagram.model.modelData.gridSize; let gridSize: number = diagram.model.modelData.gridSize; //if gridSnapping is disabled, just set 'gridSize' var to 1 so it doesn't affect endPoint calculations if (!(this.diagram.toolManager.draggingTool.isGridSnapEnabled)) gridSize = 1; // these are set in mouseMove's call to calcAngleAndLengthFromHandle() let angle: number = tool.angle; let length: number = tool.length; // snap to 90 degrees if (angle > 67.5 && angle < 112.5) { let newy: number = sPt.y + length; newy = gridSize * Math.round(newy / gridSize); newPoint = new go.Point(sPt.x, newy); } // snap to 180 degrees if (angle > 112.5 && angle < 202.5) { let newx: number = sPt.x - length; newx = gridSize * Math.round(newx / gridSize); newPoint = new go.Point(newx, sPt.y); } // snap to 270 degrees if (angle > 247.5 && angle < 292.5) { let newy: number = sPt.y - length; newy = gridSize * Math.round(newy / gridSize); newPoint = new go.Point(sPt.x, newy); } // snap to 360 degrees if (angle > 337.5 || angle < 22.5) { let newx: number = sPt.x + length; newx = gridSize * Math.round(newx / gridSize); newPoint = new go.Point(newx, sPt.y); } // snap to 45 degrees if (angle > 22.5 && angle < 67.5) { let newx: number = (Math.sin(.785) * length); newx = gridSize * Math.round(newx / gridSize) + sPt.x; let newy: number = (Math.cos(.785) * length); newy = gridSize * Math.round(newy / gridSize) + sPt.y; newPoint = new go.Point(newx, newy); } // snap to 135 degrees if (angle > 112.5 && angle < 157.5) { let newx: number = (Math.sin(.785) * length); newx = sPt.x - (gridSize * Math.round(newx / gridSize)); let newy: number = (Math.cos(.785) * length); newy = gridSize * Math.round(newy / gridSize) + sPt.y; newPoint = new go.Point(newx, newy); } // snap to 225 degrees if (angle > 202.5 && angle < 247.5) { let newx: number = (Math.sin(.785) * length); newx = sPt.x - (gridSize * Math.round(newx / gridSize)); let newy: number = (Math.cos(.785) * length); newy = sPt.y - (gridSize * Math.round(newy / gridSize)); newPoint = new go.Point(newx, newy); } // snap to 315 degrees if (angle > 292.5 && angle < 337.5) { let newx: number = (Math.sin(.785) * length); newx = sPt.x + (gridSize * Math.round(newx / gridSize)); let newy: number = (Math.cos(.785) * length); newy = sPt.y - (gridSize * Math.round(newy / gridSize)); newPoint = new go.Point(newx, newy); } gridSize = oldGridSize; // set gridSize back to what it used to be in case gridSnap is enabled again } if (this.diagram.toolManager.draggingTool.isGridSnapEnabled) newPoint = this.snapPointToGrid(newPoint); else newPoint = new go.Point(newPoint.x, newPoint.y); let type: string = this.handle.name; if (type === undefined) return; // set the appropriate point in the node's data to the newPoint value switch (type) { case 'sPt': reshapeWall(node, node.data.endpoint, node.data.startpoint, newPoint, diagram, tool); break; case 'ePt': reshapeWall(node, node.data.startpoint, node.data.endpoint, newPoint, diagram, tool); break; } this.updateAdornments(shape.part); this.showMatches(); let fp: Floorplan = <Floorplan>diagram; fp.updateWallDimensions(); } // end reshape() // Show if the wall (at the adjustment handle being moved) lines up with other wall edges public showMatches = function () { const diagram: go.Diagram = this.diagram; if (!diagram.model.modelData.preferences.showWallGuidelines) return; const tool: WallReshapingTool = this; let wall: go.Node = this.adornedShape.part; let comparePt: go.Point; if (this.handle.name === 'sPt') comparePt = wall.data.startpoint; else comparePt = wall.data.endpoint; // the wall attached to the handle being manipulated let hWall: go.Part = this.adornedShape.part; // delete any old guideline points (these are used to show guidelines, must be cleared before a new guideline can be shown) let glPoints: go.Iterator<go.Node> = <go.Iterator<go.Node>>diagram.findNodesByExample({ category: 'GLPointNode' }); diagram.removeParts(glPoints, true); let walls: go.Iterator<go.Group> = <go.Iterator<go.Group>>this.diagram.findNodesByExample({ category: 'WallGroup' }); walls.iterator.each(function (w) { if (w.data.key != hWall.data.key) { let shape: go.Shape = <go.Shape>w.findObject('SHAPE'); let geo: go.Geometry = shape.geometry; let pt1: go.Point = w.data.startpoint; let pt2: go.Point = w.data.endpoint; tool.checkPtLinedUp(pt1, comparePt.x, pt1.x, comparePt); tool.checkPtLinedUp(pt1, comparePt.y, pt1.y, comparePt); tool.checkPtLinedUp(pt2, comparePt.x, pt2.x, comparePt); tool.checkPtLinedUp(pt2, comparePt.y, pt2.y, comparePt); } }) } /* Static function -- checks if there exists a horiontal or vertical line (decided by 'coord' parameter) between pt and compare pt * if so, draws a link between the two, letting the user know the wall they're reshaping lines up with another's edge * @param {Point} pt * @param {Number} comparePtCoord * @param {Number} ptCoord * @param {Point} comparePt */ public checkPtLinedUp = function (pt: go.Point, comparePtCoord: number, ptCoord: number, comparePt: go.Point) { function makeGuideLinePoint() { let $ = go.GraphObject.make; return $(go.Node, "Spot", { locationSpot: go.Spot.TopLeft, locationObjectName: "SHAPE", desiredSize: new go.Size(1, 1), }, new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify), $(go.Shape, { stroke: null, strokeWidth: 1, name: "SHAPE", fill: "black", }) );} function makeGuideLineLink() { let $ = go.GraphObject.make; return $(go.Link, $(go.Shape, { stroke: "black", strokeWidth: 2, name: 'SHAPE', }, new go.Binding("strokeWidth", "width"), new go.Binding('stroke', 'stroke') ) );} const diagram: go.Diagram = this.diagram; let errorMargin: number = Math.abs(comparePtCoord - ptCoord); if (errorMargin < 2) { let data = { category: "GLPointNode", loc: go.Point.stringify(pt), key: "glpt" }; let data2 = { key: 'movingPt', category: "GLPointNode", loc: go.Point.stringify(comparePt) }; let data3 = { key: 'guideline', category: 'guideLine', from: 'movingPt', to: data.key, stroke: 'blue' }; let GLPoint1: go.Node = makeGuideLinePoint(); let GLPoint2: go.Node = makeGuideLinePoint(); let GLLink: go.Link = makeGuideLineLink(); diagram.add(GLPoint1); diagram.add(GLPoint2); diagram.add(GLLink); GLPoint1.data = data; GLPoint2.data = data2; GLLink.data = data3; GLLink.fromNode = GLPoint1; GLLink.toNode = GLPoint2; } } } /* * Maintain position of all wallParts as best as possible when a wall is being reshaped * Position is relative to the distance a wallPart's location is from the stationaryPoint of the wall * This is called during WallReshapingTool's reshape function * @param {Group} wall The wall being reshaped * @param {Point} stationaryPoint The endpoint of the wall not being reshaped * @param {Point} movingPoint The endpoint of the wall being reshaped * @param {Point} newPoint The point that movingPoint is going to * @param {Diagram} diagram The diagram belonging WallReshapingTool belongs to * @param {WallReshapingTool} tool */ function reshapeWall(wall: go.Group, stationaryPoint: go.Point, movingPoint: go.Point, newPoint: go.Point, diagram: go.Diagram, tool: WallReshapingTool) { let wallParts: go.Iterator<go.Node> = <go.Iterator<go.Node>>wall.memberParts; let arr: Array<go.Part> = []; let oldAngle: number = wall.rotateObject.angle; wallParts.iterator.each(function (part) { arr.push(part); }); // remember the distance each wall part's location was from the stationary point; store these in a Map let distancesMap: go.Map<string, number> = new go.Map(/*"string", "number"*/); let closestPart: go.Part = null; let closestDistance: number = Number.MAX_VALUE; for (let i: number = 0; i < arr.length; i++) { let part: go.Part = arr[i]; let distanceToStationaryPt: number = Math.sqrt(part.location.distanceSquaredPoint(stationaryPoint)); distancesMap.add(part.data.key, distanceToStationaryPt); // distanceToMovingPt is determined by whichever endpoint of the wallpart is closest to movingPoint let endpoints: Array<go.Point> = getWallPartEndpoints(part); let distanceToMovingPt: number = Math.min(Math.sqrt(endpoints[0].distanceSquaredPoint(movingPoint)), Math.sqrt(endpoints[1].distanceSquaredPoint(movingPoint))); // find and store the closest wallPart to the movingPt if (distanceToMovingPt < closestDistance) { closestDistance = distanceToMovingPt; closestPart = part; } } // if the proposed newPoint would make it so the wall would reshape past closestPart, set newPoint to the edge point of closest part if (closestPart !== null) { let loc: go.Point = closestPart.location; let partLength: number = closestPart.data.length; let angle: number = oldAngle; let point1: go.Point = new go.Point((loc.x + (partLength / 2)), loc.y); let point2: go.Point = 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); let distance1: number = Math.sqrt(stationaryPoint.distanceSquaredPoint(point1)); let distance2: number = Math.sqrt(stationaryPoint.distanceSquaredPoint(point2)); let minLength: number; let newLoc: go.Point; if (distance1 > distance2) { minLength = distance1; newLoc = point1; } else { minLength = distance2; newLoc = point2; } let testDistance: number = Math.sqrt(stationaryPoint.distanceSquaredPoint(newPoint)); if (testDistance < minLength) newPoint = newLoc; } // reshape the wall if (movingPoint === wall.data.endpoint) diagram.model.setDataProperty(wall.data, "endpoint", newPoint); else diagram.model.setDataProperty(wall.data, "startpoint", newPoint); let fp: Floorplan = <Floorplan>diagram; fp.updateWall(wall); // calculate the new angle offset let newAngle: number = wall.rotateObject.angle; let angleOffset: number = newAngle - oldAngle; // for each wallPart, maintain relative distance from the stationaryPoint distancesMap.iterator.each(function (kvp) { let wallPart: go.Node = <go.Node>diagram.findPartForKey(kvp.key); let distance: number = kvp.value; let wallLength: number = Math.sqrt(stationaryPoint.distanceSquaredPoint(movingPoint)); let newLoc: go.Point = new go.Point(stationaryPoint.x + ((distance / wallLength) * (movingPoint.x - stationaryPoint.x)), stationaryPoint.y + ((distance / wallLength) * (movingPoint.y - stationaryPoint.y))); wallPart.location = newLoc; wallPart.angle = (wallPart.angle + angleOffset) % 360; }); } // end reshapeWall() /* * 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; var angle = 0; if (wallPart.containingGroup !== null) angle = wallPart.containingGroup.rotateObject.angle; else 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; } export = WallReshapingTool;