gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
977 lines (974 loc) • 231 kB
JavaScript
/*
* 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