scripts/model/corner.js
import {EVENT_ACTION, EVENT_DELETED, EVENT_MOVED} from '../core/events.js';
import {EventDispatcher, Vector2} from 'three';
import {Utils} from '../core/utils.js';
import {Dimensioning} from '../core/dimensioning.js';
import {Configuration, configWallHeight} from '../core/configuration.js';
/** */
export const cornerTolerance = 20;
/**
* Corners are used to define Walls.
*/
export class Corner extends EventDispatcher
{
/** Constructs a corner.
* @param {Floorplan} floorplan The associated model floorplan.
* @param {Number} x X coordinate.
* @param {Number} y Y coordinate.
* @param {String} id An optional unique id. If not set, created internally.
*/
constructor(floorplan, x, y, id)
{
super();
/** @property {Array} wallStarts Array of walls that are start walls
* @type {Array}
**/
this.wallStarts = [];
/** @property {Array} wallEnds Array of walls that are end walls
* @type {Array}
**/
this.wallEnds = [];
/**
* @deprecated Not in use. The EventDispatcher from threejs is used for emit and listen events
**/
this.moved_callbacks = null;
/**
* @deprecated Not in use. The EventDispatcher from threejs is used for emit and listen events
**/
this.deleted_callbacks = null;
/**
* @deprecated Not in use. The EventDispatcher from threejs is used for emit and listen events
**/
this.action_callbacks = null;
/**
* @property {Floorplan} floorplan Reference to the model floorplan
* @type {Floorplan}
**/
this.floorplan = floorplan;
/**
* @property {Number} x The position in x dimension
* @type {Number}
**/
this.x = x;
/**
* @property {Number} y The position in y dimension
* @type {Number}
**/
this.y = y;
/**
* @property {Number} _elevation The elevation at this corner
* @type {Number}
**/
this._elevation = Configuration.getNumericValue(configWallHeight);
/**
* @property {String} id The id of this corner. Autogenerated the first time
* @type {String}
**/
this.id = id || Utils.guide();
/**
* @property {Array} attachedRooms Array of rooms that have walls using this corner
* @type {Array}
**/
this.attachedRooms = [];
}
/** @type {Number} elevation The elevation value at this corner*/
set elevation(value)
{
this._elevation = Dimensioning.cmFromMeasureRaw(Number(value));
}
/** @type {Number} elevation The elevation value at this corner*/
get elevation()
{
return this._elevation;
}
/**
* @param {Room} room - The room that should be attached to this corner
* @return {void}
*/
attachRoom(room)
{
if(room)
{
this.attachedRooms.push(room);
}
}
/**
* @return {Room[]} Array of rooms attached to this corner
*/
getAttachedRooms()
{
return this.attachedRooms;
}
/**
* @return {void} Clear all the rooms attached to this corner
*/
clearAttachedRooms()
{
this.attachedRooms = [];
}
/** Add function to moved callbacks.
* @param func The function to be added.
*/
fireOnMove(func)
{
this.moved_callbacks.add(func);
}
/** Add function to deleted callbacks.
* @param func The function to be added.
*/
fireOnDelete(func)
{
this.deleted_callbacks.add(func);
}
/** Add function to action callbacks.
* @param func The function to be added.
*/
fireOnAction(func)
{
this.action_callbacks.add(func);
}
fireAction(action)
{
this.dispatchEvent({type:EVENT_ACTION, item: this, action:action});
// this.action_callbacks.fire(action)
}
/**
* @returns
* @deprecated
*/
getX()
{
return this.x;
}
/**
* @returns
* @deprecated
*/
getY()
{
return this.y;
}
/**
* @param {Number} tolerance - The tolerance value within which it will snap to adjacent corners
* @return {Object} snapped Contains keys x and y with true/false values
* @description The object with x and y that are boolean values to indicate if the snap happens in x and y
*/
snapToAxis(tolerance)
{
// try to snap this corner to an axis
var snapped = {x: false,y: false};
var scope = this;
this.adjacentCorners().forEach((corner) => {
if (Math.abs(corner.x - scope.x) < tolerance)
{
scope.x = corner.x;
snapped.x = true;
}
if (Math.abs(corner.y - scope.y) < tolerance)
{
scope.y = corner.y;
snapped.y = true;
}
});
return snapped;
}
/** Moves corner relatively to new position.
* @param {Number} dx The delta x.
* @param {Number} dy The delta y.
*/
relativeMove(dx, dy)
{
this.move(this.x + dx, this.y + dy);
}
/**
* Dispatches an event when removed from the floorplan({@link Floorplan}) instance. The event object contains reference to this {@link Corner} instance as item.
* @example
* let corner = new Corner(floorplan, 0, 0);
* function cornerRemoved(e) { console.log('I WAS REMOVED FROM LOCATION ', e.item.x, e.item.y) };
* corner.remove();
* @emits {EVENT_DELETED}
**/
remove()
{
this.dispatchEvent({type:EVENT_DELETED, item:this});
// this.deleted_callbacks.fire(this);
}
/**
* Removes all the connected corners and itself. This in essence removes all the walls({@link Wall}) this corner is connected to.
* @example
* let corner1 = new Corner(floorplan, 0, 0);
* let corner2 = new Corner(floorplan, 10, 0);
* function cornerRemoved(e) { console.log('I WAS REMOVED FROM LOCATION ', e.item.x, e.item.y) } //Will log twice for two corners;
* corner.removeAll();
**/
removeAll()
{
var i = 0;
for (i = 0; i < this.wallStarts.length; i++)
{
this.wallStarts[i].remove();
}
for (i = 0; i < this.wallEnds.length; i++)
{
this.wallEnds[i].remove();
}
this.remove();
}
/** Moves corner to new position.
* @param {Number} newX The new x position.
* @param {Number} newY The new y position.
*/
move(newX, newY)
{
this.x = newX;
this.y = newY;
this.mergeWithIntersected();
this.dispatchEvent({type:EVENT_MOVED, item: this, position: new Vector2(this.x, this.y)});
// this.moved_callbacks.fire(this.x, this.y);
this.wallStarts.forEach((wall) => {
wall.fireMoved();
});
this.wallEnds.forEach((wall) => {
wall.fireMoved();
});
}
/**
* When a corner is moved from its location it will impact the connected rooms ({@link Room}) shape, thus their areas. This will update the rooms
* @example
* let corner = new Corner(floorplan, 0, 0);
* corner.move(10, 0);
**/
updateAttachedRooms()
{
this.attachedRooms.forEach((room)=>{
room.updateArea();
});
}
/** Gets the adjacent corners that are connected to this corner by walls ({@link Wall}).
* @returns {Corner[]} Array of corners.
*/
adjacentCorners()
{
var retArray = [];
var i = 0;
for (i = 0; i < this.wallStarts.length; i++)
{
retArray.push(this.wallStarts[i].getEnd());
}
for (i = 0; i < this.wallEnds.length; i++)
{
retArray.push(this.wallEnds[i].getStart());
}
return retArray;
}
/** Checks if a wall is connected.
* @param {Wall} wall A wall.
* @returns {boolean} in case of connection.
*/
isWallConnected(wall)
{
var i =0;
for (i = 0; i < this.wallStarts.length; i++)
{
if (this.wallStarts[i] == wall)
{
return true;
}
}
for (i = 0; i < this.wallEnds.length; i++)
{
if (this.wallEnds[i] == wall)
{
return true;
}
}
return false;
}
/**
* Returns the distance between this corner and a point in 2d space
* @param {Vector2} point
* @see https://threejs.org/docs/#api/en/math/Vector2
* @return {Number} distance The distance
**/
distanceFrom(point)
{
var distance = Utils.distance(point, new Vector2(this.x, this.y));
//console.log('x,y ' + x + ',' + y + ' to ' + this.getX() + ',' + this.getY() + ' is ' + distance);
return distance;
}
/** Gets the distance from a wall.
* @param {Wall} wall A wall.
* @returns {Number} distance The distance.
*/
distanceFromWall(wall)
{
return wall.distanceFrom(new Vector2(this.x, this.y));
}
/** Gets the distance from a corner.
* @param {Corner} corner A corner.
* @returns {Number} The distance.
*/
distanceFromCorner(corner)
{
return this.distanceFrom(new Vector2(corner.x, corner.y));
}
/** Detaches a wall.
* @param {Wall} wall A wall.
*/
detachWall(wall)
{
Utils.removeValue(this.wallStarts, wall);
Utils.removeValue(this.wallEnds, wall);
if (this.wallStarts.length == 0 && this.wallEnds.length == 0)
{
this.remove();
}
}
/** Attaches a start wall.
* @param {Wall} wall A wall.
*/
attachStart(wall)
{
this.wallStarts.push(wall);
}
/** Attaches an end wall.
* @param {Wall} wall A wall.
*/
attachEnd(wall)
{
this.wallEnds.push(wall);
}
/** Get wall to corner.
* @param {Corner} corner A corner.
* @return {Wall} The associated wall or null.
*/
wallTo(corner)
{
for (var i = 0; i < this.wallStarts.length; i++)
{
if (this.wallStarts[i].getEnd() === corner)
{
return this.wallStarts[i];
}
}
return null;
}
/** Get wall from corner.
* @param {Corner} corner A corner.
* @return {Wall} The associated wall or null.
*/
wallFrom(corner)
{
for (var i = 0; i < this.wallEnds.length; i++)
{
if (this.wallEnds[i].getStart() === corner)
{
return this.wallEnds[i];
}
}
return null;
}
/** Get wall to or from corner.
* @param {Corner} corner A corner.
* @return {Wall} The associated wall or null.
*/
wallToOrFrom(corner)
{
return this.wallTo(corner) || this.wallFrom(corner);
}
/** Get wall from corner.
* @param {Corner} corner A corner.
*/
combineWithCorner(corner)
{
var i = 0;
// update position to other corner's
this.x = corner.x;
this.y = corner.y;
// absorb the other corner's wallStarts and wallEnds
for (i = corner.wallStarts.length - 1; i >= 0; i--)
{
corner.wallStarts[i].setStart(this);
}
for (i = corner.wallEnds.length - 1; i >= 0; i--)
{
corner.wallEnds[i].setEnd(this);
}
// delete the other corner
corner.removeAll();
this.removeDuplicateWalls();
this.floorplan.update();
}
mergeWithIntersected()
{
var i =0;
//console.log('mergeWithIntersected for object: ' + this.type);
// check corners
for (i = 0; i < this.floorplan.getCorners().length; i++)
{
var corner = this.floorplan.getCorners()[i];
if (this.distanceFromCorner(corner) < cornerTolerance && corner != this)
{
this.combineWithCorner(corner);
return true;
}
}
// check walls
for (i = 0; i < this.floorplan.getWalls().length; i++)
{
var wall = this.floorplan.getWalls()[i];
if (this.distanceFromWall(wall) < cornerTolerance && !this.isWallConnected(wall))
{
// update position to be on wall
var intersection = Utils.closestPointOnLine(new Vector2(this.x, this.y), wall.getStart(), wall.getEnd());
this.x = intersection.x;
this.y = intersection.y;
// merge this corner into wall by breaking wall into two parts
this.floorplan.newWall(this, wall.getEnd());
wall.setEnd(this);
this.floorplan.update();
return true;
}
}
return false;
}
/** Ensure we do not have duplicate walls (i.e. same start and end points) */
removeDuplicateWalls()
{
var i = 0;
// delete the wall between these corners, if it exists
var wallEndpoints = {};
var wallStartpoints = {};
for (i = this.wallStarts.length - 1; i >= 0; i--)
{
if (this.wallStarts[i].getEnd() === this)
{
// remove zero length wall
this.wallStarts[i].remove();
}
else if (this.wallStarts[i].getEnd().id in wallEndpoints)
{
// remove duplicated wall
this.wallStarts[i].remove();
}
else
{
wallEndpoints[this.wallStarts[i].getEnd().id] = true;
}
}
for (i = this.wallEnds.length - 1; i >= 0; i--)
{
if (this.wallEnds[i].getStart() === this)
{
// removed zero length wall
this.wallEnds[i].remove();
}
else if (this.wallEnds[i].getStart().id in wallStartpoints)
{
// removed duplicated wall
this.wallEnds[i].remove();
}
else
{
wallStartpoints[this.wallEnds[i].getStart().id] = true;
}
}
}
}