UNPKG

gojs

Version:

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

977 lines (974 loc) 231 kB
/* * Copyright (C) 1998-2023 by Northwoods Software Corporation * All Rights Reserved. * * Floorplan Class * A Floorplan is a Diagram with special rules */ var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); (function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "../../../release/go", "./NodeLabelDraggingTool.js", "./WallBuildingTool.js", "./WallReshapingTool.js"], factory); } })(function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Floorplan = void 0; var go = require("../../../release/go"); var NodeLabelDraggingTool_js_1 = require("./NodeLabelDraggingTool.js"); var WallBuildingTool_js_1 = require("./WallBuildingTool.js"); var WallReshapingTool_js_1 = require("./WallReshapingTool.js"); var Floorplan = /** @class */ (function (_super) { __extends(Floorplan, _super); /** * A Floorplan is a special kind of Diagram. It supports walls, rooms, and many other common features a Floorplan might have. * @param div The HTML DIV element or DIV element id for the Floorplan to use */ function Floorplan(div) { var _this = _super.call(this, div) || this; /** * 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 = new Array(); // Point Nodes, Dimension Links, Angle Nodes on the Floorplan (never in model data) _this._pointNodes = new go.Set(); _this._dimensionLinks = new go.Set(); _this._angleNodes = new go.Set(); var $ = go.GraphObject.make; _this.allowLink = false; _this.undoManager.isEnabled = true; _this.layout.isInitial = false; _this.layout.isOngoing = false; _this.model = $(go.GraphLinksModel, { modelData: { 'units': 'meters', 'unitsAbbreviation': 'm', 'unitsConversionFactor': .02, 'gridSize': 10, 'wallThickness': 10, '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) { var fp = e.diagram; fp.selection.iterator.each(function (part) { if (part.category === 'WallGroup') { var w = part; fp.updateWall(w); } }); }); // If a node has been dropped onto the Floorplan from a Palette... _this.addDiagramListener('ExternalObjectsDropped', function (e) { var garbage = new Array(); var fp = e.diagram; fp.selection.iterator.each(function (node) { // if floor node dropped, try to make a room node here with that floor brush style if (node.category === 'FloorNode') { var floorNode = node; var pt = fp.lastInput.documentPoint; // try to make a room here fp.maybeAddRoomNode(pt, floorNode.data.floorImage); garbage.push(floorNode); } }); 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) { var fp = e.diagram; e.diagram.selection.iterator.each(function (node) { if (node.category === 'WallGroup') { var w = node; fp.updateWall(w); } }); }); // Display different help depending on selection context _this.addDiagramListener('ChangedSelection', function (e) { var floorplan = e.diagram; floorplan.skipsUndoManager = true; floorplan.startTransaction('remove dimension links and angle nodes'); floorplan.pointNodes.iterator.each(function (node) { e.diagram.remove(node); }); floorplan.dimensionLinks.iterator.each(function (link) { e.diagram.remove(link); }); var missedDimensionLinks = new Array(); // used only in undo situations floorplan.links.iterator.each(function (link) { if (link.data.category === 'DimensionLink') missedDimensionLinks.push(link); }); for (var i = 0; i < missedDimensionLinks.length; i++) { e.diagram.remove(missedDimensionLinks[i]); } floorplan.pointNodes.clear(); floorplan.dimensionLinks.clear(); floorplan.angleNodes.iterator.each(function (node) { e.diagram.remove(node); }); floorplan.angleNodes.clear(); floorplan.commitTransaction('remove dimension links and angle nodes'); floorplan.skipsUndoManager = false; floorplan.updateWallDimensions(); floorplan.updateWallAngles(); }); // if user deletes a wall, update rooms _this.addDiagramListener('SelectionDeleted', function (e) { var wrt = e.diagram.toolManager.mouseDownTools.elt(3); wrt.joinAllColinearWalls(); wrt.splitAllWalls(); wrt.performAllMitering(); // also update rooms var deletedParts = e.subject; // make sure to get all the walls that were just deleted, so updateAllRoomBoundaries knows about what change triggered it var walls = new go.Set(); deletedParts.iterator.each(function (p) { if (p instanceof go.Group && p.data.category === 'WallGroup') { var w = p; walls.add(w); } }); var fp = e.diagram; fp.updateAllRoomBoundaries(walls); }); /* * 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 _this.nodeTemplateMap.add('RoomNode', makeRoomNode()); // Room 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_js_1.WallBuildingTool(); _this.toolManager.mouseDownTools.insertAt(0, wallBuildingTool); var wallReshapingTool = new WallReshapingTool_js_1.WallReshapingTool(); _this.toolManager.mouseDownTools.insertAt(3, wallReshapingTool); wallBuildingTool.isEnabled = false; var nodeLabelDraggingTool = new NodeLabelDraggingTool_js_1.NodeLabelDraggingTool(); _this.toolManager.mouseMoveTools.insertAt(3, nodeLabelDraggingTool); /* * Tool Overrides */ // If a wall was dragged to intersect another wall, update angle displays _this.toolManager.draggingTool.doDeactivate = function () { // go.DraggingTool.prototype.doMouseUp.call(this); var fp = this.diagram; var tool = this; fp.updateWallAngles(); this.isGridSnapEnabled = this.diagram.model.modelData.preferences.gridSnap; // maybe recalc rooms, if dragging a wall var selectedWall = null; fp.selection.iterator.each(function (p) { if (p.category === 'WallGroup' && selectedWall == null) { var w = p; selectedWall = w; } else if (p.category === 'WallGroup' && selectedWall !== null) { // only worry about selectedWall if there is a single selected wall (cannot drag multiple walls at once) selectedWall = undefined; } }); if (selectedWall) { var selWallSet = new go.Set(); selWallSet.add(selectedWall); fp.updateAllRoomBoundaries(selWallSet); var wrt = fp.toolManager.mouseDownTools.elt(3); wrt.performMiteringOnWall(selectedWall); fp.updateWall(selectedWall); } go.DraggingTool.prototype.doDeactivate.call(this); }; // 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; floorplan.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 adornedObject = tool.adornedObject; if (adornedObject !== null) { var obj_1 = adornedObject.part; var wall = null; if (obj_1 !== null) { wall = this.diagram.findPartForKey(obj_1.data.group); } if ((wall !== null && obj_1 !== null && (obj_1.category === 'DoorNode' || obj_1.category === 'WindowNode'))) { var stationaryPt_1 = null; var movingPt_1 = null; var resizeAdornment = null; var oitr = obj_1.adornments.iterator; while (oitr.next()) { var adorn = oitr.value; if (adorn.name === 'WallPartResizeAdornment') { resizeAdornment = adorn; } } if (resizeAdornment !== null) { var ritr = resizeAdornment.elements.iterator; while (ritr.next()) { var el = ritr.value; var handle = tool.handle; if (handle !== null) { if (el instanceof go.Shape && el.alignment === handle.alignment) { movingPt_1 = el.getDocumentPoint(go.Spot.Center); } if (el instanceof go.Shape && el.alignment !== handle.alignment) { stationaryPt_1 = 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_1; var closestDist_1 = Number.MAX_VALUE; wall.memberParts.iterator.each(function (part) { if (part.data.key !== obj_1.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_1)); if (distanceToMovingPt < closestDist_1) { var distanceToStationaryPt = Math.sqrt(point.distanceSquaredPoint(stationaryPt_1)); if (distanceToStationaryPt > distanceToMovingPt) { closestDist_1 = distanceToMovingPt; constrainingPt_1 = point; } } } } }); // if we're not constrained by a wallPart endpoint, the constraint will come from a wall endpoint; figure out which one if (constrainingPt_1 === undefined || constrainingPt_1 === null) { if (wall.data.startpoint.distanceSquaredPoint(movingPt_1) > wall.data.startpoint.distanceSquaredPoint(stationaryPt_1)) constrainingPt_1 = wall.data.endpoint; else constrainingPt_1 = wall.data.startpoint; } // set the new max size of the wallPart according to the constrainingPt var maxLength = 0; if (stationaryPt_1 !== null) { maxLength = Math.sqrt(stationaryPt_1.distanceSquaredPoint(constrainingPt_1)); } return new go.Size(maxLength, wall.data.thickness); } } return go.ResizingTool.prototype.computeMaxSize.call(tool); }; _this.toolManager.draggingTool.isGridSnapEnabled = true; return _this; } // end Floorplan constructor Object.defineProperty(Floorplan.prototype, "palettes", { // Get / set array of all Palettes associated with this Floorplans get: function () { return this._palettes; }, set: function (value) { this._palettes = value; }, enumerable: false, configurable: true }); Object.defineProperty(Floorplan.prototype, "pointNodes", { // Get / set pointNodes get: function () { return this._pointNodes; }, set: function (value) { this._pointNodes = value; }, enumerable: false, configurable: true }); Object.defineProperty(Floorplan.prototype, "dimensionLinks", { // Get / set dimensionLinks get: function () { return this._dimensionLinks; }, set: function (value) { this._dimensionLinks = value; }, enumerable: false, configurable: true }); Object.defineProperty(Floorplan.prototype, "angleNodes", { // Get / set angleNodes get: function () { return this._angleNodes; }, set: function (value) { this._angleNodes = value; }, enumerable: false, configurable: true }); /** * Convert num number of pixels (document units) to units, using the adjustable conversion factor stored in modeldata * @param {number} num This is in document units (colloquially, if inaccurately, referred to as "pixels") * @return {number} */ Floorplan.prototype.convertPixelsToUnits = function (num) { var units = this.model.modelData.units; var factor = this.model.modelData.unitsConversionFactor; return num * factor; /*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) * @param {number} num This is in document units (colloquially, if inaccurately, referred to as "pixels") * @return {number} */ Floorplan.prototype.convertUnitsToPixels = function (num) { var units = this.model.modelData.units; var factor = this.model.modelData.unitsConversionFactor; return num / factor; /*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; */ }; /** * @param units string */ Floorplan.prototype.getUnitsAbbreviation = function (units) { switch (units) { case 'centimeters': { return 'cm'; } case 'meters': { return 'm'; } case 'inches': { return 'in'; } case 'feet': { return 'ft'; } } return units; }; /** * Convert a number of oldUnits to newUnits * @param {string} oldUnits cm | m | ft | in * @param {string} newUnits cm | m | ft | in * @param {number} num The number of old units to convert to new ones * @return {number} The number of new units */ Floorplan.prototype.convertUnits = function (oldUnits, newUnits, num) { var fp = this; var newNum = num; oldUnits = fp.getUnitsAbbreviation(oldUnits); newUnits = fp.getUnitsAbbreviation(newUnits); switch (oldUnits) { case 'cm': { switch (newUnits) { case 'm': { newNum *= .01; break; } case 'ft': { newNum *= 0.0328084; break; } case 'in': { newNum *= 0.393701; break; } } break; } // end cm oldUnits case case 'm': { switch (newUnits) { case 'cm': { newNum *= 100; break; } case 'ft': { newNum *= 3.28084; break; } case 'in': { newNum *= 39.3701; break; } } break; } // end m oldUnits case case 'ft': { switch (newUnits) { case 'cm': { newNum *= 30.48; break; } case 'm': { newNum *= 0.3048; break; } case 'in': { newNum *= 12; break; } } break; } // end ft oldUnits case case 'in': { switch (newUnits) { case 'cm': { newNum *= 2.54; break; } case 'm': { newNum *= 0.0254; break; } case 'ft': { newNum *= 0.0833333; break; } } break; } // end in oldUnitsCase } return newNum; }; Floorplan.prototype.makeDefaultFurniturePaletteNodeData = function () { return FURNITURE_NODE_DATA_ARRAY; }; Floorplan.prototype.makeDefaultWallpartsPaletteNodeData = function () { return WALLPARTS_NODE_DATA_ARRAY; }; /** * Turn on wall building tool, set WallBuildingTool.isBuildingDivider to false */ Floorplan.prototype.enableWallBuilding = function () { var fp = this; var wallBuildingTool = fp.toolManager.mouseDownTools.elt(0); wallBuildingTool.isBuildingDivider = false; var wallReshapingTool = fp.toolManager.mouseDownTools.elt(3); wallBuildingTool.isEnabled = true; wallReshapingTool.isEnabled = false; fp.currentCursor = 'crosshair'; // clear resize adornments on walls/windows, if there are any fp.nodes.iterator.each(function (n) { n.clearAdornments(); }); fp.clearSelection(); }; /** * Turn on wall building tool, set WallBuildingTool.isBuildingDivider to true */ Floorplan.prototype.enableDividerBuilding = function () { var fp = this; var wallBuildingTool = fp.toolManager.mouseDownTools.elt(0); fp.enableWallBuilding(); wallBuildingTool.isBuildingDivider = true; fp.currentCursor = 'crosshair'; }; /** * Turn off wall building tool */ Floorplan.prototype.disableWallBuilding = function () { var fp = this; var wallBuildingTool = fp.toolManager.mouseDownTools.elt(0); var wallReshapingTool = fp.toolManager.mouseDownTools.elt(3); wallBuildingTool.isEnabled = false; wallReshapingTool.isEnabled = true; wallBuildingTool.isBuildingDivider = false; fp.currentCursor = ''; // clear resize adornments on walls/windows, if there are any fp.nodes.iterator.each(function (n) { n.clearAdornments(); }); fp.clearSelection(); }; /** * Called when a checkbox in the options window is changed. * Perform the appropriate changes to model data. * @param checkboxId The string id of the HTML checkbox element that's been changed */ Floorplan.prototype.checkboxChanged = function (checkboxId) { var floorplan = this; floorplan.skipsUndoManager = true; floorplan.startTransaction('change preference'); var element = document.getElementById(checkboxId); switch (checkboxId) { case 'showGridCheckbox': { floorplan.grid.visible = element.checked; floorplan.model.modelData.preferences.showGrid = element.checked; break; } case 'gridSnapCheckbox': { floorplan.toolManager.draggingTool.isGridSnapEnabled = element.checked; floorplan.model.modelData.preferences.gridSnap = element.checked; break; } case 'wallGuidelinesCheckbox': floorplan.model.modelData.preferences.showWallGuidelines = element.checked; break; case 'wallLengthsCheckbox': floorplan.model.modelData.preferences.showWallLengths = element.checked; floorplan.updateWallDimensions(); break; case 'wallAnglesCheckbox': floorplan.model.modelData.preferences.showWallAngles = element.checked; floorplan.updateWallAngles(); break; case 'onlySmallWallAnglesCheckbox': { floorplan.model.modelData.preferences.showOnlySmallWallAngles = element.checked; floorplan.updateWallAngles(); break; } } floorplan.commitTransaction('change preference'); floorplan.skipsUndoManager = false; }; /** * Change the units being used by the Floorplan * @param {HTMLFormElement} form The form element containing the units radio buttons in the options menu */ Floorplan.prototype.changeUnits = function (form) { var fp = this; var radios = form.getElementsByTagName('input'); var prevUnits = fp.model.modelData.units; // find selected radio, set units in modelData to the proper units for (var i = 0; i < radios.length; i++) { var radio = radios[i]; if (radio.checked) { var unitsStr = radio.id; fp.model.setDataProperty(fp.model.modelData, 'units', unitsStr); // also set unitsAbbreviation switch (radio.id) { case 'centimeters': fp.model.setDataProperty(fp.model.modelData, 'unitsAbbreviation', 'cm'); break; case 'meters': fp.model.setDataProperty(fp.model.modelData, 'unitsAbbreviation', 'm'); break; case 'feet': fp.model.setDataProperty(fp.model.modelData, 'unitsAbbreviation', 'ft'); break; case 'inches': fp.model.setDataProperty(fp.model.modelData, 'unitsAbbreviation', 'in'); break; } } } var unitsAbbreviation = fp.model.modelData.unitsAbbreviation; var unitAbbrevInputs = document.getElementsByClassName('unitsBox'); for (var i = 0; i < unitAbbrevInputs.length; i++) { var uaInput = unitAbbrevInputs[i]; uaInput.value = unitsAbbreviation; } // explicitly set the units conversion factor by converting the old one to the new units equivalent var unitsConversionFactorInput = document.getElementById('unitsConversionFactorInput'); var oldUnitsConversionFactor = parseFloat(unitsConversionFactorInput.value); var units = fp.model.modelData.units; var newUnitsConverstionFactor = fp.convertUnits(prevUnits, units, oldUnitsConversionFactor); fp.model.setDataProperty(fp.model.modelData, 'unitsConversionFactor', newUnitsConverstionFactor); unitsConversionFactorInput.value = newUnitsConverstionFactor.toString(); var unitInputs = document.getElementsByClassName('unitsInput'); for (var i = 0; i < unitInputs.length; i++) { var input = unitInputs[i]; if (input.id !== 'unitsConversionFactorInput') { var value = parseFloat(input.value); value = parseFloat(fp.convertUnits(prevUnits, units, value).toFixed(4)); input.value = value.toString(); } } }; /** * Change the units conversion factor for the Floorplan. * @param {HTMLInputElement} unitsConversionFactorInput The input element that contains the units conversion factor for the Floorplan * @param {HTMLInputElement} gridSizeInput Optional. If provided, the grid will be updated too */ Floorplan.prototype.changeUnitsConversionFactor = function (unitsConversionFactorInput, gridSizeInput) { var floorplan = this; var val = parseFloat(unitsConversionFactorInput.value); if (isNaN(val) || !val || val === undefined) return; floorplan.skipsUndoManager = true; floorplan.model.set(floorplan.model.modelData, 'unitsConversionFactor', val); if (gridSizeInput) { floorplan.changeGridSize(gridSizeInput); } floorplan.skipsUndoManager = false; }; /** * Change the grid size being used for the Floorplan. * @param gridSizeInput The input that contains the grid size to use */ Floorplan.prototype.changeGridSize = function (gridSizeInput) { var fp = this; fp.skipsUndoManager = true; fp.startTransaction('change grid size'); var inputVal = 0; if (!isNaN(parseFloat(gridSizeInput.value)) && gridSizeInput.value != null && gridSizeInput.value !== '' && gridSizeInput.value !== undefined && parseFloat(gridSizeInput.value) > 0) inputVal = parseFloat(gridSizeInput.value); else { gridSizeInput.value = fp.convertPixelsToUnits(10).toString(); // if bad input given, revert to 20cm (10px) or unit equivalent inputVal = parseFloat(gridSizeInput.value); } inputVal = fp.convertUnitsToPixels(inputVal); fp.grid.gridCellSize = new go.Size(inputVal, inputVal); // fp.toolManager.draggingTool.gridCellSize = new go.Size(inputVal, inputVal); fp.model.setDataProperty(fp.model.modelData, 'gridSize', inputVal); fp.commitTransaction('change grid size'); fp.skipsUndoManager = false; }; /** * Get the side of a wall (1 or 2) to use as the room boundary * @param {go.Group} w * @param {go.Point} ip * @return {number} */ Floorplan.prototype.getCounterClockwiseWallSide = function (w, ip) { var fp = this; var wrt = fp.toolManager.mouseDownTools.elt(3); // these are the mitering point data properties of the wall opposite from the intersection point var prop1 = null; var prop2 = null; // If intersection point (ip) is wall (w)'s data.endpoint, prop1 = smpt1, prop2 = smpt2 if (wrt.pointsApproximatelyEqual(w.data.endpoint, ip)) { prop1 = 'smpt1'; prop2 = 'smpt2'; } else { prop1 = 'empt1'; prop2 = 'empt2'; } var A = ip; var B = w.data[prop2]; var C = w.data[prop1]; // A = intersection point, B = w.data[prop1], C = w.data.[prop2] // if AC is counterclockwise of AB, return 2; else return 1 function isClockwise(a, b, c) { var bool = ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)) > 0; return bool; } if (!isClockwise(A, B, C)) { return 1; } else return 2; }; /** * Returns the intersection point between the two lines. * Lines are implied by two endpoints each. * @param {go.Point} a1 Point Endpoint 1 of line A * @param {go.Point} a2 Point Endpoint 2 of line A * @param {go.Point} b1 Point Endpoint 1 of line B * @param {go.Point} b2 Point Endpoint 2 of line B * @return {go.Point | null} */ Floorplan.prototype.getLinesIntersection = function (a1, a2, b1, b2) { var am = (a1.y - a2.y) / (a1.x - a2.x); // slope of line 1 var bm = (b1.y - b2.y) / (b1.x - b2.x); // slope of line 2 // Line A is vertical if (am === Infinity || am === -Infinity) { var ipx = a1.x; // line B y-intercept var bi = -1 * ((bm * b1.x) - b1.y); // Solve for line B's y at x=ipx var ipy = (bm * ipx) + bi; return new go.Point(ipx, ipy); } // Line B is vertical if (bm === Infinity || bm === -Infinity) { var ipx = b1.x; // line A y-intercept var ai = -1 * ((am * a1.x) - a1.y); // Solve for line A's y at x=ipx var ipy = (am * ipx) + ai; return new go.Point(ipx, ipy); } if (Math.abs(am - bm) < Math.pow(2, -52)) { return null; } else { var ipx = (am * a1.x - bm * b1.x + b1.y - a1.y) / (am - bm); var ipy = (am * bm * (b1.x - a1.x) + bm * a1.y - am * b1.y) / (bm - am); var ip = new go.Point(ipx, ipy); return ip; } }; /** * Update the geometries of all rooms in the floorplan. This is called when a wall is added or reshaped or deleted * @param {go.Set<go.Group>} changedWalls This is the set of walls that was just added / updated / removed. Often this is a single element */ Floorplan.prototype.updateAllRoomBoundaries = function (changedWalls) { var fp = this; var wrt = fp.toolManager.mouseDownTools.elt(3); var rooms = fp.findNodesByExample({ category: 'RoomNode' }); // rooms to remove var garbage = new Array(); rooms.iterator.each(function (r) { // do this until you've tried all wall intersections for the room OR the room boundaries have been successfully updated var boundsFound = false; var triedAllIntersections = false; var seenW1 = new go.Set(); // the walls that have been used as w1 (later) while (!boundsFound && !triedAllIntersections) { // find a point "pt" that will definitely be in the room implied by the r.boundaryWalls (if the area is still enclosed) // to do so, find 2 boundaryWalls that are connected and get a point just outside their intersection (along the proper mitering side) // Note: Neither of these 2 walls may be in "changedWalls" (the walls that were added / modified) // The first of these walls must still be valid (i.e. it was not split or joined with another wall in the action that trieggered this function) var bw = r.data.boundaryWalls; var e1 = null; var e2 = null; // entries that represent wall / mitering side pairs to use to find pt for (var i = 0; i < bw.length + 1; i++) { var entry = bw[i % (bw.length)]; var wk = entry[0]; var ww = fp.findNodeForKey(wk); if (ww === null) continue; if (!changedWalls.contains(ww) && !seenW1.contains(ww)) { if (e1 === null) { e1 = entry; } else if (e1 !== null && e2 === null) { e2 = entry; } } else if (e1 !== null && e2 === null) { e2 = entry; } else if (e1 === null) { e1 = null; e2 = null; } } // with these 2 entries (walls / mitering sides), we now get a point that would definitely be in the room (if the area is still enclosed) // first, get the segments implied by mitering sides of the walls var w1 = null; var w2 = null; var s1 = null; var s2 = null; if (e1 !== null && e2 !== null) { w1 = fp.findNodeForKey(e1[0]); s1 = e1[1]; w2 = fp.findNodeForKey(e2[0]); s2 = e2[1]; } else { triedAllIntersections = true; continue; } if (e1 !== null && w1 !== null) { seenW1.add(w1); } var w1s = w1.data['smpt' + s1]; var w1e = w1.data['empt' + s1]; var w2s = w2.data['smpt' + s2]; var w2e = w2.data['empt' + s2]; // at which point do these 2 wall sides intersect? var ip = fp.getSegmentsIntersection(w1s, w1e, w2s, w2e); if (ip === null) { continue; } // the prop name of the point on the other mitering side of ip. we'll use this to get the angle of the intersection var w1os = (s1 === 1) ? 2 : 1; // let prop: string = wrt.pointsApproximatelyEqual(ip, w1s) ? "smpt" + w1os : "empt" + w1os; var distToS = ip.distanceSquaredPoint(w1.data['smpt' + w1os]); var distToE = ip.distanceSquaredPoint(w1.data['empt' + w1os]); // which other side pt is closer to ip? That's the oip var oip = distToS <= distToE ? w1.data.startpoint : w1.data.endpoint; var ang = oip.directionPoint(ip); var newPt = wrt.translateAndRotatePoint(ip, ang - 90, 0.5); // debug -- show calculated pts /* const $ = go.GraphObject.make; fp.add( $(go.Node, 'Spot', { locationSpot: go.Spot.Center, location: oip }, $(go.Shape, 'Circle', { desiredSize: new go.Size(5, 5), fill: 'red' }) ) ); fp.add( $(go.Node, 'Spot', { locationSpot: go.Spot.Center, location: ip }, $(go.Shape, 'Circle', { desiredSize: new go.Size(5, 5), fill: 'green' }) ) ); fp.add( $(go.Node, 'Spot', { locationSpot: go.Spot.Center, location: newPt }, $(go.Shape, 'Circle', { desiredSize: new go.Size(5, 5), fill: 'cyan' }) ) );*/ boundsFound = fp.maybeAddRoomNode(newPt, r.data.floorImage, r); } // if the room boundaries are never found, this room must be removed if (!boundsFound) { garbage.push(r); } }); for (var i = 0; i < garbage.length; i++) { fp.remove(garbage[i]); } // ensure proper room position by updating target bindings fp.updateAllTargetBindings(); }; /** * Tries to add a Room Node from a given point. * The point must be enclosed by walls. * @param {go.Point} pt * @param {string} floorImage The image relative path to use as the Pattern brush for the room's flooring * @param {go.Node} roomToUpdate Optional. If this is provided, the walls found for the area will be assigned to this room node * @return {boolean} Whether or not the pt is enclosed by room boundaries */ Floorplan.prototype.maybeAddRoomNode = function (pt, floorImage, roomToUpdate) { if (roomToUpdate === undefined || roomToUpdate === null) { roomToUpdate = null; } var fp = this; // if the pt is on a Room or Wall, do nothing var walls = fp.findNodesByExample({ category: 'WallGroup' }); var isPtOnRoomOrWall = false; // make sure "pt" is not inside a wall or room node. If it is, do not run this function walls.iterator.each(function (w) { if (fp.isPointInWall(w, pt)) { isPtOnRoomOrWall = true; } }); var rooms = fp.findNodesByExample({ category: 'RoomNode' }); rooms.iterator.each(function (r) { if (roomToUpdate === null || (roomToUpdate !== null && roomToUpdate !== undefined && roomToUpdate.data.key !== r.data.key)) { var isInRoom = fp.isPointInRoom(r, pt); if (isInRoom) { // Edge: it's possible we're within the polygon created by the rooms boundary walls, but over one of its holes // if so, we may still be able to make new room here var isPtInHole = false; for (var i = 0; i < r.data.holes.length; i++) { var hole = r.data.holes[i]; var polygon = fp.makePolygonFromRoomBoundaries(hole); if (polygon !== null) { if (fp.isPointInPolygon(polygon.toArray(), pt)) { isPtInHole = true; } } } if (!isPtInHole) { isPtOnRoomOrWall = true; } } } }); if (isPtOnRoomOrWall) { return false; } // get thr boundary walls for the room var boundaryWalls = fp.getRoomWalls(pt); if (boundaryWalls === null) { return false; } // also include holes in room var holes = fp.findRoomHoles(boundaryWalls, pt); // check if this is an update or add op if (roomToUpdate !== null) { fp.startTransaction('update room boundaryWalls and holes'); fp.model.setDataProperty(roomToUpdate.data, 'boundaryWalls', boundaryWalls); fp.model.setDataProperty(roomToUpdate.data, 'holes', holes); fp.commitTransaction('update room boundaryWalls and holes'); } else { if (floorImage === null || floorImage === undefined) { floorImage = 'images/textures/floor1.jpg'; } var roomData = { key: 'Room', category: 'RoomNode', name: 'Room Name', boundaryWalls: boundaryWalls, holes: holes, floorImage: floorImage, showLabel: true, showFlooringOptions: true }; fp.model.addNodeData(roomData); roomToUpdate = fp.findPartForData(roomData); } fp.updateRoom(roomToUpdate); return true; }; /** * Returns a specially formatted array that represents the boundaries of a room. * These boundaries are the walls enclosing the room, which must include the given point * The array is formatted with entries [wall, mitering side] * @param {go.Point} pt * @return {Array<any>} */ Floorplan.prototype.getRoomWalls = function (pt) { var fp = this; // get the all the walls, in order from closest to farthest, the line from pt upwards would hit var walls = fp.findNodesByExample({ category: 'WallGroup' }); var oPt = new go.Point(pt.x, pt.y - 10000); var wallsDistArr = new Array(); // array of wall/dist pairs [[wallA, 15], [wallB, 30]] -- this makes sorting easier than if we were using a Map walls.iterator.each(function (w) { var ip = fp.getSegmentsIntersection(pt, oPt, w.data.startpoint, w.data.endpoint); if (ip !== null) { var dist = Math.sqrt(ip.distanceSquaredPoint(pt)); wallsDistArr.push([w, dist]); } }); // sort all walls the line from pt to oPt intersects, in order of proximity to pt wallsDistArr.sort(function (a, b) { var distA = a[1]; var distB = b[1]; if (distA === distB) return 0; else if (distA < distB) return -1; else return 1; }); // helper function -- copies a "path" (list of walls) up to a certain wall node function selectivelyCopyPath(path, nodeToStopAt) { var p = new Array(); var copyNoMore = false; for (var i = 0; i < path.length; i++) { var entry = path[i]; var wk = entry[0]; var w = fp.findNodeForKey(wk); var side = entry[1]; if (!copyNoMore) { p.push([w.data.key, side]); if (w.data.key === nodeToStopAt.data.key) { copyNoMore = true; } } } return p; } /** * Recursively walk counter-clockwise along all walls connected to firstWall until you get back to first wall * @param {go.Group | null} wall * @param {Array<any>} path * @param {go.Set<Array<any>>} possiblePaths * @param {go.Set<go.Group> | null} seenWalls * @param {go.Group} origWall