UNPKG

create-gojs-kit

Version:

A CLI for downloading GoJS samples, extensions, and docs

2,057 lines 94.8 kB
/*
 * Copyright 1998-2025 by Northwoods Software Corporation
 * All Rights Reserved.
 *
 * FLOOR PLANNER: WALL RESHAPING TOOL
 * Used to reshape walls via their endpoints in a Floorplan
 */
import * as go from 'gojs';
export class WallReshapingTool extends go.Tool {
    /**
     * @constructor
     * This tool is responsible for allowing walls in a Floorplan to be reshaped via handles on either side.
     */
    constructor(init) {
        super();
        const h = new go.Shape();
        h.figure = 'Diamond';
        h.desiredSize = new go.Size(12, 12);
        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 = 0;
        this._isBuilding = false; // only true when a wall is first being constructed, set in WallBuildingTool's doMouseUp function
        this._isIntersecting = false;
        this._joinedWalls = new go.Set();
        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
        this._joinedWalls = new go.Set();
        this._wallIntersecting = null;
        if (init)
            Object.assign(this, init);
    }
    // Get the archetype for the handle (a Shape)
    get handleArchetype() {
        return this._handleArchetype;
    }
    // Get / set current handle being used to reshape the wall
    get handle() {
        return this._handle;
    }
    set handle(value) {
        this._handle = value;
    }
    // Get / set adorned shape (shape of the Wall Group being reshaped)
    get adornedShape() {
        return this._adornedShape;
    }
    set adornedShape(value) {
        this._adornedShape = value;
    }
    // Get / set current angle
    get angle() {
        return this._angle;
    }
    set angle(value) {
        this._angle = value;
    }
    // Get / set length of the wall being reshaped (used only with SHIFT + drag)
    get length() {
        return this._length;
    }
    set length(value) {
        this._length = value;
    }
    // Get / set the name of the object being reshaped
    get reshapeObjectName() {
        return this._reshapeObjectName;
    }
    set reshapeObjectName(value) {
        this._reshapeObjectName = value;
    }
    // Get / set flag telling tool whether it's reshaping a new wall (isBuilding = true) or reshaping an old wall (isBuilding = false)
    get isBuilding() {
        return this._isBuilding;
    }
    set isBuilding(value) {
        this._isBuilding = value;
    }
    // Get set loc data for wallParts to return to if reshape is cancelled
    get returnData() {
        return this._returnData;
    }
    set returnData(value) {
        this._returnData = value;
    }
    // Get / set the point to return the reshaping wall endpoint to if reshape is cancelled
    get returnPoint() {
        return this._returnPoint;
    }
    set returnPoint(value) {
        this._returnPoint = value;
    }
    // Get / set whether the reshaping wall is intersecting at least one other wall. if so, ignore grid snap
    get isIntersecting() {
        return this._isIntersecting;
    }
    set isIntersecting(value) {
        this._isIntersecting = value;
    }
    // Get / set the wall the reshaping endpoint is currently intersecting
    get wallIntersecting() {
        return this._wallIntersecting;
    }
    set wallIntersecting(value) {
        this._wallIntersecting = value;
    }
    // Get / set the wall created during after a reshape event by combining some colinear walls
    get joinedWalls() {
        return this._joinedWalls;
    }
    set joinedWalls(value) {
        this._joinedWalls = value;
    }
    /**
     * Places reshape handles on either end of a wall node.
     * @param {go.Part} part The wall to adorn
     */
    updateAdornments(part) {
        if (part === null || part instanceof go.Link)
            return;
        if (part.isSelected && !this.diagram.isReadOnly) {
            const seleltgo = part.findObject(this.reshapeObjectName);
            if (seleltgo !== null &&
                seleltgo.part !== null &&
                seleltgo.part.data.category === 'WallGroup') {
                const selelt = seleltgo;
                let adornment = part.findAdornment(this.name);
                if (adornment === null) {
                    adornment = this.makeAdornment(selelt);
                }
                if (adornment !== null && selelt.part !== null && selelt.geometry != null) {
                    // update the position/alignment of each handle
                    const geo = selelt.geometry;
                    const b = geo.bounds;
                    const pb = selelt.part.actualBounds;
                    // update the size of the adornment
                    const graphObj = adornment.findObject('BODY');
                    if (graphObj === null)
                        return;
                    graphObj.desiredSize = b.size;
                    adornment.elements.each(function (h) {
                        if (h.name === undefined)
                            return;
                        let x = 0;
                        let y = 0;
                        switch (h.name) {
                            case 'sPt': {
                                x = part.data.startpoint.x - pb.x;
                                y = part.data.startpoint.y - pb.y;
                                break;
                            }
                            case 'ePt': {
                                x = part.data.endpoint.x - pb.x;
                                y = part.data.endpoint.y - pb.y;
                                break;
                            }
                        }
                        let xCheck = Math.min((x - b.x) / b.width, 1);
                        let 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);
                    return;
                }
            }
        }
        part.removeAdornment(this.name);
    }
    /**
     * If the user has clicked down at a visible handle on a wall node, then the tool may start.
     * @return {boolean}
     */
    canStart() {
        if (!this.isEnabled)
            return false;
        const diagram = this.diagram;
        if (diagram === null || diagram.isReadOnly)
            return false;
        if (!diagram.allowReshape)
            return false;
        if (!diagram.lastInput.left)
            return false;
        const h = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
        return h !== null || this.isBuilding;
    }
    /**
     * Start a new transaction for the wall reshaping.
     * Store pre-reshape location of reshaping wall's reshaping endpoint.
     * Store pre-reshape locations of all wall's members (windows / doors).
     */
    doActivate() {
        const diagram = this.diagram;
        if (diagram === null)
            return;
        if (this.isBuilding) {
            // this.adornedShape has already been set in WallBuildingTool's doMouseDown function
            if (this.adornedShape !== null && this.adornedShape.part !== null) {
                const wall = this.adornedShape.part;
                this.handle = this.findToolHandleAt(wall.data.endpoint, this.name);
                this.returnPoint = wall.data.startpoint;
            }
        }
        else {
            this.handle = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
            if (this.handle === null)
                return;
            const adorn = this.handle.part;
            const shape = adorn.adornedObject;
            const wall = shape.part;
            if (!shape)
                return;
            this.adornedShape = shape;
            // store pre-reshape location of wall's reshaping endpoint
            this.returnPoint = this.handle.name === 'sPt' ? wall.data.startpoint : wall.data.endpoint;
            // store pre-reshape locations of all wall's members (windows / doors)
            const wallParts = wall.memberParts;
            if (wallParts.count !== 0) {
                const locationsMap = new go.Map();
                wallParts.iterator.each(function (wallPart) {
                    locationsMap.add(wallPart.data.key, wallPart.location);
                });
                this.returnData = locationsMap;
            }
        }
        // diagram.isMouseCaptured = true;
        this.startTransaction(this.name);
        this.isActive = true;
    }
    /**
     * Adjust the handle's coordinates, along with the wall's points.
     */
    doMouseMove() {
        const fp = this.diagram;
        const tool = this;
        if (tool.handle === null)
            return;
        const adorn = tool.handle.part;
        const wall = adorn.adornedPart;
        // the stationaryPt
        let mousePt = fp.lastInput.documentPoint;
        if (tool.isActive && fp !== null) {
            // if user is holding shift, make sure the angle of the reshaping wall (from stationaryPt to mousePt) is a multiple of 45
            if (fp.lastInput.shift) {
                // what's the current angle made from stationaryPt to mousePt?
                const type = tool.handle.name;
                const stationaryPt = type === 'sPt' ? wall.data.endpoint : wall.data.startpoint;
                let ang = stationaryPt.directionPoint(mousePt);
                const length = Math.sqrt(stationaryPt.distanceSquaredPoint(mousePt));
                ang = Math.round(ang / 45) * 45;
                let newPoint = new go.Point(stationaryPt.x + length, stationaryPt.y);
                // rotate the new point ang degrees
                const dx = stationaryPt.x;
                const dy = stationaryPt.y;
                newPoint = newPoint.offset(-dx, -dy); // move point to origin
                newPoint = newPoint.rotate(ang); // rotate ang degrees around origin
                newPoint = newPoint.offset(dx, dy); // add back offset
                mousePt = newPoint;
            }
            // if the mousePt is close to some wall's endpoint, snap the mousePt to that endpoint
            const walls = fp.findNodesByExample({
                category: 'WallGroup',
            });
            walls.iterator.each(function (w) {
                if (w.data.key !== wall.data.key) {
                    const spt = w.data.startpoint;
                    const ept = w.data.endpoint;
                    // if the mousePt is inside the geometry of another wall, project the point onto that wall
                    if (fp.isPointInWall(w, mousePt)) {
                        mousePt = mousePt.projectOntoLineSegmentPoint(w.data.startpoint, w.data.endpoint);
                        tool.isIntersecting = true; // yes, the current reshaping wall is intersecting another wall
                    }
                    // if the mousePt is close to some wall's endpoint, snap the mousePt to that endpoint
                    if (Math.sqrt(spt.distanceSquaredPoint(mousePt)) < 10) {
                        mousePt = spt;
                    }
                    else if (Math.sqrt(ept.distanceSquaredPoint(mousePt)) < 10) {
                        mousePt = ept;
                    }
                }
            });
            // if the resulting segment between stationary pt and mousePt would intersect other wall(s), project mousePt onto the first wall it would intersect
            const iw = tool.getClosestIntersectingWall(mousePt);
            // if we are or just were intersecting some wall, miter it
            if (iw === null || tool.wallIntersecting !== null) {
                if (tool.wallIntersecting !== null &&
                    tool.wallIntersecting !== undefined &&
                    tool.wallIntersecting.data !== null) {
                    tool.performMiteringOnWall(tool.wallIntersecting);
                }
            }
            if (iw != null) {
                tool.isIntersecting = true; // yes, the current reshaping wall is intersecting another wall
                tool.wallIntersecting = iw;
                mousePt = mousePt.projectOntoLineSegmentPoint(iw.data.startpoint, iw.data.endpoint);
                // if the mousePt is really close to an endpoint of its intersecting wall, make it that endpoint
                const distToSpt = Math.sqrt(mousePt.distanceSquaredPoint(iw.data.startpoint));
                const distToEpt = Math.sqrt(mousePt.distanceSquaredPoint(iw.data.endpoint));
                if (distToSpt < 25) {
                    mousePt = iw.data.startpoint;
                }
                else if (distToEpt < 10) {
                    mousePt = iw.data.endpoint;
                }
            }
            else {
                tool.isIntersecting = false;
                // if the wall we were previously intersecting is not touching the reshaping wall, forget it
                if (tool.wallIntersecting !== null &&
                    tool.wallIntersecting !== undefined &&
                    tool.wallIntersecting.data !== null &&
                    fp.getWallsIntersection(wall, tool.wallIntersecting) === null) {
                    tool.wallIntersecting = null;
                }
            }
            tool.calcAngleAndLengthFromHandle(mousePt); // sets this.angle and this.length (useful for when SHIFT is held)
            tool.reshape(mousePt);
        }
        tool.performMiteringOnWall(wall);
        fp.updateWallDimensions();
        fp.updateWallAngles();
    }
    /**
     * Get the closest wall the reshaping wall intersects with.
     * Returns null if reshaping wall does not intersect with any other wall.
     * @param {go.Point} proposedPt The proposed point for the reshaping wall's moving pt
     * @return {go.Group | null} The closest wall the reshaping wall's reshaping endpoint intersects with
     */
    getClosestIntersectingWall(proposedPt) {
        const tool = this;
        if (tool.handle === null)
            return null;
        const adorn = tool.handle.part;
        const wall = adorn.adornedPart;
        const type = tool.handle.name;
        const stationaryPt = type === 'sPt' ? wall.data.endpoint : wall.data.startpoint;
        // dummy wall is used for intersection checks, since the reshaping wall has not had its data yet set
        const dummyWallData = {
            key: 'wall',
            category: 'WallGroup',
            caption: 'Wall',
            type: 'Wall',
            startpoint: stationaryPt,
            smpt1: stationaryPt,
            smpt2: stationaryPt,
            endpoint: proposedPt,
            empt1: proposedPt,
            empt2: proposedPt,
            thickness: parseFloat(tool.diagram.model.modelData.wallThickness),
            isGroup: true,
            notes: '',
        };
        tool.diagram.model.addNodeData(dummyWallData);
        const dummyWall = tool.diagram.findPartForKey(dummyWallData.key);
        const fp = tool.diagram;
        const walls = tool.diagram.findNodesByExample({
            category: 'WallGroup',
        });
        let closestWall = null;
        let closestDistance = Number.MAX_VALUE;
        walls.iterator.each(function (w) {
            if (w.data.key !== wall.data.key && w.data.key !== dummyWall.data.key) {
                // check if wall and w intersect, and if so, where
                const intersectPoint = fp.getWallsIntersection(dummyWall, w);
                // also, don't project onto a wall the stationaryPt is already along (this would make two walls on top of each other)
                let isStationaryPtOnW = false;
                const ab = parseFloat(Math.sqrt(w.data.startpoint.distanceSquaredPoint(stationaryPt)).toFixed(2));
                const bc = parseFloat(Math.sqrt(stationaryPt.distanceSquaredPoint(w.data.endpoint)).toFixed(2));
                const ac = parseFloat(Math.sqrt(w.data.startpoint.distanceSquaredPoint(w.data.endpoint)).toFixed(2));
                if (Math.abs(ab + bc - ac) <= 0.1) {
                    isStationaryPtOnW = true;
                }
                if (intersectPoint !== null && !isStationaryPtOnW) {
                    // calc distance from stationaryPoint to proposed intersection point
                    const dist = Math.sqrt(stationaryPt.distanceSquaredPoint(intersectPoint));
                    if (dist < closestDistance) {
                        closestDistance = dist;
                        closestWall = w;
                    }
                }
            }
        });
        // remove the dummy wall
        fp.remove(dummyWall);
        return closestWall;
    }
    /**
     * Returns whether or not 2 points are "close enough" to each other.
     * "Close enough" is, by default, defined as a point whose x and y values are within .05
     * document units of another point's x and y values.
     * @param {go.Point} p1
     * @param {go.Point} p2
     * @return {boolean}
     */
    pointsApproximatelyEqual(p1, p2) {
        const x1 = p1.x;
        const x2 = p2.x;
        const y1 = p1.y;
        const y2 = p2.y;
        const diff1 = Math.abs(x2 - x1);
        const diff2 = Math.abs(y2 - y1);
        if (diff2 < 0.05 && diff1 < 0.05) {
            return true;
        }
        return false;
    }
    /**
     * Sets the counterclockwise mitering point for wallA / clockwise mitering point for wallB.
     * This algorithm based on https://math.stackexchange.com/questions/1849784/calculate-miter-points-of-stroked-vectors-in-cartesian-plane.
     * @param {go.Group} wa wallA
     * @param {go.Group} wb wallB
     */
    performMitering(wa, wb) {
        const tool = this;
        const diagram = this.diagram;
        // wall endpoints, thicknesses, lengths
        const as = wa.data.startpoint;
        const ae = wa.data.endpoint;
        const bs = wb.data.startpoint;
        const be = wb.data.endpoint;
        const wat = wa.data.thickness;
        const wbt = wb.data.thickness;
        const wal = Math.sqrt(as.distanceSquaredPoint(ae));
        const wbl = Math.sqrt(bs.distanceSquaredPoint(be));
        // points
        const B = diagram.getWallsIntersection(wa, wb); // intersection point
        if (B === null) {
            return;
        }
        let A = tool.pointsApproximatelyEqual(as, B) ? ae : as; // wallA non-intersection point
        let C = tool.pointsApproximatelyEqual(bs, B) ? be : bs; // wallB non-intersection point
        // edge case: non-endpoint intersection
        // must know which wall is outer wall (the one who has no endpoint in the intersection)
        // and which wall is inner wall (the one with an endpoint in the intersection)
        let ow = null;
        let iw = null;
        if (!tool.pointsApproximatelyEqual(as, B) && !tool.pointsApproximatelyEqual(ae, B)) {
            ow = wa;
            iw = wb;
        }
        else if (!tool.pointsApproximatelyEqual(bs, B) && !tool.pointsApproximatelyEqual(be, B)) {
            ow = wb;
            iw = wa;
        }
        // if wall A is the inner wall, use the endpoint of wall B that counterclockwise from point A for point C
        if (ow !== null && iw !== null && wa.data.key === iw.data.key) {
            if (tool.isClockwise(A, B, ow.data.startpoint)) {
                C = ow.data.startpoint;
            }
            else {
                C = ow.data.endpoint;
            }
        }
        // if wall B is the inner wall, use endpoint of wall A that's clockwise from point C for point A
        if (ow !== null && iw !== null && wb.data.key === iw.data.key) {
            if (tool.isClockwise(B, C, ow.data.startpoint)) {
                A = ow.data.startpoint;
            }
            else {
                A = ow.data.endpoint;
            }
        }
        // angle between wallA and wallB, clockwise, in degrees
        const a1 = B.directionPoint(A);
        const a2 = B.directionPoint(C);
        let ang = Math.abs(a1 - a2 + 360) % 360;
        if (Math.abs(ang - 180) < 0.1) {
            return;
        }
        ang = ang * (Math.PI / 180); // radians
        // create a parallelogram with altitudes wat/2 and wbt/2, s.t. u and v are the lengths from B to reach D (counterclockwise mitering point)
        const u = Math.abs(wbt / (2 * Math.sin(ang)));
        const v = Math.abs(wat / (2 * Math.sin(ang)));
        // get u and v vectors
        const ab = Math.sqrt(A.distanceSquaredPoint(B));
        const bc = Math.sqrt(B.distanceSquaredPoint(C));
        const ux = ((A.x - B.x) / ab) * u;
        const uy = ((A.y - B.y) / ab) * u;
        // only for endpoint-endpoint?
        const vx = ((C.x - B.x) / bc) * v;
        const vy = ((C.y - B.y) / bc) * v;
        // these are the mitering points
        const D = new go.Point(B.x + ux + vx, B.y + uy + vy);
        const E = new go.Point(B.x - ux - vx, B.y - uy - vy);
        // miter limit TODO???
        const minLength = Math.min(wal, wbl);
        if (Math.sqrt(D.distanceSquaredPoint(B)) > minLength) {
            return;
        }
        // mitering point / other mitering point
        const mpt = tool.isClockwise(B, A, D) ? E : D;
        if (isNaN(mpt.x) || isNaN(mpt.y)) {
            return;
        }
        // now figure out which mitering point of wallA's data to modify
        // only modify a mitering point in data if B is one of wallA's endpoints
        if (tool.pointsApproximatelyEqual(as, B) || tool.pointsApproximatelyEqual(ae, B)) {
            let prop = null;
            // wall A's direction to point B is from startpoint to endpoint
            if (tool.pointsApproximatelyEqual(A, as)) {
                // if ang2 is clockwise of ang1, update empt1
                if (tool.isClockwise(A, B, mpt)) {
                    prop = 'empt1';
                }
                else {
                    prop = 'empt2';
                }
            }
            else if (tool.pointsApproximatelyEqual(A, ae)) {
                // wall A's direction to point B is from endpoint to startpoint
                if (tool.isClockwise(A, B, mpt)) {
                    prop = 'smpt2';
                }
                else {
                    prop = 'smpt1';
                }
            }
            if (prop !== null) {
                diagram.model.setDataProperty(wa.data, prop, mpt);
                diagram.updateWall(wa);
            }
        }
        // same, but for wall B
        if (tool.pointsApproximatelyEqual(bs, B) || tool.pointsApproximatelyEqual(be, B)) {
            let prop = null;
            // wall A's direction to point B is from startpoint to endpoint
            if (tool.pointsApproximatelyEqual(C, bs)) {
                // if ang2 < ang1, update empt1
                if (tool.isClockwise(C, B, mpt)) {
                    prop = 'empt1';
                }
                else {
                    prop = 'empt2';
                }
            }
            else if (tool.pointsApproximatelyEqual(C, be)) {
                // wall A's direction to point B is from endpoint to startpoint
                if (tool.isClockwise(C, B, mpt)) {
                    prop = 'smpt2';
                }
                else {
                    prop = 'smpt1';
                }
            }
            if (prop !== null) {
                diagram.model.setDataProperty(wb.data, prop, mpt);
                diagram.updateWall(wb);
            }
        }
    }
    /**
     * Returns a set of all the wall intersections in the entire floorplan.
     * Each entry is a stringified points (i.e. "0 0").
     * @return {go.Set<string>}
     */
    getAllWallIntersectionPoints() {
        const tool = this;
        const diagram = tool.diagram;
        // get all walls
        const walls = diagram.findNodesByExample({
            category: 'WallGroup',
        });
        const intersectionPoints = new go.Set(); // set of Points where walls intersect
        walls.iterator.each(function (w) {
            // for each wall, go through all other walls; if this wall intersects another wall, mark it as an intersection point
            const otherWalls = diagram.findNodesByExample({
                category: 'WallGroup',
            });
            otherWalls.iterator.each(function (ow) {
                if (ow.data.key === w.data.key)
                    return; // do not check for intersection with self
                const ip = diagram.getWallsIntersection(w, ow);
                let doAdd = true;
                if (ip !== null) {
                    // make sure there is not already an intersection point in the set that's really close to this one
                    intersectionPoints.iterator.each(function (ips) {
                        const ip2 = go.Point.parse(ips);
                        if (tool.pointsApproximatelyEqual(ip2, ip)) {
                            doAdd = false;
                        }
                    });
                    if (doAdd) {
                        intersectionPoints.add(go.Point.stringify(ip));
                    }
                }
            });
        });
        return intersectionPoints;
    }
    /**
     * Get all the walls with an endpoint at a given Point.
     * Returns a List of all walls involved in that intersection.
     * @param {go.Point | null} intersectionPoint
     * @param {boolean} includeDividers Whether or not to also include Room Dividers with endpoints at intersectionPoint. Default is true.
     * @return {go.List<go.Group>}
     */
    getAllWallsAtIntersection(intersectionPoint, includeDividers) {
        if (includeDividers === undefined || includeDividers === null) {
            includeDividers = true;
        }
        const tool = this;
        const diagram = tool.diagram;
        const wallsInvolved = new go.List(); // list of walls, which will be sorted clockwise
        if (intersectionPoint === null) {
            return wallsInvolved;
        }
        diagram.findObjectsNear(intersectionPoint, 1, function (x) {
            if (x.part !== null) {
                return x.part;
            }
            return null;
        }, function (p) {
            if (!(p instanceof go.Group &&
                p.category === 'WallGroup' &&
                (includeDividers || !p.data.isDivider) &&
                !wallsInvolved.contains(p)))
                return false;
            // make sure the wall's segment includes ip
            const s = p.data.startpoint;
            const e = p.data.endpoint;
            return tool.isPointOnSegment(s, e, intersectionPoint);
        }, true, wallsInvolved);
        return wallsInvolved;
    }
    /**
     * Returns whether or not 2 walls share at least one endpoint
     * @param {go.Group} wa
     * @param {go.Group} wb
     * @return {boolean}
     */
    doWallsShareAnEndpoint(wa, wb) {
        const tool = this;
        const as = wa.data.startpoint;
        const ae = wa.data.endpoint;
        const bs = wb.data.startpoint;
        const be = wb.data.endpoint;
        if (tool.pointsApproximatelyEqual(as, bs) ||
            tool.pointsApproximatelyEqual(as, be) ||
            tool.pointsApproximatelyEqual(ae, bs) ||
            tool.pointsApproximatelyEqual(ae, be)) {
            return true;
        }
        return false;
    }
    /**
     * This function, called on {@link doMouseUp} event, checks if the reshaping wall's reshaping endpoint is now intersecting a wall.
     * If so, that intersected wall is split into 2 walls at the intersection point. All walls at the intersection point are then mitered.
     * Next, it checks if the reshapingWall has become a new, big wall (via {@link joinColinearWalls}).
     * If so, we must split the new wall at any points it intersects with others.
     * Room boundary data that depended on the split wall is then updated to reflect the split.
     */
    maybeSplitWall() {
        const tool = this;
        if (tool.handle === null)
            return;
        const adorn = tool.handle.part;
        const reshapingWall = adorn.adornedPart;
        const movingProp = tool.handle.name;
        const movingPt = movingProp === 'sPt' ? reshapingWall.data.startpoint : reshapingWall.data.endpoint;
        const jw = tool.joinedWalls;
        const wallsAtEndpoint = tool.getAllWallsAtIntersection(movingPt);
        // exclude the reshapingWall from wallsAtEndpoint
        wallsAtEndpoint.remove(reshapingWall);
        jw.iterator.each(function (ww) {
            wallsAtEndpoint.remove(ww);
        });
        if (wallsAtEndpoint.count === 1) {
            const wallToSplit = wallsAtEndpoint.first();
            if (wallToSplit !== null) {
                // make sure this is not an endpoint to endpoint connection
                if (!tool.doWallsShareAnEndpoint(reshapingWall, wallToSplit)) {
                    tool.maybePerformWallSplit(wallToSplit, movingPt);
                }
            }
        }
        // if we're building a wall, it's possible we need to split at the stationary pt too
        if (tool.isBuilding) {
            const stationaryPt = movingPt === reshapingWall.data.startpoint
                ? reshapingWall.data.endpoint
                : reshapingWall.data.startpoint;
            const wallsAtStationaryPt = tool.getAllWallsAtIntersection(stationaryPt);
            wallsAtStationaryPt.remove(reshapingWall);
            jw.iterator.each(function (ww) {
                wallsAtEndpoint.remove(ww);
            });
            if (wallsAtStationaryPt.count === 1) {
                const wallToSplit = wallsAtStationaryPt.first();
                if (wallToSplit !== null) {
                    // make sure this is not an endpoint to endpoint connection
                    if (!tool.doWallsShareAnEndpoint(reshapingWall, wallToSplit)) {
                        tool.maybePerformWallSplit(wallToSplit, stationaryPt);
                    }
                }
            }
        }
        // if this reshape event has created a big joined wall, the joined wall may need to be split
        // find out if either endpoint of the original reshaping wall is NOT one of the endpoints of joinedWall
        // if so, split the joinedWall at that endpoint
        if (jw !== null) {
            jw.iterator.each(function (ww) {
                // find all points along the joined wall where it intersects with other walls and split along them
                tool.splitNewWall(ww);
            });
        }
    }
    /**
     * Finds all points along a new wall (created via {@link joinColinearWalls}) and splits / miters at each.
     * @param w The newly joined wall
     */
    splitNewWall(w) {
        const tool = this;
        const fp = this.diagram;
        // find all walls that intersect this wall
        const walls = fp.findNodesByExample({
            category: 'WallGroup',
        });
        const ips = new go.Set();
        walls.iterator.each(function (ww) {
            const ip = fp.getWallsIntersection(w, ww);
            if (ip !== null) {
                ips.add(ip);
            }
        });
        ips.iterator.each(function (ip) {
            const wi = tool.getAllWallsAtIntersection(ip);
            wi.iterator.each(function (ww) {
                const s = ww.data.startpoint;
                const e = ww.data.endpoint;
                if (!tool.pointsApproximatelyEqual(s, ip) && !tool.pointsApproximatelyEqual(e, ip)) {
                    tool.maybePerformWallSplit(ww, ip);
                }
            });
        });
    }
    /**
     * Split a given wall into 2 at a given intersection point, if the given point is on the wall and not on one of the wall's endpoints.
     * The resultant two walls are then mitered.
     * Room boundary data that depended on the split wall is then updated to reflect the split.
     * @param {go.Group} w wall to split
     * @param {go.Point} ip intersection point where the split should occur
     */
    maybePerformWallSplit(w, ip) {
        const tool = this;
        const fp = tool.diagram;
        const s = w.data.startpoint;
        const e = w.data.endpoint;
        const type = w.data.isDivider ? 'Divider' : 'Wall';
        // this wall has neither endpoint in the intersection -- it must be split into 2 walls
        const data1 = {
            key: 'wall',
            category: 'WallGroup',
            caption: type,
            type: type,
            color: w.data.color,
            startpoint: s,
            endpoint: ip,
            smpt1: s,
            smpt2: s,
            empt1: ip,
            empt2: ip,
            thickness: w.data.thickness,
            isGroup: true,
            notes: '',
            isDivider: w.data.isDivider,
        };
        const data2 = {
            key: 'wall',
            category: 'WallGroup',
            caption: type,
            type: type,
            color: w.data.color,
            startpoint: ip,
            endpoint: e,
            smpt1: ip,
            smpt2: ip,
            empt1: e,
            empt2: e,
            thickness: w.data.thickness,
            isGroup: true,
            notes: '',
            isDivider: w.data.isDivider,
        };
        // only actually split the wall if the 2 new walls would both have at least length 1
        // and if there are no walls with endpoints very close to these proposed ones
        const l1 = Math.sqrt(data1.startpoint.distanceSquaredPoint(data1.endpoint));
        const l2 = Math.sqrt(data2.startpoint.distanceSquaredPoint(data2.endpoint));
        const walls = fp.findNodesByExample({
            category: 'WallGroup',
        });
        let alreadyExists = false;
        walls.iterator.each(function (wc) {
            const ws = wc.data.startpoint;
            const we = wc.data.endpoint;
            if ((tool.pointsApproximatelyEqual(s, ws) && tool.pointsApproximatelyEqual(ip, we)) ||
                (tool.pointsApproximatelyEqual(s, we) && tool.pointsApproximatelyEqual(ip, ws))) {
                alreadyExists = true;
            }
            if ((tool.pointsApproximatelyEqual(ip, ws) && tool.pointsApproximatelyEqual(e, we)) ||
                (tool.pointsApproximatelyEqual(ip, we) && tool.pointsApproximatelyEqual(e, ws))) {
                alreadyExists = true;
            }
        });
        if (l1 > 1 && l2 > 1 && !alreadyExists) {
            fp.model.addNodeData(data1);
            fp.model.addNodeData(data2);
            const w1 = fp.findNodeForData(data1);
            const w2 = fp.findNodeForData(data2);
            // Before removing the original wall from the Floorplan, update relevant room boundarywalls data
            // iff this method is being called as a result of a user-prompted wall reshape action
            // needed so proper mitering side of replacement entry walls can be determined
            tool.premiterWall(w1);
            tool.premiterWall(w2);
            tool.performMiteringAtPoint(ip, false);
            if (tool.handle !== null) {
                const rooms = fp.findNodesByExample({ category: 'RoomNode' });
                const adorn = tool.handle.part;
                const rw = adorn.adornedPart; // reshaping wall
                // go through rooms, find any whose boundary walls contain w (the wall that was split, soon to be removed)
                rooms.iterator.each(function (r) {
                    const bw = r.data.boundaryWalls;
                    for (let i = 0; i < bw.length; i++) {
                        const entry = bw[i];
                        const wk = entry[0];
                        if (wk === w.data.key) {
                            // then, find out if the reshaping wall, at the non-ip endpoint, is connected to another wall in that room's boundary walls
                            let isConnectedToBounds = false;
                            const nonIpEndpoint = tool.pointsApproximatelyEqual(rw.data.startpoint, ip)
                                ? rw.data.endpoint
                                : rw.data.startpoint;
                            const iw = tool.getAllWallsAtIntersection(nonIpEndpoint);
                            iw.iterator.each(function (ww) {
                                // if boundary walls contains ww and ww is not the reshaping wall, reshaping wall is connected to room boundary walls at non ip endpoint
                                for (let j = 0; j < bw.length; j++) {
                                    const ee = bw[j];
                                    const wk2 = ee[0];
                                    if (ww.data.key === wk2 && ww.data.key !== rw.data.key) {
                                        isConnectedToBounds = true;
                                    }
                                }
                            });
                            // if yes, replace the w entry in boundary walls with just one new entry, using the split wall that is connected to some other wall in bounds
                            if (isConnectedToBounds) {
                                // find out whether w1 or w2 is connected to another wall in boundary walls
                                let isW1ConnectedToBounds = false;
                                const w1NonIpEndpoint = tool.pointsApproximatelyEqual(w1.data.startpoint, ip)
                                    ? w1.data.endpoint
                                    : w1.data.startpoint;
                                const iw2 = tool.getAllWallsAtIntersection(w1NonIpEndpoint);
                                iw2.remove(w); // do not include the wall soon to be destroyed
                                // go through all walls at w1's non-ip endpoint and find out if one of those is in r's boundary walls
                                iw2.iterator.each(function (ww) {
                                    for (let j = 0; j < bw.length; j++) {
                                        const entry2 = bw[j];
                                        const wk2 = entry2[0];
                                        if (ww.data.key === wk2 && w1.data.key !== ww.data.key) {
                                            // additional followup -- make sure ww2 is still connected to r's boundary walls at other endpoint (not connected to w1NonIpEndpoint)
                                            const ww2 = fp.findNodeForKey(wk2);
                                            const ww2OtherEndpoint = tool.pointsApproximatelyEqual(ww2.data.startpoint, w1NonIpEndpoint)
                                                ? ww2.data.endpoint
                                                : ww2.data.startpoint;
                                            const iw3 = tool.getAllWallsAtIntersection(ww2OtherEndpoint);
                                            iw3.iterator.each(function (ww3) {
                                                for (let k = 0; k < bw.length; k++) {
                                                    const entry3 = bw[k];
                                                    const wk3 = entry3[0];
                                                    if (wk3 === ww3.data.key && wk3 !== ww2.data.key) {
                                                        isW1ConnectedToBounds = true;
                                                    }
                                                }
                                            });
                                        }
                                    }
                                });
                                // replace this entry of r's boundary walls with the replacementWall
                                const replacementWall = isW1ConnectedToBounds ? w1 : w2;
                                const replacementEntry = tool.getUpdatedEntry(entry, replacementWall);
                                fp.startTransaction();
                                const newBounds = bw.slice();
                                newBounds[i] = replacementEntry;
                                fp.model.setDataProperty(r.data, 'boundaryWalls', newBounds);
                                fp.commitTransaction();
                            }
                            else {
                                // if no, replace the w entry with both split walls. Order those 2 entries CC, relative to reshaping wall
                                // get a List of walls involved (reshaping wall, w1, and w2)
                                let wi = new go.List();
                                wi.add(rw);
                                wi.add(w1);
                                wi.add(w2);
                                wi = fp.sortWallsClockwiseWithSetStartWall(wi, rw);
                                // get replacement entries for the entry with w
                                const replacementEntry2 = tool.getUpdatedEntry(entry, wi.toArray()[1]);
                                const replacementEntry1 = tool.getUpdatedEntry(entry, wi.toArray()[2]);
                                // insert these replacement entries into the bw at index i, remove
                                fp.startTransaction();
                                const newBounds = bw.slice();
                                newBounds.splice(i, 1, replacementEntry1);
                                newBounds.splice(i + 1, 0, replacementEntry2);
                                fp.model.setDataProperty(r.data, 'boundaryWalls', newBounds);
                                fp.commitTransaction();
                            }
                        }
                    }
                }); // end rooms iteration
            }
            // Maintain wall parts that were on the big wall -- give them new locations on the most appropriate of the split walls, if possible
            const wallParts = fp.findNodesByExample({ group: w.data.key });
            const wallsSet = new go.Set();
            wallsSet.add(w1);
            wallsSet.add(w2);
            tool.maintainWallParts(wallParts, wallsSet);
            // remove original wall
            fp.remove(w);
            // perform mitering
            tool.premiterWall(w1);
            tool.premiterWall(w2);
            const w1op = tool.pointsApproximatelyEqual(w1.data.startpoint, ip)
                ? w1.data.endpoint
                : w1.data.startpoint;
            const w2op = tool.pointsApproximatelyEqual(w2.data.startpoint, ip)
                ? w2.data.endpoint
                : w2.data.startpoint;
            tool.performMiteringAtPoint(ip, false);
            tool.performMiteringAtPoint(w1op, false);
            tool.performMiteringAtPoint(w2op, false);
        }
    }
    /**
     * Go through all walls -- if a wall crosses another at a non-endpoint-to-endpoint connection, split that wall in 2
     * such that only endpoint to endpoint connections exist (this makes mitering much easier).
     * NOTE: Since this goes through all walls in the Floorplan, performance can get bad quickly. Use this method sparingly, if at all
     */
    splitAllWalls() {
        const tool = this;
        const intersectionPoints = tool.getAllWallIntersectionPoints();
        intersectionPoints.iterator.each(function (ips) {
            const ip = go.Point.parse(ips);
            const wallsInvolved = tool.getAllWallsAtIntersection(ip);
            // find all walls involved that do not have their start or endpoint at the intersection point
            wallsInvolved.iterator.each(function (w) {
                const s = w.data.startpoint;
                const e = w.data.endpoint;
                if (!tool.pointsApproximatelyEqual(s, ip) && !tool.pointsApproximatelyEqual(e, ip)) {
                    tool.maybePerformWallSplit(w, ip);
                }
            });
        });
    }
    /**
     * Return whether or not wall A is parallel to wall B.
     * @param {go.Group} wa Wall A
     * @param {go.Group} wb Wall B
     * @return {boolean}
     */
    areWallsParallel(wa, wb) {
        const tool = this;
        const fp = this.diagram;
        const as = wa.data.startpoint;
        const ae = wa.data.endpoint;
        const bs = wb.data.startpoint;
        const be = wb.data.endpoint;
        let isParallel = false;
        const a1 = +as.directionPoint(ae);
        const a2 = +bs.directionPoint(be);
        if (Math.abs(a1 - a2) < 1 || (Math.abs(a1 - a2) > 179 && Math.abs(a1 - a2) < 181)) {
            isParallel = true;
        }
        return isParallel;
    }
    /**
     * Returns whether wall B is colinear to wall A.
     * Wall A is colinear with Wall B if it:
     *  0) Is the same wall type as Wall B (wall | divider)
     *  1) Is parallel with Wall B
     *  2) Shares an endpoint, 'p', with Wall B, and
     *    2a) Any / all walls with endpoints at p are all parallel to wall A / B
     * @param {go.Group} wa wall A
     * @param {go.Group} wb wall B
     * @return {boolean}
     */
    isWallColinear(wa, wb) {
        const tool = this;
        const fp = this.diagram;
        if (wa.data.isDivider !== wb.data.isDivider) {
            return false;
        }
        const as = wa.data.startpoint;
        const ae = wa.data.endpoint;
        const bs = wb.data.startpoint;
        const be = wb.data.endpoint;
        let isColinear = false;
        // 1) Is wall A parallel with Wall B? (or close enough to parallel)
        if (tool.areWallsParallel(wa, wb)) {
            // get the endpoint shared by wa and wb, if it exists
            let sharedEndpoint = null;
            if (tool.pointsApproximatelyEqual(as, bs) || tool.pointsApproximatelyEqual(as, be)) {
                sharedEndpoint = as;
            }
            else if (tool.pointsApproximatelyEqual(ae, bs) || tool.pointsApproximatelyEqual(ae, be)) {
                sharedEndpoint = ae;
            }
            if (sharedEndpoint !== null) {
                // Make sure all walls with an endpoint at sharedEndpoint are parallel to wa
                const wi = tool.getAllWallsAtIntersection(sharedEndpoint);
                let endpointHasNonColinearWall = false;
                wi.iterator.each(function (w) {
                    if (!tool.areWallsParallel(w, wa)) {
                        endpointHasNonColinearWall = true;
                    }
                });
                if (!endpointHasNonColinearWall) {
                    isColinear = true;
                }
            }
        }
        return isColinear;
    }
    /**
     * Get all walls colinear to wall w and store them in a given Set.
     * @param {go.Group} w
     * @param {go.Set<go.Group>} set Optional
     * @return {go.Set<go.Group>}
     */
    findAllColinearWalls(w, set) {
        if (set === null || set === undefined) {
            set = new go.Set();
        }
        // make sure Set contains w
        set.add(w);
        const tool = this;
        const diagram = tool.diagram;
        const walls = diagram.findNodesByExample({
            category: 'WallGroup',
        });
        walls.iterator.each(function (ow) {
            if (tool.isWallColinear(w, ow) && set !== undefined && !set.contains(ow)) {
                set.add(ow);
                tool.findAllColinearWalls(ow, set);
            }
        });
        return set;
    }
    /**
     * This function, called after each {@link doMouseUp} event, checks for colinear pairs of walls at two places.
     * First, it checks for colinear walls with the reshaping wall.
     * Second, it checks for colinear walls at {@link returnPoint}.
     * If there are colinear walls found, they are joined into one big wall. Resultant wall(s) are then mitered.
     * Additionally, any rooms whose geometries depended on one of the walls that was just joined have their data updated,
     * replacing the old (removed) wall(s) in data with the new one.
     * Note: These rooms will have the final update to their geometry / data done later, in updateRoomBoundaries().
     * The data manipulation done here is just to ensure the walls removed by this function are not referenced anywhere in room data anymore.
     */
    joinColinearWalls() {
        const tool = this;
        if (tool.handle === null)
            return;
        // 1) Check if the reshaping wall is colinear with another wall at one of its endpoints
        const adorn = tool.handle.part;
        const reshapingWall = adorn.adornedPart;
        const cw1 = tool.findAllColinearWalls(reshapingWall);
        const jw = tool.performColinearWallJoining(cw1, reshapingWall);
        if (jw !== null) {
            tool.joinedWalls.add(jw);
        }
        // 2) Check if there are 2 colinear walls at returnPoint (where the reshaping endpoint originally was)
        const wallsAtReturnPoint = tool.getAllWallsAtIntersection(tool.returnPoint);
        if (wallsAtReturnPoint.count === 2) {
            const wallsArr = wallsAtReturnPoint.toArray();
            const w1 = wallsArr[0];
            const w2 = wallsArr[1];
            if (tool.isWallColinear(w1, w2)) {
                const cw2 = new go.Set();
                cw2.add(w1);
                cw2.add(w2);
                const jw2 = tool.performColinearWallJoining(cw2, w1);
                if (jw2 !== null) {
                    tool.joinedWalls.add(jw2);
                }
            }
        }
    }
    /**
     * Join a set of colinear walls into one big wall. The big wall is then mitered.
     * Constituent walls in colinear wall set are removed.
     * As such, if any rooms depend on them for boundary data, those room's data is updated
     * @param {go.Set<go.Group>} colinearWalls The set of colinear walls
     * @param {go.Group} w The wall to use as reference for color / thickness when joining the walls.
     * If this is not supplied, the first wall in the colinearWalls set is used
     * @return {go.Group | null} The new, big wall created by joining colinear walls
     */
    performColinearWallJoining(colinearWalls, w) {
        const tool = this;
        const fp = tool.diagram;
        const garbage = new go.Set();
        // all colinear "walls" must be Walls OR they must all be Room Dividers, they may not be a mix
        const cwf = colinearWalls.first();
        if (cwf === null) {
            return null;
        }
        if (w === undefined) {
            w = cwf;
        }
        const acceptedCategory = cwf.data.category;
        colinearWalls.iterator.each(function (cw) {
            if (cw.data.category !== acceptedCategory) {
                return;
            }
        });
        // Find the 2 farthest endpoints in colinear walls set
        if (colinearWalls.count > 1) {
            let pt1 = null;
            let pt2 = null;
            let farthestDist = 0;
            const cw2 = colinearWalls.copy();
            // remember all the wall parts (doors / windows) that the colinear walls have
            const wallParts = new go.Set();
            // iterate over colinear walls, finding the furthest distance between all endpoints
            colinearWalls.iterator.each(function (cw1) {
                // get all wallParts that belong to cw1 and add them to wallParts Set
                const cwParts = fp.findNodesByExample({ group: cw1.data.key });
                wallParts.addAll(cwParts);
                cw2.iterator.each(function (cw2w) {
                    const s1 = cw1.data.startpoint;
                    const e1 = cw1.data.endpoint;
                    const s2 = cw2w.data.startpoint;
                    const e2 = cw2w.data.endpoint;
                    const pts1 = [s1, e1];
                    const pts2 = [s2, e2];
                    for (let i = 0; i < pts1.length; i++) {
                        const p1 = pts1[i];
                        for (let j = 0; j < pts2.length; j++) {
                            const p2 = pts2[j];
                            const dist = Math.sqrt(p1.distanceSquaredPoint(p2));
                            if (dist > farthestDist) {
                                farthestDist = dist;
                                pt1 = p1;
                                pt2 = p2;
                            }
                        }
                    }
                });
                // all colinear walls will be destroyed later (after replaced by one big wall)
                garbage.add(cw1);
            }); // end colinearWalls iteration
            // we should now have the farthest points the colinear walls make -- just add a single wall with those endpoints
            const data = {
                key: 'wall',
                category: 'WallGroup',
                caption: 'Wall',
                type: 'Wall',
                color: w.data.color,
                startpoint: pt1,
                endpoint: pt2,
                smpt1: pt1,
                smpt2: pt1,
                empt1: pt2,
                empt2: pt2,
                thickness: w.data.thickness,
                isGroup: true,
                notes: '',
            };
            fp.model.addNodeData(data);
            const newWall = fp.findNodeForData(data);
            // Before the constituent walls are removed from Floorplan, update relevant room boundaryWalls
            // find all rooms that have any of the colinear walls in this roomBoundaryWalls or holes
            // replace those entries with constituent walls with the new wall key
            // make sure the mitering side is analogous to what it was with the consituent wall too
            const rooms = fp.findNodesByExample({ category: 'RoomNode' });
            rooms.iterator.each(function (r) {
                const bw = r.data.boundaryWalls;
                // replace entries in boundaryWalls with references to soon-to-be nonexistent walls
                for (let i = 0; i < bw.length; i++) {
                    const e = bw[i];
                    const wk = e[0];
                    const ww = fp.findNodeForKey(wk);
                    if (colinearWalls.contains(ww)) {
                        // ???
                        tool.performMiteringOnWall(newWall);
                        const newEntry = tool.getUpdatedEntry(e, newWall);
                        // replace the old entry with the new one
                        fp.startTransaction();
                        const newBounds = bw.slice();
                        newBounds[i] = newEntry;
                        fp.model.setDataProperty(r.data, 'boundaryWalls', newBounds);
                        fp.commitTransaction();
                    }
                } // end boundary walls outdated entry replacement
            }); // end rooms iteration
            // Maintain relative position of all wall parts
            const newWallSet = new go.Set();
            newWallSet.add(newWall);
            tool.performMiteringOnWall(newWall);
            tool.maintainWallParts(wallParts, newWallSet);
            // remove the constituent walls from Floorplan
            garbage.iterator.each(function (ww) {
                fp.remove(ww);
            });
            // perform mitering on the new, big wall
            tool.performMiteringOnWall(newWall);
            return newWall;
        }
        return null;
    }
    /**
     * Given a set of wall parts (doors, windows) and a set of walls, place the wall parts on the proper wall based on its location, if possible.
     * @param {go.Iterable<go.Node>} wallParts
     * @param {go.Iterable<go.Group>} walls
     */
    maintainWallParts(wallParts, walls) {
        const tool = this;
        const fp = tool.diagram;
        const garbage = [];
        wallParts.iterator.each(function (wp) {
            const loc = wp.location;
            // find the wall that has this loc in its geometry's boundaries
            walls.iterator.each(function (w) {
                if (fp.isPointInWall(w, loc)) {
                    const newLoc = fp.findClosestLocOnWall(w, wp);
                    // if the wall part can fit on this wall, add it to the wall
                    if (newLoc !== null) {
                        fp.model.setDataProperty(wp.data, 'group', w.data.key);
                        wp.location = newLoc.projectOntoLineSegmentPoint(w.data.startpoint, w.data.endpoint);
                        // wp.angle = w.data.startpoint.directionPoint(w.data.endpoint);
                        if (wp.category === 'WindowNode') {
                            fp.model.setDataProperty(wp.data, 'height', w.data.thickness);
                        }
                        if (wp.category === 'DoorNode') {
                            fp.model.setDataProperty(wp.data, 'doorOpeningHeight', w.data.thickness);
                        }
                    }
                    else {
                        // otherwise, remove it from the diagram
                        garbage.push(wp);
                    }
                }
            });
        });
        for (let i = 0; i < garbage.length; i++) {
            fp.remove(garbage[i]);
        }
    }
    /**
     * Get a replacement entry in a room's boundaryWalls or hole path for an old entry that used one of the walls
     * that was just joined together (since that wall won't exist anymore in just a moment).
     * This will replace the old entry's wall key (Array element 0) with newWall.key.
     * It will also make sure the mitering side of the replacement entry is analogous to the mitering side of the old entry.
     * @param {Array<any>} oldEntry the old entry to replace
     * @param {go.Group} nw new wall
     * @return {Array<any>} The updated entry with the new wall and analogous mitering side
     */
    getUpdatedEntry(oldEntry, nw) {
        const tool = this;
        const fp = tool.diagram;
        const oldWallKey = oldEntry[0];
        const oldWall = fp.findNodeForKey(oldWallKey);
        const oldMiteringSide = oldEntry[1];
        const newEntry = [nw.data.key, null];
        // Get the mitering side of newWall that is analogous to the mitering side of oldWall
        // see if the distance between the midpoint between newWall.smpt1 and newWall.empt1 is closer to oldWall's oldMiteringSide
        // if so, use 1 as new mitering side. Else use 2
        const oms = oldWall.data['smpt' + oldMiteringSide];
        const ome = oldWall.data['empt' + oldMiteringSide];
        /**
         * Get the point on a line at a given x value
         * @param {go.Point} a line point 1
         * @param {go.Point} b line point 2
         * @param {number} x x coordinate
         * @return {go.Point}
         */
        function pointAtX(a, b, x) {
            const slope = (b.y - a.y) / (b.x - a.x);
            const y = a.y + (x - a.x) * slope;
            return new go.Point(x, y);
        }
        // Get the point on the line implied by oms and ome at nw.smpt1 and nw.smpt2
        const pt1 = pointAtX(oms, ome, nw.data.smpt1.x);
        const pt2 = pointAtX(oms, ome, nw.data.smpt2.x);
        const dist1 = nw.data.smpt1.distanceSquaredPoint(pt1);
        const dist2 = nw.data.smpt2.distanceSquaredPoint(pt2);
        if (dist1 < dist2) {
            newEntry[1] = 1;
        }
        else {
            newEntry[1] = 2;
        }
        return newEntry;
    }
    /**
     * Join all sets of colinear walls in the Floorplan.
     * Note: This can get expensive quickly, as it goes over every single wall in the Floorplan. Use this method sparingly, if at all.
     * It may be better to call {@link joinColinearWalls}
     */
    joinAllColinearWalls() {
        const tool = this;
        const diagram = tool.diagram;
        const walls = diagram.findNodesByExample({
            category: 'WallGroup',
        });
        // const garbage: go.Set<go.Group> = new go.Set<go.Group>();
        const handledWalls = new go.Set();
        // iterate over all walls
        walls.iterator.each(function (w) {
            if (!handledWalls.contains(w)) {
                handledWalls.add(w);
                // find all walls connected to an endpoint of this wall w that are also parallel
                const colinearWalls = new go.Set();
                colinearWalls.add(w);
                tool.findAllColinearWalls(w, colinearWalls);
                handledWalls.addAll(colinearWalls);
                tool.performColinearWallJoining(colinearWalls, w);
            } // end handledWalls check
        }); // end walls iteration
        tool.premiterAllWalls();
    }
    /**
     * Get a point a given distance away from a given point at a given angle
     * @param {go.Point} point
     * @param {number} angle
     * @param {number} offset
     * @return {go.Point}
     */
    translateAndRotatePoint(point, angle, offset) {
        const oldPoint = point.copy();
        const newPoint = point.copy();
        newPoint.offset(0, offset);
        newPoint.offset(-oldPoint.x, -oldPoint.y).rotate(angle).offset(oldPoint.x, oldPoint.y);
        return newPoint;
    }
    /**
     * Set a wall's mitering points in data to points thickness/2 distance from start/endpoint at perpindicular angle
     * Then, update the wall to reflect this new geometry
     * @param {go.Group} w
     * @param {string} prop Optional: 'smpt1' | 'smpt2' | 'empt1' | 'empt2'. If this is provided, only premiter that point of the wall
     */
    premiterWall(w, prop) {
        const tool = this;
        const diagram = tool.diagram;
        const ang = w.data.startpoint.directionPoint(w.data.endpoint);
        const t = w.data.thickness;
        const sp = w.data.startpoint;
        const ep = w.data.endpoint;
        switch (prop) {
            case 'smpt1': {
                const smp1 = tool.translateAndRotatePoint(sp, ang, t / 2);
                diagram.model.setDataProperty(w.data, 'smpt1', smp1);
                break;
            }
            case 'smpt2': {
                const smp2 = tool.translateAndRotatePoint(sp, ang + 180, t / 2);
                diagram.model.setDataProperty(w.data, 'smpt2', smp2);
                break;
            }
            case 'empt1': {
                const emp1 = tool.translateAndRotatePoint(ep, ang, t / 2);
                diagram.model.setDataProperty(w.data, 'empt1', emp1);
                break;
            }
            case 'empt2': {
                const emp2 = tool.translateAndRotatePoint(ep, ang + 180, t / 2);
                diagram.model.setDataProperty(w.data, 'empt2', emp2);
                break;
            }
            default: {
                // Perpindicular mitering points (updated later during mitering function)
                // end miterpoint point 1 is the point "below" endpoint (add vector)
                const emp1 = tool.translateAndRotatePoint(ep, ang, t / 2);
                diagram.model.setDataProperty(w.data, 'empt1', emp1);
                // end mitering point 2 is the point "above" endpoint (subtract vector)
                const emp2 = tool.translateAndRotatePoint(ep, ang + 180, t / 2);
                diagram.model.setDataProperty(w.data, 'empt2', emp2);
                // start mitering point 1 is the point "below" startpoint (add vector)
                const smp1 = tool.translateAndRotatePoint(sp, ang, t / 2);
                diagram.model.setDataProperty(w.data, 'smpt1', smp1);
                // end mitering point 2 is the point "above" endpoint (subtract vector)
                const smp2 = tool.translateAndRotatePoint(sp, ang + 180, t / 2);
                diagram.model.setDataProperty(w.data, 'smpt2', smp2);
                break;
            }
        }
        diagram.updateWall(w);
    }
    /**
     * Goes through all walls and sets their mitering points in data to points
     * thickness/2 distance from start/endpoint at perpindicular angle
     * Note: Since this method goes through all walls in the Floorplan, it can get expensive quickly. Use this method sparingly, if at all.
     */
    premiterAllWalls() {
        const tool = this;
        const diagram = tool.diagram;
        const walls = diagram.findNodesByExample({
            category: 'WallGroup',
        });
        walls.iterator.each(function (w) {
            tool.premiterWall(w);
        });
    }
    /**
     * Perform corner mitering at both endpoints of a given wall
     * @param {go.Group} w The wall to perform corner mitering on
     */
    performMiteringOnWall(w) {
        const tool = this;
        const sp = w.data.startpoint;
        const ep = w.data.endpoint;
        tool.premiterWall(w);
        tool.performMiteringAtPoint(sp, true);
        tool.performMiteringAtPoint(ep, true);
    }
    /**
     * Perform corner mitering on all walls that interesct at a given point.
     * This is a more granular, cheaper operation than the larger scale {@link performAllMitering} function.
     * @param {go.Point} pt The point to perform mitering at
     * @param {boolea} performPremitering Whether or not to perform pre-miterign before this op. If true, premitering will only be done at the endpoint of walls equal to pt
     */
    performMiteringAtPoint(pt, performPremitering) {
        if (performPremitering === null || performPremitering === undefined) {
            performPremitering = true;
        }
        const tool = this;
        const fp = tool.diagram;
        // walls involved at intersection point
        const wi = tool.getAllWallsAtIntersection(pt, false);
        // premiter each wall in wi, maybe
        if (performPremitering) {
            wi.iterator.each(function (w) {
                // only perform premiter on w if one of w's endpoints is ot
                if (tool.pointsApproximatelyEqual(w.data.startpoint, pt) ||
                    tool.pointsApproximatelyEqual(w.data.endpoint, pt)) {
                    // which of w's endpoints is pt?
                    const prop = tool.pointsApproximatelyEqual(w.data.startpoint, pt) ? 's' : 'e';
                    tool.premiterWall(w, prop + 'mpt1');
                    tool.premiterWall(w, prop + 'mpt2');
                }
            });
        }
        // sort all involved walls in any COUNTERCLOCWISE order
        wi.sort(function (a, b) {
            const B = fp.getWallsIntersection(a, b);
            if (B === null)
                return 0;
            const as = a.data.startpoint;
            const ae = a.data.endpoint;
            const bs = b.data.startpoint;
            const be = b.data.endpoint;
            const A = tool.pointsApproximatelyEqual(pt, as) ? ae : as;
            const C = tool.pointsApproximatelyEqual(pt, bs) ? be : bs;
            const angA = B.directionPoint(A);
            const angB = B.directionPoint(C);
            if (angA > angB)
                return 1;
            else if (angA < angB)
                return -1;
            else
                return 0;
        });
        wi.reverse();
        const wiArr = wi.toArray();
        for (let i = 0; i < wiArr.length; i++) {
            const wa = wiArr[i];
            let wb = null;
            if (i + 1 === wiArr.length) {
                wb = wiArr[0];
            }
            else {
                wb = wiArr[i + 1];
            }
            // only miter these 2 walls if they are both walls or if they are both room dividers. no mixing
            // if (wa.data.isDivider === wb.data.isDivider) {
            tool.performMitering(wa, wb);
            // }
        }
    }
    /**
     * Performs all mitering for all walls in Floorplan.
     * Note: Since this method goes through all walls in the Floorplan, it can get expensive quickly. Use this method sparingly, if at all.
     */
    performAllMitering() {
        const tool = this;
        const diagram = tool.diagram;
        tool.premiterAllWalls();
        const intersectionPoints = tool.getAllWallIntersectionPoints();
        // iterate over all points where walls intersect
        intersectionPoints.iterator.each(function (ips) {
            const ip = go.Point.parse(ips);
            // get all walls involved in intersection
            const wallsInvolved = tool.getAllWallsAtIntersection(ip);
            // sort all involved walls in any COUNTERCLOCWISE order
            wallsInvolved.sort(function (a, b) {
                const B = diagram.getWallsIntersection(a, b);
                if (B === null)
                    return 0;
                const as = a.data.startpoint;
                const ae = a.data.endpoint;
                const bs = b.data.startpoint;
                const be = b.data.endpoint;
                const A = tool.pointsApproximatelyEqual(ip, as) ? ae : as;
                const C = tool.pointsApproximatelyEqual(ip, bs) ? be : bs;
                const angA = B.directionPoint(A);
                const angB = B.directionPoint(C);
                if (angA > angB)
                    return 1;
                else if (angA < angB)
                    return -1;
                else
                    return 0;
            });
            wallsInvolved.reverse();
            // iterate over wallsInvolved, performing cc mitering on each pair
            const wi = wallsInvolved.toArray();
            for (let i = 0; i < wi.length; i++) {
                const wa = wi[i];
                let wb = null;
                if (i + 1 === wi.length) {
                    wb = wi[0];
                }
                else {
                    wb = wi[i + 1];
                }
                // only miter these 2 walls if they are both walls or if they are both room dividers. no mixing
                // if (wa.data.isDivider === wb.data.isDivider) {
                tool.performMitering(wa, wb);
                // }
            }
        }); // end iterate over intersection points
    } // end performAllMitering
    /**
     * Checks whether segment AB is clockwise of segment BC.
     * B must be the intersection point.
     * @param {go.Point} a
     * @param {go.Point} b
     * @param {go.Point} c
     * @return {boolean}
     */
    isClockwise(a, b, c) {
        return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x) > 0;
    }
    /**
     * Check if point c is on the segment between points a and b
     * @param {go.Point} a
     * @param {go.Point} b
     * @param {go.Point} c
     * @return {boolean}
     */
    isPointOnSegment(a, b, c) {
        const ac = Math.sqrt(a.distanceSquaredPoint(c));
        const bc = Math.sqrt(b.distanceSquaredPoint(c));
        const ab = Math.sqrt(a.distanceSquaredPoint(b));
        if (Math.abs(ab - (ac + bc)) <= 0.1) {
            return true;
        }
        return false;
    }
    /**
     * Joins colinear walls, splits walls, performs mitering, ommits transaction, stops tool.
     */
    doMouseUp() {
        const tool = this;
        const fp = this.diagram;
        if (tool.handle === null)
            return;
        tool.doMouseMove();
        const adorn = tool.handle.part;
        const wall = adorn.adornedPart;
        tool.joinColinearWalls();
        tool.maybeSplitWall();
        // reset joinedWall
        tool.joinedWalls.clear();
        // update all rooms TODO only update relevant rooms
        const set = new go.Set();
        set.add(wall);
        fp.updateAllRoomBoundaries(set);
        this.stopTool();
    }
    /**
     * End the wall reshaping transaction.
     * If a wall is reshaped to less than 1 document unit long, remove it from the Floorplan.
     * If tool was cancelled with Esc key, reset reshaping wall / wall parts to what they were before.
     * Remove guidelines and update wall dimension links. Commit transaction.
     */
    doDeactivate() {
        const diagram = this.diagram;
        const fp = diagram;
        // if a wall reshaped to length < 1 px, remove it
        if (this.handle === null)
            return;
        const adorn = this.handle.part;
        const wall = adorn.adornedPart;
        const sPt = wall.data.startpoint;
        const ePt = wall.data.endpoint;
        const 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
            const wallDimensionLinkPointNodes = [];
            fp.pointNodes.iterator.each(function (node) {
                if (node.data.key.indexOf(wall.data.key) !== -1)
                    wallDimensionLinkPointNodes.push(node);
            });
            if (wallDimensionLinkPointNodes.length === 2) {
                diagram.remove(wallDimensionLinkPointNodes[0]);
                diagram.remove(wallDimensionLinkPointNodes[1]);
            }
        }
        // remove wall's dimension links if tool cancelled via esc key
        if (diagram.lastInput.key === 'Escape' && !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;
            this.performAllMitering();
            fp.updateWall(wall);
            if (this.returnData) {
                this.returnData.iterator.each(function (kvp) {
                    const key = kvp.key;
                    const loc = kvp.value;
                    const 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 point nodes
        const glPoints = this.diagram.findNodesByExample({
            category: 'GLPointNode',
        });
        diagram.removeParts(glPoints, true);
        fp.updateWallDimensions();
        // commit transaction, deactivate tool
        diagram.commitTransaction(this.name);
        this.isActive = false;
    }
    /**
     * Creates an adornment with 2 handles
     * @param {go.Shape} selelt The adorned wall's Shape element
     * @return {go.Adornment}
     */
    makeAdornment(selelt) {
        const adornment = new go.Adornment();
        adornment.type = go.Panel.Spot;
        adornment.locationObjectName = 'BODY';
        adornment.locationSpot = go.Spot.Center;
        let 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 (a small blue diamond)
     * @return {go.Shape}
     */
    makeHandle() {
        const 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 {go.Point} mousePt The mouse cursors coordinate position
     */
    calcAngleAndLengthFromHandle(mousePt) {
        const tool = this;
        const h = this.handle;
        if (h === null)
            return;
        if (tool.adornedShape === null)
            return;
        let otherH = null;
        const node = tool.adornedShape.part;
        if (node === null)
            return;
        const adornments = node.adornments.iterator;
        let adornment;
        while (adornments.next()) {
            const a = adornments.value;
            if (a.category === tool.name) {
                adornment = a;
            }
        }
        if (adornment === undefined)
            return;
        const atr = adornment.elements;
        while (atr.next()) {
            const e = atr.value;
            if (e.name !== undefined && e.name !== h.name) {
                otherH = e;
            }
        }
        // calc angle from otherH against the horizontal
        if (otherH === null)
            return;
        const otherHandlePt = otherH.getDocumentPoint(go.Spot.Center);
        const deltaY = mousePt.y - otherHandlePt.y;
        const deltaX = mousePt.x - otherHandlePt.x;
        let 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;
        const 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 {go.Point} point The point to snap to grid
     * @return {go.Point}
     */
    snapPointToGrid(point) {
        const diagram = this.diagram;
        const gs = diagram.toolManager.draggingTool.isGridSnapEnabled
            ? diagram.model.modelData.gridSize
            : 1;
        const newx = gs * Math.round(point.x / gs);
        const newy = gs * Math.round(point.y / gs);
        const newPt = new go.Point(newx, newy);
        return newPt;
    }
    /**
     * Reshapes the wall's geometry, updates model data
     * @param {go.Point} newPoint The point to move the reshaping wall's reshaping endpoint to
     */
    reshape(newPoint) {
        const diagram = this.diagram;
        const tool = this;
        const shape = this.adornedShape;
        if (shape === null)
            return;
        if (tool.handle === null)
            return;
        const node = shape.part;
        if (node === null)
            return;
        // if user holds SHIFT, make angle between startPoint / endPoint and the horizontal line a multiple of 45
        if (this.diagram.lastInput.shift && !this.isIntersecting) {
            let 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;
            const oldGridSize = diagram.model.modelData.gridSize;
            let 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()
            const angle = tool.angle;
            const length = tool.length;
            // snap to 90 degrees
            if (angle > 67.5 && angle < 112.5) {
                let 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) {
                let 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) {
                let 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) {
                let 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) {
                let newx = Math.sin(0.785) * length;
                newx = gridSize * Math.round(newx / gridSize) + sPt.x;
                let newy = Math.cos(0.785) * length;
                newy = gridSize * Math.round(newy / gridSize) + sPt.y;
                newPoint = new go.Point(newx, newy);
            }
            // snap to 135 degrees
            if (angle > 112.5 && angle < 157.5) {
                let newx = Math.sin(0.785) * length;
                newx = sPt.x - gridSize * Math.round(newx / gridSize);
                let newy = Math.cos(0.785) * length;
                newy = gridSize * Math.round(newy / gridSize) + sPt.y;
                newPoint = new go.Point(newx, newy);
            }
            // snap to 225 degrees
            if (angle > 202.5 && angle < 247.5) {
                let newx = Math.sin(0.785) * length;
                newx = sPt.x - gridSize * Math.round(newx / gridSize);
                let newy = Math.cos(0.785) * length;
                newy = sPt.y - gridSize * Math.round(newy / gridSize);
                newPoint = new go.Point(newx, newy);
            }
            // snap to 315 degrees
            if (angle > 292.5 && angle < 337.5) {
                let newx = Math.sin(0.785) * length;
                newx = sPt.x + gridSize * Math.round(newx / gridSize);
                let newy = Math.cos(0.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 &&
            !tool.isIntersecting &&
            !this.diagram.lastInput.shift) {
            newPoint = this.snapPointToGrid(newPoint);
        }
        else {
            newPoint = new go.Point(newPoint.x, newPoint.y);
        }
        const type = tool.handle.name;
        if (type === undefined)
            return;
        const stationaryPt = type === 'sPt' ? node.data.endpoint : node.data.startpoint;
        const movingPt = type === 'sPt' ? node.data.startpoint : node.data.endpoint;
        this.reshapeWall(node, stationaryPt, movingPt, newPoint, diagram);
        this.updateAdornments(node);
        this.showMatches();
        const fp = diagram;
        fp.updateWallDimensions();
    } // end reshape()
    /**
     * Show if the wall (at the adjustment handle being moved) lines up with other wall edges
     */
    showMatches() {
        const tool = this;
        const diagram = tool.diagram;
        if (!diagram.model.modelData.preferences.showWallGuidelines)
            return;
        if (tool.adornedShape === null)
            return;
        if (tool.handle === null)
            return;
        const wall = tool.adornedShape.part;
        let comparePt;
        if (tool.handle.name === 'sPt')
            comparePt = wall.data.startpoint;
        else
            comparePt = wall.data.endpoint;
        // the wall attached to the handle being manipulated
        const hWall = tool.adornedShape.part;
        // delete any old guideline points (these are used to show guidelines, must be cleared before a new guideline can be shown)
        const glPoints = diagram.findNodesByExample({
            category: 'GLPointNode',
        });
        diagram.removeParts(glPoints, true);
        const walls = this.diagram.findNodesByExample({
            category: 'WallGroup',
        });
        walls.iterator.each(function (w) {
            if (hWall !== null && w.data.key !== hWall.data.key) {
                const shape = w.findObject('SHAPE');
                const pt1 = w.data.startpoint;
                const 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);
            }
        });
    }
    /**
     * 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 {go.Point} pt
     * @param {number} comparePtCoord
     * @param {number} ptCoord
     * @param {go.Point} comparePt
     */
    checkPtLinedUp(pt, comparePtCoord, ptCoord, comparePt) {
        function makeGuideLinePoint() {
            const $ = 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() {
            const $ = go.GraphObject.make;
            return $(go.Link, $(go.Shape, { stroke: 'black', strokeWidth: 2, name: 'SHAPE' }, new go.Binding('strokeWidth', 'width'), new go.Binding('stroke', 'stroke')));
        }
        const diagram = this.diagram;
        const errorMargin = Math.abs(comparePtCoord - ptCoord);
        if (errorMargin < 2) {
            const data = { category: 'GLPointNode', loc: go.Point.stringify(pt), key: 'glpt' };
            const data2 = {
                key: 'movingPt',
                category: 'GLPointNode',
                loc: go.Point.stringify(comparePt),
            };
            const data3 = {
                key: 'guideline',
                category: 'guideLine',
                from: 'movingPt',
                to: data.key,
                stroke: 'blue',
            };
            const GLPoint1 = makeGuideLinePoint();
            const GLPoint2 = makeGuideLinePoint();
            const 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;
        }
    }
    /**
     * 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 {go.Group} wall The wall being reshaped
     * @param {go.Point} stationaryPoint The endpoint of the wall not being reshaped
     * @param {go.Point} movingPoint The endpoint of the wall being reshaped
     * @param {go.Point} newPoint The point that movingPoint is going to
     * @param {go.Diagram} diagram The diagram belonging WallReshapingTool belongs to
     */
    reshapeWall(wall, stationaryPoint, movingPoint, newPoint, diagram) {
        const tool = this;
        const wallParts = wall.memberParts;
        const arr = [];
        const oldAngle = wall.data.startpoint.directionPoint(wall.data.endpoint);
        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
        const distancesMap = new go.Map( /*"string", "number"*/);
        let closestPart = null;
        let closestDistance = Number.MAX_VALUE;
        for (let i = 0; i < arr.length; i++) {
            const part = arr[i];
            const 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
            const endpoints = tool.getWallPartEndpoints(part);
            const 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) {
            const loc = closestPart.location;
            const partLength = closestPart.data.length;
            const angle = oldAngle;
            const point1 = new go.Point(loc.x + partLength / 2, loc.y);
            const 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);
            const distance1 = Math.sqrt(stationaryPoint.distanceSquaredPoint(point1));
            const distance2 = Math.sqrt(stationaryPoint.distanceSquaredPoint(point2));
            let minLength;
            let newLoc;
            if (distance1 > distance2) {
                minLength = distance1;
                newLoc = point1;
            }
            else {
                minLength = distance2;
                newLoc = point2;
            }
            const 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);
        }
        const fp = diagram;
        fp.updateWall(wall);
        // for each wallPart, maintain relative distance from the stationaryPoint
        distancesMap.iterator.each(function (kvp) {
            const wallPart = diagram.findPartForKey(kvp.key);
            const distance = kvp.value;
            const wallLength = Math.sqrt(stationaryPoint.distanceSquaredPoint(movingPoint));
            const newLoc = new go.Point(stationaryPoint.x + (distance / wallLength) * (movingPoint.x - stationaryPoint.x), stationaryPoint.y + (distance / wallLength) * (movingPoint.y - stationaryPoint.y));
            wallPart.location = newLoc;
            // calculate the new angle
            const sToE = wall.data.startpoint.directionPoint(wall.data.endpoint);
            const eToS = wall.data.endpoint.directionPoint(wall.data.startpoint);
            const diffS = Math.abs(wallPart.angle - sToE);
            const diffE = Math.abs(wallPart.angle - eToS);
            const newAngle = diffS < diffE ? sToE : eToS;
            wallPart.angle = newAngle;
        });
    } // end reshapeWall()
    /**
     * Find and return an array of the endpoints of a given wallpart (window or door)
     * @param {go.Part} wallPart A Wall Part -- i.e. Door Node, Window Node
     * @return {Array<go.Point>}
     */
    getWallPartEndpoints(wallPart) {
        const loc = wallPart.location;
        const partLength = wallPart.data.length;
        let angle = 0;
        if (wallPart.containingGroup !== null)
            angle = wallPart.containingGroup.rotateObject.angle;
        else
            angle = 180;
        const point1 = new go.Point(loc.x + partLength / 2, loc.y);
        const 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);
        const arr = new Array();
        arr.push(point1);
        arr.push(point2);
        return arr;
    }
}
// export = WallReshapingTool;