1
0
mirror of https://github.com/beestat/app.git synced 2026-02-26 13:10:23 -05:00
2026-02-22 12:14:59 -05:00

718 lines
21 KiB
JavaScript

/**
* Floor plan opening (empty, door, window) represented as a line segment with
* two draggable endpoints.
*/
beestat.component.floor_plan_entity.opening = function() {
this.enabled_ = true;
this.point_entities_ = [];
this.snap_lines_ = {
'x': {},
'y': {}
};
beestat.component.floor_plan_entity.apply(this, arguments);
};
beestat.extend(beestat.component.floor_plan_entity.opening, beestat.component.floor_plan_entity);
/**
* Decorate.
*
* @param {SVGGElement} parent
*/
beestat.component.floor_plan_entity.opening.prototype.decorate_ = function(parent) {
this.decorate_opening_(parent);
if (this.active_ === true) {
this.decorate_points_(parent);
this.update_snap_points_();
}
if (this.enabled_ === true && this.active_ === true) {
this.set_draggable_(true);
}
};
/**
* Build opening visuals.
*
* @param {SVGGElement} parent
*/
beestat.component.floor_plan_entity.opening.prototype.decorate_opening_ = 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.fill = 'none';
this.path_.style.strokeLinecap = 'round';
this.path_.style.cursor = this.enabled_ === true ? 'pointer' : 'default';
parent.appendChild(this.path_);
this.text_ = document.createElementNS('http://www.w3.org/2000/svg', 'text');
this.text_.style.fontFamily = 'Montserrat';
this.text_.style.fontWeight = '300';
this.text_.style.fontSize = '11px';
this.text_.style.fill = '#ffffff';
this.text_.style.textAnchor = 'middle';
this.text_.style.letterSpacing = '-0.5px';
this.text_.setAttribute('dy', '1.1em');
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_);
parent.appendChild(this.text_);
if (this.enabled_ === true) {
this.path_.addEventListener('click', function(e) {
e.stopPropagation();
self.set_active(true);
});
this.path_.addEventListener('touchstart', function(e) {
e.stopPropagation();
self.set_active(true);
});
}
this.update();
};
/**
* Update visuals.
*/
beestat.component.floor_plan_entity.opening.prototype.update = function() {
if (
this.opening_ === undefined ||
this.opening_.points === undefined ||
Array.isArray(this.opening_.points) !== true ||
this.opening_.points.length < 2
) {
return;
}
this.opening_.width = Math.round(this.get_opening_width_());
const center = this.get_center_();
this.opening_.x = Math.round(center.x);
this.opening_.y = Math.round(center.y);
if (
this.path_ === undefined ||
this.text_ === undefined ||
this.text_path_ === undefined
) {
return;
}
this.update_line_();
this.update_text_();
this.update_points_();
};
/**
* Add endpoint drag points.
*
* @param {SVGGElement} parent
*/
beestat.component.floor_plan_entity.opening.prototype.decorate_points_ = function(parent) {
const self = this;
this.opening_.points.forEach(function(point) {
const point_entity = new beestat.component.floor_plan_entity.point(self.floor_plan_, self.state_)
.set_room(self)
.set_point(point)
.render(parent);
point_entity.addEventListener('lesser_update', function() {
self.update();
});
point_entity.addEventListener('update', function() {
self.update();
self.update_snap_points_();
self.dispatchEvent('update');
});
point_entity.addEventListener('mousedown', function() {
point_entity.set_active(true);
});
point_entity.addEventListener('touchstart', function() {
point_entity.set_active(true);
});
point_entity.addEventListener('activate', function() {
self.floor_plan_.update_toolbar();
});
if (
self.state_.active_point_entity !== undefined &&
self.state_.active_point_entity.get_point() === point
) {
point_entity.set_active(true);
}
self.point_entities_.push(point_entity);
});
};
/**
* Update endpoint drag points.
*/
beestat.component.floor_plan_entity.opening.prototype.update_points_ = function() {
this.point_entities_.forEach(function(point_entity) {
point_entity.update();
});
};
/**
* Update line path.
*/
beestat.component.floor_plan_entity.opening.prototype.update_line_ = function() {
const p1 = this.opening_.points[0];
const p2 = this.opening_.points[1];
this.path_.setAttribute('d', 'M' + p1.x + ',' + p1.y + ' L' + p2.x + ',' + p2.y);
this.path_.style.stroke = this.get_opening_color_();
this.path_.style.strokeWidth = this.active_ === true ? '6' : '4';
this.path_.style.opacity = this.enabled_ === true ? (this.active_ === true ? '0.95' : '0.7') : '0.3';
};
/**
* Update length text.
*/
beestat.component.floor_plan_entity.opening.prototype.update_text_ = function() {
if (this.active_ !== true) {
this.text_.style.display = 'none';
this.text_path_.textContent = '';
return;
}
this.text_.style.display = 'block';
const length = this.get_opening_width_();
if (length < 24) {
this.text_.style.fontSize = '6px';
} else if (length < 48) {
this.text_.style.fontSize = '8px';
} else {
this.text_.style.fontSize = '11px';
}
let length_string;
if (beestat.setting('units.distance') === 'ft') {
const length_feet = Math.floor(length / 12);
const length_inches = Math.round(length % 12);
length_string = length_feet + '\'' + ' ' + length_inches + '"';
} else {
length_string = beestat.distance({
'distance': length,
'units': true,
'round': 2
});
}
this.text_path_.textContent = length_string;
};
/**
* Get opening width.
*
* @return {number}
*/
beestat.component.floor_plan_entity.opening.prototype.get_opening_width_ = function() {
const p1 = this.opening_.points[0];
const p2 = this.opening_.points[1];
const dx = Number(p2.x || 0) - Number(p1.x || 0);
const dy = Number(p2.y || 0) - Number(p1.y || 0);
return Math.sqrt((dx * dx) + (dy * dy));
};
/**
* Get line center.
*
* @return {{x:number,y:number}}
*/
beestat.component.floor_plan_entity.opening.prototype.get_center_ = function() {
const p1 = this.opening_.points[0];
const p2 = this.opening_.points[1];
return {
'x': (Number(p1.x || 0) + Number(p2.x || 0)) / 2,
'y': (Number(p1.y || 0) + Number(p2.y || 0)) / 2
};
};
/**
* Handle after mousedown.
*/
beestat.component.floor_plan_entity.opening.prototype.after_mousedown_handler_ = function() {
this.drag_start_entity_ = {
'p1': {
'x': Number(this.opening_.points[0].x || 0),
'y': Number(this.opening_.points[0].y || 0)
},
'p2': {
'x': Number(this.opening_.points[1].x || 0),
'y': Number(this.opening_.points[1].y || 0)
}
};
};
/**
* Handle dragging.
*
* @param {Event} e
*/
beestat.component.floor_plan_entity.opening.prototype.after_mousemove_handler_ = function(e) {
const grid_half = this.floor_plan_.get_grid_pixels() / 2;
const snap_distance = 6;
let desired_dx = ((e.clientX || e.touches[0].clientX) - this.drag_start_mouse_.x) * this.floor_plan_.get_scale();
let desired_dy = ((e.clientY || e.touches[0].clientY) - this.drag_start_mouse_.y) * this.floor_plan_.get_scale();
if (this.state_.snapping === true) {
const snap_x_values = this.get_snap_x();
const snap_y_values = this.get_snap_y();
const points = [
{
'x': this.drag_start_entity_.p1.x + desired_dx,
'y': this.drag_start_entity_.p1.y + desired_dy
},
{
'x': this.drag_start_entity_.p2.x + desired_dx,
'y': this.drag_start_entity_.p2.y + desired_dy
}
];
let best_snap_delta_x;
let best_snap_distance_x = Number.POSITIVE_INFINITY;
let best_snap_delta_y;
let best_snap_distance_y = Number.POSITIVE_INFINITY;
for (let i = 0; i < points.length; i++) {
const point = points[i];
for (let j = 0; j < snap_x_values.length; j++) {
const snap_x = snap_x_values[j];
const distance_x = Math.abs(snap_x - point.x);
if (distance_x <= snap_distance && distance_x < best_snap_distance_x) {
best_snap_distance_x = distance_x;
best_snap_delta_x = snap_x - point.x;
}
}
for (let j = 0; j < snap_y_values.length; j++) {
const snap_y = snap_y_values[j];
const distance_y = Math.abs(snap_y - point.y);
if (distance_y <= snap_distance && distance_y < best_snap_distance_y) {
best_snap_distance_y = distance_y;
best_snap_delta_y = snap_y - point.y;
}
}
}
if (best_snap_delta_x !== undefined) {
desired_dx += best_snap_delta_x;
}
if (best_snap_delta_y !== undefined) {
desired_dy += best_snap_delta_y;
}
this.update_snap_lines_();
} else {
this.clear_snap_lines_();
}
let applied_dx = desired_dx;
let applied_dy = desired_dy;
const min_dx = -grid_half - Math.min(this.drag_start_entity_.p1.x, this.drag_start_entity_.p2.x);
const max_dx = grid_half - Math.max(this.drag_start_entity_.p1.x, this.drag_start_entity_.p2.x);
const min_dy = -grid_half - Math.min(this.drag_start_entity_.p1.y, this.drag_start_entity_.p2.y);
const max_dy = grid_half - Math.max(this.drag_start_entity_.p1.y, this.drag_start_entity_.p2.y);
applied_dx = Math.max(min_dx, Math.min(max_dx, applied_dx));
applied_dy = Math.max(min_dy, Math.min(max_dy, applied_dy));
this.opening_.points[0].x = Math.round(this.drag_start_entity_.p1.x + applied_dx);
this.opening_.points[0].y = Math.round(this.drag_start_entity_.p1.y + applied_dy);
this.opening_.points[1].x = Math.round(this.drag_start_entity_.p2.x + applied_dx);
this.opening_.points[1].y = Math.round(this.drag_start_entity_.p2.y + applied_dy);
this.update();
};
/**
* Cleanup after mouseup.
*/
beestat.component.floor_plan_entity.opening.prototype.after_mouseup_handler_ = function() {
if (this.dragged_ === true) {
this.clear_snap_lines_();
this.update_snap_points_();
}
};
/**
* Update snap lines to match the opening points.
*/
beestat.component.floor_plan_entity.opening.prototype.update_snap_lines_ = function() {
const self = this;
let current_snap_x = {};
this.opening_.points.forEach(function(point) {
current_snap_x[point.x] = true;
});
for (let x in this.snap_lines_.x) {
if (current_snap_x[x] === undefined) {
this.snap_lines_.x[x].parentNode.removeChild(this.snap_lines_.x[x]);
delete this.snap_lines_.x[x];
}
}
current_snap_x = Object.keys(current_snap_x).map(function(key) {
return Number(key);
});
const intersected_snap_x = this.get_snap_x().filter(function(x) {
return current_snap_x.includes(x) === true;
});
intersected_snap_x.forEach(function(x) {
if (self.snap_lines_.x[x] === undefined) {
self.snap_lines_.x[x] = document.createElementNS('http://www.w3.org/2000/svg', 'line');
self.snap_lines_.x[x].style.strokeDasharray = '7, 3';
self.snap_lines_.x[x].style.stroke = beestat.style.color.yellow.base;
self.snap_lines_.x[x].setAttribute('x1', x);
self.snap_lines_.x[x].setAttribute('x2', x);
self.snap_lines_.x[x].setAttribute('y1', self.floor_plan_.get_grid_pixels() / -2);
self.snap_lines_.x[x].setAttribute('y2', self.floor_plan_.get_grid_pixels() / 2);
self.floor_plan_.get_g().appendChild(self.snap_lines_.x[x]);
}
});
let current_snap_y = {};
this.opening_.points.forEach(function(point) {
current_snap_y[point.y] = true;
});
for (let y in this.snap_lines_.y) {
if (current_snap_y[y] === undefined) {
this.snap_lines_.y[y].parentNode.removeChild(this.snap_lines_.y[y]);
delete this.snap_lines_.y[y];
}
}
current_snap_y = Object.keys(current_snap_y).map(function(key) {
return Number(key);
});
const intersected_snap_y = this.get_snap_y().filter(function(y) {
return current_snap_y.includes(y) === true;
});
intersected_snap_y.forEach(function(y) {
if (self.snap_lines_.y[y] === undefined) {
self.snap_lines_.y[y] = document.createElementNS('http://www.w3.org/2000/svg', 'line');
self.snap_lines_.y[y].style.strokeDasharray = '7, 3';
self.snap_lines_.y[y].style.stroke = beestat.style.color.yellow.base;
self.snap_lines_.y[y].setAttribute('y1', y);
self.snap_lines_.y[y].setAttribute('y2', y);
self.snap_lines_.y[y].setAttribute('x1', self.floor_plan_.get_grid_pixels() / -2);
self.snap_lines_.y[y].setAttribute('x2', self.floor_plan_.get_grid_pixels() / 2);
self.floor_plan_.get_g().appendChild(self.snap_lines_.y[y]);
}
});
};
/**
* Clear all snap lines.
*/
beestat.component.floor_plan_entity.opening.prototype.clear_snap_lines_ = function() {
for (let x in this.snap_lines_.x) {
this.snap_lines_.x[x].parentNode.removeChild(this.snap_lines_.x[x]);
delete this.snap_lines_.x[x];
}
for (let y in this.snap_lines_.y) {
this.snap_lines_.y[y].parentNode.removeChild(this.snap_lines_.y[y]);
delete this.snap_lines_.y[y];
}
};
/**
* Pre-generate a list of snappable x/y values.
*/
beestat.component.floor_plan_entity.opening.prototype.update_snap_points_ = function() {
const self = this;
const group_below = this.floor_plan_.get_group_below(this.group_);
const groups = [this.group_];
if (group_below !== undefined) {
groups.push(group_below);
}
const snap_points = this.collect_snap_points_({
'groups': groups,
'shape_specs': [
{
'collection': 'rooms',
'point_mode': 'relative'
},
{
'collection': 'surfaces',
'point_mode': 'relative'
},
{
'collection': 'openings',
'point_mode': 'absolute'
}
],
'should_skip_shape': function(shape, shape_spec) {
return (
shape_spec.collection === 'openings' &&
self.opening_ !== undefined &&
shape.opening_id !== undefined &&
self.opening_.opening_id !== undefined &&
shape.opening_id === self.opening_.opening_id
);
}
});
this.snap_x_ = snap_points.snap_x;
this.snap_y_ = snap_points.snap_y;
};
/**
* Get snap x values.
*
* @return {number[]}
*/
beestat.component.floor_plan_entity.opening.prototype.get_snap_x = function() {
return this.snap_x_ || [];
};
/**
* Get snap y values.
*
* @return {number[]}
*/
beestat.component.floor_plan_entity.opening.prototype.get_snap_y = function() {
return this.snap_y_ || [];
};
/**
* Set opening.
*
* @param {object} opening
*
* @return {beestat.component.floor_plan_entity.opening}
*/
beestat.component.floor_plan_entity.opening.prototype.set_opening = function(opening) {
this.opening_ = opening;
if (this.opening_.opening_id === undefined) {
this.opening_.opening_id = window.crypto.randomUUID();
}
if (this.opening_.type === undefined) {
this.opening_.type = 'empty';
}
if (this.opening_.height === undefined) {
this.opening_.height = (this.opening_.type === 'window' || this.opening_.type === 'glass') ? 60 : 78;
}
if (this.opening_.elevation === undefined) {
this.opening_.elevation = (this.opening_.type === 'window' || this.opening_.type === 'glass') ? 24 : 0;
}
const default_width = (this.opening_.type === 'window' || this.opening_.type === 'glass') ? 48 : 36;
const width = Math.max(12, Number(this.opening_.width || default_width));
const center_x = Number(this.opening_.x || 0);
const center_y = Number(this.opening_.y || 0);
if (
this.opening_.points === undefined ||
Array.isArray(this.opening_.points) !== true ||
this.opening_.points.length !== 2
) {
this.opening_.points = [
{
'x': Math.round(center_x - (width / 2)),
'y': Math.round(center_y)
},
{
'x': Math.round(center_x + (width / 2)),
'y': Math.round(center_y)
}
];
}
this.update();
return this;
};
/**
* Set group.
*
* @param {object} group
*
* @return {beestat.component.floor_plan_entity.opening}
*/
beestat.component.floor_plan_entity.opening.prototype.set_group = function(group) {
this.group_ = group;
return this;
};
/**
* Get opening.
*
* @return {object}
*/
beestat.component.floor_plan_entity.opening.prototype.get_opening = function() {
return this.opening_;
};
/**
* Set enabled.
*
* @param {boolean} enabled
*
* @return {beestat.component.floor_plan_entity.opening}
*/
beestat.component.floor_plan_entity.opening.prototype.set_enabled = function(enabled) {
this.enabled_ = enabled;
return this;
};
/**
* Set center x/y for this opening by translating both points.
*
* @param {number} x
* @param {number} y
* @param {string} event
*
* @return {beestat.component.floor_plan_entity.opening}
*/
beestat.component.floor_plan_entity.opening.prototype.set_xy = function(x, y, event = 'lesser_update') {
if (event === 'update') {
this.floor_plan_.save_buffer();
}
const center = this.get_center_();
const target_x = x === null ? center.x : Number(x || 0);
const target_y = y === null ? center.y : Number(y || 0);
const grid_half = this.floor_plan_.get_grid_pixels() / 2;
const min_point_x = Math.min(Number(this.opening_.points[0].x || 0), Number(this.opening_.points[1].x || 0));
const max_point_x = Math.max(Number(this.opening_.points[0].x || 0), Number(this.opening_.points[1].x || 0));
const min_point_y = Math.min(Number(this.opening_.points[0].y || 0), Number(this.opening_.points[1].y || 0));
const max_point_y = Math.max(Number(this.opening_.points[0].y || 0), Number(this.opening_.points[1].y || 0));
let dx = target_x - center.x;
let dy = target_y - center.y;
dx = Math.max(-grid_half - min_point_x, Math.min(grid_half - max_point_x, dx));
dy = Math.max(-grid_half - min_point_y, Math.min(grid_half - max_point_y, dy));
this.opening_.points[0].x = Math.round(Number(this.opening_.points[0].x || 0) + dx);
this.opening_.points[0].y = Math.round(Number(this.opening_.points[0].y || 0) + dy);
this.opening_.points[1].x = Math.round(Number(this.opening_.points[1].x || 0) + dx);
this.opening_.points[1].y = Math.round(Number(this.opening_.points[1].y || 0) + dy);
this.update();
this.dispatchEvent(event);
return this;
};
/**
* Set active state.
*
* @param {boolean} active
*
* @return {beestat.component.floor_plan_entity.opening}
*/
beestat.component.floor_plan_entity.opening.prototype.set_active = function(active) {
if (active === true && this.enabled_ !== true) {
return this;
}
if (active === true) {
if (this.state_.active_point_entity !== undefined) {
this.state_.active_point_entity.set_active(false);
this.floor_plan_.update_toolbar();
}
if (this.state_.active_wall_entity !== undefined) {
this.state_.active_wall_entity.set_active(false);
this.floor_plan_.update_toolbar();
}
if (this.state_.active_tree_entity !== undefined) {
this.state_.active_tree_entity.set_active(false);
this.floor_plan_.update_toolbar();
}
if (this.state_.active_surface_entity !== undefined) {
this.state_.active_surface_entity.set_active(false);
this.floor_plan_.update_toolbar();
}
if (this.state_.active_room_entity !== undefined) {
this.state_.active_room_entity.set_active(false);
this.floor_plan_.update_toolbar();
}
if (this.state_.active_light_source_entity !== undefined) {
this.state_.active_light_source_entity.set_active(false);
this.floor_plan_.update_toolbar();
}
}
if (active !== this.active_) {
this.active_ = active;
if (this.active_ === true) {
if (
this.state_.active_opening_entity !== undefined &&
this.state_.active_opening_entity.get_opening() !== this.opening_
) {
this.state_.active_opening_entity.set_active(false);
}
this.state_.active_opening_entity = this;
this.dispatchEvent('activate');
this.update_snap_points_();
this.bring_to_front_();
} else {
delete this.state_.active_opening_entity;
this.clear_snap_lines_();
if (this.state_.active_point_entity !== undefined) {
this.state_.active_point_entity.set_active(false);
}
this.dispatchEvent('inactivate');
}
if (this.rendered_ === true) {
this.rerender();
}
}
return this;
};
/**
* Get shape-like room proxy used by point entity logic.
*
* @return {object}
*/
beestat.component.floor_plan_entity.opening.prototype.get_room = function() {
return this.opening_;
};
/**
* Get color by opening type.
*
* @return {string}
*/
beestat.component.floor_plan_entity.opening.prototype.get_opening_color_ = function() {
switch (this.opening_.type) {
case 'door':
return beestat.style.color.green.base;
case 'window':
case 'glass':
return beestat.style.color.lightblue.light;
case 'empty':
default:
return beestat.style.color.gray.light;
}
};