feat: 后端全量更新 - 含所有本次需求

- Contract.php: 返回合约账户余额(balance_contract)
- My.php: 地址管理增加BTC/ETH
- AppContract.php: 一键平仓(closeall)
- AppProxy.php: 代理专属注册链接 + 分级权限(L1/L2)
- site.php: 手续费减半(0.018→0.009)
- agent_permission_setup.sql: 代理权限SQL
- crypto_news_crawler.py: 新闻自动采集脚本
This commit is contained in:
li
2026-03-30 20:16:32 +08:00
commit 1b24994e74
6721 changed files with 1308571 additions and 0 deletions
@@ -0,0 +1,198 @@
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* FLOOR PLANNER CODE: TEMPLATES - FURNITURE
* GraphObject templates for interactional furniture nodes (and their dependecies) used in the Floor Planner sample
* Includes Default Node (Furniture), MultiPurpose Node
*/
/*
* Furniture Node Dependencies:
* Node Tool Tip, Furniture Resize Adornment Template, Furniture Rotate Adornment Template, Invert Color
*/
// Node Tool Tip
function makeNodeToolTip() {
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;
}).ofObject())
)
}
// Furniture Resize Adornment
function makeFurnitureResizeAdornmentTemplate() {
var $ = go.GraphObject.make;
function makeHandle(alignment, cursor) {
return $(go.Shape, { alignment: alignment, cursor: cursor, figure: "Rectangle", desiredSize: new go.Size(7, 7), fill: "#ffffff", stroke: "#808080" },
new go.Binding("fill", "color"),
new go.Binding("stroke", "stroke"));
}
return $(go.Adornment, "Spot",
$(go.Placeholder),
makeHandle(go.Spot.Top, "n-resize"),
makeHandle(go.Spot.TopRight, "n-resize"),
makeHandle(go.Spot.BottomRight, "se-resize"),
makeHandle(go.Spot.Right, "e-resize"),
makeHandle(go.Spot.Bottom, "s-resize"),
makeHandle(go.Spot.BottomLeft, "sw-resize"),
makeHandle(go.Spot.Left, "w-resize"),
makeHandle(go.Spot.TopLeft, "nw-resize")
);
}
// Furniture Rotate Adornment
function makeFurnitureRotateAdornmentTemplate() {
var $ = go.GraphObject.make;
return $(go.Adornment,
$(go.Shape, "Circle", { cursor: "pointer", desiredSize: new go.Size(7, 7), fill: "#ffffff", stroke: "#808080" },
new go.Binding("fill", "", function (obj) { return (obj.adornedPart === null) ? "#ffffff" : obj.adornedPart.data.color; }).ofObject(),
new go.Binding("stroke", "", function (obj) { return (obj.adornedPart === null) ? "#000000" : obj.adornedPart.data.stroke; }).ofObject())
);
}
// Return inverted color (in hex) of a given hex code color; used to determine furniture node stroke color
function invertColor(hexnum) {
if (hexnum.includes('#')) hexnum = hexnum.substring(1);
if (hexnum.length != 6) {
console.error("Hex color must be six hex numbers in length.");
return false;
}
hexnum = hexnum.toUpperCase();
var splitnum = hexnum.split("");
var resultnum = "";
var simplenum = "FEDCBA9876".split("");
var complexnum = new Array();
complexnum.A = "5";
complexnum.B = "4";
complexnum.C = "3";
complexnum.D = "2";
complexnum.E = "1";
complexnum.F = "0";
for (i = 0; i < 6; i++) {
if (!isNaN(splitnum[i])) {
resultnum += simplenum[splitnum[i]];
} else if (complexnum[splitnum[i]]) {
resultnum += complexnum[splitnum[i]];
} else {
console.error("Hex colors must only include hex numbers 0-9, and A-F");
return false;
}
}
return '#' + resultnum;
}
/*
* Furniture Node Templates:
* Default Node, MultiPurpose Node
*/
// Default Node
function makeDefaultNode() {
var $ = go.GraphObject.make;
return $(go.Node, "Spot",
{
resizable: true,
rotatable: true,
toolTip: makeNodeToolTip(),
resizeAdornmentTemplate: makeFurnitureResizeAdornmentTemplate(),
rotateAdornmentTemplate: makeFurnitureRotateAdornmentTemplate(),
contextMenu: makeContextMenu(),
locationObjectName: "SHAPE",
resizeObjectName: "SHAPE",
rotateObjectName: "SHAPE",
minSize: new go.Size(5, 5),
locationSpot: go.Spot.Center,
selectionAdorned: false, // use a Binding on the Shape.stroke to show selection
doubleClick: function (e) {
if (e.diagram.floorplanUI) e.diagram.floorplanUI.hideShow("selectionInfoWindow")
}
},
// remember Node location
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
// move selected Node to Foreground layer so it's not obscuerd by non-selected Parts
new go.Binding("layerName", "isSelected", function (s) {
return s ? "Foreground" : "";
}).ofObject(),
$(go.Shape,
{
name: "SHAPE", stroke: "#000000",
fill: "rgba(128, 128, 128, 0.5)"
},
new go.Binding("figure", "shape"),
new go.Binding("geometryString", "geo"),
new go.Binding("width").makeTwoWay(),
new go.Binding("height").makeTwoWay(),
new go.Binding("angle").makeTwoWay(),
new go.Binding("fill", "color"),
new go.Binding("stroke", "isSelected", function (s, obj) {
return s ? go.Brush.lightenBy(obj.stroke, .5) : invertColor(obj.part.data.color);
}).ofObject())
)
}
// MultiPurpose Node
function makeMultiPurposeNode() {
var $ = go.GraphObject.make;
return $(go.Node, "Spot",
{
contextMenu: makeContextMenu(),
toolTip: makeNodeToolTip(),
locationSpot: go.Spot.Center,
resizeAdornmentTemplate: makeFurnitureResizeAdornmentTemplate(),
rotateAdornmentTemplate: makeFurnitureRotateAdornmentTemplate(),
locationObjectName: "SHAPE",
resizable: true,
rotatable: true,
resizeObjectName: "SHAPE",
rotateObjectName: "SHAPE",
minSize: new go.Size(5, 5),
selectionAdorned: false,
doubleClick: function (e) {
if (e.diagram.floorplanUI) e.diagram.floorplanUI.hideShow("selectionInfoWindow")
}
},
// remember location, angle, height, and width of the node
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
// move a selected part into the Foreground layer so it's not obscuerd by non-selected Parts
new go.Binding("layerName", "isSelected", function (s) { return s ? "Foreground" : ""; }).ofObject(),
$(go.Shape,
{ strokeWidth: 1, name: "SHAPE", fill: "rgba(128, 128, 128, 0.5)", },
new go.Binding("angle").makeTwoWay(),
new go.Binding("width").makeTwoWay(),
new go.Binding("height").makeTwoWay(),
new go.Binding("fill", "color"),
new go.Binding("stroke", "isSelected", function (s, obj) {
return s ? go.Brush.lightenBy(obj.stroke, .5) : invertColor(obj.part.data.color);
}).ofObject()
),
$(go.TextBlock,
{
margin: 5,
wrap: go.TextBlock.WrapFit,
textAlign: "center",
editable: true,
isMultiline: false,
stroke: '#454545',
font: "10pt sans-serif"
},
new go.Binding("text").makeTwoWay(),
new go.Binding("angle", "angle").makeTwoWay(),
new go.Binding("font", "height", function (height) {
if (height > 25) return "10pt sans-serif";
if (height < 25 && height > 15) return "8pt sans-serif";
else return "6pt sans-serif";
}),
new go.Binding("stroke", "color", function (color) { return invertColor(color); })
)
)
}
@@ -0,0 +1,388 @@
/*
* Copyright (C) 1998-2020 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 (i = 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.toolManager.contextMenuTool.mouseDownPoint) } }
),
// 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(),
doubleClick: function (e) {
if (e.diagram.floorplanUI) e.diagram.floorplanUI.hideShow("selectionInfoWindow");
},
toolTip: makeGroupToolTip()
},
new go.Binding("location", "loc"),
$(go.Panel, "Auto",
$(go.Shape, "RoundedRectangle", { fill: "rgba(128,128,128,0.15)", stroke: 'rgba(128, 128, 128, .05)', name: 'SHAPE', strokeCap: 'square' },
new go.Binding("fill", "isSelected", function (s, obj) {
return s ? "rgba(128, 128, 128, .15)" : "rgba(128, 128, 128, 0.10)";
}).ofObject()
),
$(go.Placeholder, { padding: 5 }) // extra padding around group members
)
)
}
/*
* Dependencies for Angle Nodes:
* Make Arc
*/
// Return arc geometry for Angle Nodes
function makeArc(node) {
var ang = node.data.angle;
var sweep = node.data.sweep;
var rad = Math.min(30, node.data.maxRadius);
if (typeof sweep === "number" && sweep > 0) {
var start = new go.Point(rad, 0).rotate(ang);
// this is much more efficient than calling go.GraphObject.make:
return new go.Geometry()
.add(new go.PathFigure(start.x + rad, start.y + rad) // start point
.add(new go.PathSegment(go.PathSegment.Arc,
ang, sweep, // angles
rad, rad, // center
rad, rad) // radius
))
.add(new go.PathFigure(0, 0))
.add(new go.PathFigure(2 * rad, 2 * rad));
} else { // make sure this arc always occupies the same circular area of RAD radius
return new go.Geometry()
.add(new go.PathFigure(0, 0))
.add(new go.PathFigure(2 * rad, 2 * rad));
}
}
/*
* Dependencies for Dimension Links
* Make Point Node
*/
// Return a Point Node (used for Dimension Links)
function makePointNode() {
var $ = go.GraphObject.make
return $(go.Node, "Position", new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify));
}
/*
* Dynamically appearing parts:
* Angle Node, Dimension Link
*/
// Return an Angle Node (for each angle ndeeded in the diagram, one angle node is made)
function makeAngleNode() {
var $ = go.GraphObject.make;
return $(go.Node, "Spot",
{ locationSpot: go.Spot.Center, locationObjectName: "SHAPE", selectionAdorned: false },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
$(go.Shape, "Circle", // placed where walls intersect, is invisible
{ name: "SHAPE", height: 0, width: 0 }),
$(go.Shape, // arc
{ strokeWidth: 1.5, fill: null },
new go.Binding("geometry", "", makeArc).ofObject(),
new go.Binding("stroke", "sweep", function (sweep) {
return (sweep % 45 < 1 || sweep % 45 > 44) ? "dodgerblue" : "lightblue";
})),
// Arc label panel
$(go.Panel, "Auto",
{ name: "ARCLABEL" },
// position the label in the center of the arc
new go.Binding("alignment", "sweep", function (sweep, panel) {
var rad = Math.min(30, panel.part.data.maxRadius);
var angle = panel.part.data.angle;
var cntr = new go.Point(rad, 0).rotate(angle + sweep / 2);
return new go.Spot(0.5, 0.5, cntr.x, cntr.y);
}),
// rectangle containing angle text
$(go.Shape,
{ fill: "white" },
new go.Binding("stroke", "sweep", function (sweep) {
return (sweep % 45 < 1 || sweep % 45 > 44) ? "dodgerblue" : "lightblue";
})),
// angle text
$(go.TextBlock,
{ font: "7pt sans-serif", margin: 2 },
new go.Binding("text", "sweep", function (sweep) {
return sweep.toFixed(2) + String.fromCharCode(176);
}))
)
);
}
// Returns a Dimension Link
function makeDimensionLink() {
var $ = go.GraphObject.make
return $(go.Link,
// link itself
$(go.Shape,
{ stroke: "gray", strokeWidth: 2, name: 'SHAPE' }),
// to arrow shape
$(go.Shape,
{ toArrow: "OpenTriangle", stroke: "gray", strokeWidth: 2 }),
$(go.Shape,
// from arrow shape
{ fromArrow: "BackwardOpenTriangle", stroke: "gray", strokeWidth: 2 }),
// dimension link text
$(go.TextBlock,
{ text: 'sometext', segmentOffset: new go.Point(0, -10), font: "13px sans-serif" },
new go.Binding("text", "", function (link) {
var floorplan = link.diagram;
if (floorplan) {
var fromPtNode = null; var toPtNode = null;
floorplan.pointNodes.iterator.each(function (node) {
if (node.data.key === link.data.from) fromPtNode = node;
if (node.data.key === link.data.to) toPtNode = node;
});
if (fromPtNode !== null) {
var fromPt = fromPtNode.location;
var toPt = toPtNode.location;
return floorplan.convertPixelsToUnits(Math.sqrt(fromPt.distanceSquaredPoint(toPt))).toFixed(2) + floorplan.model.modelData.unitsAbbreviation;
} return null;
} return null;
}).ofObject(),
// bind angle of textblock to angle of link -- always make text rightside up and readable
new go.Binding("angle", "angle", function (angle, link) {
if (angle > 90 && angle < 270) return (angle + 180) % 360;
return angle;
}),
// default poisiton text above / below dimension link based on angle
new go.Binding("segmentOffset", "angle", function (angle, textblock) {
var floorplan = textblock.part.diagram;
if (floorplan) {
var wall = floorplan.findPartForKey(textblock.part.data.wall);
if (wall.rotateObject.angle > 135 && wall.rotateObject.angle < 315) return new go.Point(0, 10);
return new go.Point(0, -10);
} return new go.Point(0, 0);
}).ofObject(),
// scale font size according to the length of the link
new go.Binding("font", "", function (link) {
var floorplan = link.diagram;
var fromPtNode = null; var toPtNode = null;
floorplan.pointNodes.iterator.each(function (node) {
if (node.data.key === link.data.from) fromPtNode = node;
if (node.data.key === link.data.to) toPtNode = node;
});
if (fromPtNode !== null) {
var fromPt = fromPtNode.location;
var toPt = toPtNode.location;
var distance = Math.sqrt(fromPt.distanceSquaredPoint(toPt));
if (distance > 40) return "13px sans-serif";
if (distance <= 40 && distance >= 20) return "11px sans-serif";
else return "9px sans-serif";
} return "13px sans-serif";
}).ofObject()
)
)
}
@@ -0,0 +1,499 @@
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* FLOOR PLANNER CODE: TEMPLATES - WALLS
* GraphObject templates for Wall Groups, Wall Part Nodes (and their dependecies) used in the Floor Planner sample
* Includes Wall Group, Palette Wall Node, Window Node, Door Node
*/
/*
* Wall Group Dependencies:
* Snap Walls, Find Closest Loc on Wall, Add Wall Part, Wall Part Drag Over, Wall Part Drag Away
*/
/*
* Drag computation function to snap walls to the grid properly while dragging
* @param {Node} part A reference to dragged Part
* @param {Point} pt The Point describing the proposed location
* @param {Point} gridPt Snapped location
*/
var snapWalls = function (part, pt, gridPt) {
var floorplan = part.diagram;
floorplan.updateWallDimensions();
floorplan.updateWallAngles();
floorplan.updateWall(part);
var grid = part.diagram.grid;
var sPt = part.data.startpoint.copy();
var ePt = part.data.endpoint.copy();
var dx = pt.x - part.location.x;
var dy = pt.y - part.location.y;
var newSpt = sPt.offset(dx, dy);
var newEpt = ePt.offset(dx, dy);
if (floorplan.toolManager.draggingTool.isGridSnapEnabled) {
newSpt = newSpt.snapToGridPoint(grid.gridOrigin, grid.gridCellSize);
newEpt = newEpt.snapToGridPoint(grid.gridOrigin, grid.gridCellSize);
}
floorplan.model.setDataProperty(part.data, "startpoint", newSpt);
floorplan.model.setDataProperty(part.data, "endpoint", newEpt);
return new go.Point((newSpt.x + newEpt.x) / 2, (newSpt.y + newEpt.y) / 2);
}
/*
* Find closest loc (to mouse point) on wall a wallPart can be dropped onto without extending beyond wall endpoints or intruding into another wallPart
* @param {Group} wall A reference to a Wall Group
* @param {Node} part A reference to a Wall Part Node -- i.e. Door Node, Window Node
*/
function findClosestLocOnWall(wall, part) {
var orderedConstrainingPts = []; // wall endpoints and wallPart endpoints
var startpoint = wall.data.startpoint.copy();
var endpoint = wall.data.endpoint.copy();
// store all possible constraining endpoints (wall endpoints and wallPart endpoints) in the order in which they appear (left/top to right/bottom)
var firstWallPt = ((startpoint.x + startpoint.y) <= (endpoint.x + endpoint.y)) ? startpoint : endpoint;
var lastWallPt = ((startpoint.x + startpoint.y) > (endpoint.x + endpoint.y)) ? startpoint : endpoint;
var wallPartEndpoints = [];
wall.memberParts.iterator.each(function (wallPart) {
var endpoints = getWallPartEndpoints(wallPart);
wallPartEndpoints.push(endpoints[0]);
wallPartEndpoints.push(endpoints[1]);
});
// sort all wallPartEndpoints by x coordinate left to right
wallPartEndpoints.sort(function (a, b) {
if ((a.x + a.y) > (b.x + b.y)) return 1;
if ((a.x + a.y) < (b.x + b.y)) return -1;
else return 0;
});
orderedConstrainingPts.push(firstWallPt);
orderedConstrainingPts = orderedConstrainingPts.concat(wallPartEndpoints);
orderedConstrainingPts.push(lastWallPt);
// go through all constraining points; if there's a free stretch along the wall "part" could fit in, remember it
var possibleStretches = [];
for (var i = 0; i < orderedConstrainingPts.length; i += 2) {
var point1 = orderedConstrainingPts[i];
var point2 = orderedConstrainingPts[i + 1];
var distanceBetween = Math.sqrt(point1.distanceSquaredPoint(point2));
if (distanceBetween >= part.data.length) possibleStretches.push({ pt1: point1, pt2: point2 });
}
// go through all possible stretches along the wall the part *could* fit in; find the one closest to the part's current location
var closestDist = Number.MAX_VALUE; var closestStretch = null;
for (var i = 0; i < possibleStretches.length; i++) {
var testStretch = possibleStretches[i];
var testPoint1 = testStretch.pt1;
var testPoint2 = testStretch.pt2;
var testDistance1 = Math.sqrt(testPoint1.distanceSquaredPoint(part.location));
var testDistance2 = Math.sqrt(testPoint2.distanceSquaredPoint(part.location));
if (testDistance1 < closestDist) {
closestDist = testDistance1;
closestStretch = testStretch;
}
if (testDistance2 < closestDist) {
closestDist = testDistance2;
closestStretch = testStretch;
}
}
// Edge Case: If there's no space for the wallPart, return null
if (closestStretch === null) return null;
// using the closest free stretch along the wall, calculate endpoints that make the stretch's line segment, then project part.location onto the segment
var closestStretchLength = Math.sqrt(closestStretch.pt1.distanceSquaredPoint(closestStretch.pt2));
var offset = part.data.length / 2;
var point1 = new go.Point(closestStretch.pt1.x + ((offset / closestStretchLength) * (closestStretch.pt2.x - closestStretch.pt1.x)),
closestStretch.pt1.y + ((offset / closestStretchLength) * (closestStretch.pt2.y - closestStretch.pt1.y)));
var point2 = new go.Point(closestStretch.pt2.x + ((offset / closestStretchLength) * (closestStretch.pt1.x - closestStretch.pt2.x)),
closestStretch.pt2.y + ((offset / closestStretchLength) * (closestStretch.pt1.y - closestStretch.pt2.y)));
var newLoc = part.location.copy().projectOntoLineSegmentPoint(point1, point2);
return newLoc;
}
// MouseDrop event for wall groups; if a door or window is dropped on a wall, add it to the wall group
// Do not allow dropping wallParts that would extend beyond wall endpoints or intrude into another wallPart
var addWallPart = function (e, wall) {
var floorplan = e.diagram;
var wallPart = floorplan.selection.first();
if ((wallPart && (wallPart.category === "WindowNode" || wallPart.category === "DoorNode") && wallPart.containingGroup === null)) {
var newLoc = findClosestLocOnWall(wall, wallPart);
if (newLoc !== null) {
wall.findObject("SHAPE").stroke = makeBrush(wall.data);
floorplan.model.setDataProperty(wallPart.data, "group", wall.data.key);
wallPart.location = newLoc.projectOntoLineSegmentPoint(wall.data.startpoint, wall.data.endpoint);
wallPart.angle = wall.rotateObject.angle;
if (wallPart.category === "WindowNode") floorplan.model.setDataProperty(wallPart.data, "height", wall.data.thickness);
if (wallPart.category === "DoorNode") floorplan.model.setDataProperty(wallPart.data, "doorOpeningHeight", wall.data.thickness);
} else {
floorplan.remove(wallPart);
alert("There's not enough room on the wall!");
return;
}
}
if (floorplan.floorplanUI) floorplan.floorplanUI.setSelectionInfo(floorplan.selection.first(), floorplan);
floorplan.updateWallDimensions();
}
// MouseDragEnter event for walls; if a door or window is dragged over a wall, highlight the wall and change its angle
var wallPartDragOver = function (e, wall) {
var floorplan = e.diagram;
var parts = floorplan.toolManager.draggingTool.draggingParts;
parts.iterator.each(function (part) {
if ((part.category === "WindowNode" || part.category === "DoorNode") && part.containingGroup === null) {
wall.findObject("SHAPE").stroke = "lightblue";
part.angle = wall.rotateObject.angle;
}
});
}
// MouseDragLeave event for walls; if a wall part is dragged past a wall, unhighlight the wall and change back the wall part's angle to 0
var wallPartDragAway = function (e, wall) {
var floorplan = e.diagram;
wall.findObject("SHAPE").stroke = makeBrush(wall.data);
var parts = floorplan.toolManager.draggingTool.draggingParts;
parts.iterator.each(function (part) {
if ((part.category === "WindowNode" || part.category === "DoorNode") && part.containingGroup === null) part.angle = 0
});
}
/*
* Wall Group Template
*/
// Wall Group
function makeWallGroup() {
var $ = go.GraphObject.make;
return $(go.Group, "Spot",
{
contextMenu: makeContextMenu(),
toolTip: makeGroupToolTip(),
selectionObjectName: "SHAPE",
rotateObjectName: "SHAPE",
locationSpot: go.Spot.Center,
reshapable: true,
minSize: new go.Size(1, 1),
dragComputation: snapWalls,
selectionAdorned: false,
mouseDrop: addWallPart,
mouseDragEnter: wallPartDragOver,
mouseDragLeave: wallPartDragAway,
doubleClick: function (e) { if (e.diagram.floorplanUI) e.diagram.floorplanUI.hideShow("selectionInfoWindow"); }
},
$(go.Shape,
{ name: "SHAPE" },
new go.Binding("strokeWidth", "thickness"),
new go.Binding("stroke", "isSelected", function (s, obj) {
if (obj.part.containingGroup != null) {
var group = obj.part.containingGroup;
if (s) { group.data.isSelected = true; }
}
return s ? "dodgerblue" : makeBrush(obj.part.data);
}).ofObject()
))
}
function makeBrush(data) {
return "black";
}
/*
* Wall Part Node Dependencies:
* Get Wall Part Endpoints, Get Wall Part Stretch, Drag Wall Parts (Drag Computation Function),
* Wall Part Resize Adornment, Door Selection Adornment (Door Nodes only)
*/
/*
* Find and return an array of the endpoints of a given wallpart (window or door)
* @param {Node} wallPart A Wall Part Node -- i.e. Door Node, Window Node
*/
function getWallPartEndpoints(wallPart) {
var loc = wallPart.location;
var partLength = wallPart.data.length;
if (wallPart.containingGroup !== null) var angle = wallPart.containingGroup.rotateObject.angle;
else var angle = 180;
var point1 = new go.Point((loc.x + (partLength / 2)), loc.y);
var point2 = new go.Point((loc.x - (partLength / 2)), loc.y);
point1.offset(-loc.x, -loc.y).rotate(angle).offset(loc.x, loc.y);
point2.offset(-loc.x, -loc.y).rotate(angle).offset(loc.x, loc.y);
var arr = []; arr.push(point1); arr.push(point2);
return arr;
}
/*
* Returns a "stretch" (2 Points) that constrains a wallPart (door or window), comprised of "part"'s containing wall endpoints or other wallPart endpoints
* @param {Node} part A Wall Part Node -- i.e. Door Node, Window Node, that is attached to a wall
*/
function getWallPartStretch(part) {
var wall = part.containingGroup;
var startpoint = wall.data.startpoint.copy();
var endpoint = wall.data.endpoint.copy();
// sort all possible endpoints into either left/above or right/below
var leftOrAbove = new go.Set(/*go.Point*/); var rightOrBelow = new go.Set(/*go.Point*/);
wall.memberParts.iterator.each(function (wallPart) {
if (wallPart.data.key !== part.data.key) {
var endpoints = getWallPartEndpoints(wallPart);
for (var i = 0; i < endpoints.length; i++) {
if (endpoints[i].x < part.location.x || (endpoints[i].y > part.location.y && endpoints[i].x === part.location.x)) leftOrAbove.add(endpoints[i]);
else rightOrBelow.add(endpoints[i]);
}
}
});
// do the same with the startpoint and endpoint of the dragging part's wall
if (parseFloat(startpoint.x.toFixed(2)) < parseFloat(part.location.x.toFixed(2)) || (startpoint.y > part.location.y && parseFloat(startpoint.x.toFixed(2)) === parseFloat(part.location.x.toFixed(2)))) leftOrAbove.add(startpoint);
else rightOrBelow.add(startpoint);
if (parseFloat(endpoint.x.toFixed(2)) < parseFloat(part.location.x.toFixed(2)) || (endpoint.y > part.location.y && parseFloat(endpoint.x.toFixed(2)) === parseFloat(part.location.x.toFixed(2)))) leftOrAbove.add(endpoint);
else rightOrBelow.add(endpoint);
// of each set, find the closest point to the dragging part
var leftOrAbovePt; var closestDistLeftOrAbove = Number.MAX_VALUE;
leftOrAbove.iterator.each(function (point) {
var distance = Math.sqrt(point.distanceSquaredPoint(part.location));
if (distance < closestDistLeftOrAbove) {
closestDistLeftOrAbove = distance;
leftOrAbovePt = point;
}
});
var rightOrBelowPt; var closestDistRightOrBelow = Number.MAX_VALUE;
rightOrBelow.iterator.each(function (point) {
var distance = Math.sqrt(point.distanceSquaredPoint(part.location));
if (distance < closestDistRightOrBelow) {
closestDistRightOrBelow = distance;
rightOrBelowPt = point;
}
});
var stretch = { point1: leftOrAbovePt, point2: rightOrBelowPt };
return stretch;
}
/*
* Drag computation function for WindowNodes and DoorNodes; ensure wall parts stay in walls when dragged
* @param {Node} part A reference to dragged Part
* @param {Point} pt The Point describing the proposed location
* @param {Point} gridPt Snapped location
*/
var dragWallParts = function (part, pt, gridPt) {
if (part.containingGroup !== null && part.containingGroup.category === 'WallGroup') {
var floorplan = part.diagram;
// Edge Case: if part is not on its wall (due to incorrect load) snap part.loc onto its wall immediately; ideally this is never called
var wall = part.containingGroup;
var wStart = wall.data.startpoint;
var wEnd = wall.data.endpoint;
var dist1 = Math.sqrt(wStart.distanceSquaredPoint(part.location));
var dist2 = Math.sqrt(part.location.distanceSquaredPoint(wEnd));
var totalDist = Math.sqrt(wStart.distanceSquaredPoint(wEnd));
if (dist1 + dist2 !== totalDist) part.location = part.location.copy().projectOntoLineSegmentPoint(wStart, wEnd);
// main behavior
var stretch = getWallPartStretch(part);
var leftOrAbovePt = stretch.point1;
var rightOrBelowPt = stretch.point2;
// calc points along line created by the endpoints that are half the width of the moving window/door
var totalLength = Math.sqrt(leftOrAbovePt.distanceSquaredPoint(rightOrBelowPt));
var distance = (part.data.length / 2);
var point1 = new go.Point(leftOrAbovePt.x + ((distance / totalLength) * (rightOrBelowPt.x - leftOrAbovePt.x)),
leftOrAbovePt.y + ((distance / totalLength) * (rightOrBelowPt.y - leftOrAbovePt.y)));
var point2 = new go.Point(rightOrBelowPt.x + ((distance / totalLength) * (leftOrAbovePt.x - rightOrBelowPt.x)),
rightOrBelowPt.y + ((distance / totalLength) * (leftOrAbovePt.y - rightOrBelowPt.y)));
// calc distance from pt to line (part's wall) - use point to 2pt line segment distance formula
var distFromWall = Math.abs(((wEnd.y - wStart.y) * pt.x) - ((wEnd.x - wStart.x) * pt.y) + (wEnd.x * wStart.y) - (wEnd.y * wStart.x)) /
Math.sqrt(Math.pow((wEnd.y - wStart.y), 2) + Math.pow((wEnd.x - wStart.x), 2));
var tolerance = (20 * wall.data.thickness < 100) ? (20 * wall.data.thickness) : 100;
// if distance from pt to line > some tolerance, detach the wallPart from the wall
if (distFromWall > tolerance) {
part.containingGroup = null;
delete part.data.group;
part.angle = 0;
floorplan.pointNodes.iterator.each(function (node) { floorplan.remove(node) });
floorplan.dimensionLinks.iterator.each(function (link) { floorplan.remove(link) });
floorplan.pointNodes.clear();
floorplan.dimensionLinks.clear();
floorplan.updateWallDimensions();
if (floorplan.floorplanUI) floorplan.floorplanUI.setSelectionInfo(part);
}
// project the proposed location onto the line segment created by the new points (ensures wall parts are constrained properly when dragged)
pt = pt.copy().projectOntoLineSegmentPoint(point1, point2);
floorplan.skipsUndoManager = true;
floorplan.startTransaction("set loc");
floorplan.model.setDataProperty(part.data, "loc", go.Point.stringify(pt));
floorplan.commitTransaction("set loc");
floorplan.skipsUndoManager = false;
floorplan.updateWallDimensions(); // update the dimension links created by having this wall part selected
} return pt;
}
// Resize Adornment for Wall Part Nodes
function makeWallPartResizeAdornment() {
var $ = go.GraphObject.make;
return $(go.Adornment, "Spot",
{ name: "WallPartResizeAdornment" },
$(go.Placeholder),
$(go.Shape, { alignment: go.Spot.Left, cursor: "w-resize", figure: "Diamond", desiredSize: new go.Size(7, 7), fill: "#ffffff", stroke: "#808080" }),
$(go.Shape, { alignment: go.Spot.Right, cursor: "e-resize", figure: "Diamond", desiredSize: new go.Size(7, 7), fill: "#ffffff", stroke: "#808080" })
);
}
// Selection Adornment for Door Nodes
function makeDoorSelectionAdornment() {
var $ = go.GraphObject.make;
return $(go.Adornment, "Vertical",
{ name: "DoorSelectionAdornment" },
$(go.Panel, "Auto",
$(go.Shape, { fill: null, stroke: null }),
$(go.Placeholder)),
$(go.Panel, "Horizontal", { defaultStretch: go.GraphObject.Vertical },
$("Button",
$(go.Picture, { source: "icons/flipDoorOpeningLeft.png", column: 0, desiredSize: new go.Size(12, 12) },
new go.Binding("source", "", function (obj) {
if (obj.adornedPart === null) return "icons/flipDoorOpeningRight.png";
else if (obj.adornedPart.data.swing === "left") return "icons/flipDoorOpeningRight.png";
else return "icons/flipDoorOpeningLeft.png";
}).ofObject()
),
{
click: function (e, obj) {
var floorplan = obj.part.diagram;
floorplan.startTransaction("flip door");
var door = obj.part.adornedPart;
if (door.data.swing === "left") floorplan.model.setDataProperty(door.data, "swing", "right");
else floorplan.model.setDataProperty(door.data, "swing", "left");
floorplan.commitTransaction("flip door");
},
toolTip: $(go.Adornment, "Auto",
$(go.Shape, { fill: "#FFFFCC" }),
$(go.TextBlock, { margin: 4, text: "Flip Door Opening" }
))
},
new go.Binding("visible", "", function (obj) { return (obj.adornedPart === null) ? false : (obj.adornedPart.containingGroup !== null); }).ofObject()
),
$("Button",
$(go.Picture, { source: "icons/flipDoorSide.png", column: 0, desiredSize: new go.Size(12, 12) }),
{
click: function (e, obj) {
var floorplan = obj.part.diagram;
floorplan.startTransaction("rotate door");
var door = obj.part.adornedPart;
door.angle = (door.angle + 180) % 360;
floorplan.commitTransaction("rotate door");
},
toolTip: $(go.Adornment, "Auto",
$(go.Shape, { fill: "#FFFFCC" }),
$(go.TextBlock, { margin: 4, text: "Flip Door Side" }
))
}
),
new go.Binding("visible", "", function (obj) { return (obj.adornedPart === null) ? false : (obj.adornedPart.containingGroup !== null); }).ofObject()
)
);
}
/*
* Wall Part Nodes:
* Window Node, Door Node, Palette Wall Node
*/
// Window Node
function makeWindowNode() {
var $ = go.GraphObject.make;
return $(go.Node, "Spot",
{
contextMenu: makeContextMenu(),
selectionObjectName: "SHAPE",
selectionAdorned: false,
locationSpot: go.Spot.Center,
toolTip: makeNodeToolTip(),
minSize: new go.Size(5, 5),
resizable: true,
resizeAdornmentTemplate: makeWallPartResizeAdornment(),
resizeObjectName: "SHAPE",
rotatable: false,
doubleClick: function (e) { if (e.diagram.floorplanUI) e.diagram.floorplanUI.hideShow("selectionInfoWindow"); },
dragComputation: dragWallParts,
layerName: 'Foreground' // make sure windows are always in front of walls
},
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
new go.Binding("angle").makeTwoWay(),
$(go.Shape,
{ name: "SHAPE", fill: "white", strokeWidth: 0 },
new go.Binding("width", "length").makeTwoWay(),
new go.Binding("height").makeTwoWay(),
new go.Binding("stroke", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject(),
new go.Binding("fill", "isSelected", function (s, obj) { return s ? "lightgray" : "white"; }).ofObject()
),
$(go.Shape,
{ name: "LINESHAPE", fill: "darkgray", strokeWidth: 0, height: 10 },
new go.Binding("width", "length", function (width, obj) { return width - 10; }), // 5px padding each side
new go.Binding("height", "height", function (height, obj) { return (height / 5); }),
new go.Binding("stroke", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject()
)
);
}
// Door Node
function makeDoorNode() {
var $ = go.GraphObject.make;
return $(go.Node, "Spot",
{
contextMenu: makeContextMenu(),
selectionObjectName: "SHAPE",
selectionAdornmentTemplate: makeDoorSelectionAdornment(),
locationSpot: go.Spot.BottomCenter,
resizable: true,
resizeObjectName: "OPENING_SHAPE",
toolTip: makeNodeToolTip(),
minSize: new go.Size(10, 10),
doubleClick: function (e) { if (e.diagram.floorplanUI) e.diagram.floorplanUI.hideShow("selectionInfoWindow"); },
dragComputation: dragWallParts,
resizeAdornmentTemplate: makeWallPartResizeAdornment(),
layerName: 'Foreground' // make sure windows are always in front of walls
},
// remember location of the Node
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
new go.Binding("angle").makeTwoWay(),
// the door's locationSpot is affected by it's openingHeight, which is affected by the thickness of its containing wall
new go.Binding("locationSpot", "doorOpeningHeight", function (doh, obj) { return new go.Spot(0.5, 1, 0, -(doh / 2)); }),
// this is the shape that reprents the door itself and its swing
$(go.Shape,
{ name: "SHAPE" },
new go.Binding("width", "length"),
new go.Binding("height", "length").makeTwoWay(),
new go.Binding("stroke", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject(),
new go.Binding("fill", "color"),
new go.Binding("geometryString", "swing", function (swing) {
if (swing === "left") return "F1 M0,0 v-150 a150,150 0 0,1 150,150 ";
else return "F1 M275,175 v-150 a150,150 0 0,0 -150,150 ";
})
),
// door opening shape
$(go.Shape,
{
name: "OPENING_SHAPE", fill: "white",
strokeWidth: 0, height: 5, width: 40,
alignment: go.Spot.BottomCenter, alignmentFocus: go.Spot.Center
},
new go.Binding("height", "doorOpeningHeight").makeTwoWay(),
new go.Binding("stroke", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject(),
new go.Binding("fill", "isSelected", function (s, obj) { return s ? "lightgray" : "white"; }).ofObject(),
new go.Binding("width", "length").makeTwoWay()
)
);
}
// Palette Wall Node (becomes WallGroup when dropped from Palette onto diagram)
function makePaletteWallNode() {
var $ = go.GraphObject.make;
return $(go.Node, "Spot",
{ selectionAdorned: false },
$(go.Shape,
{ name: "SHAPE", fill: "black", strokeWidth: 0, height: 10, figure: "Rectangle" },
new go.Binding("width", "length").makeTwoWay(),
new go.Binding("height").makeTwoWay(),
new go.Binding("fill", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject(),
new go.Binding("stroke", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject())
);
}
@@ -0,0 +1,134 @@
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* FLOOR PLANNER - WALL BUILDING TOOL
* Used to construct new Walls in a Floorplan with mouse clicking / mouse point
* Depends on functionality in Floorplan.js
*/
// Constructor
function WallBuildingTool() {
go.Tool.call(this);
this.name = "WallBuilding";
this._startPoint = null;
this._endPoint = null;
this._wallReshapingTool = null;
} go.Diagram.inherit(WallBuildingTool, go.Tool);
// Get / set the current startPoint
Object.defineProperty(WallBuildingTool.prototype, "startPoint", {
get: function () { return this._startPoint; },
set: function (val) { this._startPoint = val; }
});
// Get / set the current endPoint
Object.defineProperty(WallBuildingTool.prototype, "endPoint", {
get: function () { return this._endPoint; },
set: function (val) { this._endPoint = val; }
});
// Get / set the diagram's wallReshapingTool
Object.defineProperty(WallBuildingTool.prototype, "wallReshapingTool", {
get: function () { return this._wallReshapingTool; },
set: function (val) { this._wallReshapingTool = val; }
});
// Tool can start iff diagram exists, is editable, and tool is enabled
WallBuildingTool.prototype.canStart = function () {
var diagram = this.diagram;
if (diagram !== null && !diagram.isReadOnly && this.isEnabled) return true;
return false;
}
// Start transaction, capture the mouse, use a crosshair cursor
WallBuildingTool.prototype.doActivate = function () {
this.endPoint = null;
this.startTransaction(this.name);
this.isMouseCaptured = true;
var diagram = this.diagram;
var tool = this;
// update wallThickness, based on the current value of the HTML input element
// pre-condition: diagram.floorplanUI exists
if (diagram.floorplanUI) {
var el = document.getElementById(diagram.floorplanUI.state.wallThicknessInputId);
if (isNaN(el.value) || el.value === null || el.value === '' || el.value === undefined) el.value = diagram.convertPixelsToUnits(5);
diagram.model.setDataProperty(diagram.model.modelData, "wallThickness", diagram.convertUnitsToPixels(parseFloat(el.value)));
}
else diagram.model.setDataProperty(diagram.model.modelData, "wallThickness", diagram.convertUnitsToPixels(parseFloat(5)));
// assign startpoint based on grid
var point1 = tool.diagram.lastInput.documentPoint;
var gs = diagram.model.modelData.gridSize;
if (!(tool.diagram.toolManager.draggingTool.isGridSnapEnabled)) gs = 1;
var newx = gs * Math.round(point1.x / gs);
var newy = gs * Math.round(point1.y / gs);
var newPoint1 = new go.Point(newx, newy);
this.startPoint = newPoint1;
this.wallReshapingTool = tool.diagram.toolManager.mouseDownTools.elt(3);
// Default functionality:
this.isActive = true;
}
// When user clicks, add wall model data to model and initialize a Wall Reshaping Tool to handle the shaping of its construction
WallBuildingTool.prototype.doMouseDown = function () {
var diagram = this.diagram;
var tool = this;
tool.diagram.currentCursor = 'crosshair';
var data = { key: "wall", category: "WallGroup", caption: "Wall", type: "Wall", startpoint: tool.startPoint, endpoint: tool.startPoint, thickness: parseFloat(diagram.model.modelData.wallThickness), isGroup: true, notes: "" };
this.diagram.model.addNodeData(data);
var wall = diagram.findPartForKey(data.key);
diagram.updateWall(wall);
var part = diagram.findPartForData(data);
// set the TransactionResult before raising event, in case it changes the result or cancels the tool
tool.transactionResult = tool.name;
diagram.raiseDiagramEvent('PartCreated', part);
// start the wallReshapingTool, tell it what wall it's reshaping (more accurately, the shape that will have the reshape handle)
tool.wallReshapingTool.isEnabled = true;
diagram.select(part);
tool.wallReshapingTool.isBuilding = true;
tool.wallReshapingTool.adornedShape = part.findObject("SHAPE");
tool.wallReshapingTool.doActivate();
}
// If user presses Esc key, cancel the wall building
WallBuildingTool.prototype.doKeyDown = function () {
var diagram = this.diagram;
var e = diagram.lastInput;
if (e.key === "Esc") {
var wall = diagram.selection.first();
diagram.remove(wall);
diagram.pointNodes.iterator.each(function (node) { diagram.remove(node); });
diagram.dimensionLinks.iterator.each(function (link) { diagram.remove(link); });
diagram.pointNodes.clear();
diagram.dimensionLinks.clear();
this.doDeactivate();
}
go.Tool.prototype.doKeyDown.call(this);
}
// When the mouse moves, reshape the wall
WallBuildingTool.prototype.doMouseMove = function () {
this.wallReshapingTool.doMouseMove();
}
// End transaction
WallBuildingTool.prototype.doDeactivate = function () {
var diagram = this.diagram;
this.diagram.currentCursor = "";
this.diagram.isMouseCaptured = false;
this.wallReshapingTool.isEnabled = false;
this.wallReshapingTool.adornedShape = null;
this.wallReshapingTool.doDeactivate();
this.wallReshapingTool.isBuilding = false;
diagram.updateWallDimensions();
this.stopTransaction(this.name);
this.isActive = false; // Default functionality
}
@@ -0,0 +1,595 @@
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* FLOOR PLANNER: WALL RESHAPING TOOL
* Used to reshape walls via their endpoints in a Floorplan
* Depends on functionality in Floorplan.js
*/
// Constructor
function WallReshapingTool() {
go.Tool.call(this);
this.name = "WallReshaping";
var h = 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
} go.Diagram.inherit(WallReshapingTool, go.Tool);
// Get the archetype for the handle (a Shape)
Object.defineProperty(WallReshapingTool.prototype, "handleArchetype", {
get: function () { return this._handleArchetype; }
});
// Get / set current handle being used to reshape the wall
Object.defineProperty(WallReshapingTool.prototype, "handle", {
get: function () { return this._handle; },
set: function (val) { this._handle = val; }
});
// Get / set adorned shape (shape of the Wall Group being reshaped)
Object.defineProperty(WallReshapingTool.prototype, "adornedShape", {
get: function () { return this._adornedShape; },
set: function (val) { this._adornedShape = val; }
});
// Get / set current angle
Object.defineProperty(WallReshapingTool.prototype, "angle", {
get: function () { return this._angle; },
set: function (val) { this._angle = val; }
});
// Get / set length of the wall being reshaped (used only with SHIFT + drag)
Object.defineProperty(WallReshapingTool.prototype, "length", {
get: function () { return this._length; },
set: function (val) { this._length = val; }
});
// Get / set the name of the object being reshaped
Object.defineProperty(WallReshapingTool.prototype, "reshapeObjectName", {
get: function () { return this._reshapeObjectName; },
set: function (val) { this._reshapeObjectName = val; }
});
// Get / set flag telling tool whether it's reshaping a new wall (isBuilding = true) or reshaping an old wall (isBuilding = false)
Object.defineProperty(WallReshapingTool.prototype, "isBuilding", {
get: function () { return this._isBuilding; },
set: function (val) { this._isBuilding = val; }
});
// Get set loc data for wallParts to return to if reshape is cancelled
Object.defineProperty(WallReshapingTool.prototype, "returnData", {
get: function () { return this._returnData; },
set: function (val) { this._returnData = val; }
});
// Get / set the point to return the reshaping wall endpoint to if reshape is cancelled
Object.defineProperty(WallReshapingTool.prototype, "returnPoint", {
get: function () { return this._returnPoint; },
set: function (val) { this._returnPoint = val; }
});
/*
* Places reshape handles on either end of a wall node
* @param {part} The wall to adorn
*/
WallReshapingTool.prototype.updateAdornments = function (part) {
if (part === null || part instanceof go.Link) return;
if (part.isSelected && !this.diagram.isReadOnly) {
var selelt = part.findObject(this.reshapeObjectName);
if (selelt.part.data.category === "WallGroup") {
var 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
var geo = selelt.geometry;
var b = geo.bounds;
// update the size of the adornment
adornment.findObject("BODY").desiredSize = b.size;
adornment.elements.each(function (h) {
if (h.name === undefined) return;
var x = 0;
var y = 0;
switch (h.name) {
case 'sPt': x = geo.startX; y = geo.startY; break;
case 'ePt': x = geo.endX; y = geo.endY; break;
}
var xCheck = Math.min((x - b.x) / b.width, 1);
var yCheck = 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
WallReshapingTool.prototype.canStart = function () {
if (!this.isEnabled) return false;
var diagram = this.diagram;
if (diagram === null || diagram.isReadOnly) return false;
if (!diagram.allowReshape) return false;
if (!diagram.lastInput.left) return false;
var h = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
return (h !== null || this.isBuilding);
}
// Start a new transaction for the wall reshaping
WallReshapingTool.prototype.doActivate = function () {
var diagram = this.diagram;
if (diagram === null) return;
if (this.isBuilding) {
// this.adornedShape has already been set in WallBuildingTool's doMouseDown function
var wall = 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;
var shape = this.handle.part.adornedObject;
var wall = 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)
var wallParts = wall.memberParts;
if (wallParts.count != 0) {
var locationsMap = new go.Map(/*"string", go.Point*/);
wallParts.iterator.each(function (wallPart) {
locationsMap.add(wallPart.data.key, wallPart.location);
});
this.returnData = locationsMap;
}
}
this.calcAngleAndLengthFromHandle(diagram.firstInput.documentPoint);
this.startTransaction(this.name);
this.isActive = true;
}
// Adjust the handle's coordinates, along with the wall's points
WallReshapingTool.prototype.doMouseMove = function () {
var diagram = this.diagram;
var tool = this;
var wall = this.handle.part.adornedPart;
if (this.isActive && diagram !== null) {
var mousePt = diagram.lastInput.documentPoint;
tool.calcAngleAndLengthFromHandle(mousePt); // sets this.angle and this.length (useful for when SHIFT is held)
var newpt = diagram.lastInput.documentPoint;
if (diagram.floorplanUI) diagram.floorplanUI.setSelectionInfo(this.adornedShape.part, diagram); // update selection info window
this.reshape(newpt);
}
diagram.updateWallAngles();
}
// Does one final reshape, commits the transaction, then stops the tool
WallReshapingTool.prototype.doMouseUp = function () {
var diagram = this.diagram;
if (this.isActive && diagram !== null) {
var newpt = diagram.lastInput.documentPoint;
this.reshape(newpt);
this.transactionResult = this.name; // success
}
this.stopTool();
}
// End the wall reshaping transaction
WallReshapingTool.prototype.doDeactivate = function () {
var diagram = this.diagram;
var returnData = this.returnData;
// if a wall reshaped to length < 1 px, remove it
var wall = this.handle.part.adornedPart;
var sPt = wall.data.startpoint;
var ePt = wall.data.endpoint;
var length = 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
var wallDimensionLinkPointNodes = [];
diagram.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;
diagram.updateWall(wall);
if (this.returnData) {
this.returnData.iterator.each(function (kvp) {
var key = kvp.key;
var loc = kvp.value;
var wallPart = diagram.findPartForKey(key);
wallPart.location = loc;
wallPart.rotateObject.angle = wall.rotateObject.angle;
});
}
diagram.commitTransaction("reset to old data");
diagram.skipsUndoManager = false;
}
// remove guide line points
var glPoints = this.diagram.findNodesByExample({ category: 'GLPointNode' });
diagram.removeParts(glPoints, true);
diagram.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
*/
WallReshapingTool.prototype.makeAdornment = function (selelt) {
var adornment = new go.Adornment;
adornment.type = go.Panel.Spot;
adornment.locationObjectName = "BODY";
adornment.locationSpot = go.Spot.Center;
var h = 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
WallReshapingTool.prototype.makeHandle = function () {
var h = 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
*/
WallReshapingTool.prototype.calcAngleAndLengthFromHandle = function (mousePt) {
var tool = this;
var diagram = this.diagram;
var h = this.handle;
var otherH;
var node = this.adornedShape.part;
var adornments = node.adornments.iterator;
var 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
var otherHandlePt = otherH.getDocumentPoint(go.Spot.Center);
deltaY = mousePt.y - otherHandlePt.y
deltaX = mousePt.x - otherHandlePt.x
var angle = 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;
var distanceBetween = 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
*/
WallReshapingTool.prototype.snapPointToGrid = function (point) {
var diagram = this.diagram;
var newx = diagram.model.modelData.gridSize * Math.round(point.x / diagram.model.modelData.gridSize);
var newy = diagram.model.modelData.gridSize * Math.round(point.y / diagram.model.modelData.gridSize);
var newPt = 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
*/
WallReshapingTool.prototype.reshape = function (newPoint) {
var diagram = this.diagram;
var tool = this;
var shape = this.adornedShape;
var node = shape.part;
// if user holds SHIFT, make angle between startPoint / endPoint and the horizontal line a multiple of 45
if (this.diagram.lastInput.shift) {
var sPt; // 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;
var oldGridSize = diagram.model.modelData.gridSize;
var gridSize = 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()
var angle = tool.angle;
var length = tool.length;
// snap to 90 degrees
if (angle > 67.5 && angle < 112.5) {
var newy = 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) {
var newx = 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) {
var newy = 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) {
var newx = 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) {
var newx = (Math.sin(.785) * length);
newx = gridSize * Math.round(newx / gridSize) + sPt.x;
var newy = (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) {
var newx = (Math.sin(.785) * length);
newx = sPt.x - (gridSize * Math.round(newx / gridSize));
var newy = (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) {
var newx = (Math.sin(.785) * length);
newx = sPt.x - (gridSize * Math.round(newx / gridSize));
var newy = (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) {
var newx = (Math.sin(.785) * length);
newx = sPt.x + (gridSize * Math.round(newx / gridSize));
var newy = (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);
var type = 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();
diagram.updateWallDimensions();
}
/*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, stationaryPoint, movingPoint, newPoint, diagram, tool) {
var wallParts = wall.memberParts;
var arr = [];
var oldAngle = 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
var distancesMap = new go.Map(/*"string", "number"*/);
var closestPart = null; var closestDistance = Number.MAX_VALUE;
for (var i = 0; i < arr.length; i++) {
var part = arr[i];
var distanceToStationaryPt = 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
var endpoints = getWallPartEndpoints(part);
var distanceToMovingPt = 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) {
var loc = closestPart.location;
var partLength = closestPart.data.length;
var angle = oldAngle;
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 distance1 = Math.sqrt(stationaryPoint.distanceSquaredPoint(point1));
var distance2 = Math.sqrt(stationaryPoint.distanceSquaredPoint(point2));
var minLength; var newLoc;
if (distance1 > distance2) {
minLength = distance1;
newLoc = point1;
} else {
minLength = distance2;
newLoc = point2;
}
var testDistance = 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);
diagram.updateWall(wall);
// calculate the new angle offset
var newAngle = wall.rotateObject.angle;
var angleOffset = newAngle - oldAngle;
// for each wallPart, maintain relative distance from the stationaryPoint
distancesMap.iterator.each(function (kvp) {
var wallPart = diagram.findPartForKey(kvp.key);
var distance = kvp.value;
var wallLength = Math.sqrt(stationaryPoint.distanceSquaredPoint(movingPoint));
var newLoc = 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;
});
}
// Show if the wall (at the adjustment handle being moved) lines up with other wall edges
WallReshapingTool.prototype.showMatches = function () {
//if (!(document.getElementById('wallGuidelinesCheckbox').checked)) return;
var diagram = this.diagram;
if (!diagram.model.modelData.preferences.showWallGuidelines) return;
var tool = this;
var wall = this.adornedShape.part;
var comparePt;
if (this.handle.name === 'sPt') comparePt = wall.data.startpoint;
else comparePt = wall.data.endpoint;
// the wall attached to the handle being manipulated
var hWall = this.adornedShape.part;
// delete any old guideline points (these are used to show guidelines, must be cleared before a new guideline can be shown)
var glPoints = diagram.findNodesByExample({ category: 'GLPointNode' });
diagram.removeParts(glPoints, true);
var walls = this.diagram.findNodesByExample({ category: 'WallGroup' });
walls.iterator.each(function (w) {
if (w.data.key != hWall.data.key) {
var shape = w.findObject('SHAPE');
var geo = shape.geometry;
var pt1 = w.data.startpoint;
var pt2 = 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
*/
WallReshapingTool.prototype.checkPtLinedUp = function (pt, comparePtCoord, ptCoord, comparePt) {
function makeGuideLinePoint() {
var $ = 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() {
var $ = go.GraphObject.make;
return $(go.Link,
$(go.Shape, { stroke: "black", strokeWidth: 2, name: 'SHAPE', },
new go.Binding("strokeWidth", "width"),
new go.Binding('stroke', 'stroke')
)
);
}
var diagram = this.diagram;
var errorMargin = Math.abs(comparePtCoord - ptCoord);
if (errorMargin < 2) {
var data = { category: "GLPointNode", loc: go.Point.stringify(pt) };
var data2 = { key: 'movingPt', category: "GLPointNode", loc: go.Point.stringify(comparePt) };
var data3 = { key: 'guideline', category: 'guideLine', from: 'movingPt', to: data.key, stroke: 'blue' };
var GLPoint1 = makeGuideLinePoint();
var GLPoint2 = makeGuideLinePoint();
var GLLink = 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;
}
}
+262
View File
@@ -0,0 +1,262 @@
/********************************************** MAIN CONTENT STYLING ***********************************************/
body {font-family: Arial; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none;}
canvas:focus {outline:0;} /*don't outline palette and diagram when they are focused*/
p, h1,h2,h3,h4,h5,h6{cursor: default}
/*Style placeholder text to be black on all browsers*/
::-webkit-input-placeholder { color: black; }
::-moz-placeholder { color: black; }
:-ms-input-placeholder { color: black; }
table {
width: 100%;
}
input{
padding: 0px;
border: 1px solid gray;
}
.unitsBox{ /*A special input that displays units*/
text-align: right;
width: 20px;
border: 1px solid gray;
border-left: 0px;
}
#notesTextarea {
resize: none;
font-family: Arial;
}
label{ font-size: 9pt; color: #757575;}
.paletteLabel {
text-align: center;
font: bold 12px sans-serif;
width: 100%;
margin: 0;
}
#furnitureSearchBar{ width: 100%; border: none; }
#furnitureSearchBar::-webkit-input-placeholder { color: gray; }
#furnitureSearchBar::-moz-placeholder { color: gray; }
#furnitureSearchBar:-ms-input-placeholder { color: gray; }
#furniturePaletteDiv {width: inherit; height: 300px; background: #e2e2e2;}
#wallPartsPaletteDiv {width: inherit; height: 150px; background: #e2e2e2;}
#myPaletteWindow{
height: inherit;
top:12%;
left:.5%;
}
#palettes {
height: 100%;
width: 300px;
padding: 0px;
}
#myFloorplanDiv {
width: 100%;
background-color: #DAE4E4;
border: 1px solid gray;
height: 85vh;
}
#currentFile {
background-color: #4b545f;
color: white;
text-align: center;
padding: 5px;
}
#diagramHelpDiv {
width: 100%;
text-align: center;
border: 1px solid gray;
background-color: #4b545f;
color: white;
}
/****************************************************************** GENERAL DRAGGABLE WINDOWS STYLING ********************************************************/
.draggable {
border: 1px solid gray;
background-color: #e2e2e2;
position: absolute;
top: 40%;
left: 50%;
width: 300px;
height: 200px;
z-index: 10;
text-align: center;
}
#optionsWindow {
height: auto;
}
.windowButtons{
float: right;
border: none;
font: bold 12px sans-serif;
}
.handle {
background-color: #4b545f;
text-align: center;
font: bold 12px sans-serif;
color: white;
cursor: move;
width: 100%;
}
.mySavedFiles {width:75%;}
/*Selection Info Window specific styling*/
#selectionInfoTextDiv p {margin: 0; padding: 0;} /*used only when no node is selected*/
#selectionInfoTextDiv #name {margin: 3px;}
#selectionInfoTextDiv .nameNotesInput {
width: 80%;
}
#selectionInfoTextDiv .dimensionsInput {width: 85%; border-right: 0px;}
.data {font-size: 10pt;}
.clickable {cursor: pointer;}
.selectedKey {color: dodgerblue;}
/*-- set border box on all elements inside the grid*/
.grid-container * {box-sizing: border-box;}
.row:before, .row::after {
content: "";
display: table;
clear: both;
}
[class*='col-'] {
float: left;
min-height: 1px;
width: 16.66%;
padding: 0px;
}
.col-1 {width: 100%;}
.col-2 {width: 50%;}
.col-3 {width: 33.33%;}
.col-4 {width: 25%;}
.col-5 {width: 20%;}
.col-6 {width: 16.66%;}
/************************************************************* NAV BAR STYLING *************************************************/
nav {background: linear-gradient(#efefef ,#bbbbbb );}
nav ul { /*file menus and tool menus*/
list-style: none;
position: relative;
display: flex;
margin: 0; padding: 0;
}
nav ul li { /*menu tabs*/
float: left;
transition-property: background;
transition-duration: 0.3s;
}
nav ul li:hover {
transition-delay: 0.1s, 0.1s;
background: #4f5964;
}
nav ul li a { /*menu tabs text*/
display: block;
padding-left: 25px; padding-right: 25px; padding-top: 2px;
color: #757575; text-decoration: none;
transition-property: background, color;
transition-duration: 0.4s, 0.4s;
}
nav ul li:hover a {color: #fff;}
nav ul ul { /*drop down menus, general*/
display: block;
z-index: 20; /*in front of everything, even .draggables*/
visibility: hidden; opacity: 0;
transition-property: opacity;
transition-duration: 0.3s;
background: #5f6975; padding: 0;
position: absolute; top: 100%;
/* width: 220%; /*band aid fix for menu width -- TODO*/
}
nav ul li:hover > ul { /*drop down menus, visible*/
visibility: visible; opacity: 1;
transition-delay: 0.1s;
}
nav ul ul li { /*drop down menu items*/
border-top: 1px solid #6b727c;
border-bottom: 1px solid #575f6a;
position: relative; float: none;
}
nav ul ul li a { /*drown down menu items text*/
padding: 5px;
transition-property: background;
transition-duration: 0.1s;
}
nav ul ul li a:hover {
background: #4b545f;
transition-delay: 0.1s;
}
nav ul ul ul { /*drop down submenu (units menu)*/
left: 100%; top:0; width: 100px;
}
nav p { /* grid input area, wall width area, and 'Tools' all wrapped in <p> tag*/
float: left;
margin-bottom: 0px;
margin-top: 3px;
padding-left: 25px;
padding-right: 25px;
color: #4b545f;
}
nav p.shortcut {
font-size: 9pt;
float: right;
color: #efefef;
padding: 0;
}
.scaleItems{ /*special styling for the 'Scale' item of the 'View' menu*/
float: left;
text-align: center;
box-sizing: border-box;
width: 12.5%;
}
#scaleDisplay{width: 75%;}
#wallThicknessInput, #gridSizeInput {
width: 50px;
float: left;
border-right: 0px;
}
#gridSizeInput { margin-left: 10%; }
#wallThicknesshUnitsInput, #gridSizeUnitsInput{float: left;}
#wallThicknessInputLabel{ float: left; }
#setGridButton { padding: 0; }
/* Scaling for small screens, TODO*/
@media screen and (max-device-width: 480px) {
nav ul li ul li{ font-size: 9pt; }
}
/*Icons*/
#wallBuildingButton {background: url(icons/wallBuildingTool.png);}
#draggingButton {background: url(icons/selectionTool.png);}
#wallWidthBox { visibility: hidden; display: none;} /*box for setting wall width; invisible when Wall Tool not active*/
/* jQuery UI specific stylings */
.ui-accordion .ui-accordion-content, .ui-accordion .ui-accordion-icons { padding: 0px; }
.ui-widget * { outline: none; }
.ui-widget-content { border: none; }
+279
View File
@@ -0,0 +1,279 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8" />
<title>Floor Planner</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation -->
<script src="../../release/go.js"></script>
<script src="FloorPlanner-WallBuildingTool.js"></script>
<script src="FloorPlanner-WallReshapingTool.js"></script>
<script src="FloorPlanner-Templates-General.js"></script>
<script src="FloorPlanner-Templates-Furniture.js"></script>
<script src="FloorPlanner-Templates-Walls.js"></script>
<script src="Floorplan.js"></script>
<script src="FloorplanFilesystem.js"></script>
<script src="FloorplanUI.js"></script>
<script src="Floorplanner-Constants.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/jquery-ui.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/themes/smoothness/jquery-ui.css" />
<link rel="stylesheet" type="text/css" href="FloorPlanner.css" />
</head>
<body id="body" onload="init()">
<div id="currentFile">(Unsaved File)</div>
<!-- File menus-->
<!-- The Nav Bar / Windows are specific to a certain floorplan and its classes -- unlike most of the Floorplanner code, which is generic -->
<nav>
<ul id="fileMenus">
<li>
<a href="#">File</a>
<ul>
<li><a href="#" onclick="filesystem.newFloorplan()">New <p class="shortcut">(Ctrl + D)</p></a></li>
<li><a href="#" onclick="filesystem.showOpenWindow()">Open... <p class="shortcut">(Ctrl + O)</p></a></li>
<li><a href="#" onclick="filesystem.saveFloorplan()">Save <p class="shortcut">(Ctrl + S)</p></a></li>
<li><a href="#" onclick="filesystem.saveFloorplanAs()">Save As...</a></li>
<li><a href="#" onclick="filesystem.showRemoveWindow()">Remove... <p class="shortcut">(Ctrl + R)</p></a></li>
</ul>
</li>
<li>
<a href="#">Edit</a>
<ul>
<li><a href="#" onclick="myFloorplan.commandHandler.undo()">Undo <p class="shortcut">(Ctrl + Z)</p></a></li>
<li><a href="#" onclick="myFloorplan.commandHandler.redo()">Redo <p class="shortcut">(Ctrl + Y)</p></a></li>
<li><a href="#" onclick="myFloorplan.commandHandler.copySelection()">Copy <p class="shortcut">(Ctrl + C)</p></a></li>
<li><a href="#" onclick="myFloorplan.commandHandler.cutSelection()">Cut <p class="shortcut">(Ctrl + X)</p></a></li>
<li><a href="#" onclick="myFloorplan.commandHandler.pasteSelection()">Paste <p class="shortcut">(Ctrl + V)</p></a></li>
<li><a href="#" onclick="myFloorplan.commandHandler.deleteSelection()">Delete <p class="shortcut">(Del)</p></a></li>
<li><a href="#" onclick="myFloorplan.commandHandler.selectAll()">Select All <p class="shortcut">(Ctrl + A)</p></a></li>
</ul>
</li>
<li>
<a href="#">View</a>
<ul>
<li><a href="#" onclick="ui.hideShow('diagramHelpDiv')" id="diagramHelpDivButton">Hide Diagram Help <p class="shortcut"> (Ctrl + H)</p></a></li>
<li><a href="#" onclick="ui.hideShow('selectionInfoWindow')" id="selectionInfoWindowButton">Show Selection Help <p class="shortcut"> (Ctrl + I)</p></a></li>
<li><a href="#" onclick="ui.hideShow('myPaletteWindow')" id="myPaletteWindowButton">Hide Palettes <p class="shortcut"> (Ctrl + P)</p></a></li>
<li><a href="#" onclick="ui.hideShow('myOverviewWindow')" id="myOverviewWindowButton">Show Overview <p class="shortcut"> (Ctrl + E)</p></a></li>
<li><a href="#" onclick="ui.hideShow('statisticsWindow')" id="statisticsWindowButton">Show Statistics <p class="shortcut"> (Ctrl + G)</p></a></li>
<li>
<a href="#" id="optionsWindowButton" onclick="ui.hideShow('optionsWindow')">Show Options <p class="shortcut"> (Ctrl + B)</p> </a>
</li>
<li>
<a href="#" class="scaleItems" onclick="ui.adjustScale('-')">-</a>
<a href="#" class="scaleItems" id="scaleDisplay">Scale: 100%</a>
<a href="#" class="scaleItems" onclick="ui.adjustScale('+')">+</a>
</li>
</ul>
</li>
<li><button class="setBehavior toolIcons" id="wallBuildingButton" title="Wall Building Tool (Ctrl + 1)" onclick="ui.setBehavior('wallBuilding')">&nbsp;</button></li>
<li><button class="setBehavior toolIcons" id="draggingButton" title="Select/Move Tool (Ctrl + 2)" onclick="ui.setBehavior('dragging')">&nbsp;</button></li>
<p id="wallThicknessBox">
<label for="wallThicknessInput" id="wallThicknessInputLabel">Wall Thickness:</label>
<input id="wallThicknessInput" class="unitsInput" placeholder="width" />
<input id="wallThicknessUnitsInput" class="unitsBox" value="cm" disabled="disabled" />
</p>
</ul>
</nav>
<!-- Floorplan / Help bar -->
<div id="myFloorplanDiv"></div>
<div id="diagramHelpDiv" style="visibility: visible">
<div id="diagramHelpTextDiv">
<p>Drag a node to the Diagram or select the Wall Drawing Tool (Ctrl + 1) to begin</p>
</div>
</div>
<!-- Floating windows-->
<div id="myPaletteWindow" style="visibility: visible" class="draggable ui-draggable">
<div id="myPaletteWindowHandle" class="handle ui-draggable-handle">Palettes<button id="myPaletteClose" class="windowButtons clickable" onclick="ui.hideShow('myPaletteWindow')">X</button></div>
<div id="palettes">
<!-- jQuery accordion -->
<h3 class="paletteLabel">Furniture</h3>
<div>
<input id="furnitureSearchBar" placeholder="Search Furniture" oninput="ui.searchFurniture()" />
<div id="furniturePaletteDiv" class="paletteClass"></div>
</div>
<h3 class="paletteLabel">Wall Parts</h3>
<div id="wallPartsPaletteDiv" class="paletteClass"></div>
</div>
</div>
<div id="openDocument" style="visibility: hidden;" class="draggable ui-draggable">
<div id="openDocumentHandle" class="handle ui-draggable-handle">Open File<button id="openDocumentClose" class="windowButtons clickable" onclick="ui.hideShow('openDocument')">X</button></div>
<div id="openText" class="elementText">Choose file to open...</div>
<select id="filesToOpen" class="mySavedFiles"></select>
<br />
<button id="openBtn" class="elementBtn" type="button" onclick="filesystem.loadFloorplan()">Open</button>
</div>
<div id="removeDocument" style="visibility: hidden;" class="draggable ui-draggable">
<div id="removeDocumentHandle" class="handle">Delete File <button id="removeDocumentClose" class="windowButtons clickable" onclick="ui.hideShow('removeDocument')">X</button></div>
<div id="removeText" class="elementText">Choose file to remove...</div>
<select id="filesToRemove" class="mySavedFiles"></select>
<br />
<button id="removeBtn" class="elementBtn" type="button" onclick="filesystem.removeFloorplan()" style="margin-left:70px">Remove</button>
</div>
<div id="myOverviewWindow" style="visibility: hidden;" class="draggable ui-draggable">
<div id="myOverviewWindowHandle" class="handle ui-draggable-handle">Overview<button id="myOverviewClose" title="Close" class="windowButtons clickable" onclick="ui.hideShow('myOverviewWindow')">X</button></div>
<div id="myOverviewDiv" style="height:187px; width: 300px;"></div>
</div>
<div id="selectionInfoWindow" style="visibility: hidden" class="draggable ui-draggable">
<div id="selectionInfoWindowHandle" class="handle ui-draggable-handle">Selection Info <button id="selectionInfoClose" class="windowButtons clickable" onclick="ui.hideShow('selectionInfoWindow')">X</button></div>
<div id="selectionInfoTextDiv" class="grid-container">Nothing selected</div>
</div>
<div id="optionsWindow" style="visibility: hidden" class="draggable ui-draggable">
<div id="optionsWindowHandle" class="handle ui-draggable-handle">Options <button id="optionsWindowClose" class="windowButtons clickable" onclick="ui.hideShow('optionsWindow')">X</button></div>
Units
<div id="unitsRow" class="row data">
<form id="unitsForm" onchange="ui.changeUnits()">
<div class="col-4">
<input type="radio" name="units" id="centimeters" checked />cm
</div>
<div class="col-4">
<input type="radio" name="units" id="meters" /> m
</div>
<div class="col-4">
<input type="radio" name="units" id="inches" />in
</div>
<div class="col-4">
<input type="radio" name="units" id="feet" />ft
</div>
</form>
</div>
Grid
<div id="gridRow" class="row">
<div class="col-2">
<input id="gridSizeInput" placeholder="grid size" class="unitsInput" onchange="ui.changeGridSize()" />
<input id="gridSizeUnitsInput" class="unitsBox" value="cm" disabled />
<!--<button id="setGridButton" onclick="ui.changeGridSize()">Set Grid</button>-->
</div>
<div class="col-2">
<input type="checkbox" id="showGridCheckbox" onchange="ui.checkboxChanged('showGridCheckbox', myFloorplan)" checked />Show Grid
</div>
</div>
<div id="gridRow" class="row">
<div class="col-1">
<label for="unitsConversionFactorInput">Units/1px (at scale 100%)</label>
<input id="unitsConversionFactorInput" placeholder="2" onchange="ui.changeUnitsConversionFactor()" />
</div>
</div>
Preferences
<div id="miscRow" class="row data">
<div class="col-1">
<input type="checkbox" id="gridSnapCheckbox" onchange="ui.checkboxChanged('gridSnapCheckbox', myFloorplan)" checked />Grid Snap
</div>
<div class="col-1">
<input type="checkbox" id="wallGuidelinesCheckbox" onchange="ui.checkboxChanged('wallGuidelinesCheckbox', myFloorplan)" checked /> Show Wall Guidelines
</div>
<div class="col-1">
<input type="checkbox" id="wallLengthsCheckbox" onchange="ui.checkboxChanged('wallLengthsCheckbox', myFloorplan)" checked /> Show Wall Lengths
</div>
<div class="col-1">
<input type="checkbox" id="wallAnglesCheckbox" onchange="ui.checkboxChanged('wallAnglesCheckbox', myFloorplan)" checked /> Show Wall Angles
</div>
<div class="col-1">
<input type="checkbox" id="smallWallAnglesCheckbox" onchange="ui.checkboxChanged('smallWallAnglesCheckbox', myFloorplan)" unchecked /> Show Only Small Wall Angles
</div>
</div>
</div>
<div id="statisticsWindow" style="visibility: hidden" class="draggable ui-draggable">
<div id="statisticsWindowHandle" class="handle ui-draggable-handle">Floor Plan Statistics <button id="statisticsWindowClose" class="windowButtons clickable" onclick="ui.hideShow('statisticsWindow')">X</button></div>
Stats
<div id="statisticsWindowTextDiv" class="grid-container"></div>
</div>
<p>This Floorplanner samples makes use of multiple classes to allow for users to build, edit, save, and load feature-rich Floorplans. To start, build walls with the Wall Building Tool (Ctrl + 1), or drag furniture from Palettes onto the Floorplan area. The help at the bottom of the Floorplan area is context-specific and should aid in providing tips on how to better use this software.</p>
<p>This sample uses the following files.</p>
<ul>
<li><a href="Floorplan.js">Floorplan.js</a> - A special kind of <a href="https://gojs.net/latest/api/symbols/Diagram.html">Diagram</a> with rules and listeners that help with floorplanning</li>
<li><a href="FloorplanFilesystem.js">FloorplanFilesystem.js</a> - A class to handle saving and loading floorplans (through localstorage). Linked to a Floorplan instance</li>
<li><a href="Floorplanner-WallBuildingTool.js">WallBuildingTool.js</a> - For constructing new walls</li>
<li><a href="Floorplanner-WallReshapingTool.js">WallReshapingTool.js</a> - For reshaping walls from their endpoints</li>
<li><a href="FloorplanUI.js">FloorplanUI.js</a> - A class to handle GUI interaction as events on the Floorplan take place. Linked to a Floorplan instance</li>
</ul>
<p> In addition, three files are used to store Flooplanner-specific <a href="https://gojs.net/latest/api/symbols/GraphObject.html">Graph Object</a> templates.</p>
<ul>
<li><a href="Floorplanner-Templates-General.js">Floorplanner-Templates-General.js</a> - Contains templates for Context Menu, Diagram, Default Group, AngleNode, DimensionLink, PointNode, all all their dependencies</li>
<li><a href="Floorplanner-Templates-Furniture.js">Floorplanner-Templates-Furniture.js</a> - Contains templates for Default Node (Furniture), MultiPurpose Node, and all their dependencies</li>
<li><a href="Floorplanner-Templates-Walls.js">Floorplanner-Templates-Walls.js</a> - Contains templates for Wall Group, Palette Wall Node, Window Node, Door Node, and all their dependencies</li>
</ul>
<script>
// enables draggable windows (jQuery), defining their handles and behavior (most recently dragged window stacks over other windows)
$(function () {
$("#palettes").accordion({
activate: function (event, ui) {
furniturePalette.requestUpdate();
wallPartsPalette.requestUpdate();
}
});
$("#openDocument").draggable({ handle: "#openDocumentHandle", stack: ".draggable", containment: 'window', scroll: false });
$('#optionsWindow').draggable({ handle: "#optionsWindowHandle", stack: ".draggable", containment: 'window', scroll: false });
$("#removeDocument").draggable({ handle: "#removeDocumentHandle", stack: ".draggable", containment: 'window', scroll: false });
$("#myOverviewWindow").draggable({ handle: "#myOverviewWindowHandle", stack: ".draggable", containment: 'window', scroll: false });
$('#statisticsWindow').draggable({ handle: "#statisticsWindowHandle", stack: ".draggable", containment: 'window', scroll: false });
$("#selectionInfoWindow").draggable({ handle: "#selectionInfoWindowHandle", stack: ".draggable", containment: 'window', scroll: false });
$("#myPaletteWindow").draggable({ handle: "#myPaletteWindowHandle", stack: ".draggable", containment: 'window', scroll: false });
$("#myPaletteWindow").resize(function () {
furniturePalette.requestUpdate();
wallPartsPalette.requestUpdate();
});
});
function init() {
// Floorplan
myFloorplan = new Floorplan("myFloorplanDiv");
// 'FILESYSTEM_UI_STATE', 'GUI_STATE' defined in Floorplanner-Constants.js
filesystem = new FloorplanFilesystem(myFloorplan, FILESYSTEM_UI_STATE);
ui = new FloorplanUI(myFloorplan, "ui", "myFloorplan", GUI_STATE);
// Overview
var $ = go.GraphObject.make;
myOverview = $(go.Overview, "myOverviewDiv", { observed: myFloorplan, maxScale: 0.5 });
furniturePalette = $(go.Palette, "furniturePaletteDiv");
furniturePalette.nodeTemplateMap = myFloorplan.nodeTemplateMap;
furniturePalette.model = new go.GraphLinksModel(FURNITURE_NODE_DATA_ARRAY);
wallPartsPalette = $(go.Palette, "wallPartsPaletteDiv");
wallPartsPalette.nodeTemplateMap = myFloorplan.nodeTemplateMap;
wallPartsPalette.model = new go.GraphLinksModel(WALLPARTS_NODE_DATA_ARRAY);
// enable hotkeys
var body = document.getElementById('body');
body.addEventListener("keydown", function (e) {
var keynum = e.which;
if (e.ctrlKey) {
e.preventDefault();
switch (keynum) {
case 83: filesystem.saveFloorplan(); break; // ctrl + s
case 79: filesystem.showOpenWindow(); break; // ctrl + o
case 68: e.preventDefault(); filesystem.newFloorplan(); break; // ctrl + d
case 82: filesystem.showRemoveWindow(); break; // ctrl + r
case 49: ui.setBehavior('wallBuilding', myFloorplan); break; // ctrl + 1
case 50: ui.setBehavior('dragging', myFloorplan); break; // ctrl + 2
case 72: ui.hideShow('diagramHelpDiv'); break; // ctrl + h
case 73: ui.hideShow('selectionInfoWindow'); break; // ctrl + i
case 80: ui.hideShow('myPaletteWindow'); break; // ctrl + p
case 69: ui.hideShow('myOverviewWindow'); break; // ctrl + e
case 66: ui.hideShow('optionsWindow'); break; // ctrl + b
case 71: ui.hideShow('statisticsWindow'); break; // ctrl + g
}
}
});
// default model data stored in Floorplanner-Constants.js
myFloorplan.floorplanFilesystem.loadFloorplanFromModel(DEFAULT_MODEL_DATA);
ui.setBehavior("dragging");
}
</script>
</body>
</html>
+813
View File
@@ -0,0 +1,813 @@
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* Floorplan Class
* A Floorplan is a Diagram with special rules
* Dependencies: Floorplanner-Templates-General.js, Floorplanner-Templates-Furniture.js, Floorplanner-Templates-Walls.js
*/
/*
* Floorplan Constructor
* @param {HTMLDivElement|string} div A reference to a div or its ID as a string
*/
function Floorplan(div) {
/*
* Floor Plan Setup:
* Initialize Floor Plan, Floor Plan Listeners, Floor Plan Overview
*/
go.Diagram.call(this, div);
// By default there is no filesystem / UI control for a floorplan, though they can be added
this._floorplanFilesystem = null;
this._floorplanUI = null;
// 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",
"unitsConversionFactor": 2,
"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 = true;
this.commandHandler.canUngroupSelection = true;
this.commandHandler.archetypeGroupData = { isGroup: true };
// When floorplan model is changed, update stats in Statistics Window
this.addModelChangedListener(function (e) {
if (e.isTransactionFinished) {
// find floorplan changed
var floorplan = null;
if (e.object !== null) {
e.object.changes.each(function (change) {
if (change.diagram instanceof Floorplan) floorplan = change.diagram;
});
}
if (floorplan) {
if (floorplan.floorplanUI) floorplan.floorplanUI.updateStatistics();
}
}
});
// When floorplan is modified, change document title to include a *
this.addDiagramListener("Modified", function (e) {
var floorplan = e.diagram;
if (floorplan.floorplanFilesystem) {
var currentFile = document.getElementById(floorplan.floorplanFilesystem.state.currentFileId);
if (currentFile) {
var idx = currentFile.textContent.indexOf("*");
if (floorplan.isModified) {
if (idx < 0) currentFile.textContent = currentFile.textContent + "*";
}
else {
if (idx >= 0) currentFile.textContent = currentFile.textContent.substr(0, idx);
}
}
}
});
// if a wall is copied, update its geometry
this.addDiagramListener("SelectionCopied", function (e) {
e.diagram.selection.iterator.each(function(part){
if (part.category == "WallGroup") {
e.diagram.updateWall(part);
}
});
});
// If floorplan scale has been changed update the 'Scale' item in the View menu
this.addDiagramListener("ViewportBoundsChanged", function (e) {
var floorplan = e.diagram;
if (floorplan.floorplanUI) {
var scaleEl = document.getElementById(floorplan.floorplanUI.state.scaleDisplayId);
if (scaleEl) scaleEl.innerHTML = "Scale: " + (e.diagram.scale * 100).toFixed(2) + "%";
}
});
// If a node has been dropped onto the Floorplan from a Palette...
this.addDiagramListener("ExternalObjectsDropped", function (e) {
var garbage = [];
var paletteWallNodes = [];
var otherNodes = [];
e.diagram.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") {
paletteWallNodes.push(node);
}
if (e.diagram.floorplanUI) {
otherNodes.push(node);
}
});
for (var i in paletteWallNodes) {
var node = paletteWallNodes[i];
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);
var wall = e.diagram.findPartForKey(data.key);
e.diagram.updateWall(wall);
garbage.push(paletteWallNode);
}
for (var i in otherNodes) {
var node = otherNodes[i];
var floorplanUI = e.diagram.floorplanUI;
// Event 2: Update the text of the Diagram Helper
if (node.category === "WindowNode" || node.category === "DoorNode") floorplanUI.setDiagramHelper("Drag part so the cursor is over a wall to add this part to a wall");
else floorplanUI.setDiagramHelper("Drag, resize, or rotate your selection (hold SHIFT for no grid-snapping)");
// Event 3: If the select tool is not active, make it active
if (e.diagram.toolManager.mouseDownTools.elt(0).isEnabled) floorplanUI.setBehavior('dragging', e.diagram);
}
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) {
e.diagram.selection.iterator.each(function (node) { if (node.category === "WallGroup") e.diagram.updateWall(node); });
});
// 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 = []; // 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 (floorplan.floorplanUI) {
var floorplanUI = floorplan.floorplanUI;
var selection = floorplan.selection;
var node = floorplan.selection.first(); // only used if selection.count === 1
if (selection.count === 0) floorplan.floorplanUI.setSelectionInfo('Nothing selected');
else if (selection.count === 1) floorplan.floorplanUI.setSelectionInfo(floorplan.selection.first());
else floorplan.floorplanUI.setSelectionInfo('Selection: ');
if (selection.count === 0) floorplanUI.setDiagramHelper("Click to select a part, drag one from a Palette, or draw a wall with the Wall Tool (Ctr + 1)");
else if (selection.count > 1) {
var ungroupable = false;
selection.iterator.each(function (node) { if (node.category === "WindowNode" || node.category === "DoorNode" || node.category === "WallGroup") ungroupable = true; });
if (!ungroupable) floorplanUI.setDiagramHelper("You may group your selection with the context menu (Right Click anywhere)");
}
else if (node.category === "WallGroup") floorplanUI.setDiagramHelper("Drag wall endpoints or add doors and windows to the wall from the Wall Parts Palette");
else if (selection.first().category === "WindowNode" || selection.first().category === "DoorNode") {
if (node.containingGroup !== null) floorplanUI.setDiagramHelper("Drag and resize wall part along the wall; drag away from wall to detach");
else floorplanUI.setDiagramHelper("Drag part so the cursor is over a wall to add this part to a wall");
}
else if (selection.first().category === "MultiPurposeNode") floorplanUI.setDiagramHelper("Double click on part text to revise it");
else floorplanUI.setDiagramHelper("Drag, resize, or rotate (hold SHIFT for no snap) your selection");
}
});
/*
* Node Templates
* Add Default Node, Multi-Purpose Node, Window Node, Palette Wall Node, and Door Node to the Node Template Map
* Template functions defined in FloorPlanner-Templates-* js files
*/
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
* Template functions defined in FloorPlanner-Templates-* js files
*/
this.groupTemplateMap.add("", makeDefaultGroup()); // Default Group
this.groupTemplateMap.add("WallGroup", makeWallGroup()); // Wall Group
/*
* Install Custom Tools
* Wall Building Tool, Wall Reshaping Tool
* Tools are defined in their own FloorPlanner-<Tool>.js files
*/
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;
// if node is the only thing selected, display its info as its resized
if (floorplan.selection.count === 1 && floorplan.floorplanUI) floorplan.floorplanUI.setSelectionInfo(node);
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;
} go.Diagram.inherit(Floorplan, go.Diagram);
// Get/set the Floorplan Filesystem instance associated with this Floorplan
Object.defineProperty(Floorplan.prototype, "floorplanFilesystem", {
get: function () { return this._floorplanFilesystem; },
set: function (val) {
val instanceof FloorplanFilesystem ? this._floorplanFilesystem = val : this._floorplanFilesystem = null;
}
});
// Get/set the FloorplanUI instance associated with this Floorplan
Object.defineProperty(Floorplan.prototype, "floorplanUI", {
get: function () { return this._floorplanUI; },
set: function (val) {
val instanceof FloorplanUI ? this._floorplanUI = val : this._floorplanUI = null;
}
});
// Get array of all FloorplanPalettes associated with this Floorplan
Object.defineProperty(Floorplan.prototype, "palettes", {
get: function () { return this._palettes; }
});
// Get / set Set of all Point Nodes in the Floorplan
Object.defineProperty(Floorplan.prototype, "pointNodes", {
get: function () { return this._pointNodes; },
set: function (val) { this._pointNodes = val; }
});
// Get / set Set of all Dimension Links in the Floorplan
Object.defineProperty(Floorplan.prototype, "dimensionLinks", {
get: function () { return this._dimensionLinks; },
set: function () { this._dimensionLinks = val; }
});
// Get / set Set of all Angle Nodes in the Floorplan
Object.defineProperty(Floorplan.prototype, "angleNodes", {
get: function () { return this._angleNodes; },
set: function () { this._angleNodes = val; }
});
// 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)
Floorplan.prototype.convertPixelsToUnits = function (num) {
var units = this.model.modelData.units;
var factor = 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)
Floorplan.prototype.convertUnitsToPixels = function (num) {
var units = this.model.modelData.units;
var factor = 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)
*/
Floorplan.prototype.updateWall = function (wall) {
if (wall.data.startpoint && wall.data.endpoint) {
var shape = wall.findObject("SHAPE");
var geo = new go.Geometry(go.Geometry.Line);
var sPt = wall.data.startpoint;
var ePt = wall.data.endpoint;
var mPt = 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
var angle = 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)
*/
Floorplan.prototype.getAdjustedPoint = function (point, wall, angle, wallOffset) {
var oldPoint = 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
*/
Floorplan.prototype.buildDimensionLink = function (wall, index, point1, point2, angle, wallOffset, soloWallFlag, floorplan) {
point1 = floorplan.getAdjustedPoint(point1, wall, angle, wallOffset);
point2 = floorplan.getAdjustedPoint(point2, wall, angle, wallOffset);
var data1 = { key: wall.data.key + "PointNode" + index, category: "PointNode", loc: go.Point.stringify(point1) };
var data2 = { key: wall.data.key + "PointNode" + (index + 1), category: "PointNode", loc: go.Point.stringify(point2) };
var data3 = { key: wall.data.key + "DimensionLink", category: 'DimensionLink', from: data1.key, to: data2.key, stroke: 'gray', angle: angle, wall: wall.data.key, soloWallFlag: soloWallFlag };
var pointNode1 = makePointNode();
var pointNode2 = makePointNode();
var 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
*/
Floorplan.prototype.updateWallDimensions = function () {
var 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; });
var selection = floorplan.selection;
// gather all selected walls, including walls of selected DoorNodes and WindowNodes
var walls = 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) {
var soloWallLink = null;
floorplan.dimensionLinks.iterator.each(function (link) { if (link.data.soloWallFlag && link.data.wall === part.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
var linkPoint1 = null; var linkPoint2 = null;
floorplan.pointNodes.iterator.each(function (node) {
if (node.data.key === part.data.key + "PointNode1") linkPoint1 = node;
if (node.data.key === part.data.key + "PointNode2") linkPoint2 = node;
});
var startpoint = part.data.startpoint; var endpoint = part.data.endpoint;
// adjust left/top-most / right/bottom-most wall endpoints so link angle is correct (else text appears on wrong side of Link)
var firstWallPt = ((startpoint.x + startpoint.y) <= (endpoint.x + endpoint.y)) ? startpoint : endpoint;
var lastWallPt = ((startpoint.x + startpoint.y) > (endpoint.x + endpoint.y)) ? startpoint : endpoint;
var newLoc1 = floorplan.getAdjustedPoint(firstWallPt.copy(), part, part.rotateObject.angle, 10);
var newLoc2 = floorplan.getAdjustedPoint(lastWallPt.copy(), part, part.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 = part.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 {
var startpoint = part.data.startpoint;
var endpoint = part.data.endpoint;
var firstWallPt = ((startpoint.x + startpoint.y) <= (endpoint.x + endpoint.y)) ? startpoint : endpoint;
var lastWallPt = ((startpoint.x + startpoint.y) > (endpoint.x + endpoint.y)) ? startpoint : endpoint;
floorplan.buildDimensionLink(part, 1, firstWallPt.copy(), lastWallPt.copy(), part.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) {
var startpoint = wall.data.startpoint;
var endpoint = wall.data.endpoint;
var firstWallPt = ((startpoint.x + startpoint.y) <= (endpoint.x + endpoint.y)) ? startpoint : endpoint;
var lastWallPt = ((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)
var wallPartEndpoints = [];
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);
var angle = wall.rotateObject.angle;
var k = 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 (var j = 0; j < wallPartEndpoints.length - 1; j++) {
var linkPoint1 = null; linkPoint2 = 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) {
var newLoc1 = floorplan.getAdjustedPoint(wallPartEndpoints[j].copy(), wall, angle, 5);
var newLoc2 = 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
var 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) {
var linkPoint1 = null; var linkPoint2 = 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;
});
var newLoc1 = floorplan.getAdjustedPoint(wallPartEndpoints[0].copy(), wall, angle, 25);
var newLoc2 = 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) {
var canStay = false;
floorplan.pointNodes.iterator.each(function (node) {
if (node.data.key == link.data.to) canStay = true;
});
if (!canStay) floorplan.remove(link);
else {
var length = 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;
}
/*
* Helper function for updateWallAngles(); returns the Point where two walls intersect; if they do not intersect, return null
* @param {Wall} wall1
* @param {Wall} wall2
*/
Floorplan.prototype.getWallsIntersection = function (wall1, wall2) {
if (wall1 === null || wall2 === null) return null;
// treat walls as lines; get lines in formula of ax + by = c
var a1 = wall1.data.endpoint.y - wall1.data.startpoint.y;
var b1 = wall1.data.startpoint.x - wall1.data.endpoint.x;
var c1 = (a1 * wall1.data.startpoint.x) + (b1 * wall1.data.startpoint.y);
var a2 = wall2.data.endpoint.y - wall2.data.startpoint.y;
var b2 = wall2.data.startpoint.x - wall2.data.endpoint.x;
var c2 = (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
**/
var det = a1 * b2 - a2 * b1;
var x = null; var y = 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)
var inWall1 = ((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));
var inWall2 = ((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
*/
Floorplan.prototype.updateWallAngles = function () {
var 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; });
var selectedWalls = [];
floorplan.selection.iterator.each(function (part) { if (part.category === "WallGroup") selectedWalls.push(part); });
for (var i = 0; i < selectedWalls.length; i++) {
var seen = new go.Set(/*"string"*/); // Set of all walls "seen" thus far for "wall"
var wall = selectedWalls[i];
var possibleWalls = 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
var intersectionPoint = floorplan.getWallsIntersection(wall, otherWall);
var wallsInvolved = floorplan.findObjectsNear(intersectionPoint,
1,
function (x) { if (x.part !== null) return x.part; },
function (p) { return p.category === "WallGroup"; },
false);
var endpoints = []; // 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) {
var tolerance = (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)
var maxRadius = 30;
for (var i = 0; i < endpoints.length; i++) {
var distance = 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 true;
if (a.x - intersectionPoint.x < 0 && b.x - intersectionPoint.x >= 0) return false;
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;
return b.y > a.y;
}
// compute the cross product of vectors (center -> a) x (center -> b)
var det = (a.x - intersectionPoint.x) * (b.y - intersectionPoint.y) - (b.x - intersectionPoint.x) * (a.y - intersectionPoint.y);
if (det < 0) return true;
if (det > 0) return false;
// points a and b are on the same line from the center; check which point is closer to the center
var d1 = (a.x - intersectionPoint.x) * (a.x - intersectionPoint.x) + (a.y - intersectionPoint.y) * (a.y - intersectionPoint.y);
var d2 = (b.x - intersectionPoint.x) * (b.x - intersectionPoint.x) + (b.y - intersectionPoint.y) * (b.y - intersectionPoint.y);
return d1 > d2;
}); // end endpoints sort
// for each pair of endpoints, construct or modify an angleNode
for (var i = 0; i < endpoints.length; i++) {
var p1 = endpoints[i];
if (endpoints[i + 1] != null) var p2 = endpoints[i + 1];
else var p2 = endpoints[0];
var a1 = intersectionPoint.directionPoint(p1.point);
var a2 = intersectionPoint.directionPoint(p2.point);
var sweep = Math.abs(a2 - a1 + 360) % 360;
var angle = 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
*/
var keyArray = []; // used to construct proper key
wallsInvolved.iterator.each(function (wall) { keyArray.push(wall); });
keyArray.sort(function (a, b) {
var aIndex = a.data.key.match(/\d+/g);
var bIndex = b.data.key.match(/\d+/g);
if (isNaN(aIndex)) return true;
if (isNaN(bIndex)) return false;
else return aIndex > bIndex;
});
var key = "";
for (var j = 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)
var angleNode = 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 {
var data = { key: key, category: "AngleNode", loc: go.Point.stringify(intersectionPoint), stroke: "dodgerblue", angle: angle, sweep: sweep, maxRadius: maxRadius };
var newAngleNode = makeAngleNode();
newAngleNode.data = data;
floorplan.add(newAngleNode);
newAngleNode.updateTargetBindings();
floorplan.angleNodes.add(newAngleNode);
}
}
}
});
}
// garbage collection (angleNodes that should not exist any more)
var garbage = [];
floorplan.angleNodes.iterator.each(function (node) {
var keyNums = node.data.key.match(/\d+/g); // values X for all wall keys involved, given key "wallX"
var numWalls = (node.data.key.match(/wall/g) || []).length; // # of walls involved in in "node"'s construction
var wallsInvolved = [];
// add all walls involved in angleNode's construction to wallsInvolved
for (var i = 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 (var i = 0; i < wallsInvolved.length - 1; i++) {
var wall1 = floorplan.findPartForKey(wallsInvolved[i]);
var wall2 = floorplan.findPartForKey(wallsInvolved[i + 1]);
var intersectionPoint = 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"
var possibleAngleNodes = new go.Set(/*go.Node*/);
var 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 (var i = 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;
}
@@ -0,0 +1,258 @@
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* Floorplan Filesystem Class
* Handles Floorplan-specific saving / loading model data events
* Attached to a specific instance of Floorplan (via constructor); can be assigned to Floorplan.floorplanFilesystem
* Currently only supports saving / loading from localstorage
*/
/*
* Floorplan Filesystem Constructor
* @param {Floorplan} floorplan A reference to a valid instance of Floorplan
* @param {Object} state A JSON object with string ids for UI objects (windows, listboxes, HTML elements)
* {
* openWindowId: {String} the id of the HTML window to open a file
* removeWindowId: {String} the id of the HTML window to remove files
* currentFileId: {String} the id of the HTML element containing the name of the currently open file
* filesToOpenListId: {String} the id of the HTML listbox in the openWindow
* filesToRemoveListId: {String} the id of the HTML listbox of the removeWindow
* }
*/
function FloorplanFilesystem(floorplan, state) {
this._floorplan = floorplan;
this._floorplan.floorplanFilesystem = this;
this._UNSAVED_FILENAME = "(Unsaved File)";
this._DEFAULT_MODELDATA = {
"units": "centimeters",
"unitsAbbreviation": "cm",
"unitsConversionFactor": 2,
"gridSize": 10,
"wallWidth": 5,
"preferences": {
showWallGuidelines: true,
showWallLengths: true,
showWallAngles: true,
showOnlySmallWallAngles: true,
showGrid: true,
gridSnap: true
}
};
this._state = state;
}
// Get the Floorplan associated with this Floorplan Filesystem
Object.defineProperty(FloorplanFilesystem.prototype, "floorplan", {
get: function () { return this._floorplan; }
});
// Get constant name for an unsaved Floorplan
Object.defineProperty(FloorplanFilesystem.prototype, "UNSAVED_FILENAME", {
get: function () { return this._UNSAVED_FILENAME; }
});
// Get constant default model data (default Floorplan model data for a new Floorplan)
Object.defineProperty(FloorplanFilesystem.prototype, "DEFAULT_MODELDATA", {
get: function () { return this._DEFAULT_MODELDATA; }
});
// Get state information about this app's UI
Object.defineProperty(FloorplanFilesystem.prototype, "state", {
get: function () { return this._state; }
});
/*
* Helper functions
* Check Local Storage, Update File List, Open Window
*/
// Ensure local storage works in browser (not supported by MS IE/Edge)
function checkLocalStorage() {
try {
window.localStorage.setItem('item', 'item');
window.localStorage.removeItem('item');
return true;
} catch (e) {
return false;
}
}
// Update listbox of files in window (for loading / removing files)
function updateFileList(id) {
// displays cached floor plan files in the listboxes
var listbox = document.getElementById(id);
// remove any old listing of files
var last;
while (last = listbox.lastChild) listbox.removeChild(last);
// now add all saved files to the listbox
for (key in window.localStorage) {
var storedFile = window.localStorage.getItem(key);
if (!storedFile) continue;
var option = document.createElement("option");
option.value = key;
option.text = key;
listbox.add(option, null)
}
}
// Open a specifed window element -- used for Remove / Open file windows
function openWindow(id, listid) {
var panel = document.getElementById(id);
if (panel.style.visibility === "hidden") {
updateFileList(listid);
panel.style.visibility = "visible";
}
}
/*
* Instance methods
* New Floorplan, Save Floorplan, Save Floorplan As, Load Floorplan, Remove Floorplan
* Show Open Window, Show Remove Window
* Set Current File Name, Get Current File Name
*/
// Create new floorplan (Ctrl + D or File -> New)
FloorplanFilesystem.prototype.newFloorplan = function () {
var floorplan = this.floorplan;
// checks to see if all changes have been saved
if (floorplan.isModified) {
var save = confirm("Would you like to save changes to " + this.getCurrentFileName() + "?");
if (save) {
this.saveFloorplan();
}
}
this.setCurrentFileName(this.UNSAVED_FILENAME);
// loads an empty diagram
var model = new go.GraphLinksModel;
// initialize all modelData
model.modelData = this.DEFAULT_MODELDATA;
floorplan.model = model;
floorplan.undoManager.isEnabled = true;
floorplan.isModified = false;
if (floorplan.floorplanUI) {
floorplan.floorplanUI.updateUI();
floorplan.floorplanUI.updateStatistics();
}
}
// Save current floor plan to local storage (Ctrl + S or File -> Save)
FloorplanFilesystem.prototype.saveFloorplan = function () {
if (checkLocalStorage()) {
var saveName = this.getCurrentFileName();
if (saveName === this.UNSAVED_FILENAME || saveName == null || saveName == undefined) {
this.saveFloorplanAs();
} else {
window.localStorage.setItem(saveName, this.floorplan.model.toJson());
this.floorplan.isModified = false;
}
}
}
// Save floor plan to local storage with a new name (File -> Save As)
FloorplanFilesystem.prototype.saveFloorplanAs = function () {
if (checkLocalStorage()) {
var saveName = prompt("Save file as...", this.getCurrentFileName());
// if saveName is already in list of files, ask if overwrite is ok
if (saveName && saveName !== this.UNSAVED_FILENAME) {
var override = true;
if (window.localStorage.getItem(saveName) !== null) {
override = confirm("Do you want to overwrite " + saveName + "?");
}
if (override) {
this.setCurrentFileName(saveName);
window.localStorage.setItem(saveName, this.floorplan.model.toJson());
this.floorplan.isModified = false;
}
}
}
}
// Load floorplan model data in "Open" window (Ctrl + O or File -> Open)
FloorplanFilesystem.prototype.loadFloorplan = function () {
var floorplan = this.floorplan;
var listbox = document.getElementById(this.state.filesToOpenListId);
var fileName = undefined; // get selected filename
for (var i = 0; i < listbox.options.length; i++) {
if (listbox.options[i].selected) fileName = listbox.options[i].text; // selected file
}
if (fileName !== undefined) {
var savedFile = window.localStorage.getItem(fileName);
this.loadFloorplanFromModel(savedFile);
floorplan.isModified = false;
this.setCurrentFileName(fileName);
}
if (floorplan.floorplanUI) floorplan.floorplanUI.hideShow(this.state.openWindowId);
}
FloorplanFilesystem.prototype.loadFloorplanFromModel = function (str) {
var floorplan = this.floorplan;
floorplan.model = go.Model.fromJson(str);
floorplan.skipsUndoManager = true;
floorplan.startTransaction("generate walls");
floorplan.nodes.each(function (node) {
if (node.category === "WallGroup") floorplan.updateWall(node);
});
if (floorplan.floorplanUI) {
floorplan.floorplanUI.updateUI();
floorplan.floorplanUI.updateStatistics();
}
floorplan.commitTransaction("generate walls");
floorplan.undoManager.isEnabled = true;
}
// Delete selected floorplan from local storage
FloorplanFilesystem.prototype.removeFloorplan = function () {
var floorplan = this.floorplan;
var listbox = document.getElementById(this.state.filesToRemoveListId);
var fileName = undefined; // get selected filename
for (var i = 0; i < listbox.options.length; i++) {
if (listbox.options[i].selected) fileName = listbox.options[i].text; // selected file
}
if (fileName !== undefined) {
// removes file from local storage
window.localStorage.removeItem(fileName);
}
if (floorplan.floorplanUI) floorplan.floorplanUI.hideShow(this.state.removeWindowId);
}
// Check to see if all changes have been saved -> show the "Open" window
FloorplanFilesystem.prototype.showOpenWindow = function () {
if (checkLocalStorage()) {
if (this.floorplan.isModified) {
var save = confirm("Would you like to save changes to " + this.getCurrentFileName() + "?");
if (save) {
this.saveFloorplan();
}
}
openWindow(this.state.openWindowId, this.state.filesToOpenListId);
}
}
// Show the Remove File window
FloorplanFilesystem.prototype.showRemoveWindow = function () {
if (checkLocalStorage()) {
openWindow(this.state.removeWindowId, this.state.filesToRemoveListId);
}
}
// Add * to current file element if diagram has been modified
FloorplanFilesystem.prototype.setCurrentFileName = function (name) {
var currentFile = document.getElementById(this.state.currentFileId);
if (currentFile) {
if (this.floorplan.isModified) name += "*";
currentFile.textContent = name;
}
}
// Get current file name from the current file element
FloorplanFilesystem.prototype.getCurrentFileName = function () {
var currentFile = document.getElementById(this.state.currentFileId);
if (currentFile) {
var name = currentFile.textContent;
if (name[name.length - 1] === "*") return name.substr(0, name.length - 1);
}
return name;
}
+773
View File
@@ -0,0 +1,773 @@
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* FLOOR PLANN UI CLASS
* Handle GUI manipulation (showing/changing data, populating windows, etc) for Floorplanner.html
*/
/*
* Floorplan UI Constructor
* @param {Floorplan} floorplan A reference to a valid instance of Floorplan
* @param {String} name The name of this FloorplanUI instance known to the DOM
* @param {String} The name of this UI's floorplan known to the DOM
* @param {Object} state A JSON object with string ids for UI HTML elements. Format is as follows:
menuButtons: {
selectionInfoWindowButtonId:
palettesWindowButtonId:
overviewWindowButtonId:
optionsWindowButtonId:
statisticsWindowButtonId:
}
windows: {
diagramHelpDiv: {
id:
}
selectionInfoWindow: {
id:
textDivId:
handleId:
colorPickerId:
heightLabelId:
heightInputId:
widthInputId:
nodeGroupInfoId:
nameInputId:
notesTextareaId:
}
palettesWindow:{
id:
furnitureSearchInputId:
furniturePaletteId:
}
overviewWindow: {
id:
}
optionsWindow: {
id:
gridSizeInputId:
unitsConversionFactorInputId:
unitsFormId:
unitsFormName:
checkboxes: {
showGridCheckboxId:
gridSnapCheckboxId:
wallGuidelinesCheckboxId:
wallLengthsCheckboxId:
wallAnglesCheckboxId:
smallWallAnglesCheckboxId:
}
}
statisticsWindow: {
id:
textDivId:
numsTableId:
totalsTableId:
}
}
scaleDisplayId:
setBehaviorClass:
wallThicknessInputId:
wallThicknessBoxId:
unitsBoxId:
unitsInputId:
*/
function FloorplanUI(floorplan, name, floorplanName, state) {
this._floorplan = floorplan;
this._name = name;
this._floorplanName = floorplanName;
this._state = state;
this._furnitureNodeData = null; // used for searchFurniture function. set only once
this.floorplan.floorplanUI = this;
}
// Get Floorplan associated with this UI
Object.defineProperty(FloorplanUI.prototype, "floorplan", {
get: function () { return this._floorplan; }
});
// Get state object containing many ids of various UI elements
Object.defineProperty(FloorplanUI.prototype, "state", {
get: function () { return this._state; }
});
// Get name of this FloorplanUI instance known to the DOM
Object.defineProperty(FloorplanUI.prototype, "name", {
get: function () { return this._name; }
});
// Get name of the Floorplan associated with this FloorplanUI instance known to the DOM
Object.defineProperty(FloorplanUI.prototype, "floorplanName", {
get: function () { return this._floorplanName; }
});
Object.defineProperty(FloorplanUI.prototype, "furnitureData", {
get: function () { return this._furnitureData; },
set: function (val) { this._furnitureData = val; }
});
/*
* UI manipulation:
* Hide/Show Element, Adjust Scale, ChangeGridSize, Change Units Conversion Factor
* Search Furniture, Checkbox Changed, Change Units, Set Behavior, Update UI
*/
/*
* Hide or show specific help/windows (used mainly with hotkeys)
* @param {String} id The ID of the window to show / hide
*/
FloorplanUI.prototype.hideShow = function(id) {
var element = document.getElementById(id); var str;
var windows = this.state.windows;
switch (id) {
case windows.diagramHelpDiv.id: str = 'Diagram Help'; char = 'H'; break;
case windows.selectionInfoWindow.id: str = 'Selection Help'; char = 'I'; break;
case windows.overviewWindow.id: str = 'Overview'; char = 'E'; break;
case windows.optionsWindow.id: str = 'Options'; char = 'B'; break;
case windows.statisticsWindow.id: str = 'Statistics'; char = 'G'; break;
case windows.palettesWindow.id: str = 'Palettes'; char = 'P'; {
furniturePalette.layoutDiagram(true);
wallPartsPalette.layoutDiagram(true);
break;
}
}
var button = document.getElementById(id + 'Button');
element.style.visibility = element.style.visibility === "visible" ? "hidden" : "visible";
var verb = element.style.visibility === "visible" ? "Hide " : "Show ";
if (button) button.innerHTML = verb + str + "<p class='shortcut'> (Ctrl + " + char + " )</p>";
}
/*
* Set text under Diagram to suggest most common functions user could perform
* @param {String} str The text to display in the Diagram Help div
*/
FloorplanUI.prototype.setDiagramHelper = function(str) {
var helper = document.getElementById(this.state.windows.diagramHelpDiv.id);
if (helper) helper.innerHTML = '<p>' + str + '</p>';
}
/*
* Increase / decrease diagram scale to the nearest 10%
* @param {String} sign Accepted values are "+" and "-"
*/
FloorplanUI.prototype.adjustScale = function(sign) {
var floorplan = this.floorplan;
var el = document.getElementById(this.state.scaleDisplayId);
floorplan.startTransaction('Change Scale');
switch (sign) {
case '-': floorplan.scale -= .1; break;
case '+': floorplan.scale += .1; break;
}
floorplan.scale = parseFloat((Math.round(floorplan.scale / .1) * .1).toFixed(2));
var scale = (floorplan.scale * 100).toFixed(2);
el.innerHTML = 'Scale: ' + scale + '%';
floorplan.commitTransaction('Change Scale');
}
// Change edge length of the grid based on input
FloorplanUI.prototype.changeGridSize = function () {
var floorplan = this.floorplan;
floorplan.skipsUndoManager = true;
floorplan.startTransaction("change grid size");
var el = document.getElementById(this.state.windows.optionsWindow.gridSizeInputId); var input;
if (!isNaN(el.value) && el.value != null && el.value != '' && el.value != undefined && el.value > 1) input = parseFloat(el.value);
else {
el.value = floorplan.convertPixelsToUnits(10); // if bad input given, revert to 20cm (10px) or unit equivalent
input = parseFloat(el.value);
}
input = floorplan.convertUnitsToPixels(input);
floorplan.grid.gridCellSize = new go.Size(input, input);
floorplan.toolManager.draggingTool.gridCellSize = new go.Size(input, input);
floorplan.model.setDataProperty(floorplan.model.modelData, "gridSize", input);
floorplan.commitTransaction("change grid size");
floorplan.skipsUndoManager = false;
}
FloorplanUI.prototype.changeUnitsConversionFactor = function () {
var floorplan = this.floorplan;
var val = document.getElementById(this.state.windows.optionsWindow.unitsConversionFactorInputId).value;
if (isNaN(val) || !val || val == undefined) return;
floorplan.skipsUndoManager = true;
floorplan.model.set(floorplan.model.modelData, "unitsConversionFactor", val);
floorplan.skipsUndoManager = false;
}
// Search through all elements in the furniture palette (useful for a palette with many furniture nodes)
FloorplanUI.prototype.searchFurniture = function () {
var ui = this;
var floorplan = this.floorplan;
var furniturePaletteId = ui.state.windows.palettesWindow.furniturePaletteId;
var str = document.getElementById(ui.state.windows.palettesWindow.furnitureSearchInputId).value;
var furniturePalette = window.furniturePalette;
/*var furniturePalette = null;
for (var i = 0; i < floorplan.palettes.length; i++) {
var palette = floorplan.palettes[i];
if (palette.div.id == furniturePaletteId) {
furniturePalette = floorplan.palettes[i];
}
}*/
if (ui.furnitureData == null) ui.furnitureData = furniturePalette.model.nodeDataArray;
var items = furniturePalette.model.nodeDataArray.slice();
if (str !== null && str !== undefined && str !== "") {
for (var i = 0; i < items.length; i += 0) {
var item = items[i];
if (item.type.toLowerCase().indexOf(str.toLowerCase()) === -1) {
items.splice(i, 1);
}
else i++;
}
furniturePalette.model.nodeDataArray = items;
}
else furniturePalette.model.nodeDataArray = ui.furnitureData;
furniturePalette.updateAllRelationshipsFromData();
}
/*
* Change the "checked" value of checkboxes in the Options Menu, and to have those changes reflected in app behavior / model data
* @param {String} id The ID of the changed checkbox
*/
FloorplanUI.prototype.checkboxChanged = function (id) {
var floorplan = this.floorplan;
var checkboxes = this.state.windows.optionsWindow.checkboxes;
floorplan.skipsUndoManager = true;
floorplan.startTransaction("change preference");
var element = document.getElementById(id);
switch (id) {
case checkboxes.showGridCheckboxId: {
floorplan.grid.visible = element.checked;
floorplan.model.modelData.preferences.showGrid = element.checked;
break;
}
case checkboxes.gridSnapCheckboxId: {
floorplan.toolManager.draggingTool.isGridSnapEnabled = element.checked;
floorplan.model.modelData.preferences.gridSnap = element.checked;
break;
}
case checkboxes.wallGuidelinesCheckboxId: floorplan.model.modelData.preferences.showWallGuidelines = element.checked; break;
case checkboxes.wallLengthsCheckboxId: floorplan.model.modelData.preferences.showWallLengths = element.checked; floorplan.updateWallDimensions(); break;
case checkboxes.wallAnglesCheckboxId: floorplan.model.modelData.preferences.showWallAngles = element.checked; floorplan.updateWallAngles(); break;
case checkboxes.smallWallAnglesCheckboxId: floorplan.model.modelData.preferences.showOnlySmallWallAngles = element.checked; floorplan.updateWallAngles(); break;
}
floorplan.commitTransaction("change preference");
floorplan.skipsUndoManager = false;
}
// Adjust units based on the selected radio button in the Options Menu
FloorplanUI.prototype.changeUnits = function() {
var floorplan = this.floorplan;
floorplan.startTransaction("set units");
var prevUnits = floorplan.model.modelData.units;
var radios = document.forms[this.state.windows.optionsWindow.unitsFormId].elements[this.state.windows.optionsWindow.unitsFormName];
for (var i = 0; i < radios.length; i++) {
if (radios[i].checked) {
floorplan.model.setDataProperty(floorplan.model.modelData, "units", radios[i].id);
}
}
var units = floorplan.model.modelData.units;
switch (units) {
case 'centimeters': floorplan.model.setDataProperty(floorplan.model.modelData, "unitsAbbreviation", 'cm'); break;
case 'meters': floorplan.model.setDataProperty(floorplan.model.modelData, "unitsAbbreviation", 'm'); break;
case 'feet': floorplan.model.setDataProperty(floorplan.model.modelData, "unitsAbbreviation", 'ft'); break;
case 'inches': floorplan.model.setDataProperty(floorplan.model.modelData, "unitsAbbreviation", 'in'); break;
}
var unitsAbbreviation = floorplan.model.modelData.unitsAbbreviation;
// update all units boxes with new units
var unitAbbrevInputs = document.getElementsByClassName(this.state.unitsBoxClass);
for (var i = 0; i < unitAbbrevInputs.length; i++) {
unitAbbrevInputs[i].value = unitsAbbreviation;
}
var unitInputs = document.getElementsByClassName(this.state.unitsInputClass);
for (var i = 0; i < unitInputs.length; i++) {
var input = unitInputs[i];
floorplan.model.setDataProperty(floorplan.model.modelData, "units", prevUnits);
var value = floorplan.convertUnitsToPixels(input.value);
floorplan.model.setDataProperty(floorplan.model.modelData, "units", units)
value = floorplan.convertPixelsToUnits(value);
input.value = value;
}
if (floorplan.selection.count === 1) this.setSelectionInfo(floorplan.selection.first()); // reload node info measurements according to new units
floorplan.commitTransaction("set units");
}
/*
* Set current tool (selecting/dragging or wallbuilding/reshaping)
* @param {String} string Informs what behavior to switch to. Accepted values: "dragging", "wallbuilding"
*/
FloorplanUI.prototype.setBehavior = function (string) {
var floorplan = this.floorplan;
var ui = this;
var wallBuildingTool = floorplan.toolManager.mouseDownTools.elt(0);
var wallReshapingTool = floorplan.toolManager.mouseDownTools.elt(3);
// style the current tool HTML button accordingly
var elements = document.getElementsByClassName(this.state.setBehaviorClass);
for (var i = 0; i < elements.length; i++) {
var el = elements[i];
if (el.id === string + "Button") el.style.backgroundColor = '#4b545f';
else el.style.backgroundColor = '#bbbbbb';
}
var wallThicknessBox = document.getElementById(this.state.wallThicknessBoxId)
if (string === 'wallBuilding') {
wallBuildingTool.isEnabled = true;
wallReshapingTool.isEnabled = false;
floorplan.skipsUndoManager = true;
floorplan.startTransaction("change wallThickness");
// create walls with wallThickness in input box
floorplan.model.setDataProperty(floorplan.model.modelData, 'wallThickness', parseFloat(document.getElementById(ui.state.wallThicknessInputId).value));
var wallThickness = floorplan.model.modelData.wallThickness;
if (isNaN(wallThickness)) floorplan.model.setDataProperty(floorplan.model.modelData, 'wallThickness', 5);
else {
var width = floorplan.convertUnitsToPixels(wallThickness);
floorplan.model.setDataProperty(floorplan.model.modelData, 'wallThickness', width);
}
floorplan.commitTransaction("change wallThickness");
floorplan.skipsUndoManager = false;
wallThicknessBox.style.visibility = 'visible';
wallThicknessBox.style.display = 'inline-block';
ui.setDiagramHelper("Click and drag on the diagram to draw a wall (hold SHIFT for 45 degree angles)");
}
if (string === 'dragging') {
wallBuildingTool.isEnabled = false;
wallReshapingTool.isEnabled = true;
wallThicknessBox.style.visibility = 'hidden';
wallThicknessBox.style.display = 'none';
}
// clear resize adornments on walls/windows, if there are any
floorplan.nodes.iterator.each(function (n) { n.clearAdornments(); })
floorplan.clearSelection();
}
/*
* Populating UI Windows from Floorplan data:
* Update UI, Update Statistics, Fill Rows With Nodes, Set Selection Info, Set Color, Set Height, Set Width, Apply Selection Changes
*/
// Update the UI properly in accordance with model.modelData (called only when a new floorplan is loaded or created)
FloorplanUI.prototype.updateUI = function () {
var floorplan = this.floorplan;
var modelData = floorplan.model.modelData;
var checkboxes = this.state.windows.optionsWindow.checkboxes;
if (floorplan.floorplanUI) floorplan.floorplanUI.changeUnits();
document.getElementById(this.state.wallThicknessInputId).value = floorplan.convertPixelsToUnits(modelData.wallThickness);
// update options GUI based on floorplan.model.modelData.preferences
var preferences = modelData.preferences;
document.getElementById(checkboxes.showGridCheckboxId).checked = preferences.showGrid;
document.getElementById(checkboxes.gridSnapCheckboxId).checked = preferences.gridSnap;
document.getElementById(checkboxes.wallGuidelinesCheckboxId).checked = preferences.showWallGuidelines;
document.getElementById(checkboxes.wallLengthsCheckboxId).checked = preferences.showWallLengths;
document.getElementById(checkboxes.wallAnglesCheckboxId).checked = preferences.showWallAngles;
document.getElementById(checkboxes.smallWallAnglesCheckboxId).checked = preferences.showOnlySmallWallAngles;
}
// Update all statistics in Statistics Window - called when a Floorplan's model is changed
FloorplanUI.prototype.updateStatistics = function () {
var floorplan = this.floorplan;
var statsWindow = this.state.windows.statisticsWindow;
var element = document.getElementById(statsWindow.textDivId);
if (element) {
element.innerHTML = "<div class='row'><div class='col-2' style='height: 165px; overflow: auto;'> Item Types <table id='" + statsWindow.numsTableId + "'></table></div><div class='col-2'> Totals <table id='totalsTable'></table></div></div>";
// fill Item Types table with node type/count of all nodes in diagram
var numsTable = document.getElementById(statsWindow.numsTableId);
// get all palette nodes associated with this Floorplan
var palettes = floorplan.palettes;
var allPaletteNodes = [];
for (var i = 0; i < palettes.length; i++) {
allPaletteNodes = allPaletteNodes.concat(palettes[i].model.nodeDataArray);
}
for (var i = 0; i < allPaletteNodes.length; i++) {
var type = allPaletteNodes[i].type;
var num = floorplan.findNodesByExample({ type: type }).count;
if (num > 0) // only display data for nodes that exist on the diagram
numsTable.innerHTML += "<tr class='data'> <td style='float: left;'>" + type + "</td> <td style='float: right;'> " + num + "</td></tr>";
}
// fill Totals table with lengths of all walls
totalsTable = document.getElementById('totalsTable');
var walls = floorplan.findNodesByExample({ category: "WallGroup" });
var totalLength = 0;
walls.iterator.each(function (wall) {
var wallLength = Math.sqrt(wall.data.startpoint.distanceSquaredPoint(wall.data.endpoint));
totalLength += wallLength;
});
totalLength = floorplan.convertPixelsToUnits(totalLength).toFixed(2);
var unitsAbbreviation = floorplan.model.modelData.unitsAbbreviation;
totalsTable.innerHTML += "<tr class='data'><td style='float: left;'>Wall Lengths</td><td style='float: right;'>" + totalLength + unitsAbbreviation + "</td></tr>";
}
}
/* Helper function for setSelectionInfo(); displays all nodes in a given set in a given 1 or 2 rows in a given HTML element
* @param {Iterable | Array} iterator A iterable collection of nodes to display in rows
* @param {String} element The ID of the HTML element to fill with this data
* @param {String} selectedKey The key of the currently selected node -- this node's name will be styled differently in the rows
*/
// TODO some repetitive code here
FloorplanUI.prototype.fillRowsWithNodes = function(iterator, element, selectedKey) {
var floorplan = this.floorplan;
var ui = this;
var arr = [];
if (iterator.constructor !== Array) iterator.each(function (p) { arr.push(p); });
else arr = iterator;
// helper
function makeOnClick(key) {
return ui.name + '.setSelectionInfo(' + ui.floorplanName + '.findPartForKey(' + "'" + key + "'" + '))';
}
for (var i = 0; i < arr.length; i += 2) {
if (arr[i].data === null) { this.setSelectionInfo('Nothing selected'); return; }
var key1 = arr[i].data.key; // keys used to locate the node if clicked on...
var name1 = (arr[i].data.caption !== "MultiPurposeNode") ? arr[i].data.caption : arr[i].data.text; // ... names are editable, so users can distinguish between nodes
// if there are two nodes for this row...
if (arr[i + 1] != undefined && arr[i + 1] != null) {
var key2 = arr[i + 1].data.key;
var name2 = (arr[i + 1].data.caption !== "MultiPurposeNode") ? arr[i + 1].data.caption : arr[i + 1].data.text;
// if there's a non-null selectedKey, highlight the selected node in the list
if (key1 === selectedKey) element.innerHTML += '<div class="row"><div class="col-2"><p class="data clickable selectedKey" onclick="' + makeOnClick(key1) + '">' + name1 +
'</p></div><div class="col-2"><p class="data clickable" onclick="' + makeOnClick(key2) + '">' + name2 + '</p></div></div>';
else if (key2 === selectedKey) element.innerHTML += '<div class="row"><div class="col-2"><p class="data clickable" onclick="' + makeOnClick(key1) + '">' + name1 +
'</p></div><div class="col-2"><p class="data clickable selectedKey" onclick="' + makeOnClick(key2) + '">' + name2 + '</p></div></div>';
else element.innerHTML += '<div class="row"><div class="col-2"><p class="data clickable"' + 'onclick="' + makeOnClick(key1) + '">' + name1 +
'</p></div><div class="col-2"><p class="data clickable"' + 'onclick="' + makeOnClick(key2) + '">' + name2 + '</p></div></div>';
}
// if there's only one node for this row...
else {
if (key1 === selectedKey) element.innerHTML += '<div class="row"><div class="col-2"><p class="data clickable selectedKey" onclick="' + makeOnClick(key1) + '">' + name1 + '</p></div></div>';
else element.innerHTML += '<div class="row"><div class="col-2"><p class="data clickable" onclick="' + makeOnClick(key1) + '">' + name1 + '</p></div></div>';
}
}
}
/*
* Displays dynamic, editable info about selection in Selection Info window (height/length, width, name, group info, color, etc.)
* @param {Node | String} node Can be: Reference to Node / Group, go.Node/go.Group key, "Selection" (indicating multi-selection), "Nothing selected"
*/
FloorplanUI.prototype.setSelectionInfo = function(node) {
var floorplan = this.floorplan;
var ui = this;
if (node instanceof go.GraphObject) node = node.part;
var selectionInfoWindow = this.state.windows.selectionInfoWindow;
var element = document.getElementById(selectionInfoWindow.textDivId);
var state = this.state;
var infoWindow = document.getElementById(selectionInfoWindow.id);
if (element === null || infoWindow === null) return;
if (node === 'Nothing selected' || node.layer === null || node === null) { element.innerHTML = '<p>' + node + '</p>'; return; }
// if there are multiple nodes selected, show all their names, allowing user to click on the node they want
if (node === 'Selection: ') {
var selectionIterator = floorplan.selection.iterator; var arr = [];
element.innerHTML = '<p id="name"> Selection (' + selectionIterator.count + ' items selected): </p>';
this.fillRowsWithNodes(selectionIterator, element, null);
infoWindow.style.height = document.getElementById(selectionInfoWindow.textDivId).offsetHeight + document.getElementById(selectionInfoWindow.handleId).offsetHeight + 5 + 'px';
return;
}
// TODO clean this to be usable by a more general template scheme
// if we have one node selected, gather pertinent information for that node....
floorplan.select(node);
var name = ''; var length; var width; var nodeGroupCount = 0; var notes = node.data.notes;
// get node name
if (node.category === 'MultiPurposeNode') name = node.data.text;
else name = node.data.caption;
// get node height / width / length (dependent on node category)
// Wall Groups
if (node.category === 'WallGroup') {
length = floorplan.convertPixelsToUnits(Math.sqrt(node.data.startpoint.distanceSquared(node.data.endpoint.x, node.data.endpoint.y))).toFixed(2); // wall length
thickness = floorplan.convertPixelsToUnits(node.data.thickness).toFixed(2); // wall thickness
}
// Wall Parts (i.e. Door / Window Nodes)
else if (node.category === 'DoorNode' || node.category === "WindowNode") {
length = floorplan.convertPixelsToUnits(node.data.length).toFixed(2);
}
// Generic Groups
else if (node.data.isGroup && node.category !== "WallGroup") {
height = floorplan.convertPixelsToUnits(node.actualBounds.height).toFixed(2);
width = floorplan.convertPixelsToUnits(node.actualBounds.width).toFixed(2);
}
// Furniture Nodes
else {
height = floorplan.convertPixelsToUnits(node.data.height).toFixed(2);
width = floorplan.convertPixelsToUnits(node.data.width).toFixed(2);
}
// get node group info
if (node.containingGroup != null && node.containingGroup != undefined) {
var nodeGroupParts = node.containingGroup.memberParts;
nodeGroupCount = nodeGroupParts.count;
}
if (node.data.isGroup) {
var nodeGroupParts = node.memberParts;
nodeGroupCount = nodeGroupParts.count;
}
var unitsAbbreviation = floorplan.model.modelData.unitsAbbreviation;
// display information in the selection info window
element.innerHTML = '<p id="name" >Name: ' + '<input id="nameInput" class="nameNotesInput" value="' + name + '"/>' + '</p>'; // name
element.innerHTML += "<p>Notes: <textarea id='"+ selectionInfoWindow.notesTextareaId +"' class='nameNotesInput' >" + notes + "</textarea></p>"; // notes
// display color as a color picker element (furniture nodes only)
if (!node.data.isGroup && node.data.category !== "DoorNode" && node.data.category !== "WindowNode") {
element.innerHTML += '<p>Color: <input type="color" id="'+ selectionInfoWindow.colorPickerId +'" value="' + node.data.color + '" name="selectionColor"></input></p>';
}
// display ONLY "Length" input (Door / Window Nodes only) (this is still technically the "width input" as far as IDs are concerned)
if (node.category === "DoorNode" || node.category === "WindowNode") element.innerHTML += '<div class="row"><p id="'+ selectionInfoWindow.widthLabelId +'" class="data">Length: <br/><input id = "' + selectionInfoWindow.widthInputId + '" class = "dimensionsInput" name = "width" value = "' + length + '"/>'
+ '<input id="widthUnits" class="' + state.unitsBoxClass + '" value=' + unitsAbbreviation + ' disabled/></p>';
// for walls "width" is displayed as "length", "height" is displayed as "Thickness"
else if (node.category === 'WallGroup') {
element.innerHTML += '<div class="row"><div class="col-2"><p id="' + selectionInfoWindow.heightLabelId + '" class="data">Thickness: <br/><input id ="' + selectionInfoWindow.heightInputId + '" class = "dimensionsInput" name = "height" value = "' + thickness
+ '"/><input id="heightUnits" class="' + state.unitsBoxClass + '" value=' + unitsAbbreviation + ' disabled/></p> ' + '</div><div class="col-2"><p class="data">Length: <br/><input id="' + selectionInfoWindow.widthInputId + '" class="dimensionsInput" value = "'
+ length + '"/><input id="widthUnits" class="' + state.unitsBoxClass + '" value="' + unitsAbbreviation + '" disabled/></p>' + '</p></div></div>';
}
// display editable properties height and width (non Door / Window nodes)
else element.innerHTML += '<div class="row"><div class="col-2"><p id="' + selectionInfoWindow.heightLabelId + '" class="data">Height: <br/><input id ="' + selectionInfoWindow.heightInputId + '" class = "dimensionsInput" name = "height" value = "' + height
+ '"/><input id="heightUnits" class="' + state.unitsBoxClass + '" value=' + unitsAbbreviation + ' disabled/></p> ' + '</div><div class="col-2"><p class="data">Width: <br/><input id="' + selectionInfoWindow.widthInputId + '" class="dimensionsInput" value = "'
+ width + '"/><input id="widthUnits" class="' + state.unitsBoxClass + '" value="' + unitsAbbreviation + '" disabled/></p>' + '</p></div></div>';
// do not allow height or width adjustment for group info
if (node.data.isGroup && node.category !== "WallGroup") {
document.getElementById(selectionInfoWindow.heightInputId).disabled = true;
document.getElementById(selectionInfoWindow.widthInputId).disabled = true;
}
// "Apply Changes" button
element.innerHTML += '<div class="row"> <button id="applySelectionChanges" onClick="' + this.name + '.applySelectionChanges()">Apply Changes</button></div>';
// display group info for standard groups, wallParts for walls
var groupName = null; var groupKey = null; var selectedKey = "";
if (node.data.isGroup === true) {
groupName = node.data.caption;
groupKey = node.data.key;
selectedKey = "selectedKey"; // the 'group' node is selected; make it blue to show this
}
if (node.containingGroup !== null) {
groupName = node.containingGroup.data.caption;
groupKey = node.containingGroup.data.key;
}
if (groupName !== null) {
groupKey = "'" + groupKey + "'";
element.innerHTML += '<div class="row data" id="' + selectionInfoWindow.nodeGroupInfoId +'"> <span class="clickable ' + selectedKey + '" onclick="' + ui.name + '.setSelectionInfo(' + ui.floorplanName + '.findPartForKey(' + groupKey + '))">' +
groupName + '</span> Info (' + nodeGroupCount + ' member(s) in <span class="clickable ' + selectedKey + '" onclick="' + ui.name + '.setSelectionInfo(' + ui.floorplanName + '.findPartForKey(' + groupKey + '))">' + groupName + '</span>) </div>';
if (nodeGroupCount != 0) ui.fillRowsWithNodes(nodeGroupParts, document.getElementById(selectionInfoWindow.nodeGroupInfoId), node.data.key);
}
var nameInput = document.getElementById(selectionInfoWindow.nameInputId);
if (!floorplan.isReadOnly) {
// dynamically adjust name of selected node based on user input
nameInput.addEventListener('input', function (e) {
var value = nameInput.value;
floorplan.skipsUndoManager = true;
floorplan.startTransaction("rename node");
if (value === null || value === "" || value === undefined) { floorplan.commitTransaction("rename node"); return; }
floorplan.model.setDataProperty(node.data, "caption", value);
floorplan.model.setDataProperty(node.data, "text", value); // if node is a multi purpose node, update the text on it
floorplan.commitTransaction("rename node");
floorplan.skipsUndoManager = false;
});
// dynamically adjust notes of selected node based on user input
var notesTextarea = document.getElementById(selectionInfoWindow.notesTextareaId);
notesTextarea.addEventListener('input', function (e) {
var value = notesTextarea.value;
floorplan.skipsUndoManager = true;
floorplan.startTransaction("edit node notes");
if (value === null || value === undefined) return;
floorplan.model.setDataProperty(node.data, "notes", value);
floorplan.commitTransaction("edit node notes");
floorplan.skipsUndoManager = false;
});
infoWindow.style.height = document.getElementById(selectionInfoWindow.textDivId).offsetHeight + document.getElementById(selectionInfoWindow.handleId).offsetHeight + 5 + 'px';
}
}
// Triggered by "Apply Changes"; set model data for fill color of the current selection
FloorplanUI.prototype.setColor = function () {
var floorplan = this.floorplan;
var node = floorplan.selection.first();
var colorPicker = document.getElementById(this.state.windows.selectionInfoWindow.colorPickerId);
if (colorPicker !== null) {
floorplan.startTransaction("recolor node");
floorplan.model.setDataProperty(node.data, "color", colorPicker.value);
floorplan.model.setDataProperty(node.data, "stroke", invertColor(colorPicker.value))
floorplan.commitTransaction("recolor node");
}
}
// Triggered by "Apply Changes"; set model data for height of the currently selected node (also handles door length for doors, wall length for walls)
FloorplanUI.prototype.setHeight = function () {
var heightInput = document.getElementById(this.state.windows.selectionInfoWindow.heightInputId);
if (heightInput) {
var floorplan = this.floorplan;
var ui = this;
var node = floorplan.selection.first();
var value = parseFloat(floorplan.convertUnitsToPixels(heightInput.value));
if (isNaN(value)) {
alert("Please enter a valid number");
ui.setSelectionInfo(node, floorplan);
return;
}
floorplan.skipsUndoManager = true;
floorplan.startTransaction("resize node");
if (!floorplan.isReadOnly) {
// Case: Furniture Nodes and Window Nodes; basic height adjustment
if (node.category !== 'WallGroup' && node.category !== 'DoorNode') {
floorplan.model.setDataProperty(node.data, "height", value);
}
// Case: Wall Groups; set wall's data.strokeWidth
else if (node.category === 'WallGroup') {
floorplan.model.setDataProperty(node.data, "thickness", value);
node.memberParts.iterator.each(function (part) {
if (part.category === 'DoorNode') floorplan.model.setDataProperty(part.data, "doorOpeningHeight", value);
if (part.category === 'WindowNode') floorplan.model.setDataProperty(part.data, "height", value);
});
}
// Note: Door Nodes are purposefully unaccounted for, as width (length) adjustment (in setWidth()) also adjusts node height
}
floorplan.commitTransaction("resize node");
floorplan.updateWallDimensions(floorplan);
floorplan.skipsUndoManager = false;
}
}
// Triggered by "Apply Changes"; set model data for width of the currently selected node
FloorplanUI.prototype.setWidth = function() {
var floorplan = this.floorplan;
var ui = this;
var node = floorplan.selection.first();
var widthInput = document.getElementById(this.state.windows.selectionInfoWindow.widthInputId);
if (widthInput === null) return;
var value = parseFloat(floorplan.convertUnitsToPixels(widthInput.value));
if (isNaN(value)) {
alert("Please enter a valid number");
ui.setSelectionInfo(node, floorplan);
return;
}
floorplan.skipsUndoManager = true;
floorplan.startTransaction("resize node");
if (!floorplan.isReadOnly) {
// Case: Window / Door Nodes (part.width is part.data.length), keeps windows and doors within wall boundaries (and surrounding wall part boundaries)
if (node.category === 'WindowNode' || node.category === "DoorNode") {
var wall = floorplan.findPartForKey(node.data.group);
var loc = node.location.copy();
// constrain max width "value" by the free stretch on the wall "node" is in
if (wall !== null) {
var containingStretch = getWallPartStretch(node);
var stretchLength = Math.sqrt(containingStretch.point1.distanceSquaredPoint(containingStretch.point2));
if (stretchLength < value) {
value = stretchLength;
loc = new go.Point((containingStretch.point1.x + containingStretch.point2.x) / 2,
(containingStretch.point1.y + containingStretch.point2.y) / 2);
}
}
floorplan.model.setDataProperty(node.data, "length", value);
node.location = loc;
floorplan.updateWallDimensions();
}
// Case: Wall Groups; wall length adjustment; do not allow walls to be shorter than the distance between their fathest apart wallParts
else if (node.category === "WallGroup") {
var sPt = node.data.startpoint.copy();
var ePt = node.data.endpoint.copy();
var angle = sPt.directionPoint(ePt);
var midPoint = new go.Point(((sPt.x + ePt.x) / 2), ((sPt.y + ePt.y) / 2));
var newEpt = new go.Point((midPoint.x + (value / 2)), midPoint.y);
var newSpt = new go.Point((midPoint.x - (value / 2)), midPoint.y);
newEpt.offset(-midPoint.x, -midPoint.y).rotate(angle).offset(midPoint.x, midPoint.y);
newSpt.offset(-midPoint.x, -midPoint.y).rotate(angle).offset(midPoint.x, midPoint.y);
// Edge Case 1: The user has input a length shorter than the edge wallPart's endpoints allow
// find the endpoints of the wallparts closest to the endpoints of the wall
var closestPtToSpt = null; var farthestPtFromSpt;
var closestDistToSpt = Number.MAX_VALUE; var farthestDistFromSpt = 0;
node.memberParts.iterator.each(function (wallPart) {
var endpoints = getWallPartEndpoints(wallPart);
var endpoint1 = endpoints[0];
var endpoint2 = endpoints[1];
var distance1 = Math.sqrt(endpoint1.distanceSquaredPoint(sPt));
var distance2 = Math.sqrt(endpoint2.distanceSquaredPoint(sPt));
if (distance1 < closestDistToSpt) {
closestDistToSpt = distance1;
closestPtToSpt = endpoint1;
} if (distance1 > farthestDistFromSpt) {
farthestDistFromSpt = distance1;
farthestPtFromSpt = endpoint1;
} if (distance2 < closestDistToSpt) {
closestDistToSpt = distance2;
closestPtToSpt = endpoint2;
} if (distance2 > farthestDistFromSpt) {
farthestDistFromSpt = distance2;
farthestPtFromSpt = endpoint2;
}
});
if (closestPtToSpt !== null) {
// if the proposed length is smaller than the minDistance, set wall length to minDistance
var proposedDistance = Math.sqrt(newSpt.distanceSquaredPoint(newEpt));
var minDistance = Math.sqrt(closestPtToSpt.distanceSquaredPoint(farthestPtFromSpt));
if (proposedDistance < minDistance) {
newSpt = closestPtToSpt;
newEpt = farthestPtFromSpt;
}
}
/*
* Edge Case 2: The new wall endpoints constructed based on user input do not generate a wall too short for the wall's edge wallPart's endpoints;
* however, there is/are a/some wallPart(s) that do not fit along the new wall endpoints (due to midpoint construction)
* if a wallPart endpoint is outside the line created by newSpt and newEpt, adjust the endpoints accordingly
*/
var farthestPtFromWallPt = null; var farthestFromWallPtDist = 0;
node.memberParts.iterator.each(function (part) {
var endpoints = getWallPartEndpoints(part);
// check for endpoints of wallParts not along the line segment made by newSpt and newEpt
for (var i = 0; i < endpoints.length; i++) {
var point = endpoints[i];
var distanceToStartPoint = parseFloat(Math.sqrt(point.distanceSquaredPoint(newSpt)).toFixed(2));
var distanceToEndPoint = parseFloat(Math.sqrt(point.distanceSquaredPoint(newEpt)).toFixed(2));
var wallLength = parseFloat(Math.sqrt(newSpt.distanceSquaredPoint(newEpt)).toFixed(2));
if ((distanceToStartPoint + distanceToEndPoint).toFixed(2) !== wallLength.toFixed(2)) {
var testDistance = Math.sqrt(point.distanceSquaredPoint(newSpt));
if (testDistance > farthestFromWallPtDist) {
farthestFromWallPtDist = testDistance;
farthestPtFromWallPt = point;
}
}
}
});
// if a wallPart endpoint is outside the wall, adjust the endpoints of the wall to accomodate it
if (farthestPtFromWallPt !== null) {
var distance = Math.sqrt(newSpt.distanceSquaredPoint(newEpt));
if (farthestPtFromWallPt.distanceSquaredPoint(newSpt) < farthestPtFromWallPt.distanceSquaredPoint(newEpt)) {
newSpt = farthestPtFromWallPt;
var totalLength = Math.sqrt(newSpt.distanceSquaredPoint(newEpt));
newEpt = new go.Point(newSpt.x + ((distance / totalLength) * (newEpt.x - newSpt.x)),
newSpt.y + ((distance / totalLength) * (newEpt.y - newSpt.y)));
} else {
newEpt = farthestPtFromWallPt;
var totalLength = Math.sqrt(newSpt.distanceSquaredPoint(newEpt));
newSpt = new go.Point(newEpt.x + ((distance / totalLength) * (newSpt.x - newEpt.x)),
newEpt.y + ((distance / totalLength) * (newSpt.y - newEpt.y)));
}
}
floorplan.model.setDataProperty(node.data, "startpoint", newSpt);
floorplan.model.setDataProperty(node.data, "endpoint", newEpt);
floorplan.updateWall(node);
}
// Case: Standard / Multi-Purpose Nodes; basic width ajustment
else floorplan.model.setDataProperty(node.data, "width", value);
}
floorplan.commitTransaction("resize node");
floorplan.skipsUndoManager = false;
}
// Set height, width, and color of the selection based on user input in the Selection Info Window
FloorplanUI.prototype.applySelectionChanges = function() {
var floorplan = this.floorplan;
var ui = this;
this.setHeight();
this.setWidth();
this.setColor();
ui.setSelectionInfo(floorplan.selection.first(), floorplan);
}
@@ -0,0 +1,377 @@
/*
* Copyright (C) 1998-2020 by Northwoods Software Corporation
* All Rights Reserved.
*
* Floorplanner Constants
*/
// The diagram model data for the default floorplanner
DEFAULT_MODEL_DATA = {
"class": "go.GraphLinksModel",
"modelData": { "units": "centimeters", "unitsAbbreviation": "cm", "unitsConversionFactor": 2, "gridSize": 10, "wallThickness": 5, "preferences": { "showWallGuidelines": true, "showWallLengths": true, "showWallAngles": true, "showOnlySmallWallAngles": true, "showGrid": true, "gridSnap": true } },
"nodeDataArray": [
{ "key": "wall", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": -430, "y": 240 }, "endpoint": { "class": "go.Point", "x": -430, "y": -240 }, "thickness": 10, "isGroup": true, "notes": "" },
{ "key": "wall3", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": -430, "y": -240 }, "endpoint": { "class": "go.Point", "x": 260, "y": -240 }, "thickness": 10, "isGroup": true, "notes": "" },
{ "key": "wall4", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 260, "y": -240 }, "endpoint": { "class": "go.Point", "x": 260, "y": 240 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall5", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 260, "y": 140 }, "endpoint": { "class": "go.Point", "x": 550, "y": 140 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall6", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 260, "y": 240 }, "endpoint": { "class": "go.Point", "x": 390, "y": 240 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall7", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 550, "y": 140 }, "endpoint": { "class": "go.Point", "x": 550, "y": 400 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall8", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 390, "y": 240 }, "endpoint": { "class": "go.Point", "x": 390, "y": 400 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall9", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 390, "y": 400 }, "endpoint": { "class": "go.Point", "x": 550, "y": 400 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "staircase", "color": "#ffffff", "stroke": "#000000", "caption": "Staircase", "type": "Staircase", "geo": "F1 M0 0 L 0 100 250 100 250 0 0 0 M25 100 L 25 0 M 50 100 L 50 0 M 75 100 L 75 0 M 100 100 L 100 0 M 125 100 L 125 0 M 150 100 L 150 0 M 175 100 L 175 0 M 200 100 L 200 0 M 225 100 L 225 0", "width": 125, "height": 50, "notes": "", "loc": "430 330", "angle": 270 },
{ "key": "staircase2", "color": "#ffffff", "stroke": "#000000", "caption": "Staircase", "type": "Staircase", "geo": "F1 M0 0 L 0 100 250 100 250 0 0 0 M25 100 L 25 0 M 50 100 L 50 0 M 75 100 L 75 0 M 100 100 L 100 0 M 125 100 L 125 0 M 150 100 L 150 0 M 175 100 L 175 0 M 200 100 L 200 0 M 225 100 L 225 0", "width": 125, "height": 50, "notes": "", "loc": "500 330", "angle": 270 },
{ "key": "wall10", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": -230, "y": -130 }, "endpoint": { "class": "go.Point", "x": 70, "y": -130 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall11", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 70, "y": -130 }, "endpoint": { "class": "go.Point", "x": 130, "y": -70 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall12", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 130, "y": -70 }, "endpoint": { "class": "go.Point", "x": 130, "y": 40 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall13", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 130, "y": 40 }, "endpoint": { "class": "go.Point", "x": 70, "y": 100 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall14", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 70, "y": 100 }, "endpoint": { "class": "go.Point", "x": -230, "y": 100 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall15", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": -230, "y": -130 }, "endpoint": { "class": "go.Point", "x": -290, "y": -70 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall16", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": -290, "y": -70 }, "endpoint": { "class": "go.Point", "x": -290, "y": 40 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall17", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": -290, "y": 40 }, "endpoint": { "class": "go.Point", "x": -230, "y": 100 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "door", "category": "DoorNode", "color": "rgba(0, 0, 0, 0)", "caption": "Door", "type": "Door", "length": 56, "doorOpeningHeight": 5, "swing": "left", "notes": "", "loc": "260 188.20000076293945", "group": "wall4", "angle": 90 },
{ "key": "diningTable", "color": "#704332", "stroke": "#8FBCCD", "caption": "Dining Table", "type": "Dining Table", "geo": "F1 M 0 0 L 0 100 200 100 200 0 0 0 M 25 0 L 25 -10 75 -10 75 0 M 125 0 L 125 -10 175 -10 175 0 M 200 25 L 210 25 210 75 200 75 M 125 100 L 125 110 L 175 110 L 175 100 M 25 100 L 25 110 75 110 75 100 M 0 75 -10 75 -10 25 0 25", "width": 205, "height": 70.5, "notes": "", "loc": "-80 -20" },
{ "key": "wall18", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 550, "y": -240 }, "endpoint": { "class": "go.Point", "x": 550, "y": 140 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall19", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 260, "y": -240 }, "endpoint": { "class": "go.Point", "x": 310, "y": -310 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall20", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 310, "y": -310 }, "endpoint": { "class": "go.Point", "x": 500, "y": -310 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall21", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 500, "y": -310 }, "endpoint": { "class": "go.Point", "x": 550, "y": -240 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "door2", "category": "DoorNode", "color": "rgba(0, 0, 0, 0)", "caption": "Door", "type": "Door", "length": 40, "doorOpeningHeight": 5, "swing": "left", "notes": "", "loc": "-290 5.200000762939453", "group": "wall16", "angle": 90 },
{ "category": "WindowNode", "key": "window", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 5, "length": 230, "notes": "", "loc": "-80.80000019073492 -130.00000000000006", "group": "wall10" },
{ "category": "WindowNode", "key": "window2", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 5, "length": 230, "notes": "", "loc": "-80.80000019073492 100.00000000000003", "group": "wall14", "angle": 180 },
{ "category": "WindowNode", "key": "window3", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 10, "length": 60, "notes": "", "loc": "-400 -240", "group": "wall3" },
{ "category": "WindowNode", "key": "window32", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 10, "length": 60, "notes": "", "loc": "-234.80000019073486 -240", "group": "wall3" },
{ "category": "WindowNode", "key": "window4", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 10, "length": 60, "notes": "", "loc": "-89.80000019073486 -240", "group": "wall3" },
{ "category": "WindowNode", "key": "window5", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 10, "length": 60, "notes": "", "loc": "80.19999980926514 -240", "group": "wall3" },
{ "category": "WindowNode", "key": "window6", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 10, "length": 60, "notes": "", "loc": "201.19999980926514 -240", "group": "wall3" },
{ "category": "WindowNode", "key": "window7", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 10, "length": 175, "notes": "", "loc": "-430 -152.5", "group": "wall", "angle": 90 },
{ "category": "WindowNode", "key": "window8", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 10, "length": 233, "notes": "", "loc": "-430 123.49999999999999", "group": "wall", "angle": 270 },
{ "key": "wall32", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": -430, "y": 240 }, "endpoint": { "class": "go.Point", "x": 260, "y": 240 }, "thickness": 10, "isGroup": true, "notes": "" },
{ "category": "WindowNode", "key": "window33", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 10, "length": 60, "notes": "", "loc": "-400 240", "group": "wall32" },
{ "category": "WindowNode", "key": "window322", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 10, "length": 60, "notes": "", "loc": "-234.80000019073486 240", "group": "wall32" },
{ "category": "WindowNode", "key": "window42", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 10, "length": 60, "notes": "", "loc": "-89.80000019073486 240", "group": "wall32" },
{ "category": "WindowNode", "key": "window52", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 10, "length": 60, "notes": "", "loc": "80.19999980926514 240", "group": "wall32" },
{ "category": "WindowNode", "key": "window62", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 10, "length": 60, "notes": "", "loc": "201.19999980926514 240", "group": "wall32" },
{ "key": "wall2", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 260, "y": 0 }, "endpoint": { "class": "go.Point", "x": 380, "y": 0 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "door5", "category": "DoorNode", "color": "rgba(0, 0, 0, 0)", "caption": "Door", "type": "Door", "length": 40, "doorOpeningHeight": 5, "swing": "right", "notes": "", "loc": "-290 -34.79999923706055", "angle": 90, "group": "wall16" },
{ "key": "door52", "category": "DoorNode", "color": "rgba(0, 0, 0, 0)", "caption": "Door", "type": "Door", "length": 40, "doorOpeningHeight": 5, "swing": "left", "notes": "", "loc": "130 -37.79999923706055", "angle": 270, "group": "wall12" },
{ "key": "door22", "category": "DoorNode", "color": "rgba(0, 0, 0, 0)", "caption": "Door", "type": "Door", "length": 40, "doorOpeningHeight": 5, "swing": "right", "notes": "", "loc": "130 2.200000762939453", "group": "wall12", "angle": 270 },
{ "key": "sink", "color": "#c0c0c0", "stroke": "#3F3F3F", "caption": "Sink", "type": "Sink", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0z M5 7.5 L18.5 7.5 M 21.5 7.5 L35 7.5 35 35 5 35 5 7.5 M 15 21.25 A 5 5 180 1 0 15 21.24 M23 3.75 A 3 3 180 1 1 23 3.74 M21.5 6.25 L 21.5 12.5 18.5 12.5 18.5 6.25 M15 3.75 A 1 1 180 1 1 15 3.74 M 10 4.25 L 10 3.25 13 3.25 M 13 4.25 L 10 4.25 M27 3.75 A 1 1 180 1 1 27 3.74 M 26.85 3.25 L 30 3.25 30 4.25 M 26.85 4.25 L 30 4.25", "width": 27, "height": 27, "notes": "", "loc": "361.5 110", "angle": 180, "group": -52 },
{ "key": "shower", "color": "#b9cece", "stroke": "#463131", "caption": "Shower/Tub", "type": "Shower/Tub", "geo": "F1 M0 0 L40 0 40 60 0 60 0 0 M35 15 L35 55 5 55 5 15 Q5 5 20 5 Q35 5 35 15 M22.5 20 A2.5 2.5 180 1 1 22.5 19.99", "width": 57, "height": 109, "notes": "", "loc": "296 67", "group": -52 },
{ "key": "toilet", "color": "#f7f9e3", "stroke": "#08061C", "caption": "Toilet", "type": "Toilet", "geo": "F1 M0 0 L25 0 25 10 0 10 0 0 M20 10 L20 15 5 15 5 10 20 10 M5 15 Q0 15 0 25 Q0 40 12.5 40 Q25 40 25 25 Q25 15 20 15", "width": 25, "height": 35, "notes": "", "loc": "350 30", "group": -52 },
{ "key": "wall22", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 380, "y": 0 }, "endpoint": { "class": "go.Point", "x": 380, "y": 140 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "door3", "category": "DoorNode", "color": "rgba(0, 0, 0, 0)", "caption": "Door", "type": "Door", "length": 36, "doorOpeningHeight": 5, "swing": "left", "notes": "", "loc": "380 65.20000076293945", "group": "wall22", "angle": 270 },
{ "key": "wall23", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 430, "y": 0 }, "endpoint": { "class": "go.Point", "x": 550, "y": 0 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall24", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 430, "y": 0 }, "endpoint": { "class": "go.Point", "x": 430, "y": 140 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "isGroup": true, "key": -52, "caption": "Group", "notes": "" },
{ "isGroup": true, "key": -53, "caption": "Group", "notes": "" },
{ "key": "shower2", "color": "#b9cece", "stroke": "#463131", "caption": "Shower/Tub", "type": "Shower/Tub", "geo": "F1 M0 0 L40 0 40 60 0 60 0 0 M35 15 L35 55 5 55 5 15 Q5 5 20 5 Q35 5 35 15 M22.5 20 A2.5 2.5 180 1 1 22.5 19.99", "width": 57, "height": 109, "notes": "", "loc": "510 70", "group": -53 },
{ "key": "toilet2", "color": "#f7f9e3", "stroke": "#08061C", "caption": "Toilet", "type": "Toilet", "geo": "F1 M0 0 L25 0 25 10 0 10 0 0 M20 10 L20 15 5 15 5 10 20 10 M5 15 Q0 15 0 25 Q0 40 12.5 40 Q25 40 25 25 Q25 15 20 15", "width": 25, "height": 35, "notes": "", "loc": "460 30", "group": -53 },
{ "key": "sink2", "color": "#c0c0c0", "stroke": "#3F3F3F", "caption": "Sink", "type": "Sink", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0z M5 7.5 L18.5 7.5 M 21.5 7.5 L35 7.5 35 35 5 35 5 7.5 M 15 21.25 A 5 5 180 1 0 15 21.24 M23 3.75 A 3 3 180 1 1 23 3.74 M21.5 6.25 L 21.5 12.5 18.5 12.5 18.5 6.25 M15 3.75 A 1 1 180 1 1 15 3.74 M 10 4.25 L 10 3.25 13 3.25 M 13 4.25 L 10 4.25 M27 3.75 A 1 1 180 1 1 27 3.74 M 26.85 3.25 L 30 3.25 30 4.25 M 26.85 4.25 L 30 4.25", "width": 27, "height": 27, "notes": "", "loc": "460 112", "angle": 180, "group": -53 },
{ "key": "door32", "category": "DoorNode", "color": "rgba(0, 0, 0, 0)", "caption": "Door", "type": "Door", "length": 36, "doorOpeningHeight": 5, "swing": "left", "notes": "", "loc": "430 69.20000076293945", "group": "wall24", "angle": 90 },
{ "key": "sofaMedium", "color": "#c6a8c5", "stroke": "#39573A", "caption": "Sofa", "type": "Sofa", "geo": "F1 M0 0 L80 0 80 40 0 40 0 0 M10 35 L10 10 M0 0 Q8 0 10 10 M0 40 Q40 15 80 40 M70 10 Q72 0 80 0 M70 10 L70 35", "height": 45, "width": 90, "notes": "", "loc": "320 -40" },
{ "key": "sofaMedium2", "color": "#c6a8c5", "stroke": "#39573A", "caption": "Sofa", "type": "Sofa", "geo": "F1 M0 0 L80 0 80 40 0 40 0 0 M10 35 L10 10 M0 0 Q8 0 10 10 M0 40 Q40 15 80 40 M70 10 Q72 0 80 0 M70 10 L70 35", "height": 45, "width": 90, "notes": "", "loc": "490 -40" },
{ "key": "roundTable", "color": "#dadada", "stroke": "#252525", "caption": "Round Table", "type": "Round Table", "shape": "Ellipse", "width": 61, "height": 61, "notes": "", "loc": "410 -170", "group": -74 },
{ "category": "MultiPurposeNode", "key": "MultiPurposeNode", "caption": "Multi Purpose Node", "color": "#ffffff", "stroke": "#000000", "name": "Writable Node", "type": "Writable Node", "shape": "Rectangle", "text": "Fridge", "width": 55, "height": 40, "notes": "", "loc": "342.5 -287.5" },
{ "key": "doubleSink", "color": "#d9d9d9", "stroke": "#262626", "caption": "Double Sink", "type": "Double Sink", "geo": "F1 M0 0 L75 0 75 40 0 40 0 0 M5 7.5 L35 7.5 35 35 5 35 5 7.5 M44 7.5 L70 7.5 70 35 40 35 40 9 M15 21.25 A5 5 180 1 0 15 21.24 M50 21.25 A 5 5 180 1 0 50 21.24 M40.5 3.75 A3 3 180 1 1 40.5 3.74 M40.5 3.75 L50.5 13.75 47.5 16.5 37.5 6.75 M32.5 3.75 A 1 1 180 1 1 32.5 3.74 M 27.5 4.25 L 27.5 3.25 30.5 3.25 M 30.5 4.25 L 27.5 4.25 M44.5 3.75 A 1 1 180 1 1 44.5 3.74 M 44.35 3.25 L 47.5 3.25 47.5 4.25 M 44.35 4.25 L 47.5 4.25", "height": 27, "width": 52, "notes": "", "loc": "510 -260", "angle": 53.07333893129521 },
{ "category": "WindowNode", "key": "window9", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 5, "length": 60, "notes": "", "loc": "284.9054049801182 -274.8675669721655", "group": "wall19", "angle": 305.5376777919744 },
{ "category": "WindowNode", "key": "window10", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 5, "length": 60, "notes": "", "loc": "522.9324327288447 -277.8945941796174", "group": "wall21", "angle": 234.46232220802563 },
{ "category": "MultiPurposeNode", "key": "MultiPurposeNode3", "caption": "Multi Purpose Node", "color": "#f7f9e3", "stroke": "#08061C", "name": "Writable Node", "type": "Writable Node", "shape": "Rectangle", "text": "Fridge", "width": 55, "height": 40, "notes": "", "loc": "342.5 -287.5" },
{ "category": "MultiPurposeNode", "key": "MultiPurposeNode32", "caption": "Multi Purpose Node", "color": "#f7f9e3", "stroke": "#08061C", "name": "Writable Node", "type": "Writable Node", "shape": "Rectangle", "text": "Counter", "width": 55, "height": 40, "notes": "", "loc": "395 -287" },
{ "category": "WindowNode", "key": "window11", "color": "white", "caption": "Window", "type": "Window", "shape": "Rectangle", "height": 5, "length": 60, "notes": "", "loc": "397.19999980926514 -310", "group": "wall20" },
{ "key": "door4", "category": "DoorNode", "color": "rgba(0, 0, 0, 0)", "caption": "Door", "type": "Door", "length": 56, "doorOpeningHeight": 5, "swing": "left", "notes": "", "loc": "260 -179.79999923706055", "group": "wall4", "angle": 90 },
{ "key": "stove", "color": "#f7f9e3", "stroke": "#08061C", "caption": "Stove", "type": "Stove", "geo": "F1 M 0 0 L 0 100 100 100 100 0 0 0 M 30 15 A 15 15 180 1 0 30.01 15 M 70 15 A 15 15 180 1 0 70.01 15M 30 55 A 15 15 180 1 0 30.01 55 M 70 55 A 15 15 180 1 0 70.01 55", "width": 55, "height": 40, "notes": "", "loc": "450.22782650708155 -288" },
{ "key": "armChair", "color": "#c0c0c0", "stroke": "#3F3F3F", "caption": "Arm Chair", "type": "Arm Chair", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0 M10 30 L10 10 M0 0 Q8 0 10 10 M0 40 Q20 15 40 40 M30 10 Q32 0 40 0 M30 10 L30 30", "width": 32.865, "height": 32, "notes": "", "loc": "410 -120", "group": -74 },
{ "key": "armChair2", "color": "#c0c0c0", "stroke": "#3F3F3F", "caption": "Arm Chair", "type": "Arm Chair", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0 M10 30 L10 10 M0 0 Q8 0 10 10 M0 40 Q20 15 40 40 M30 10 Q32 0 40 0 M30 10 L30 30", "width": 32.865, "height": 32, "notes": "", "loc": "460 -170", "angle": 270, "group": -74 },
{ "key": "armChair22", "color": "#c0c0c0", "stroke": "#3F3F3F", "caption": "Arm Chair", "type": "Arm Chair", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0 M10 30 L10 10 M0 0 Q8 0 10 10 M0 40 Q20 15 40 40 M30 10 Q32 0 40 0 M30 10 L30 30", "width": 32.865, "height": 32, "notes": "", "loc": "410 -220", "angle": 180, "group": -74 },
{ "key": "armChair222", "color": "#c0c0c0", "stroke": "#3F3F3F", "caption": "Arm Chair", "type": "Arm Chair", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0 M10 30 L10 10 M0 0 Q8 0 10 10 M0 40 Q20 15 40 40 M30 10 Q32 0 40 0 M30 10 L30 30", "width": 32.865, "height": 32, "notes": "", "loc": "360 -170", "angle": 90, "group": -74 },
{ "isGroup": true, "key": -74, "caption": "Group", "notes": "" },
{ "key": "wall25", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": -300, "y": -240 }, "endpoint": { "class": "go.Point", "x": -300, "y": -180 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall252", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": -160, "y": -240 }, "endpoint": { "class": "go.Point", "x": -160, "y": -180 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall2522", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 0, "y": -240 }, "endpoint": { "class": "go.Point", "x": 0, "y": -180 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall25222", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 140, "y": -240 }, "endpoint": { "class": "go.Point", "x": 140, "y": -180 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall253", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": -300, "y": 180 }, "endpoint": { "class": "go.Point", "x": -300, "y": 240 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall2523", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": -160, "y": 180 }, "endpoint": { "class": "go.Point", "x": -160, "y": 240 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall25223", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 0, "y": 180 }, "endpoint": { "class": "go.Point", "x": 0, "y": 240 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall252222", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": 140, "y": 180 }, "endpoint": { "class": "go.Point", "x": 140, "y": 240 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "armChair3", "color": "#e1ddd0", "stroke": "#1E222F", "caption": "Arm Chair", "type": "Arm Chair", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0 M10 30 L10 10 M0 0 Q8 0 10 10 M0 40 Q20 15 40 40 M30 10 Q32 0 40 0 M30 10 L30 30", "width": 29, "height": 27, "notes": "", "loc": "-231.93243243243245 -192.5" },
{ "category": "MultiPurposeNode", "key": "MultiPurposeNode2", "caption": "Multi Purpose Node", "color": "#e1ddd0", "stroke": "#1E222F", "name": "Writable Node", "type": "Writable Node", "shape": "Rectangle", "text": "Desk", "width": 116, "height": 14, "notes": "", "loc": "-232 -220" },
{ "category": "MultiPurposeNode", "key": "MultiPurposeNode25", "caption": "Multi Purpose Node", "color": "#e1ddd0", "stroke": "#1E222F", "name": "Writable Node", "type": "Writable Node", "shape": "Rectangle", "text": "Desk", "width": 116, "height": 14, "notes": "", "loc": "-230 221" },
{ "key": "armChair35", "color": "#e1ddd0", "stroke": "#1E222F", "caption": "Arm Chair", "type": "Arm Chair", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0 M10 30 L10 10 M0 0 Q8 0 10 10 M0 40 Q20 15 40 40 M30 10 Q32 0 40 0 M30 10 L30 30", "width": 29, "height": 27, "notes": "", "loc": "-230 190", "angle": 180 },
{ "key": "armChair352", "color": "#e1ddd0", "stroke": "#1E222F", "caption": "Arm Chair", "type": "Arm Chair", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0 M10 30 L10 10 M0 0 Q8 0 10 10 M0 40 Q20 15 40 40 M30 10 Q32 0 40 0 M30 10 L30 30", "width": 29, "height": 27, "notes": "", "loc": "-80 190", "angle": 180 },
{ "category": "MultiPurposeNode", "key": "MultiPurposeNode252", "caption": "Multi Purpose Node", "color": "#e1ddd0", "stroke": "#1E222F", "name": "Writable Node", "type": "Writable Node", "shape": "Rectangle", "text": "Desk", "width": 116, "height": 14, "notes": "", "loc": "-80 220" },
{ "key": "armChair353", "color": "#e1ddd0", "stroke": "#1E222F", "caption": "Arm Chair", "type": "Arm Chair", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0 M10 30 L10 10 M0 0 Q8 0 10 10 M0 40 Q20 15 40 40 M30 10 Q32 0 40 0 M30 10 L30 30", "width": 29, "height": 27, "notes": "", "loc": "70 190", "angle": 180 },
{ "category": "MultiPurposeNode", "key": "MultiPurposeNode253", "caption": "Multi Purpose Node", "color": "#e1ddd0", "stroke": "#1E222F", "name": "Writable Node", "type": "Writable Node", "shape": "Rectangle", "text": "Desk", "width": 116, "height": 14, "notes": "", "loc": "70 220" },
{ "key": "armChair3532", "color": "#e1ddd0", "stroke": "#1E222F", "caption": "Arm Chair", "type": "Arm Chair", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0 M10 30 L10 10 M0 0 Q8 0 10 10 M0 40 Q20 15 40 40 M30 10 Q32 0 40 0 M30 10 L30 30", "width": 29, "height": 27, "notes": "", "loc": "200 190", "angle": 180 },
{ "category": "MultiPurposeNode", "key": "MultiPurposeNode2532", "caption": "Multi Purpose Node", "color": "#e1ddd0", "stroke": "#1E222F", "name": "Writable Node", "type": "Writable Node", "shape": "Rectangle", "text": "Desk", "width": 116, "height": 14, "notes": "", "loc": "200 220" },
{ "key": "armChair32", "color": "#e1ddd0", "stroke": "#1E222F", "caption": "Arm Chair", "type": "Arm Chair", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0 M10 30 L10 10 M0 0 Q8 0 10 10 M0 40 Q20 15 40 40 M30 10 Q32 0 40 0 M30 10 L30 30", "width": 29, "height": 27, "notes": "", "loc": "-80 -190" },
{ "category": "MultiPurposeNode", "key": "MultiPurposeNode22", "caption": "Multi Purpose Node", "color": "#e1ddd0", "stroke": "#1E222F", "name": "Writable Node", "type": "Writable Node", "shape": "Rectangle", "text": "Desk", "width": 116, "height": 14, "notes": "", "loc": "-80 -220" },
{ "category": "MultiPurposeNode", "key": "MultiPurposeNode222", "caption": "Multi Purpose Node", "color": "#e1ddd0", "stroke": "#1E222F", "name": "Writable Node", "type": "Writable Node", "shape": "Rectangle", "text": "Desk", "width": 116, "height": 14, "notes": "", "loc": "70 -220" },
{ "key": "armChair322", "color": "#e1ddd0", "stroke": "#1E222F", "caption": "Arm Chair", "type": "Arm Chair", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0 M10 30 L10 10 M0 0 Q8 0 10 10 M0 40 Q20 15 40 40 M30 10 Q32 0 40 0 M30 10 L30 30", "width": 29, "height": 27, "notes": "", "loc": "70 -190" },
{ "key": "armChair3222", "color": "#e1ddd0", "stroke": "#1E222F", "caption": "Arm Chair", "type": "Arm Chair", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0 M10 30 L10 10 M0 0 Q8 0 10 10 M0 40 Q20 15 40 40 M30 10 Q32 0 40 0 M30 10 L30 30", "width": 29, "height": 27, "notes": "", "loc": "200 -190" },
{ "category": "MultiPurposeNode", "key": "MultiPurposeNode2222", "caption": "Multi Purpose Node", "color": "#e1ddd0", "stroke": "#1E222F", "name": "Writable Node", "type": "Writable Node", "shape": "Rectangle", "text": "Desk", "width": 116, "height": 14, "notes": "", "loc": "200 -220" },
{ "key": "sofaMedium3", "color": "#b9fde0", "stroke": "#46021F", "caption": "Sofa", "type": "Sofa", "geo": "F1 M0 0 L80 0 80 40 0 40 0 0 M10 35 L10 10 M0 0 Q8 0 10 10 M0 40 Q40 15 80 40 M70 10 Q72 0 80 0 M70 10 L70 35", "height": 27, "width": 90, "notes": "", "loc": "-410 -30", "angle": 90 },
{ "key": "sofaMedium32", "color": "#b9fde0", "stroke": "#46021F", "caption": "Sofa", "type": "Sofa", "geo": "F1 M0 0 L80 0 80 40 0 40 0 0 M10 35 L10 10 M0 0 Q8 0 10 10 M0 40 Q40 15 80 40 M70 10 Q72 0 80 0 M70 10 L70 35", "height": 27, "width": 90, "notes": "", "loc": "240 -20", "angle": 270 },
{ "category": "MultiPurposeNode", "key": "MultiPurposeNode4", "caption": "Multi Purpose Node", "color": "#d6b196", "stroke": "#294E69", "name": "Writable Node", "type": "Writable Node", "shape": "Rectangle", "text": "Desk", "width": 60, "height": 23, "notes": "", "loc": "-381.1676743184333 -190.94449856461264", "angle": 137.27258112448646 },
{ "key": "wall26", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": -300, "y": -180 }, "endpoint": { "class": "go.Point", "x": -380, "y": -100 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall27", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": -380, "y": -100 }, "endpoint": { "class": "go.Point", "x": -430, "y": -100 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall28", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": -300, "y": 180 }, "endpoint": { "class": "go.Point", "x": -380, "y": 100 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "wall29", "category": "WallGroup", "caption": "Wall", "type": "Wall", "startpoint": { "class": "go.Point", "x": -380, "y": 100 }, "endpoint": { "class": "go.Point", "x": -430, "y": 100 }, "thickness": 5, "isGroup": true, "notes": "" },
{ "key": "armChair4", "color": "#d6b196", "stroke": "#294E69", "caption": "Arm Chair", "type": "Arm Chair", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0 M10 30 L10 10 M0 0 Q8 0 10 10 M0 40 Q20 15 40 40 M30 10 Q32 0 40 0 M30 10 L30 30", "width": 21, "height": 21, "notes": "", "loc": "-400 -210", "angle": 135 },
{ "key": "roundTable2", "color": "#d6dfc8", "stroke": "#292037", "caption": "Plant", "type": "Round Table", "shape": "Ellipse", "width": 21, "height": 21, "notes": "", "loc": "-402.5 -116.5", "text": "Plant" },
{ "key": "armChair42", "color": "#d6b196", "stroke": "#294E69", "caption": "Arm Chair", "type": "Arm Chair", "geo": "F1 M0 0 L40 0 40 40 0 40 0 0 M10 30 L10 10 M0 0 Q8 0 10 10 M0 40 Q20 15 40 40 M30 10 Q32 0 40 0 M30 10 L30 30", "width": 21, "height": 21, "notes": "", "loc": "-400 210", "angle": 45 },
{ "category": "MultiPurposeNode", "key": "MultiPurposeNode42", "caption": "Multi Purpose Node", "color": "#d6b196", "stroke": "#294E69", "name": "Writable Node", "type": "Writable Node", "shape": "Rectangle", "text": "Desk", "width": 60, "height": 23, "notes": "", "loc": "-380 190", "angle": 47.002533598871146 },
{ "key": "roundTable22", "color": "#d6dfc8", "stroke": "#292037", "caption": "Plant", "type": "Round Table", "shape": "Ellipse", "width": 21, "height": 21, "notes": "", "loc": "-400 120", "text": "Plant" },
{ "key": "roundTable222", "color": "#d6dfc8", "stroke": "#292037", "caption": "Plant", "type": "Round Table", "shape": "Ellipse", "width": 21, "height": 21, "notes": "", "loc": "-320 200", "text": "Plant" },
{ "key": "door6", "category": "DoorNode", "color": "rgba(0, 0, 0, 0)", "caption": "Door", "type": "Door", "length": 40, "doorOpeningHeight": 5, "swing": "left", "notes": "", "loc": "-334.00000047683716 -145.99999952316284", "group": "wall26", "angle": 315 },
{ "key": "door7", "category": "DoorNode", "color": "rgba(0, 0, 0, 0)", "caption": "Door", "type": "Door", "length": 40, "doorOpeningHeight": 5, "swing": "left", "notes": "", "loc": "-343.7999997138977 136.2000002861023", "group": "wall28", "angle": 225 }
],
"linkDataArray": []
};
// UI Interaction state object for FlooplaUI
GUI_STATE = {
menuButtons: {
selectionInfoWindowButtonId: "selectionInfoWindowButton",
palettesWindowButtonId: "myPaletteWindowButton",
overviewWindowButtonId: "myOverviewWindowButton",
optionsWindowButtonId: "optionsWindowButton",
statisticsWindowButtonId: "statisticsWindowButton"
},
windows: {
diagramHelpDiv: {
id: "diagramHelpDiv"
},
selectionInfoWindow: {
id: "selectionInfoWindow",
textDivId: "selectionInfoTextDiv",
handleId: "selectionInfoWindowHandle",
colorPickerId: "colorPicker",
heightLabelId: "heightLabel",
heightInputId: "heightInput",
widthInputId: "widthInput",
nodeGroupInfoId: "nodeGroupInfo",
nameInputId: "nameInput",
notesTextareaId: "notesTextarea"
},
palettesWindow: {
id: "myPaletteWindow",
furnitureSearchInputId: "furnitureSearchBar",
furniturePaletteId: "furniturePaletteDiv"
},
overviewWindow: {
id: "myOverviewWindow"
},
optionsWindow: {
id: "optionsWindow",
gridSizeInputId: "gridSizeInput",
unitsConversionFactorInputId: "unitsConversionFactorInput",
unitsFormId: "unitsForm",
unitsFormName: "units",
checkboxes: {
showGridCheckboxId: "showGridCheckbox",
gridSnapCheckboxId: "gridSnapCheckbox",
wallGuidelinesCheckboxId: "wallGuidelinesCheckbox",
wallLengthsCheckboxId: "wallLengthsCheckbox",
wallAnglesCheckboxId: "wallAnglesCheckbox",
smallWallAnglesCheckboxId: "smallWallAnglesCheckbox"
},
},
statisticsWindow: {
id: "statisticsWindow",
textDivId: "statisticsWindowTextDiv",
numsTableId: "numsTable",
totalsTableId: "totalsTable"
}
},
scaleDisplayId: "scaleDisplay",
setBehaviorClass: "setBehavior",
wallThicknessInputId: "wallThicknessInput",
wallThicknessBoxId: "wallThicknessBox",
unitsBoxClass: "unitsBox",
unitsInputClass: "unitsInput"
};
// Filesystem state object for FloorplanFilesystem
FILESYSTEM_UI_STATE = {
openWindowId: "openDocument",
removeWindowId: "removeDocument",
currentFileId: "currentFile",
filesToRemoveListId: "filesToRemove",
filesToOpenListId: "filesToOpen"
};
// Node Data Array for Furniture Palette
FURNITURE_NODE_DATA_ARRAY = [
{
category: "MultiPurposeNode",
key: "MultiPurposeNode",
caption: "Multi Purpose Node",
color: "#ffffff",
stroke: '#000000',
name: "Writable Node",
type: "Writable Node",
shape: "Rectangle",
text: "Write here",
width: 60,
height: 60,
notes: ""
},
{
key: "roundTable",
color: "#ffffff",
stroke: '#000000',
caption: "Round Table",
type: "Round Table",
shape: "Ellipse",
width: 61,
height: 61,
notes: ""
},
{
key: "armChair",
color: "#ffffff",
stroke: '#000000',
caption: "Arm Chair",
type: "Arm Chair",
geo: "F1 M0 0 L40 0 40 40 0 40 0 0 M10 30 L10 10 M0 0 Q8 0 10 10 M0 40 Q20 15 40 40 M30 10 Q32 0 40 0 M30 10 L30 30",
width: 45,
height: 45,
notes: ""
},
{
key: "sofaMedium",
color: "#ffffff",
stroke: "#000000",
caption: "Sofa",
type: "Sofa",
geo: "F1 M0 0 L80 0 80 40 0 40 0 0 M10 35 L10 10 M0 0 Q8 0 10 10 M0 40 Q40 15 80 40 M70 10 Q72 0 80 0 M70 10 L70 35",
height: 45,
width: 90,
notes: ""
},
{
key: "sink",
color: "#ffffff",
stroke: '#000000',
caption: "Sink",
type: "Sink",
geo: "F1 M0 0 L40 0 40 40 0 40 0 0z M5 7.5 L18.5 7.5 M 21.5 7.5 L35 7.5 35 35 5 35 5 7.5 M 15 21.25 A 5 5 180 1 0 15 21.24 M23 3.75 A 3 3 180 1 1 23 3.74 M21.5 6.25 L 21.5 12.5 18.5 12.5 18.5 6.25 M15 3.75 A 1 1 180 1 1 15 3.74 M 10 4.25 L 10 3.25 13 3.25 M 13 4.25 L 10 4.25 M27 3.75 A 1 1 180 1 1 27 3.74 M 26.85 3.25 L 30 3.25 30 4.25 M 26.85 4.25 L 30 4.25",
width: 27,
height: 27,
notes: ""
},
{
key: "doubleSink",
color: "#ffffff",
stroke: '#000000',
caption: "Double Sink",
type: "Double Sink",
geo: "F1 M0 0 L75 0 75 40 0 40 0 0 M5 7.5 L35 7.5 35 35 5 35 5 7.5 M44 7.5 L70 7.5 70 35 40 35 40 9 M15 21.25 A5 5 180 1 0 15 21.24 M50 21.25 A 5 5 180 1 0 50 21.24 M40.5 3.75 A3 3 180 1 1 40.5 3.74 M40.5 3.75 L50.5 13.75 47.5 16.5 37.5 6.75 M32.5 3.75 A 1 1 180 1 1 32.5 3.74 M 27.5 4.25 L 27.5 3.25 30.5 3.25 M 30.5 4.25 L 27.5 4.25 M44.5 3.75 A 1 1 180 1 1 44.5 3.74 M 44.35 3.25 L 47.5 3.25 47.5 4.25 M 44.35 4.25 L 47.5 4.25",
height: 27,
width: 52,
notes: ""
},
{
key: "toilet",
color: "#ffffff",
stroke: '#000000',
caption: "Toilet",
type: "Toilet",
geo: "F1 M0 0 L25 0 25 10 0 10 0 0 M20 10 L20 15 5 15 5 10 20 10 M5 15 Q0 15 0 25 Q0 40 12.5 40 Q25 40 25 25 Q25 15 20 15",
width: 25,
height: 35,
notes: ""
},
{
key: "shower",
color: "#ffffff",
stroke: '#000000',
caption: "Shower/Tub",
type: "Shower/Tub",
geo: "F1 M0 0 L40 0 40 60 0 60 0 0 M35 15 L35 55 5 55 5 15 Q5 5 20 5 Q35 5 35 15 M22.5 20 A2.5 2.5 180 1 1 22.5 19.99",
width: 45,
height: 75,
notes: ""
},
{
key: "bed",
color: "#ffffff",
stroke: '#000000',
caption: "Bed",
type: "Bed",
geo: "F1 M0 0 L40 0 40 60 0 60 0 0 M 7.5 2.5 L32.5 2.5 32.5 17.5 7.5 17.5 7.5 2.5 M0 20 L40 20 M0 25 L40 25",
width: 76.2,
height: 101.6,
notes: ""
},
{
key: "staircase",
color: "#ffffff",
stroke: '#000000',
caption: "Staircase",
type: "Staircase",
geo: "F1 M0 0 L 0 100 250 100 250 0 0 0 M25 100 L 25 0 M 50 100 L 50 0 M 75 100 L 75 0 M 100 100 L 100 0 M 125 100 L 125 0 M 150 100 L 150 0 M 175 100 L 175 0 M 200 100 L 200 0 M 225 100 L 225 0",
width: 125,
height: 50,
notes: ""
},
{
key: "stove",
color: "#ffffff",
stroke: '#000000',
caption: "Stove",
type: "Stove",
geo: "F1 M 0 0 L 0 100 100 100 100 0 0 0 M 30 15 A 15 15 180 1 0 30.01 15 M 70 15 A 15 15 180 1 0 70.01 15"
+ "M 30 55 A 15 15 180 1 0 30.01 55 M 70 55 A 15 15 180 1 0 70.01 55",
width: 75,
height: 75,
notes: ""
},
{
key: "diningTable",
color: "#ffffff",
stroke: '#000000',
caption: "Dining Table",
type: "Dining Table",
geo: "F1 M 0 0 L 0 100 200 100 200 0 0 0 M 25 0 L 25 -10 75 -10 75 0 M 125 0 L 125 -10 175 -10 175 0 M 200 25 L 210 25 210 75 200 75 M 125 100 L 125 110 L 175 110 L 175 100 M 25 100 L 25 110 75 110 75 100 M 0 75 -10 75 -10 25 0 25",
width: 125,
height: 62.5,
notes: ""
}
];
// Node Data Array for Wall Parts Palette
WALLPARTS_NODE_DATA_ARRAY = [
{
category: "PaletteWallNode",
key: "wall",
caption: "Wall",
type: "Wall",
color: "#000000",
shape: "Rectangle",
height: 10,
length: 60,
notes: "",
},
{
category: "WindowNode",
key: "window",
color: "white",
caption: "Window",
type: "Window",
shape: "Rectangle",
height: 10,
length: 60,
notes: ""
},
{
key: "door",
category: "DoorNode",
color: "rgba(0, 0, 0, 0)",
caption: "Door",
type: "Door",
length: 40,
doorOpeningHeight: 5,
swing: "left",
notes: ""
}
];
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB