mirror of
https://github.com/beestat/app.git
synced 2025-05-23 18:04:14 -04:00
571 lines
16 KiB
JavaScript
571 lines
16 KiB
JavaScript
/**
|
|
* Wall.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall = function() {
|
|
this.snap_lines_ = {};
|
|
|
|
beestat.component.floor_plan_entity.apply(this, arguments);
|
|
};
|
|
beestat.extend(beestat.component.floor_plan_entity.wall, beestat.component.floor_plan_entity);
|
|
|
|
/**
|
|
* Decorate
|
|
*
|
|
* @param {SVGGElement} parent
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.decorate_ = function(parent) {
|
|
this.decorate_line_(parent);
|
|
|
|
this.set_draggable_(true);
|
|
|
|
parent.appendChild(this.path_);
|
|
};
|
|
|
|
/**
|
|
* Decorate path.
|
|
*
|
|
* @param {SVGGElement} parent
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.decorate_line_ = function(parent) {
|
|
const self = this;
|
|
|
|
this.path_ = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
|
|
this.path_id_ = String(Math.random());
|
|
this.path_.setAttribute('id', this.path_id_);
|
|
this.path_.style.strokeWidth = '6';
|
|
|
|
if (this.active_ === true) {
|
|
this.path_.style.stroke = beestat.style.color.green.base;
|
|
this.path_.style.opacity = 0.5;
|
|
} else {
|
|
this.path_.style.stroke = '#ffffff';
|
|
this.path_.style.opacity = 0.2;
|
|
}
|
|
|
|
this.path_.addEventListener('mousedown', function() {
|
|
self.dispatchEvent('mousedown');
|
|
});
|
|
|
|
this.decorate_text_(parent);
|
|
|
|
this.update_line_();
|
|
|
|
// Update the line if the ctrl key is pressed.
|
|
this.floor_plan_.addEventListener('ctrl_key', function() {
|
|
self.update_line_();
|
|
});
|
|
|
|
this.path_.addEventListener('dblclick', this.add_point.bind(this));
|
|
};
|
|
|
|
/**
|
|
* Add a point to the room along this wall.
|
|
*
|
|
* @param {Event} e
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.add_point = function(e) {
|
|
const room = this.room_.get_room();
|
|
for (let i = 0; i < room.points.length; i++) {
|
|
if (this.point_1_ === room.points[i]) {
|
|
/**
|
|
* First convert the window coordinate space into SVG coordinate space.
|
|
* Then project the clicked point onto the line so the line stays nice
|
|
* and straight.
|
|
*/
|
|
let local_point;
|
|
if (e !== undefined) {
|
|
local_point = this.floor_plan_.get_local_point(e);
|
|
} else {
|
|
local_point = {
|
|
'x': room.x + ((this.point_1_.x + this.point_2_.x) / 2),
|
|
'y': room.y + ((this.point_1_.y + this.point_2_.y) / 2)
|
|
};
|
|
}
|
|
|
|
const projected_point = this.project_point_(
|
|
{
|
|
'x': (local_point.x - room.x),
|
|
'y': (local_point.y - room.y)
|
|
},
|
|
this.point_1_,
|
|
this.point_2_
|
|
);
|
|
|
|
room.points.splice(
|
|
i + 1,
|
|
0,
|
|
projected_point
|
|
);
|
|
|
|
this.state_.active_point = projected_point;
|
|
if (this.state_.active_wall_entity !== undefined) {
|
|
this.state_.active_wall_entity.set_active(false);
|
|
}
|
|
|
|
this.dispatchEvent('add_point');
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update everything to match current data.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.update = function() {
|
|
this.update_line_();
|
|
this.update_text_();
|
|
};
|
|
|
|
/**
|
|
* Update line to match current data.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.update_line_ = function() {
|
|
// Draw the path in a specific direction so the text attached is consistent.
|
|
const x_distance = Math.abs(this.point_1_.x - this.point_2_.x);
|
|
const y_distance = Math.abs(this.point_1_.y - this.point_2_.y);
|
|
|
|
let from_point;
|
|
let to_point;
|
|
if (x_distance > y_distance) {
|
|
if (this.point_1_.x < this.point_2_.x) {
|
|
from_point = this.point_1_;
|
|
to_point = this.point_2_;
|
|
} else {
|
|
from_point = this.point_2_;
|
|
to_point = this.point_1_;
|
|
}
|
|
} else {
|
|
if (this.point_2_.y < this.point_1_.y) {
|
|
from_point = this.point_1_;
|
|
to_point = this.point_2_;
|
|
} else {
|
|
from_point = this.point_2_;
|
|
to_point = this.point_1_;
|
|
}
|
|
}
|
|
|
|
const path_parts = [];
|
|
path_parts.push('M' + from_point.x + ',' + from_point.y);
|
|
path_parts.push('L' + to_point.x + ',' + to_point.y);
|
|
this.path_.setAttribute('d', path_parts.join(' '));
|
|
|
|
if (this.is_horizontal_() === true) {
|
|
this.path_.style.cursor = 'n-resize';
|
|
} else if (this.is_vertical_() === true) {
|
|
this.path_.style.cursor = 'e-resize';
|
|
} else {
|
|
this.path_.style.cursor = 'copy';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Project a point onto a line.
|
|
*
|
|
* @link https://jsfiddle.net/soulwire/UA6H5/
|
|
* @link https://stackoverflow.com/q/32281168
|
|
*
|
|
* @param {object} p The point.
|
|
* @param {object} a The first point of the line.
|
|
* @param {object} b The second point of the line.
|
|
*
|
|
* @return {object} The projected point.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.project_point_ = function(p, a, b) {
|
|
var atob = {'x': b.x - a.x,
|
|
'y': b.y - a.y};
|
|
var atop = {'x': p.x - a.x,
|
|
'y': p.y - a.y};
|
|
var len = (atob.x * atob.x) + (atob.y * atob.y);
|
|
var dot = (atop.x * atob.x) + (atop.y * atob.y);
|
|
var t = Math.min(1, Math.max(0, dot / len));
|
|
|
|
dot = ((b.x - a.x) * (p.y - a.y)) - ((b.y - a.y) * (p.x - a.x));
|
|
|
|
return {
|
|
'x': a.x + (atob.x * t),
|
|
'y': a.y + (atob.y * t)
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Decorate the wall length.
|
|
*
|
|
* @param {SVGGElement} parent
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.decorate_text_ = function(parent) {
|
|
this.text_ = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
this.text_.style.fontFamily = 'Montserrat';
|
|
this.text_.style.fontWeight = '300';
|
|
this.text_.style.fill = '#ffffff';
|
|
this.text_.style.textAnchor = 'middle';
|
|
this.text_.style.letterSpacing = '-0.5px';
|
|
this.text_.style.dominantBaseline = 'hanging';
|
|
this.text_.setAttribute('dy', '5');
|
|
|
|
this.text_path_ = document.createElementNS('http://www.w3.org/2000/svg', 'textPath');
|
|
this.text_path_.setAttribute('href', '#' + this.path_id_);
|
|
this.text_path_.setAttribute('startOffset', '50%');
|
|
this.text_.appendChild(this.text_path_);
|
|
|
|
this.update_text_();
|
|
|
|
parent.appendChild(this.text_);
|
|
};
|
|
|
|
/**
|
|
* Update the wall length to match the current data.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.update_text_ = function() {
|
|
// Set the string content
|
|
const length = this.get_length_();
|
|
|
|
// Shrink the font slightly for short walls.
|
|
if (length < 24) {
|
|
this.text_.style.fontSize = '6px';
|
|
} else if (length < 48) {
|
|
this.text_.style.fontSize = '8px';
|
|
} else {
|
|
this.text_.style.fontSize = '11px';
|
|
}
|
|
|
|
const length_feet = Math.floor(length / 12);
|
|
const length_inches = length % 12;
|
|
|
|
let length_parts = [];
|
|
length_parts.push(length_feet + '\'');
|
|
length_parts.push(length_inches + '"');
|
|
|
|
const length_string = length_parts.join(' ');
|
|
this.text_path_.textContent = length_string;
|
|
};
|
|
|
|
/**
|
|
* Set the first point. Assume this is the position of the wall.
|
|
*
|
|
* @param {object} point_1
|
|
*
|
|
* @return {beestat.component.floor_plan_entity.wall} This.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.set_point_1 = function(point_1) {
|
|
this.point_1_ = point_1;
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Get point_1.
|
|
*
|
|
* @return {object} point_1
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.get_point_1 = function() {
|
|
return this.point_1_;
|
|
};
|
|
|
|
/**
|
|
* Set the second point
|
|
*
|
|
* @param {object} point_2
|
|
*
|
|
* @return {beestat.component.floor_plan_entity.wall} This.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.set_point_2 = function(point_2) {
|
|
this.point_2_ = point_2;
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Get point_2.
|
|
*
|
|
* @return {object} point_2
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.get_point_2 = function() {
|
|
return this.point_2_;
|
|
};
|
|
|
|
/**
|
|
* Set the room the wall is part of.
|
|
*
|
|
* @param {beestat.component.floor_plan_entity.room} room
|
|
*
|
|
* @return {beestat.component.floor_plan_entity.wall} This.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.set_room = function(room) {
|
|
this.room_ = room;
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Set the x and y positions of this entity. This just updates the points as
|
|
* the entity itself is not translated.
|
|
*
|
|
* @param {number} x The x position of this entity.
|
|
* @param {number} y The y position of this entity.
|
|
*
|
|
* @return {beestat.component.floor_plan_entity.wall} This.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.set_xy = function(x, y) {
|
|
if (x !== null) {
|
|
this.point_1_.x = Math.round(x);
|
|
this.point_2_.x = Math.round(x);
|
|
}
|
|
|
|
if (y !== null) {
|
|
this.point_1_.y = Math.round(y);
|
|
this.point_2_.y = Math.round(y);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Handle dragging a wall around. Snaps to X and Y of other handles.
|
|
*
|
|
* @param {Event} e
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.after_mousemove_handler_ = function(e) {
|
|
const snap_distance = 6;
|
|
|
|
if (this.is_vertical_() === true) {
|
|
let desired_x = this.drag_start_entity_.x + ((e.clientX - this.drag_start_mouse_.x) * this.floor_plan_.get_scale());
|
|
|
|
if (this.state_.snapping === true) {
|
|
const point_x = this.room_.get_x() + desired_x;
|
|
|
|
if (this.snap_line_x_ !== undefined) {
|
|
this.snap_line_x_.stxle.visibility = 'hidden';
|
|
}
|
|
|
|
// Snap x
|
|
const room_snap_x = this.room_.get_snap_x();
|
|
for (let i = 0; i < room_snap_x.length; i++) {
|
|
const snap_x = room_snap_x[i];
|
|
const distance = Math.abs(snap_x - point_x);
|
|
if (distance <= snap_distance) {
|
|
desired_x = snap_x - this.room_.get_x();
|
|
break;
|
|
}
|
|
}
|
|
this.update_snap_lines_();
|
|
} else {
|
|
this.clear_snap_lines_();
|
|
}
|
|
|
|
this.set_xy(
|
|
desired_x,
|
|
null
|
|
);
|
|
} else if (this.is_horizontal_() === true) {
|
|
let desired_y = this.drag_start_entity_.y + ((e.clientY - this.drag_start_mouse_.y) * this.floor_plan_.get_scale());
|
|
|
|
if (this.state_.snapping === true) {
|
|
const point_y = this.room_.get_y() + desired_y;
|
|
|
|
if (this.snap_line_y_ !== undefined) {
|
|
this.snap_line_y_.style.visibility = 'hidden';
|
|
}
|
|
|
|
// Snap Y
|
|
const room_snap_y = this.room_.get_snap_y();
|
|
for (let i = 0; i < room_snap_y.length; i++) {
|
|
const snap_y = room_snap_y[i];
|
|
const distance = Math.abs(snap_y - point_y);
|
|
if (distance <= snap_distance) {
|
|
desired_y = snap_y - this.room_.get_y();
|
|
break;
|
|
}
|
|
}
|
|
this.update_snap_lines_();
|
|
} else {
|
|
this.clear_snap_lines_();
|
|
}
|
|
|
|
this.set_xy(
|
|
null,
|
|
desired_y
|
|
);
|
|
}
|
|
|
|
this.dispatchEvent('update');
|
|
};
|
|
|
|
/**
|
|
* Handle what happens when you stop moving the wall.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.after_mouseup_handler_ = function() {
|
|
this.clear_snap_lines_();
|
|
};
|
|
|
|
/**
|
|
* Override to prevent this from happening if the ctrl key is pressed as that
|
|
* adds a point and should not start a drag.
|
|
*
|
|
* @param {Event} e
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.mousedown_handler_ = function(e) {
|
|
if (e.ctrlKey === true) {
|
|
e.stopPropagation();
|
|
return;
|
|
}
|
|
|
|
beestat.component.floor_plan_entity.prototype.mousedown_handler_.apply(this, arguments);
|
|
};
|
|
|
|
/**
|
|
* Set an appropriate drag_start_entity_ on mousedown.
|
|
*
|
|
* @param {Event} e
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.after_mousedown_handler_ = function() {
|
|
this.drag_start_entity_ = {
|
|
'x': this.point_1_.x,
|
|
'y': this.point_1_.y
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Update snap lines to match the current data.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.update_snap_lines_ = function() {
|
|
/**
|
|
* If the current x matches one of the room snap x positions, then
|
|
* add/update the current snap line. Otherwise remove it.
|
|
*/
|
|
if (this.is_vertical_() === true) {
|
|
const point_x = this.room_.get_x() + this.point_1_.x;
|
|
if (this.room_.get_snap_x().includes(point_x) === true) {
|
|
if (this.snap_lines_.x === undefined) {
|
|
this.snap_lines_.x = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
this.snap_lines_.x.style.strokeDasharray = '7, 3';
|
|
this.snap_lines_.x.style.stroke = beestat.style.color.yellow.base;
|
|
this.snap_lines_.x.setAttribute('y1', this.floor_plan_.get_grid_pixels() / -2);
|
|
this.snap_lines_.x.setAttribute('y2', this.floor_plan_.get_grid_pixels() / 2);
|
|
this.floor_plan_.get_g().appendChild(this.snap_lines_.x);
|
|
}
|
|
this.snap_lines_.x.setAttribute('x1', point_x);
|
|
this.snap_lines_.x.setAttribute('x2', point_x);
|
|
} else if (this.snap_lines_.x !== undefined) {
|
|
this.snap_lines_.x.parentNode.removeChild(this.snap_lines_.x);
|
|
delete this.snap_lines_.x;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the current x matches one of the room snap y positions, then
|
|
* add/update the current snap line. Otherwise remove it.
|
|
*/
|
|
if (this.is_horizontal_() === true) {
|
|
const point_y = this.room_.get_y() + this.point_1_.y;
|
|
if (this.room_.get_snap_y().includes(point_y) === true) {
|
|
if (this.snap_lines_.y === undefined) {
|
|
this.snap_lines_.y = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
this.snap_lines_.y.style.strokeDasharray = '7, 3';
|
|
this.snap_lines_.y.style.stroke = beestat.style.color.yellow.base;
|
|
this.snap_lines_.y.setAttribute('x1', this.floor_plan_.get_grid_pixels() / -2);
|
|
this.snap_lines_.y.setAttribute('x2', this.floor_plan_.get_grid_pixels() / 2);
|
|
this.floor_plan_.get_g().appendChild(this.snap_lines_.y);
|
|
}
|
|
this.snap_lines_.y.setAttribute('y1', point_y);
|
|
this.snap_lines_.y.setAttribute('y2', point_y);
|
|
} else if (this.snap_lines_.y !== undefined) {
|
|
this.snap_lines_.y.parentNode.removeChild(this.snap_lines_.y);
|
|
delete this.snap_lines_.y;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Clear all existing snap lines.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.clear_snap_lines_ = function() {
|
|
if (this.snap_lines_.x !== undefined) {
|
|
this.snap_lines_.x.parentNode.removeChild(this.snap_lines_.x);
|
|
delete this.snap_lines_.x;
|
|
}
|
|
if (this.snap_lines_.y !== undefined) {
|
|
this.snap_lines_.y.parentNode.removeChild(this.snap_lines_.y);
|
|
delete this.snap_lines_.y;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the midpoint of the wall.
|
|
*
|
|
* @return {number} The midpoint of the wall.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.get_midpoint_ = function() {
|
|
return {
|
|
'x': (this.point_1_.x + this.point_2_.x) / 2,
|
|
'y': (this.point_1_.y + this.point_2_.y) / 2
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Get the midpoint of the wall.
|
|
*
|
|
* @return {number} The midpoint of the wall.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.get_length_ = function() {
|
|
return Math.round(Math.hypot(
|
|
this.point_2_.x - this.point_1_.x,
|
|
this.point_2_.y - this.point_1_.y
|
|
));
|
|
};
|
|
|
|
/**
|
|
* Get whether or not this wall is horizontal.
|
|
*
|
|
* @return {boolean} Whether or not this wall is horizontal.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.is_horizontal_ = function() {
|
|
return Math.abs(this.point_1_.y - this.point_2_.y) === 0;
|
|
};
|
|
|
|
/**
|
|
* Get whether or not this wall is vertical.
|
|
*
|
|
* @return {boolean} Whether or not this wall is vertical.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.is_vertical_ = function() {
|
|
return Math.abs(this.point_1_.x - this.point_2_.x) === 0;
|
|
};
|
|
|
|
/**
|
|
* Make this wall active or not.
|
|
*
|
|
* @param {boolean} active Whether or not the wall is active.
|
|
*
|
|
* @return {beestat.component.floor_plan_entity.wall} This.
|
|
*/
|
|
beestat.component.floor_plan_entity.wall.prototype.set_active = function(active) {
|
|
if (active !== this.active_) {
|
|
this.active_ = active;
|
|
|
|
if (this.active_ === true) {
|
|
// Inactivate any other active wall.
|
|
if (this.state_.active_wall_entity !== undefined) {
|
|
this.state_.active_wall_entity.set_active(false);
|
|
}
|
|
|
|
this.state_.active_wall_entity = this;
|
|
|
|
// Deactivate the active point.
|
|
if (this.state_.active_point !== undefined) {
|
|
this.state_.active_point_entity.set_active(false);
|
|
}
|
|
|
|
this.dispatchEvent('activate');
|
|
} else {
|
|
delete this.state_.active_wall_entity;
|
|
}
|
|
|
|
if (this.rendered_ === true) {
|
|
this.rerender();
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|