diff --git a/css/dashboard.css b/css/dashboard.css
index aeb364e..f4d154a 100644
--- a/css/dashboard.css
+++ b/css/dashboard.css
@@ -491,12 +491,13 @@ input[type=range]::-moz-range-thumb {
.icon.information:before { content: "\F02FC"; }
.icon.key:before { content: "\F0306"; }
.icon.label:before { content: "\F0315"; }
-.icon.label:before { content: "\F0315"; }
-.icon.label_off:before { content: "\F0ACB"; }
-.icon.layers:before { content: "\F0328"; }
-.icon.layers_plus:before { content: "\F0E4D"; }
-.icon.link_off:before { content: "\F0338"; }
-.icon.magnify_close:before { content: "\F0980"; }
+.icon.label:before { content: "\F0315"; }
+.icon.label_off:before { content: "\F0ACB"; }
+.icon.layers:before { content: "\F0328"; }
+.icon.layers_plus:before { content: "\F0E4D"; }
+.icon.lightbulb_on:before { content: "\F06E8"; }
+.icon.link_off:before { content: "\F0338"; }
+.icon.magnify_close:before { content: "\F0980"; }
.icon.magnify_minus_outline:before { content: "\F06EC"; }
.icon.magnify_plus_outline:before { content: "\F06ED"; }
.icon.map_marker:before { content: "\F05F8"; }
diff --git a/js/component/card/floor_plan_editor.js b/js/component/card/floor_plan_editor.js
index 7110f47..84187ff 100644
--- a/js/component/card/floor_plan_editor.js
+++ b/js/component/card/floor_plan_editor.js
@@ -85,6 +85,9 @@ beestat.component.card.floor_plan_editor.prototype.decorate_contents_ = function
if (group.openings === undefined) {
group.openings = [];
}
+ if (group.light_sources === undefined) {
+ group.light_sources = [];
+ }
group.rooms.forEach(function(room) {
if (room.room_id === undefined) {
@@ -136,15 +139,88 @@ beestat.component.card.floor_plan_editor.prototype.decorate_contents_ = function
if (opening.editor_locked === undefined) {
opening.editor_locked = false;
}
- if (['empty', 'door', 'window'].includes(opening.type) !== true) {
+ if (opening.type === 'garage') {
+ opening.type = 'door';
+ }
+ if (['empty', 'door', 'window', 'glass'].includes(opening.type) !== true) {
opening.type = 'empty';
}
+ const is_window_like = opening.type === 'window' || opening.type === 'glass';
+ const default_opening_width = is_window_like ? 48 : 36;
+ const default_opening_height = is_window_like ? 42 : 78;
+ const default_opening_elevation = is_window_like ? 36 : 0;
+ const default_opening_color = '#7a573b';
+ const center_x = Number(opening.x || 0);
+ const center_y = Number(opening.y || 0);
+ const width = Number(opening.width || default_opening_width);
+ const rotation_radians = (Number(opening.rotation || 0) * Math.PI) / 180;
if (opening.width === undefined) {
- opening.width = 36;
+ opening.width = default_opening_width;
}
if (opening.height === undefined) {
- opening.height = 80;
+ opening.height = default_opening_height;
}
+ if (opening.elevation === undefined) {
+ opening.elevation = default_opening_elevation;
+ }
+ if (opening.rotation === undefined) {
+ opening.rotation = 0;
+ }
+ if (
+ opening.points === undefined ||
+ Array.isArray(opening.points) !== true ||
+ opening.points.length !== 2
+ ) {
+ const half_width = Math.max(12, width) / 2;
+ const axis_x = Math.cos(rotation_radians);
+ const axis_y = Math.sin(rotation_radians);
+ opening.points = [
+ {
+ 'x': center_x - (axis_x * half_width),
+ 'y': center_y - (axis_y * half_width)
+ },
+ {
+ 'x': center_x + (axis_x * half_width),
+ 'y': center_y + (axis_y * half_width)
+ }
+ ];
+ }
+ opening.x = (Number(opening.points[0].x || 0) + Number(opening.points[1].x || 0)) / 2;
+ opening.y = (Number(opening.points[0].y || 0) + Number(opening.points[1].y || 0)) / 2;
+ const dx = Number(opening.points[1].x || 0) - Number(opening.points[0].x || 0);
+ const dy = Number(opening.points[1].y || 0) - Number(opening.points[0].y || 0);
+ opening.width = Math.max(12, Math.round(Math.sqrt((dx * dx) + (dy * dy))));
+ delete opening.rotation;
+ if (opening.type === 'door') {
+ if (opening.color === undefined) {
+ opening.color = default_opening_color;
+ }
+ } else {
+ delete opening.color;
+ }
+ });
+
+ group.light_sources.forEach(function(light_source) {
+ if (light_source.light_source_id === undefined) {
+ light_source.light_source_id = window.crypto.randomUUID();
+ }
+ if (light_source.editor_hidden === undefined) {
+ light_source.editor_hidden = light_source.editor_visible === false;
+ }
+ delete light_source.editor_visible;
+ if (light_source.editor_locked === undefined) {
+ light_source.editor_locked = false;
+ }
+ light_source.x = Number(light_source.x || 0);
+ light_source.y = Number(light_source.y || 0);
+ light_source.elevation = Number(light_source.elevation !== undefined ? light_source.elevation : 84);
+ light_source.intensity = ['dim', 'normal', 'bright'].includes(light_source.intensity)
+ ? light_source.intensity
+ : 'normal';
+ light_source.temperature_k = Math.max(
+ 1000,
+ Math.min(12000, Math.round(Number(light_source.temperature_k || 4000)))
+ );
});
});
@@ -450,6 +526,14 @@ beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = func
self.update_floor_plan_();
self.rerender();
});
+ this.floor_plan_.addEventListener('add_light_source', function() {
+ self.update_floor_plan_();
+ self.rerender();
+ });
+ this.floor_plan_.addEventListener('remove_light_source', function() {
+ self.update_floor_plan_();
+ self.rerender();
+ });
this.floor_plan_.addEventListener('undo', function() {
self.update_floor_plan_();
self.rerender();
@@ -464,7 +548,8 @@ beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = func
'rooms': {},
'surfaces': {},
'trees': {},
- 'openings': {}
+ 'openings': {},
+ 'light_sources': {}
};
const on_entity_update = function() {
@@ -529,6 +614,17 @@ beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = func
.set_group(self.state_.active_group);
opening_entity.render(self.floor_plan_.get_g());
});
+ (group_below.light_sources || []).slice().reverse().forEach(function(light_source) {
+ if (light_source.editor_hidden === true) {
+ return;
+ }
+
+ const light_source_entity = new beestat.component.floor_plan_entity.light_source(self.floor_plan_, self.state_)
+ .set_enabled(false)
+ .set_light_source(light_source)
+ .set_group(self.state_.active_group);
+ light_source_entity.render(self.floor_plan_.get_g());
+ });
}
// Loop over the rooms in this group and add them.
@@ -652,6 +748,42 @@ beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = func
this.state_.active_opening_entity.render(this.floor_plan_.get_g());
}
+ // Loop over light sources in this group and add them.
+ let active_light_source_entity;
+ (this.state_.active_group.light_sources || []).slice().reverse().forEach(function(light_source) {
+ if (light_source.editor_hidden === true) {
+ return;
+ }
+
+ const light_source_entity = new beestat.component.floor_plan_entity.light_source(self.floor_plan_, self.state_)
+ .set_enabled(light_source.editor_locked !== true)
+ .set_light_source(light_source)
+ .set_group(self.state_.active_group);
+
+ light_source_entity.addEventListener('update', on_entity_update);
+ light_source_entity.addEventListener('activate', on_entity_activate);
+ light_source_entity.addEventListener('inactivate', on_entity_inactivate);
+
+ if (
+ self.state_.active_light_source_entity !== undefined &&
+ light_source.light_source_id === self.state_.active_light_source_entity.get_light_source().light_source_id
+ ) {
+ delete self.state_.active_light_source_entity;
+ active_light_source_entity = light_source_entity;
+ }
+
+ light_source_entity.render(self.floor_plan_.get_g());
+ self.entity_index_.light_sources[light_source.light_source_id] = light_source_entity;
+ });
+
+ if (active_light_source_entity !== undefined) {
+ active_light_source_entity.set_active(true);
+ }
+
+ if (this.state_.active_light_source_entity !== undefined) {
+ this.state_.active_light_source_entity.render(this.floor_plan_.get_g());
+ }
+
// Trees are only editable on the first floor.
const tree_group = this.floor_plan_.get_tree_group_();
if (tree_group === this.state_.active_group) {
@@ -722,6 +854,8 @@ beestat.component.card.floor_plan_editor.prototype.select_layer_object_ = functi
normalized_type = 'rooms';
} else if (normalized_type === 'opening') {
normalized_type = 'openings';
+ } else if (normalized_type === 'light_source') {
+ normalized_type = 'light_sources';
}
const object = this.get_layer_object_by_id_(group, normalized_type, object_id);
const is_active_group = (
@@ -796,6 +930,13 @@ beestat.component.card.floor_plan_editor.prototype.set_layer_object_visibility_
) {
this.state_.active_opening_entity.set_active(false);
}
+ if (
+ type === 'light_sources' &&
+ this.state_.active_light_source_entity !== undefined &&
+ this.state_.active_light_source_entity.get_light_source().light_source_id === object_id
+ ) {
+ this.state_.active_light_source_entity.set_active(false);
+ }
}
this.floor_plan_.update_infobox();
@@ -850,6 +991,13 @@ beestat.component.card.floor_plan_editor.prototype.set_layer_object_locked_ = fu
) {
this.state_.active_opening_entity.set_active(false);
}
+ if (
+ type === 'light_sources' &&
+ this.state_.active_light_source_entity !== undefined &&
+ this.state_.active_light_source_entity.get_light_source().light_source_id === object_id
+ ) {
+ this.state_.active_light_source_entity.set_active(false);
+ }
}
this.floor_plan_.update_infobox();
@@ -906,7 +1054,7 @@ beestat.component.card.floor_plan_editor.prototype.set_layer_visible_ = function
* @param {boolean} locked
*/
beestat.component.card.floor_plan_editor.prototype.set_group_locked_ = function(group, locked) {
- ['rooms', 'surfaces', 'openings', 'trees'].forEach(function(type) {
+ ['rooms', 'surfaces', 'openings', 'trees', 'light_sources'].forEach(function(type) {
const collection = group[type] || [];
collection.forEach(function(object) {
object.editor_locked = locked;
@@ -918,6 +1066,7 @@ beestat.component.card.floor_plan_editor.prototype.set_group_locked_ = function(
this.deactivate_active_entity_for_group_type_(group, 'surfaces');
this.deactivate_active_entity_for_group_type_(group, 'openings');
this.deactivate_active_entity_for_group_type_(group, 'trees');
+ this.deactivate_active_entity_for_group_type_(group, 'light_sources');
}
this.sync_after_layer_change_();
@@ -930,7 +1079,7 @@ beestat.component.card.floor_plan_editor.prototype.set_group_locked_ = function(
* @param {boolean} visible
*/
beestat.component.card.floor_plan_editor.prototype.set_group_visible_ = function(group, visible) {
- ['rooms', 'surfaces', 'openings', 'trees'].forEach(function(type) {
+ ['rooms', 'surfaces', 'openings', 'trees', 'light_sources'].forEach(function(type) {
const collection = group[type] || [];
collection.forEach(function(object) {
object.editor_hidden = visible !== true;
@@ -942,6 +1091,7 @@ beestat.component.card.floor_plan_editor.prototype.set_group_visible_ = function
this.deactivate_active_entity_for_group_type_(group, 'surfaces');
this.deactivate_active_entity_for_group_type_(group, 'openings');
this.deactivate_active_entity_for_group_type_(group, 'trees');
+ this.deactivate_active_entity_for_group_type_(group, 'light_sources');
}
this.sync_after_layer_change_();
@@ -979,6 +1129,13 @@ beestat.component.card.floor_plan_editor.prototype.deactivate_active_entity_for_
if (this.state_.active_opening_entity.group_ === group) {
this.state_.active_opening_entity.set_active(false);
}
+ return;
+ }
+
+ if (type === 'light_sources' && this.state_.active_light_source_entity !== undefined) {
+ if (this.state_.active_light_source_entity.group_ === group) {
+ this.state_.active_light_source_entity.set_active(false);
+ }
}
};
@@ -1076,6 +1233,16 @@ beestat.component.card.floor_plan_editor.prototype.ensure_active_entity_visibili
) {
delete this.state_.active_opening_entity;
}
+
+ if (
+ this.state_.active_light_source_entity !== undefined &&
+ (
+ this.state_.active_light_source_entity.get_light_source().editor_hidden === true ||
+ this.state_.active_light_source_entity.get_light_source().editor_locked === true
+ )
+ ) {
+ delete this.state_.active_light_source_entity;
+ }
};
/**
@@ -1125,6 +1292,9 @@ beestat.component.card.floor_plan_editor.prototype.get_layer_object_id_key_ = fu
if (type === 'openings') {
return 'opening_id';
}
+ if (type === 'light_sources') {
+ return 'light_source_id';
+ }
return 'tree_id';
};
@@ -1158,6 +1328,8 @@ beestat.component.card.floor_plan_editor.prototype.expand_layers_for_active_enti
let type;
if (this.state_.active_tree_entity !== undefined) {
type = 'trees';
+ } else if (this.state_.active_light_source_entity !== undefined) {
+ type = 'light_sources';
} else if (this.state_.active_opening_entity !== undefined) {
type = 'openings';
} else if (this.state_.active_surface_entity !== undefined) {
@@ -1187,6 +1359,9 @@ beestat.component.card.floor_plan_editor.prototype.scroll_layers_to_active_entit
if (this.state_.active_tree_entity !== undefined) {
type = 'trees';
object_id = this.state_.active_tree_entity.get_tree().tree_id;
+ } else if (this.state_.active_light_source_entity !== undefined) {
+ type = 'light_sources';
+ object_id = this.state_.active_light_source_entity.get_light_source().light_source_id;
} else if (this.state_.active_opening_entity !== undefined) {
type = 'openings';
object_id = this.state_.active_opening_entity.get_opening().opening_id;
@@ -1250,6 +1425,12 @@ beestat.component.card.floor_plan_editor.prototype.restore_entity_draw_order_ =
'opening_id'
);
+ append_entities_in_order(
+ this.state_.active_group.light_sources || [],
+ this.entity_index_.light_sources || {},
+ 'light_source_id'
+ );
+
const tree_group = this.floor_plan_.get_tree_group_();
if (tree_group === this.state_.active_group) {
append_entities_in_order(
@@ -1268,6 +1449,8 @@ beestat.component.card.floor_plan_editor.prototype.restore_entity_draw_order_ =
beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_ = function(parent) {
if (this.state_.active_tree_entity !== undefined) {
this.decorate_info_pane_tree_(parent);
+ } else if (this.state_.active_light_source_entity !== undefined) {
+ this.decorate_info_pane_light_source_(parent);
} else if (this.state_.active_opening_entity !== undefined) {
this.decorate_info_pane_opening_(parent);
} else if (this.state_.active_surface_entity !== undefined) {
@@ -1534,6 +1717,140 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_tree_ = fu
});
};
+/**
+ * Decorate the info pane for a light source.
+ *
+ * @param {rocket.Elements} parent
+ */
+beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_light_source_ = function(parent) {
+ const self = this;
+ const light_source = this.state_.active_light_source_entity.get_light_source();
+
+ const grid = $.createElement('div')
+ .style({
+ 'display': 'grid',
+ 'grid-template-columns': 'repeat(auto-fit, minmax(150px, 1fr))',
+ 'column-gap': beestat.style.size.gutter
+ });
+ parent.appendChild(grid);
+
+ const div = $.createElement('div');
+ grid.appendChild(div);
+ const name_input = new beestat.component.input.text()
+ .set_label('Light Source Name')
+ .set_placeholder('Unnamed Light Source')
+ .set_width('100%')
+ .set_maxlength(50)
+ .render(div);
+
+ if (light_source.name !== undefined) {
+ name_input.set_value(light_source.name);
+ }
+
+ name_input.addEventListener('input', function() {
+ light_source.name = name_input.get_value();
+ self.update_layers_sidebar_();
+ });
+ name_input.addEventListener('change', function() {
+ light_source.name = name_input.get_value();
+ self.update_floor_plan_();
+ self.update_layers_sidebar_();
+ });
+
+ const intensity_div = $.createElement('div');
+ grid.appendChild(intensity_div);
+ const intensity_input = new beestat.component.input.select()
+ .set_label('Intensity')
+ .set_width('100%')
+ .add_option({'label': 'Dim', 'value': 'dim'})
+ .add_option({'label': 'Normal', 'value': 'normal'})
+ .add_option({'label': 'Bright', 'value': 'bright'})
+ .render(intensity_div);
+
+ const normalized_intensity = ['dim', 'normal', 'bright'].includes(light_source.intensity)
+ ? light_source.intensity
+ : 'normal';
+ intensity_input.set_value(normalized_intensity);
+ intensity_input.addEventListener('change', function() {
+ const value = intensity_input.get_value();
+ light_source.intensity = ['dim', 'normal', 'bright'].includes(value) ? value : 'normal';
+ self.update_floor_plan_();
+ });
+
+ const temperature_div = $.createElement('div');
+ grid.appendChild(temperature_div);
+ const temperature_input = new beestat.component.input.select()
+ .set_label('Temperature (K)')
+ .set_width('100%')
+ .add_option({'label': '2200K (Candle)', 'value': '2200'})
+ .add_option({'label': '2700K (Warm)', 'value': '2700'})
+ .add_option({'label': '3000K (Soft Warm)', 'value': '3000'})
+ .add_option({'label': '3500K (Neutral Warm)', 'value': '3500'})
+ .add_option({'label': '4000K (Neutral)', 'value': '4000'})
+ .add_option({'label': '5000K (Cool)', 'value': '5000'})
+ .add_option({'label': '6500K (Daylight)', 'value': '6500'})
+ .render(temperature_div);
+
+ const common_temperatures = [2200, 2700, 3000, 3500, 4000, 5000, 6500];
+ const current_temperature = Math.max(1000, Math.min(12000, Math.round(Number(light_source.temperature_k || 4000))));
+ let closest_temperature = common_temperatures[0];
+ let closest_distance = Math.abs(current_temperature - closest_temperature);
+ for (let i = 1; i < common_temperatures.length; i++) {
+ const candidate = common_temperatures[i];
+ const distance = Math.abs(current_temperature - candidate);
+ if (distance < closest_distance) {
+ closest_distance = distance;
+ closest_temperature = candidate;
+ }
+ }
+ temperature_input.set_value(String(closest_temperature));
+
+ temperature_input.addEventListener('change', function() {
+ light_source.temperature_k = Number(temperature_input.get_value() || 4000);
+ self.update_floor_plan_();
+ });
+
+ const elevation_div = $.createElement('div');
+ grid.appendChild(elevation_div);
+ const elevation_input = new beestat.component.input.text()
+ .set_label('Elevation (' + beestat.setting('units.distance') + ')')
+ .set_width('100%')
+ .set_maxlength(6)
+ .set_value(beestat.distance({
+ 'distance': Number(light_source.elevation !== undefined ? light_source.elevation : 84),
+ 'round': 2
+ }) || '')
+ .set_requirements({
+ 'type': 'decimal',
+ 'min_value': beestat.distance(-600),
+ 'max_value': beestat.distance(600),
+ 'required': true
+ })
+ .set_transform({
+ 'type': 'round',
+ 'decimals': 2
+ })
+ .render(elevation_div);
+
+ elevation_input.addEventListener('change', function() {
+ if (elevation_input.meets_requirements() === true) {
+ light_source.elevation = beestat.distance({
+ 'distance': elevation_input.get_value(),
+ 'input_distance_unit': beestat.setting('units.distance'),
+ 'output_distance_unit': 'in',
+ 'round': 2
+ });
+ self.update_floor_plan_();
+ return;
+ }
+
+ elevation_input.set_value(beestat.distance({
+ 'distance': Number(light_source.elevation !== undefined ? light_source.elevation : 84),
+ 'round': 2
+ }) || '', false);
+ });
+};
+
/**
* Decorate the info pane for a surface.
*
@@ -1827,61 +2144,58 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_opening_ =
const type_input = new beestat.component.input.select()
.set_label('Type')
.set_width('100%')
- .add_option({'label': 'Empty', 'value': 'empty'})
- .add_option({'label': 'Door', 'value': 'door'})
.add_option({'label': 'Window', 'value': 'window'})
+ .add_option({'label': 'Door', 'value': 'door'})
+ .add_option({'label': 'Glass', 'value': 'glass'})
+ .add_option({'label': 'Opening', 'value': 'empty'})
.render(div);
- type_input.set_value(['empty', 'door', 'window'].includes(opening.type) ? opening.type : 'empty');
+ type_input.set_value(['empty', 'door', 'window', 'glass'].includes(opening.type) ? opening.type : 'empty');
type_input.addEventListener('change', function() {
+ const previous_type = opening.type;
opening.type = type_input.get_value();
+ const previous_is_window_like = previous_type === 'window' || previous_type === 'glass';
+ const next_is_window_like = opening.type === 'window' || opening.type === 'glass';
+ const previous_default_height = previous_is_window_like ? 42 : 78;
+ const previous_default_elevation = previous_is_window_like ? 36 : 0;
+ const next_default_height = next_is_window_like ? 42 : 78;
+ const next_default_elevation = next_is_window_like ? 36 : 0;
+ if (Number(opening.height || 0) === previous_default_height) {
+ opening.height = next_default_height;
+ }
+ if (Number(opening.elevation || 0) === previous_default_elevation) {
+ opening.elevation = next_default_elevation;
+ }
+ if (opening.type === 'door') {
+ opening.color = opening.color || '#7a573b';
+ } else {
+ delete opening.color;
+ }
self.update_floor_plan_();
self.rerender();
});
- // Width
- div = $.createElement('div');
- grid.appendChild(div);
- const width_input = new beestat.component.input.text()
- .set_label('Width (' + beestat.setting('units.distance') + ')')
- .set_placeholder(beestat.distance({
- 'distance': opening.width || 0,
- 'round': 2
- }))
- .set_value(beestat.distance({
- 'distance': opening.width || 0,
- 'round': 2
- }) || '')
- .set_width('100%')
- .set_maxlength(5)
- .set_requirements({
- 'type': 'decimal',
- 'min_value': beestat.distance(1),
- 'required': true
- })
- .set_transform({
- 'type': 'round',
- 'decimals': 2
- })
- .render(div);
+ if (opening.type === 'door') {
+ div = $.createElement('div');
+ grid.appendChild(div);
+ const door_color_input = new beestat.component.input.select()
+ .set_label('Door Color')
+ .set_width('100%')
+ .add_option({'label': 'Black', 'value': '#4a4a4a'})
+ .add_option({'label': 'Blue', 'value': '#365e9d'})
+ .add_option({'label': 'Brown', 'value': '#7a573b'})
+ .add_option({'label': 'Gray', 'value': '#808890'})
+ .add_option({'label': 'Green', 'value': '#4b6a4b'})
+ .add_option({'label': 'Red', 'value': '#8a3e3a'})
+ .add_option({'label': 'White', 'value': '#f4f4f2'})
+ .render(div);
- width_input.addEventListener('change', function() {
- if (width_input.meets_requirements() === true) {
- opening.width = beestat.distance({
- 'distance': width_input.get_value(),
- 'input_distance_unit': beestat.setting('units.distance'),
- 'output_distance_unit': 'in',
- 'round': 2
- });
+ door_color_input.set_value(String(opening.color || '#7a573b'));
+ door_color_input.addEventListener('change', function() {
+ opening.color = door_color_input.get_value();
self.update_floor_plan_();
- self.rerender();
- } else {
- width_input.set_value(beestat.distance({
- 'distance': opening.width || 0,
- 'round': 2
- }) || '', false);
- }
- });
+ });
+ }
// Height
div = $.createElement('div');
@@ -1925,6 +2239,50 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_opening_ =
}) || '', false);
}
});
+
+ // Elevation
+ div = $.createElement('div');
+ grid.appendChild(div);
+ const elevation_input = new beestat.component.input.text()
+ .set_label('Elevation (' + beestat.setting('units.distance') + ')')
+ .set_placeholder(beestat.distance({
+ 'distance': opening.elevation || 0,
+ 'round': 2
+ }))
+ .set_value(beestat.distance({
+ 'distance': opening.elevation || 0,
+ 'round': 2
+ }) || '')
+ .set_width('100%')
+ .set_maxlength(5)
+ .set_requirements({
+ 'type': 'decimal',
+ 'min_value': beestat.distance(-600),
+ 'max_value': beestat.distance(600),
+ 'required': true
+ })
+ .set_transform({
+ 'type': 'round',
+ 'decimals': 2
+ })
+ .render(div);
+
+ elevation_input.addEventListener('change', function() {
+ if (elevation_input.meets_requirements() === true) {
+ opening.elevation = beestat.distance({
+ 'distance': elevation_input.get_value(),
+ 'input_distance_unit': beestat.setting('units.distance'),
+ 'output_distance_unit': 'in',
+ 'round': 2
+ });
+ self.update_floor_plan_();
+ } else {
+ elevation_input.set_value(beestat.distance({
+ 'distance': opening.elevation || 0,
+ 'round': 2
+ }) || '', false);
+ }
+ });
};
/**
diff --git a/js/component/card/three_d.js b/js/component/card/three_d.js
index 368a1b6..2a1b0f3 100644
--- a/js/component/card/three_d.js
+++ b/js/component/card/three_d.js
@@ -408,7 +408,12 @@ beestat.component.card.three_d.prototype.decorate_drawing_pane_ = function(paren
// Set some defaults on the scene.
this.scene_.set_date(this.date_m_);
- this.scene_.set_labels(beestat.setting('visualize.three_d.show_labels'));
+ this.scene_.set_labels(
+ this.get_show_environment_() === true
+ ? false
+ : beestat.setting('visualize.three_d.show_labels')
+ );
+ this.scene_.set_room_interaction_enabled(this.get_show_environment_() === false);
this.scene_.set_auto_rotate(beestat.setting('visualize.three_d.auto_rotate'));
const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
@@ -563,10 +568,17 @@ beestat.component.card.three_d.prototype.apply_layer_visibility_ = function() {
const group_visible = beestat.setting(setting_key) !== false;
this.scene_.set_layer_visible(
group.group_id,
- show_environment === true ? false : group_visible
+ group_visible
);
});
+ this.scene_.set_labels(
+ show_environment === true
+ ? false
+ : beestat.setting('visualize.three_d.show_labels')
+ );
+ this.scene_.set_room_interaction_enabled(show_environment === false);
+
this.update_environment_date_visibility_();
if (this.controls_container_ !== undefined) {
diff --git a/js/component/floor_plan.js b/js/component/floor_plan.js
index 4e336e8..a80c3d5 100644
--- a/js/component/floor_plan.js
+++ b/js/component/floor_plan.js
@@ -131,7 +131,8 @@ beestat.component.floor_plan.prototype.render = function(parent) {
self.state_.active_room_entity !== undefined ||
self.state_.active_surface_entity !== undefined ||
self.state_.active_tree_entity !== undefined ||
- self.state_.active_opening_entity !== undefined
+ self.state_.active_opening_entity !== undefined ||
+ self.state_.active_light_source_entity !== undefined
) {
self.clear_room_();
}
@@ -154,9 +155,13 @@ beestat.component.floor_plan.prototype.render = function(parent) {
self.add_tree_();
}
} else if (e.key.toLowerCase() === 'o') {
- if (e.ctrlKey === false) {
+ if (e.ctrlKey === false && self.has_early_access_() === true) {
self.add_opening_();
}
+ } else if (e.key.toLowerCase() === 'l') {
+ if (e.ctrlKey === false && self.has_early_access_() === true) {
+ self.add_light_source_();
+ }
} else if (e.key.toLowerCase() === 's') {
self.toggle_snapping_();
} else if (
@@ -188,6 +193,24 @@ beestat.component.floor_plan.prototype.render = function(parent) {
'type': 'tree',
'data': beestat.clone(self.state_.active_tree_entity.get_tree())
};
+ } else if (
+ e.key.toLowerCase() === 'c' &&
+ e.ctrlKey === true &&
+ self.state_.active_opening_entity !== undefined
+ ) {
+ self.state_.copied_object = {
+ 'type': 'opening',
+ 'data': beestat.clone(self.state_.active_opening_entity.get_opening())
+ };
+ } else if (
+ e.key.toLowerCase() === 'c' &&
+ e.ctrlKey === true &&
+ self.state_.active_light_source_entity !== undefined
+ ) {
+ self.state_.copied_object = {
+ 'type': 'light_source',
+ 'data': beestat.clone(self.state_.active_light_source_entity.get_light_source())
+ };
} else if (
e.key.toLowerCase() === 'v' &&
e.ctrlKey === true &&
@@ -211,6 +234,20 @@ beestat.component.floor_plan.prototype.render = function(parent) {
self.state_.copied_object.type === 'room'
) {
self.add_room_(self.state_.copied_object.data);
+ } else if (
+ e.key.toLowerCase() === 'v' &&
+ e.ctrlKey === true &&
+ self.state_.copied_object !== undefined &&
+ self.state_.copied_object.type === 'opening'
+ ) {
+ self.add_opening_(self.state_.copied_object.data);
+ } else if (
+ e.key.toLowerCase() === 'v' &&
+ e.ctrlKey === true &&
+ self.state_.copied_object !== undefined &&
+ self.state_.copied_object.type === 'light_source'
+ ) {
+ self.add_light_source_(self.state_.copied_object.data);
} else if (
e.key.toLowerCase() === 'z' &&
e.ctrlKey === true
@@ -231,13 +268,23 @@ beestat.component.floor_plan.prototype.render = function(parent) {
self.state_.active_point_entity ||
self.state_.active_wall_entity ||
self.state_.active_opening_entity ||
+ self.state_.active_light_source_entity ||
self.state_.active_surface_entity ||
self.state_.active_room_entity ||
self.state_.active_tree_entity;
if (entity !== undefined) {
- const x = entity.get_x();
- const y = entity.get_y();
+ let x = entity.get_x();
+ let y = entity.get_y();
+ if (
+ self.state_.active_opening_entity !== undefined &&
+ entity === self.state_.active_opening_entity &&
+ typeof entity.get_center_ === 'function'
+ ) {
+ const opening_center = entity.get_center_();
+ x = opening_center.x;
+ y = opening_center.y;
+ }
switch (e.key) {
case 'ArrowLeft':
@@ -516,19 +563,19 @@ beestat.component.floor_plan.prototype.update_toolbar = function() {
})
);
- // Add opening
- this.tile_group_.add_tile(new beestat.component.tile()
- .set_icon('window_closed_variant')
- .set_title('Add Opening [O]')
- .set_text_color(beestat.style.color.gray.light)
- .set_background_color(beestat.style.color.bluegray.base)
- .set_background_hover_color(beestat.style.color.bluegray.light)
- .addEventListener('click', function() {
- self.add_opening_();
- })
- );
-
if (this.has_early_access_() === true) {
+ // Add opening
+ this.tile_group_.add_tile(new beestat.component.tile()
+ .set_icon('window_closed_variant')
+ .set_title('Add Opening [O]')
+ .set_text_color(beestat.style.color.gray.light)
+ .set_background_color(beestat.style.color.bluegray.base)
+ .set_background_hover_color(beestat.style.color.bluegray.light)
+ .addEventListener('click', function() {
+ self.add_opening_();
+ })
+ );
+
// Add surface
this.tile_group_.add_tile(new beestat.component.tile()
.set_icon('texture_box')
@@ -566,6 +613,18 @@ beestat.component.floor_plan.prototype.update_toolbar = function() {
add_tree_button
.set_text_color(beestat.style.color.bluegray.dark);
}
+
+ // Add light source
+ this.tile_group_.add_tile(new beestat.component.tile()
+ .set_icon('lightbulb_on')
+ .set_title('Add Light Source [L]')
+ .set_text_color(beestat.style.color.gray.light)
+ .set_background_color(beestat.style.color.bluegray.base)
+ .set_background_hover_color(beestat.style.color.bluegray.light)
+ .addEventListener('click', function() {
+ self.add_light_source_();
+ })
+ );
}
// Remove selected room, opening, surface, or tree
@@ -579,7 +638,8 @@ beestat.component.floor_plan.prototype.update_toolbar = function() {
this.state_.active_room_entity !== undefined ||
this.state_.active_opening_entity !== undefined ||
this.state_.active_surface_entity !== undefined ||
- this.state_.active_tree_entity !== undefined
+ this.state_.active_tree_entity !== undefined ||
+ this.state_.active_light_source_entity !== undefined
) {
remove_button
.set_background_hover_color(beestat.style.color.bluegray.light)
@@ -821,15 +881,18 @@ beestat.component.floor_plan.prototype.update_infobox = function() {
);
} else if (this.state_.active_opening_entity !== undefined) {
const opening = this.state_.active_opening_entity.get_opening();
+ const opening_width = this.get_opening_width_(opening);
parts.push('Opening');
parts.push((opening.type || 'empty').toUpperCase());
parts.push(
beestat.distance({
- 'distance': opening.width || 0,
+ 'distance': opening_width,
'units': true,
'round': 0
}) + ' w'
);
+ } else if (this.state_.active_light_source_entity !== undefined) {
+ parts.push('Light Source');
} else {
parts.push(this.state_.active_group.name || 'Unnamed Floor');
parts.push(
@@ -1049,6 +1112,11 @@ beestat.component.floor_plan.prototype.remove_room_ = function() {
* Remove the currently active selectable entity (surface, room, opening, or tree).
*/
beestat.component.floor_plan.prototype.remove_active_entity_ = function() {
+ if (this.state_.active_light_source_entity !== undefined) {
+ this.remove_light_source_();
+ return;
+ }
+
if (this.state_.active_opening_entity !== undefined) {
this.remove_opening_();
return;
@@ -1099,6 +1167,9 @@ beestat.component.floor_plan.prototype.set_active_group = function(group) {
if (this.state_.active_opening_entity !== undefined) {
this.state_.active_opening_entity.set_active(false);
}
+ if (this.state_.active_light_source_entity !== undefined) {
+ this.state_.active_light_source_entity.set_active(false);
+ }
this.state_.active_group = group;
this.dispatchEvent('change_group');
@@ -1148,6 +1219,10 @@ beestat.component.floor_plan.prototype.remove_surface_ = function() {
* @param {object} opening Optional opening to copy from.
*/
beestat.component.floor_plan.prototype.add_opening_ = function(opening) {
+ if (this.has_early_access_() !== true) {
+ return;
+ }
+
this.save_buffer();
if (this.state_.active_group.openings === undefined) {
@@ -1155,16 +1230,52 @@ beestat.component.floor_plan.prototype.add_opening_ = function(opening) {
}
const svg_view_box = this.view_box_;
- const width = Math.max(12, Number((opening || {}).width || 36));
- const height = Math.max(1, Number((opening || {}).height || 80));
+ const requested_opening_type = (opening || {}).type;
+ const opening_type = requested_opening_type === 'garage'
+ ? 'door'
+ : (
+ ['empty', 'door', 'window', 'glass'].includes(requested_opening_type)
+ ? requested_opening_type
+ : 'empty'
+ );
+
+ let default_width = 36;
+ let default_height = 78;
+ let default_elevation = 0;
+ let default_color = '#7a573b';
+ if (opening_type === 'window' || opening_type === 'glass') {
+ default_width = 48;
+ default_height = 42;
+ default_elevation = 36;
+ }
+
+ const width = Math.max(12, Number((opening || {}).width || default_width));
+ const height = Math.max(1, Number((opening || {}).height || default_height));
+ const elevation = Number((opening || {}).elevation !== undefined ? opening.elevation : default_elevation);
+ const center_x = Number((opening || {}).x || (svg_view_box.x + (svg_view_box.width / 2)));
+ const center_y = Number((opening || {}).y || (svg_view_box.y + (svg_view_box.height / 2)));
+ const half_width = width / 2;
const new_opening = {
'opening_id': window.crypto.randomUUID(),
- 'x': Number((opening || {}).x || (svg_view_box.x + (svg_view_box.width / 2))),
- 'y': Number((opening || {}).y || (svg_view_box.y + (svg_view_box.height / 2))),
- 'width': width,
+ 'x': center_x,
+ 'y': center_y,
'height': height,
- 'type': ['empty', 'door', 'window'].includes((opening || {}).type) ? opening.type : 'empty',
+ 'elevation': elevation,
+ 'points': (opening || {}).points && (opening || {}).points.length === 2
+ ? beestat.clone(opening.points)
+ : [
+ {
+ 'x': center_x - half_width,
+ 'y': center_y
+ },
+ {
+ 'x': center_x + half_width,
+ 'y': center_y
+ }
+ ],
+ 'type': opening_type,
+ 'color': opening_type === 'door' ? String((opening || {}).color || default_color) : undefined,
'name': (opening || {}).name,
'editor_hidden': false,
'editor_locked': false
@@ -1179,6 +1290,27 @@ beestat.component.floor_plan.prototype.add_opening_ = function(opening) {
this.dispatchEvent('add_opening');
};
+/**
+ * Get opening width in floor-plan units from points.
+ *
+ * @param {object} opening
+ *
+ * @return {number}
+ */
+beestat.component.floor_plan.prototype.get_opening_width_ = function(opening) {
+ if (
+ opening !== undefined &&
+ opening.points !== undefined &&
+ opening.points.length === 2
+ ) {
+ const dx = Number(opening.points[1].x || 0) - Number(opening.points[0].x || 0);
+ const dy = Number(opening.points[1].y || 0) - Number(opening.points[0].y || 0);
+ return Math.max(0, Math.sqrt((dx * dx) + (dy * dy)));
+ }
+
+ return Math.max(0, Number(opening.width || 0));
+};
+
/**
* Remove the currently active opening.
*/
@@ -1207,6 +1339,74 @@ beestat.component.floor_plan.prototype.remove_opening_ = function() {
this.dispatchEvent('remove_opening');
};
+/**
+ * Add a new light source to the active floor.
+ *
+ * @param {object} light_source Optional light source to copy from.
+ */
+beestat.component.floor_plan.prototype.add_light_source_ = function(light_source) {
+ if (this.has_early_access_() !== true) {
+ return;
+ }
+
+ this.save_buffer();
+
+ if (this.state_.active_group.light_sources === undefined) {
+ this.state_.active_group.light_sources = [];
+ }
+
+ const svg_view_box = this.view_box_;
+ const new_light_source = {
+ 'light_source_id': window.crypto.randomUUID(),
+ 'x': Number((light_source || {}).x || (svg_view_box.x + (svg_view_box.width / 2))),
+ 'y': Number((light_source || {}).y || (svg_view_box.y + (svg_view_box.height / 2))),
+ 'elevation': Number((light_source || {}).elevation !== undefined ? light_source.elevation : 84),
+ 'intensity': ['dim', 'normal', 'bright'].includes((light_source || {}).intensity)
+ ? light_source.intensity
+ : 'normal',
+ 'temperature_k': Math.max(1000, Math.min(12000, Math.round(Number((light_source || {}).temperature_k || 4000)))),
+ 'name': (light_source || {}).name,
+ 'editor_hidden': false,
+ 'editor_locked': false
+ };
+
+ this.state_.active_group.light_sources.unshift(new_light_source);
+ new beestat.component.floor_plan_entity.light_source(this, this.state_)
+ .set_light_source(new_light_source)
+ .set_group(this.state_.active_group)
+ .set_active(true);
+
+ this.dispatchEvent('add_light_source');
+};
+
+/**
+ * Remove the currently active light source.
+ */
+beestat.component.floor_plan.prototype.remove_light_source_ = function() {
+ this.save_buffer();
+
+ if (
+ this.state_.active_light_source_entity === undefined ||
+ this.state_.active_group.light_sources === undefined
+ ) {
+ return;
+ }
+
+ const self = this;
+ const index = this.state_.active_group.light_sources.findIndex(function(light_source) {
+ return light_source === self.state_.active_light_source_entity.get_light_source();
+ });
+
+ if (index === -1) {
+ return;
+ }
+
+ this.state_.active_light_source_entity.set_active(false);
+ this.state_.active_group.light_sources.splice(index, 1);
+
+ this.dispatchEvent('remove_light_source');
+};
+
/**
* Add a new tree to the first floor.
*
@@ -1318,6 +1518,9 @@ beestat.component.floor_plan.prototype.clear_room_ = function() {
if (this.state_.active_opening_entity !== undefined) {
this.state_.active_opening_entity.set_active(false);
}
+ if (this.state_.active_light_source_entity !== undefined) {
+ this.state_.active_light_source_entity.set_active(false);
+ }
};
/**
@@ -1581,6 +1784,7 @@ beestat.component.floor_plan.prototype.save_buffer = function(clear = true) {
'active_surface_entity': this.state_.active_surface_entity,
'active_tree_entity': this.state_.active_tree_entity,
'active_opening_entity': this.state_.active_opening_entity,
+ 'active_light_source_entity': this.state_.active_light_source_entity,
'active_group_id': this.state_.active_group.group_id
});
@@ -1632,6 +1836,8 @@ beestat.component.floor_plan.prototype.undo_ = function() {
this.state_.buffer[this.state_.buffer_pointer].active_tree_entity;
this.state_.active_opening_entity =
this.state_.buffer[this.state_.buffer_pointer].active_opening_entity;
+ this.state_.active_light_source_entity =
+ this.state_.buffer[this.state_.buffer_pointer].active_light_source_entity;
// Restore any active group.
this.state_.active_group_id =
@@ -1683,6 +1889,8 @@ beestat.component.floor_plan.prototype.redo_ = function() {
this.state_.buffer[this.state_.buffer_pointer].active_tree_entity;
this.state_.active_opening_entity =
this.state_.buffer[this.state_.buffer_pointer].active_opening_entity;
+ this.state_.active_light_source_entity =
+ this.state_.buffer[this.state_.buffer_pointer].active_light_source_entity;
// Restore any active group.
this.state_.active_group_id =
diff --git a/js/component/floor_plan_entity/light_source.js b/js/component/floor_plan_entity/light_source.js
new file mode 100644
index 0000000..ce34e82
--- /dev/null
+++ b/js/component/floor_plan_entity/light_source.js
@@ -0,0 +1,446 @@
+/**
+ * Floor plan light source.
+ */
+beestat.component.floor_plan_entity.light_source = function() {
+ this.enabled_ = true;
+ this.snap_lines_ = {};
+
+ beestat.component.floor_plan_entity.apply(this, arguments);
+};
+beestat.extend(beestat.component.floor_plan_entity.light_source, beestat.component.floor_plan_entity);
+
+/**
+ * Decorate.
+ *
+ * @param {SVGGElement} parent
+ */
+beestat.component.floor_plan_entity.light_source.prototype.decorate_ = function(parent) {
+ this.decorate_circle_(parent);
+
+ if (this.active_ === true && this.enabled_ === true) {
+ this.set_draggable_(true);
+ this.update_snap_points_();
+ }
+};
+
+/**
+ * Draw the light-source circle.
+ *
+ * @param {SVGGElement} parent
+ */
+beestat.component.floor_plan_entity.light_source.prototype.decorate_circle_ = function(parent) {
+ const self = this;
+
+ this.circle_ = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ this.circle_.setAttribute('r', 7);
+ this.circle_.style.strokeWidth = '2';
+ parent.appendChild(this.circle_);
+
+ const fill_color = '#f0cf59';
+
+ if (this.active_ === true) {
+ this.circle_.style.cursor = 'pointer';
+ this.circle_.style.fillOpacity = '0.7';
+ this.circle_.style.fill = fill_color;
+ this.circle_.style.stroke = '#ffffff';
+ this.circle_.style.filter = 'brightness(1.15)';
+ } else if (this.enabled_ === true) {
+ this.circle_.style.cursor = 'pointer';
+ this.circle_.style.fillOpacity = '0.58';
+ this.circle_.style.fill = fill_color;
+ this.circle_.style.stroke = beestat.style.color.gray.base;
+ this.circle_.style.filter = 'none';
+ } else {
+ this.circle_.style.cursor = 'default';
+ this.circle_.style.fillOpacity = '0.25';
+ this.circle_.style.fill = beestat.style.color.gray.base;
+ this.circle_.style.stroke = beestat.style.color.gray.dark;
+ this.circle_.style.filter = 'none';
+ }
+
+ if (this.enabled_ === true) {
+ this.circle_.addEventListener('click', function(e) {
+ e.stopPropagation();
+ self.set_active(true);
+ });
+ }
+
+ this.update_circle_();
+};
+
+/**
+ * Update circle geometry.
+ */
+beestat.component.floor_plan_entity.light_source.prototype.update_circle_ = function() {
+ this.circle_.setAttribute('cx', 0);
+ this.circle_.setAttribute('cy', 0);
+};
+
+/**
+ * Set light source.
+ *
+ * @param {object} light_source
+ *
+ * @return {beestat.component.floor_plan_entity.light_source}
+ */
+beestat.component.floor_plan_entity.light_source.prototype.set_light_source = function(light_source) {
+ this.light_source_ = light_source;
+
+ this.light_source_.light_source_id = this.light_source_.light_source_id || window.crypto.randomUUID();
+ this.light_source_.x = Number(this.light_source_.x || 0);
+ this.light_source_.y = Number(this.light_source_.y || 0);
+ this.light_source_.elevation = Number(this.light_source_.elevation !== undefined ? this.light_source_.elevation : 84);
+ if (this.light_source_.name === undefined) {
+ this.light_source_.name = '';
+ }
+ this.light_source_.intensity = ['dim', 'normal', 'bright'].includes(this.light_source_.intensity)
+ ? this.light_source_.intensity
+ : 'normal';
+ this.light_source_.temperature_k = Math.max(1000, Math.min(12000, Math.round(Number(this.light_source_.temperature_k || 4000))));
+
+ this.x_ = this.light_source_.x;
+ this.y_ = this.light_source_.y;
+
+ return this;
+};
+
+/**
+ * Set group.
+ *
+ * @param {object} group
+ *
+ * @return {beestat.component.floor_plan_entity.light_source}
+ */
+beestat.component.floor_plan_entity.light_source.prototype.set_group = function(group) {
+ this.group_ = group;
+ return this;
+};
+
+/**
+ * Set enabled.
+ *
+ * @param {boolean} enabled
+ *
+ * @return {beestat.component.floor_plan_entity.light_source}
+ */
+beestat.component.floor_plan_entity.light_source.prototype.set_enabled = function(enabled) {
+ this.enabled_ = enabled;
+ return this;
+};
+
+/**
+ * Get light source.
+ *
+ * @return {object}
+ */
+beestat.component.floor_plan_entity.light_source.prototype.get_light_source = function() {
+ return this.light_source_;
+};
+
+/**
+ * Set active state.
+ *
+ * @param {boolean} active
+ *
+ * @return {beestat.component.floor_plan_entity.light_source}
+ */
+beestat.component.floor_plan_entity.light_source.prototype.set_active = function(active) {
+ if (active === true && this.enabled_ !== true) {
+ return this;
+ }
+
+ if (active !== this.active_) {
+ this.active_ = active;
+
+ if (this.active_ === true) {
+ if (
+ this.state_.active_light_source_entity !== undefined &&
+ this.state_.active_light_source_entity.get_light_source().light_source_id !== this.light_source_.light_source_id
+ ) {
+ this.state_.active_light_source_entity.set_active(false);
+ }
+
+ if (this.state_.active_point_entity !== undefined) {
+ this.state_.active_point_entity.set_active(false);
+ }
+ if (this.state_.active_wall_entity !== undefined) {
+ this.state_.active_wall_entity.set_active(false);
+ }
+ if (this.state_.active_room_entity !== undefined) {
+ this.state_.active_room_entity.set_active(false);
+ }
+ if (this.state_.active_surface_entity !== undefined) {
+ this.state_.active_surface_entity.set_active(false);
+ }
+ if (this.state_.active_opening_entity !== undefined) {
+ this.state_.active_opening_entity.set_active(false);
+ }
+ if (this.state_.active_tree_entity !== undefined) {
+ this.state_.active_tree_entity.set_active(false);
+ }
+
+ this.state_.active_light_source_entity = this;
+ this.dispatchEvent('activate');
+ this.update_snap_points_();
+ this.bring_to_front_();
+ } else {
+ delete this.state_.active_light_source_entity;
+ this.clear_snap_lines_();
+ this.dispatchEvent('inactivate');
+ }
+
+ if (this.rendered_ === true) {
+ this.rerender();
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Set position and clamp to grid.
+ *
+ * @param {number} x
+ * @param {number} y
+ * @param {string} event
+ *
+ * @return {beestat.component.floor_plan_entity.light_source}
+ */
+beestat.component.floor_plan_entity.light_source.prototype.set_xy = function(x, y, event = 'lesser_update') {
+ if (event === 'update') {
+ this.floor_plan_.save_buffer();
+ }
+
+ const half_grid = this.floor_plan_.get_grid_pixels() / 2;
+ let clamped_x = Math.round(Number(x || 0));
+ let clamped_y = Math.round(Number(y || 0));
+ clamped_x = Math.min(clamped_x, half_grid);
+ clamped_x = Math.max(clamped_x, -half_grid);
+ clamped_y = Math.min(clamped_y, half_grid);
+ clamped_y = Math.max(clamped_y, -half_grid);
+
+ this.light_source_.x = clamped_x;
+ this.light_source_.y = clamped_y;
+
+ this.dispatchEvent(event);
+
+ return beestat.component.floor_plan_entity.prototype.set_xy.apply(this, [clamped_x, clamped_y]);
+};
+
+/**
+ * Drag start.
+ */
+beestat.component.floor_plan_entity.light_source.prototype.after_mousedown_handler_ = function() {
+ this.drag_start_entity_ = {
+ 'x': this.light_source_.x,
+ 'y': this.light_source_.y
+ };
+};
+
+/**
+ * Drag move with snap-point behavior.
+ *
+ * @param {Event} e
+ */
+beestat.component.floor_plan_entity.light_source.prototype.after_mousemove_handler_ = function(e) {
+ if (this.drag_start_entity_ === undefined) {
+ return;
+ }
+
+ const snap_distance = 6;
+ let desired_x = this.drag_start_entity_.x + (((e.clientX || e.touches[0].clientX) - this.drag_start_mouse_.x) * this.floor_plan_.get_scale());
+ let desired_y = this.drag_start_entity_.y + (((e.clientY || e.touches[0].clientY) - this.drag_start_mouse_.y) * this.floor_plan_.get_scale());
+
+ if (this.state_.snapping === true) {
+ let best_x;
+ let best_x_distance = Number.POSITIVE_INFINITY;
+ let best_y;
+ let best_y_distance = Number.POSITIVE_INFINITY;
+
+ for (let i = 0; i < this.get_snap_x().length; i++) {
+ const candidate_x = this.get_snap_x()[i];
+ const distance_x = Math.abs(candidate_x - desired_x);
+ if (distance_x <= snap_distance && distance_x < best_x_distance) {
+ best_x = candidate_x;
+ best_x_distance = distance_x;
+ }
+ }
+ for (let i = 0; i < this.get_snap_y().length; i++) {
+ const candidate_y = this.get_snap_y()[i];
+ const distance_y = Math.abs(candidate_y - desired_y);
+ if (distance_y <= snap_distance && distance_y < best_y_distance) {
+ best_y = candidate_y;
+ best_y_distance = distance_y;
+ }
+ }
+
+ if (best_x !== undefined) {
+ desired_x = best_x;
+ }
+ if (best_y !== undefined) {
+ desired_y = best_y;
+ }
+
+ this.update_snap_lines_();
+ } else {
+ this.clear_snap_lines_();
+ }
+
+ this.set_xy(desired_x, desired_y);
+};
+
+/**
+ * Drag stop.
+ */
+beestat.component.floor_plan_entity.light_source.prototype.after_mouseup_handler_ = function() {
+ if (this.dragged_ === true) {
+ this.clear_snap_lines_();
+ this.update_snap_points_();
+ }
+};
+
+/**
+ * Pre-generate snap points.
+ */
+beestat.component.floor_plan_entity.light_source.prototype.update_snap_points_ = function() {
+ const self = this;
+ const snap_x = {};
+ const snap_y = {};
+
+ const append_shapes = function(shapes, skip_self_light_source) {
+ if (Array.isArray(shapes) !== true) {
+ return;
+ }
+
+ shapes.forEach(function(shape) {
+ if (shape.editor_hidden === true) {
+ return;
+ }
+
+ if (
+ skip_self_light_source === true &&
+ shape.light_source_id !== undefined &&
+ self.light_source_ !== undefined &&
+ self.light_source_.light_source_id === shape.light_source_id
+ ) {
+ return;
+ }
+
+ if (Array.isArray(shape.points) === true) {
+ shape.points.forEach(function(point) {
+ const is_opening = shape.opening_id !== undefined;
+ const absolute_x = is_opening
+ ? Number(point.x || 0)
+ : Number(point.x || 0) + Number(shape.x || 0);
+ const absolute_y = is_opening
+ ? Number(point.y || 0)
+ : Number(point.y || 0) + Number(shape.y || 0);
+ snap_x[absolute_x] = true;
+ snap_y[absolute_y] = true;
+ });
+ } else {
+ snap_x[Number(shape.x || 0)] = true;
+ snap_y[Number(shape.y || 0)] = true;
+ }
+ });
+ };
+
+ append_shapes(this.group_.rooms, false);
+ append_shapes(this.group_.surfaces, false);
+ append_shapes(this.group_.openings, false);
+ append_shapes(this.group_.light_sources, true);
+
+ const group_below = this.floor_plan_.get_group_below(this.group_);
+ if (group_below !== undefined) {
+ append_shapes(group_below.rooms, false);
+ append_shapes(group_below.surfaces, false);
+ append_shapes(group_below.openings, false);
+ append_shapes(group_below.light_sources, false);
+ }
+
+ this.snap_x_ = Object.keys(snap_x).map(function(key) {
+ return Number(key);
+ });
+ this.snap_y_ = Object.keys(snap_y).map(function(key) {
+ return Number(key);
+ });
+};
+
+/**
+ * Get snap x values.
+ *
+ * @return {number[]}
+ */
+beestat.component.floor_plan_entity.light_source.prototype.get_snap_x = function() {
+ return this.snap_x_ || [];
+};
+
+/**
+ * Get snap y values.
+ *
+ * @return {number[]}
+ */
+beestat.component.floor_plan_entity.light_source.prototype.get_snap_y = function() {
+ return this.snap_y_ || [];
+};
+
+/**
+ * Update snap lines.
+ */
+beestat.component.floor_plan_entity.light_source.prototype.update_snap_lines_ = function() {
+ const point_x = this.light_source_.x;
+ if (this.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) {
+ if (this.snap_lines_.x.parentNode !== undefined && this.snap_lines_.x.parentNode !== null) {
+ this.snap_lines_.x.parentNode.removeChild(this.snap_lines_.x);
+ }
+ delete this.snap_lines_.x;
+ }
+
+ const point_y = this.light_source_.y;
+ if (this.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) {
+ if (this.snap_lines_.y.parentNode !== undefined && this.snap_lines_.y.parentNode !== null) {
+ this.snap_lines_.y.parentNode.removeChild(this.snap_lines_.y);
+ }
+ delete this.snap_lines_.y;
+ }
+};
+
+/**
+ * Clear snap lines.
+ */
+beestat.component.floor_plan_entity.light_source.prototype.clear_snap_lines_ = function() {
+ if (this.snap_lines_.x !== undefined) {
+ if (this.snap_lines_.x.parentNode !== undefined && this.snap_lines_.x.parentNode !== null) {
+ this.snap_lines_.x.parentNode.removeChild(this.snap_lines_.x);
+ }
+ delete this.snap_lines_.x;
+ }
+ if (this.snap_lines_.y !== undefined) {
+ if (this.snap_lines_.y.parentNode !== undefined && this.snap_lines_.y.parentNode !== null) {
+ this.snap_lines_.y.parentNode.removeChild(this.snap_lines_.y);
+ }
+ delete this.snap_lines_.y;
+ }
+};
diff --git a/js/component/floor_plan_entity/opening.js b/js/component/floor_plan_entity/opening.js
index e996395..d9abe2d 100644
--- a/js/component/floor_plan_entity/opening.js
+++ b/js/component/floor_plan_entity/opening.js
@@ -1,9 +1,14 @@
/**
- * Floor plan opening (empty, door, window).
+ * 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.resize_mode_ = null;
+ this.point_entities_ = [];
+ this.snap_lines_ = {
+ 'x': {},
+ 'y': {}
+ };
beestat.component.floor_plan_entity.apply(this, arguments);
};
@@ -17,7 +22,12 @@ beestat.extend(beestat.component.floor_plan_entity.opening, beestat.component.fl
beestat.component.floor_plan_entity.opening.prototype.decorate_ = function(parent) {
this.decorate_opening_(parent);
- if (this.enabled_ === true) {
+ if (this.active_ === true) {
+ this.decorate_points_(parent);
+ this.update_snap_points_();
+ }
+
+ if (this.enabled_ === true && this.active_ === true) {
this.set_draggable_(true);
}
};
@@ -31,35 +41,27 @@ beestat.component.floor_plan_entity.opening.prototype.decorate_opening_ = functi
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 ? 'move' : 'default';
+ this.path_.style.cursor = this.enabled_ === true ? 'pointer' : 'default';
parent.appendChild(this.path_);
- this.left_handle_ = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
- this.right_handle_ = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
- [this.left_handle_, this.right_handle_].forEach(function(handle, index) {
- handle.setAttribute('width', '8');
- handle.setAttribute('height', '8');
- handle.setAttribute('rx', '1');
- handle.setAttribute('ry', '1');
- handle.style.cursor = 'ew-resize';
- handle.style.strokeWidth = '1';
- parent.appendChild(handle);
+ 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');
- if (self.enabled_ === true) {
- handle.addEventListener('mousedown', function(e) {
- e.stopPropagation();
- self.resize_mode_ = index === 0 ? 'left' : 'right';
- self.mousedown_handler_(e);
- });
- handle.addEventListener('touchstart', function(e) {
- e.stopPropagation();
- self.resize_mode_ = index === 0 ? 'left' : 'right';
- self.mousedown_handler_(e);
- });
- }
- });
+ 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) {
@@ -73,34 +75,168 @@ beestat.component.floor_plan_entity.opening.prototype.decorate_opening_ = functi
}
this.update();
- this.apply_transform_();
};
/**
* Update visuals.
*/
beestat.component.floor_plan_entity.opening.prototype.update = function() {
- const width = Math.max(12, Number(this.opening_.width || 0));
- const half_width = width / 2;
+ if (
+ this.opening_ === undefined ||
+ this.opening_.points === undefined ||
+ Array.isArray(this.opening_.points) !== true ||
+ this.opening_.points.length < 2
+ ) {
+ return;
+ }
- this.path_.setAttribute('d', 'M' + (-half_width) + ',0 L' + half_width + ',0');
+ 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';
+};
- const handles_visible = this.active_ === true && this.enabled_ === true;
- const handle_fill = this.get_opening_color_();
- [this.left_handle_, this.right_handle_].forEach(function(handle) {
- handle.style.visibility = handles_visible === true ? 'visible' : 'hidden';
- handle.style.fill = handle_fill;
- handle.style.stroke = '#ffffff';
- });
- this.left_handle_.setAttribute('x', String(-half_width - 4));
- this.left_handle_.setAttribute('y', '-4');
- this.right_handle_.setAttribute('x', String(half_width - 4));
- this.right_handle_.setAttribute('y', '-4');
+/**
+ * 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.set_xy(this.opening_.x, this.opening_.y);
+ 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
+ };
};
/**
@@ -108,9 +244,14 @@ beestat.component.floor_plan_entity.opening.prototype.update = function() {
*/
beestat.component.floor_plan_entity.opening.prototype.after_mousedown_handler_ = function() {
this.drag_start_entity_ = {
- 'x': this.opening_.x || 0,
- 'y': this.opening_.y || 0,
- 'width': this.opening_.width || 0
+ '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)
+ }
};
};
@@ -121,53 +262,255 @@ beestat.component.floor_plan_entity.opening.prototype.after_mousedown_handler_ =
*/
beestat.component.floor_plan_entity.opening.prototype.after_mousemove_handler_ = function(e) {
const grid_half = this.floor_plan_.get_grid_pixels() / 2;
- const min_width = 12;
+ const snap_distance = 6;
- const dx = ((e.clientX || e.touches[0].clientX) - this.drag_start_mouse_.x) * this.floor_plan_.get_scale();
- const dy = ((e.clientY || e.touches[0].clientY) - this.drag_start_mouse_.y) * this.floor_plan_.get_scale();
+ 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.resize_mode_ === 'left' || this.resize_mode_ === 'right') {
- const start_width = Math.max(min_width, Number(this.drag_start_entity_.width || 0));
- const start_left = this.drag_start_entity_.x - (start_width / 2);
- const start_right = this.drag_start_entity_.x + (start_width / 2);
+ 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 next_left = start_left;
- let next_right = start_right;
+ 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;
- if (this.resize_mode_ === 'left') {
- next_left = Math.min(start_left + dx, start_right - min_width);
- next_left = Math.max(-grid_half, next_left);
- } else {
- next_right = Math.max(start_right + dx, start_left + min_width);
- next_right = Math.min(grid_half, next_right);
+ 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;
+ }
+ }
}
- const next_width = Math.max(min_width, next_right - next_left);
- const next_x = Math.max(-grid_half + (next_width / 2), Math.min(grid_half - (next_width / 2), (next_left + next_right) / 2));
+ 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.opening_.width = Math.round(next_width);
- this.opening_.x = Math.round(next_x);
- this.set_xy(this.opening_.x, this.opening_.y);
- this.update();
- return;
+ this.update_snap_lines_();
+ } else {
+ this.clear_snap_lines_();
}
- const width = Math.max(min_width, Number(this.opening_.width || 0));
- const half_width = width / 2;
- const next_x = this.drag_start_entity_.x + dx;
- const next_y = this.drag_start_entity_.y + dy;
+ let applied_dx = desired_dx;
+ let applied_dy = desired_dy;
- this.opening_.x = Math.round(Math.max(-grid_half + half_width, Math.min(grid_half - half_width, next_x)));
- this.opening_.y = Math.round(Math.max(-grid_half, Math.min(grid_half, next_y)));
- this.set_xy(this.opening_.x, this.opening_.y);
+ 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() {
- this.resize_mode_ = null;
- this.update();
+ 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 snap_x = {};
+ const snap_y = {};
+
+ const append_shapes = function(shapes, skip_self_opening) {
+ if (Array.isArray(shapes) !== true) {
+ return;
+ }
+
+ shapes.forEach(function(shape) {
+ if (shape.editor_hidden === true || Array.isArray(shape.points) !== true) {
+ return;
+ }
+ if (
+ skip_self_opening === true &&
+ self.opening_ !== undefined &&
+ shape.opening_id !== undefined &&
+ self.opening_.opening_id !== undefined &&
+ shape.opening_id === self.opening_.opening_id
+ ) {
+ return;
+ }
+ shape.points.forEach(function(point) {
+ const is_opening = shape.opening_id !== undefined;
+ const absolute_x = is_opening
+ ? Number(point.x || 0)
+ : Number(point.x || 0) + Number(shape.x || 0);
+ const absolute_y = is_opening
+ ? Number(point.y || 0)
+ : Number(point.y || 0) + Number(shape.y || 0);
+ snap_x[absolute_x] = true;
+ snap_y[absolute_y] = true;
+ });
+ });
+ };
+
+ append_shapes(this.group_.rooms, false);
+ append_shapes(this.group_.surfaces, false);
+ append_shapes(this.group_.openings, true);
+
+ const group_below = this.floor_plan_.get_group_below(this.group_);
+ if (group_below !== undefined) {
+ append_shapes(group_below.rooms, false);
+ append_shapes(group_below.surfaces, false);
+ append_shapes(group_below.openings, false);
+ }
+
+ this.snap_x_ = Object.keys(snap_x).map(function(key) {
+ return Number(key);
+ });
+ this.snap_y_ = Object.keys(snap_y).map(function(key) {
+ return Number(key);
+ });
+};
+
+/**
+ * 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_ || [];
};
/**
@@ -186,14 +529,36 @@ beestat.component.floor_plan_entity.opening.prototype.set_opening = function(ope
if (this.opening_.type === undefined) {
this.opening_.type = 'empty';
}
- if (this.opening_.width === undefined) {
- this.opening_.width = 36;
- }
if (this.opening_.height === undefined) {
- this.opening_.height = 80;
+ this.opening_.height = (this.opening_.type === 'window' || this.opening_.type === 'glass') ? 42 : 78;
+ }
+ if (this.opening_.elevation === undefined) {
+ this.opening_.elevation = (this.opening_.type === 'window' || this.opening_.type === 'glass') ? 36 : 0;
}
- this.set_xy(this.opening_.x || 0, this.opening_.y || 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;
};
@@ -231,7 +596,7 @@ beestat.component.floor_plan_entity.opening.prototype.set_enabled = function(ena
};
/**
- * Set x/y with clamping.
+ * Set center x/y for this opening by translating both points.
*
* @param {number} x
* @param {number} y
@@ -244,23 +609,28 @@ beestat.component.floor_plan_entity.opening.prototype.set_xy = function(x, y, ev
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 half_width = Math.max(12, Number(this.opening_.width || 0)) / 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));
- const clamped_x = Math.max(-grid_half + half_width, Math.min(grid_half - half_width, Number(x || 0)));
- const clamped_y = Math.max(-grid_half, Math.min(grid_half, Number(y || 0)));
+ let dx = target_x - center.x;
+ let dy = target_y - center.y;
- this.opening_.x = Math.round(clamped_x);
- this.opening_.y = Math.round(clamped_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));
- beestat.component.floor_plan_entity.prototype.set_xy.apply(
- this,
- [
- this.opening_.x,
- this.opening_.y
- ]
- );
+ 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;
};
@@ -280,18 +650,23 @@ beestat.component.floor_plan_entity.opening.prototype.set_active = function(acti
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();
}
}
@@ -308,9 +683,16 @@ beestat.component.floor_plan_entity.opening.prototype.set_active = function(acti
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');
}
@@ -322,6 +704,15 @@ beestat.component.floor_plan_entity.opening.prototype.set_active = function(acti
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.
*
@@ -330,8 +721,10 @@ beestat.component.floor_plan_entity.opening.prototype.set_active = function(acti
beestat.component.floor_plan_entity.opening.prototype.get_opening_color_ = function() {
switch (this.opening_.type) {
case 'door':
+ case 'garage':
return beestat.style.color.green.base;
case 'window':
+ case 'glass':
return beestat.style.color.lightblue.light;
case 'empty':
default:
diff --git a/js/component/floor_plan_entity/room.js b/js/component/floor_plan_entity/room.js
index d82bd61..04d1c95 100644
--- a/js/component/floor_plan_entity/room.js
+++ b/js/component/floor_plan_entity/room.js
@@ -318,6 +318,16 @@ beestat.component.floor_plan_entity.room.prototype.update_snap_points_ = functio
snap_y[point.y + room.y] = true;
});
});
+ (this.group_.openings || []).forEach(function(opening) {
+ if (opening.editor_hidden === true || Array.isArray(opening.points) !== true) {
+ return;
+ }
+ opening.points.forEach(function(point) {
+ // Opening points are stored in absolute editor coordinates.
+ snap_x[point.x] = true;
+ snap_y[point.y] = true;
+ });
+ });
// Snap to rooms in the group under this one.
const group_below = this.floor_plan_.get_group_below(this.group_);
@@ -331,6 +341,16 @@ beestat.component.floor_plan_entity.room.prototype.update_snap_points_ = functio
snap_y[point.y + room.y] = true;
});
});
+ (group_below.openings || []).forEach(function(opening) {
+ if (opening.editor_hidden === true || Array.isArray(opening.points) !== true) {
+ return;
+ }
+ opening.points.forEach(function(point) {
+ // Opening points are stored in absolute editor coordinates.
+ snap_x[point.x] = true;
+ snap_y[point.y] = true;
+ });
+ });
}
this.snap_x_ = Object.keys(snap_x).map(function(key) {
diff --git a/js/component/floor_plan_entity/surface.js b/js/component/floor_plan_entity/surface.js
index 89c22ba..80d9392 100644
--- a/js/component/floor_plan_entity/surface.js
+++ b/js/component/floor_plan_entity/surface.js
@@ -174,23 +174,28 @@ beestat.component.floor_plan_entity.surface.prototype.update_snap_points_ = func
}
shapes.forEach(function(shape) {
- if (shape.editor_hidden === true) {
+ if (shape.editor_hidden === true || Array.isArray(shape.points) !== true) {
return;
}
shape.points.forEach(function(point) {
- snap_x[point.x + shape.x] = true;
- snap_y[point.y + shape.y] = true;
+ const is_opening = shape.opening_id !== undefined;
+ const absolute_x = is_opening ? Number(point.x || 0) : Number(point.x || 0) + Number(shape.x || 0);
+ const absolute_y = is_opening ? Number(point.y || 0) : Number(point.y || 0) + Number(shape.y || 0);
+ snap_x[absolute_x] = true;
+ snap_y[absolute_y] = true;
});
});
};
append_shapes(this.group_.rooms);
append_shapes(this.group_.surfaces);
+ append_shapes(this.group_.openings);
const group_below = this.floor_plan_.get_group_below(this.group_);
if (group_below !== undefined) {
append_shapes(group_below.rooms);
append_shapes(group_below.surfaces);
+ append_shapes(group_below.openings);
}
this.snap_x_ = Object.keys(snap_x).map(function(key) {
diff --git a/js/component/floor_plan_layers_sidebar.js b/js/component/floor_plan_layers_sidebar.js
index 4ac8c61..c11353c 100644
--- a/js/component/floor_plan_layers_sidebar.js
+++ b/js/component/floor_plan_layers_sidebar.js
@@ -96,6 +96,7 @@ beestat.component.floor_plan_layers_sidebar.prototype.decorate_ = function(paren
sidebar_state.collapsed_types[group.group_id + '.trees'] = true;
sidebar_state.collapsed_types[group.group_id + '.surfaces'] = true;
sidebar_state.collapsed_types[group.group_id + '.openings'] = true;
+ sidebar_state.collapsed_types[group.group_id + '.light_sources'] = true;
sidebar_state.collapsed_types[group.group_id + '.rooms'] = true;
});
sidebar_state.initialized_collapsed = true;
@@ -114,6 +115,9 @@ beestat.component.floor_plan_layers_sidebar.prototype.decorate_ = function(paren
if (sidebar_state.collapsed_types[group.group_id + '.openings'] === undefined) {
sidebar_state.collapsed_types[group.group_id + '.openings'] = true;
}
+ if (sidebar_state.collapsed_types[group.group_id + '.light_sources'] === undefined) {
+ sidebar_state.collapsed_types[group.group_id + '.light_sources'] = true;
+ }
if (sidebar_state.collapsed_types[group.group_id + '.rooms'] === undefined) {
sidebar_state.collapsed_types[group.group_id + '.rooms'] = true;
}
@@ -127,6 +131,7 @@ beestat.component.floor_plan_layers_sidebar.prototype.decorate_ = function(paren
.concat(group.trees || [])
.concat(group.surfaces || [])
.concat(group.openings || [])
+ .concat(group.light_sources || [])
.concat(group.rooms || []);
const has_group_objects = group_objects.length > 0;
const group_all_hidden = has_group_objects === true && group_objects.every(function(object) {
@@ -220,6 +225,9 @@ beestat.component.floor_plan_layers_sidebar.prototype.decorate_ = function(paren
if ((group.openings || []).length > 0) {
self.on_toggle_layer_visibility_(group, 'openings', group_all_hidden === true);
}
+ if ((group.light_sources || []).length > 0) {
+ self.on_toggle_layer_visibility_(group, 'light_sources', group_all_hidden === true);
+ }
if ((group.rooms || []).length > 0) {
self.on_toggle_layer_visibility_(group, 'rooms', group_all_hidden === true);
}
@@ -257,6 +265,9 @@ beestat.component.floor_plan_layers_sidebar.prototype.decorate_ = function(paren
if ((group.openings || []).length > 0) {
self.on_toggle_layer_lock_(group, 'openings', !group_all_locked);
}
+ if ((group.light_sources || []).length > 0) {
+ self.on_toggle_layer_lock_(group, 'light_sources', !group_all_locked);
+ }
if ((group.rooms || []).length > 0) {
self.on_toggle_layer_lock_(group, 'rooms', !group_all_locked);
}
@@ -321,6 +332,15 @@ beestat.component.floor_plan_layers_sidebar.prototype.decorate_ = function(paren
scroll_to,
scroll_to_row
);
+ scroll_to_row = self.decorate_group_type_(
+ group_panel,
+ group,
+ 'light_sources',
+ 'Light Source',
+ font_size_small,
+ scroll_to,
+ scroll_to_row
+ );
scroll_to_row = self.decorate_group_type_(
group_panel,
group,
@@ -991,6 +1011,9 @@ beestat.component.floor_plan_layers_sidebar.prototype.get_type_icon_ = function(
if (type === 'openings') {
return 'window_closed_variant';
}
+ if (type === 'light_sources') {
+ return 'lightbulb_on';
+ }
return 'view_quilt';
};
@@ -1039,6 +1062,9 @@ beestat.component.floor_plan_layers_sidebar.prototype.get_object_id_ = function(
if (type === 'openings') {
return object.opening_id;
}
+ if (type === 'light_sources') {
+ return object.light_source_id;
+ }
return object.tree_id;
};
@@ -1113,6 +1139,13 @@ beestat.component.floor_plan_layers_sidebar.prototype.is_active_row_ = function(
) {
return true;
}
+ if (
+ type === 'light_sources' &&
+ this.state_.active_light_source_entity !== undefined &&
+ this.state_.active_light_source_entity.get_light_source().light_source_id === object_id
+ ) {
+ return true;
+ }
return false;
};
diff --git a/js/component/scene.js b/js/component/scene.js
index 4806b42..dffd810 100644
--- a/js/component/scene.js
+++ b/js/component/scene.js
@@ -212,7 +212,21 @@ beestat.component.scene.sun_light_intensity = 0.6;
*
* @type {number}
*/
-beestat.component.scene.moon_light_intensity = 0.35;
+beestat.component.scene.moon_light_intensity = 0.13125;
+
+/**
+ * Peak per-room interior light intensity used at night.
+ *
+ * @type {number}
+ */
+beestat.component.scene.interior_light_intensity = 0.9;
+
+/**
+ * Max number of interior point lights allowed to cast shadows.
+ *
+ * @type {number}
+ */
+beestat.component.scene.interior_light_shadow_max = 1;
/**
* Number of star sprites generated in the sky dome.
@@ -509,9 +523,12 @@ beestat.component.scene.prototype.decorate_ = function(parent) {
'watcher': false,
'roof_edges': false,
'straight_skeleton': false,
- 'openings': true,
- 'opening_cutters': false
+ 'openings': false,
+ 'opening_cutters': false,
+ 'hide_tree_branches': false,
+ 'light_source_orbs': false
};
+ this.room_interaction_enabled_ = true;
this.width_ = this.state_.scene_width || 800;
this.height_ = 500;
@@ -715,6 +732,20 @@ beestat.component.scene.prototype.add_raycaster_ = function() {
*/
beestat.component.scene.prototype.update_raycaster_ = function() {
if (this.raycaster_ !== undefined) {
+ if (this.room_interaction_enabled_ !== true) {
+ if (this.intersected_mesh_ !== undefined) {
+ document.body.style.cursor = '';
+ if (
+ this.intersected_mesh_.material !== undefined &&
+ this.intersected_mesh_.material.emissive !== undefined
+ ) {
+ this.intersected_mesh_.material.emissive.setHex(0x000000);
+ }
+ delete this.intersected_mesh_;
+ }
+ return;
+ }
+
this.raycaster_.setFromCamera(this.raycaster_pointer_, this.camera_);
const intersects = this.raycaster_.intersectObject(this.scene_);
@@ -736,7 +767,9 @@ beestat.component.scene.prototype.update_raycaster_ = function() {
intersects[i].object.type === 'Mesh' &&
intersects[i].object.material !== undefined &&
intersects[i].object.material.emissive !== undefined &&
+ intersects[i].object.userData.room !== undefined &&
intersects[i].object.userData.is_wall !== true &&
+ intersects[i].object.userData.is_opening !== true &&
intersects[i].object.userData.is_surface !== true &&
intersects[i].object.userData.is_roof !== true &&
intersects[i].object.userData.is_environment !== true &&
@@ -1167,7 +1200,7 @@ beestat.component.scene.prototype.add_celestial_lights_ = function() {
this.sun_visual_group_.layers.set(beestat.component.scene.layer_visible);
this.celestial_light_group_.add(this.sun_visual_group_);
- const sun_core_geometry = new THREE.SphereGeometry(180, 24, 24);
+ const sun_core_geometry = new THREE.SphereGeometry(146, 24, 24);
const sun_core_material = new THREE.MeshBasicMaterial({
'color': 0xffffff,
'transparent': true,
@@ -1189,7 +1222,7 @@ beestat.component.scene.prototype.add_celestial_lights_ = function() {
});
this.sun_glow_sprite_ = new THREE.Sprite(sun_glow_material);
this.sun_glow_sprite_.userData.is_celestial_object = true;
- this.sun_glow_sprite_.scale.set(1280, 1280, 1);
+ this.sun_glow_sprite_.scale.set(1037, 1037, 1);
this.sun_visual_group_.add(this.sun_glow_sprite_);
if (this.debug_.sun_light_helper === true) {
@@ -1244,7 +1277,7 @@ beestat.component.scene.prototype.add_celestial_lights_ = function() {
});
this.moon_sprite_ = new THREE.Sprite(moon_material);
this.moon_sprite_.userData.is_celestial_object = true;
- this.moon_sprite_.scale.set(500, 500, 1);
+ this.moon_sprite_.scale.set(405, 405, 1);
this.moon_visual_group_.add(this.moon_sprite_);
if (this.debug_.moon_light_helper === true) {
@@ -1438,6 +1471,14 @@ beestat.component.scene.prototype.update_celestial_lights_ = function(date, lati
Math.min(1, (-sun_pos.altitude - 0.05) / 0.25)
);
+ const interior_night_factor = Math.max(
+ 0,
+ Math.min(1, (-sun_pos.altitude + 0.03) / 0.3)
+ );
+ this.target_interior_light_intensity_ =
+ beestat.component.scene.interior_light_intensity * interior_night_factor;
+ this.target_light_source_intensity_multiplier_ = interior_night_factor;
+
// Moon
const moon_pos = SunCalc.getMoonPosition(js_date, latitude, longitude);
// Keep moon conversion consistent with the sun conversion.
@@ -1512,6 +1553,18 @@ beestat.component.scene.prototype.update_celestial_light_intensities_ = function
if (this.target_moon_intensity_ === undefined) {
this.target_moon_intensity_ = 0;
}
+ if (this.target_interior_light_intensity_ === undefined) {
+ const hour = this.date_ !== undefined ? Number(this.date_.format('H')) : 12;
+ this.target_interior_light_intensity_ = (
+ (hour >= 19 || hour <= 5)
+ ? beestat.component.scene.interior_light_intensity
+ : 0
+ );
+ }
+ if (this.target_light_source_intensity_multiplier_ === undefined) {
+ const hour = this.date_ !== undefined ? Number(this.date_.format('H')) : 12;
+ this.target_light_source_intensity_multiplier_ = (hour >= 19 || hour <= 5) ? 1 : 0;
+ }
// Lerp factor - lower = smoother but slower, higher = faster but jumpier
const lerp_factor = 0.05;
@@ -1522,6 +1575,19 @@ beestat.component.scene.prototype.update_celestial_light_intensities_ = function
// Lerp moon intensity
this.moon_light_.intensity += (this.target_moon_intensity_ - this.moon_light_.intensity) * lerp_factor;
+ if (this.interior_lights_ !== undefined) {
+ this.interior_lights_.forEach((light) => {
+ light.intensity += (this.target_interior_light_intensity_ - light.intensity) * lerp_factor;
+ });
+ }
+ if (Array.isArray(this.light_sources_) === true) {
+ this.light_sources_.forEach((light) => {
+ const base_intensity = Number(light.userData.base_intensity || 0);
+ const target_intensity = base_intensity * this.target_light_source_intensity_multiplier_;
+ light.intensity += (target_intensity - light.intensity) * lerp_factor;
+ });
+ }
+
// Match visible sun brightness to actual sun light intensity, with smooth
// fade at/under the horizon.
if (this.sun_core_mesh_ !== undefined && this.sun_glow_sprite_ !== undefined) {
@@ -2163,11 +2229,15 @@ beestat.component.scene.prototype.build_opening_cutter_mesh_ = function(group, o
return null;
}
- const width = Math.max(12, Number(opening.width || 0));
- const height = Math.max(1, Number(opening.height || 0));
+ const opening_line = this.get_opening_line_params_(opening);
+ const width = opening_line.width;
+ const height = Math.max(1, Number(opening.height || this.get_opening_default_height_(opening.type)));
const wall_thickness = Number(beestat.component.scene.wall_thickness || 4);
- const depth = Math.max(0.5, wall_thickness);
- const elevation = Number(group.elevation || 0);
+ // Slightly oversize cutter depth so CSG fully clears wall thickness even when
+ // a snapped opening is numerically near-coplanar with one wall face.
+ const cutter_depth_padding = 4;
+ const depth = Math.max(0.5, wall_thickness + cutter_depth_padding);
+ const center_z = this.get_opening_center_z_(group, opening, height);
if (this.csg_cutter_material_ === undefined) {
this.csg_cutter_material_ = new THREE.MeshBasicMaterial({
@@ -2178,16 +2248,105 @@ beestat.component.scene.prototype.build_opening_cutter_mesh_ = function(group, o
const geometry = new THREE.BoxGeometry(width, depth, height);
const cutter = new THREE.Mesh(geometry, this.csg_cutter_material_);
cutter.position.set(
- Number(opening.x || 0),
- Number(opening.y || 0),
- -elevation - (height / 2)
+ opening_line.center_x,
+ opening_line.center_y,
+ center_z
);
+ cutter.rotation.z = opening_line.rotation_radians;
cutter.updateMatrix();
cutter.updateMatrixWorld(true);
return cutter;
};
+/**
+ * Get default opening width by type.
+ *
+ * @param {string} type
+ *
+ * @return {number}
+ */
+beestat.component.scene.prototype.get_opening_default_width_ = function(type) {
+ return (type === 'window' || type === 'glass') ? 48 : 36;
+};
+
+/**
+ * Get default opening height by type.
+ *
+ * @param {string} type
+ *
+ * @return {number}
+ */
+beestat.component.scene.prototype.get_opening_default_height_ = function(type) {
+ return (type === 'window' || type === 'glass') ? 42 : 78;
+};
+
+/**
+ * Get default opening elevation by type.
+ *
+ * @param {string} type
+ *
+ * @return {number}
+ */
+beestat.component.scene.prototype.get_opening_default_elevation_ = function(type) {
+ return (type === 'window' || type === 'glass') ? 36 : 0;
+};
+
+/**
+ * Resolve opening line parameters from endpoint data.
+ *
+ * @param {object} opening
+ *
+ * @return {{center_x:number, center_y:number, width:number, rotation_radians:number}}
+ */
+beestat.component.scene.prototype.get_opening_line_params_ = function(opening) {
+ if (
+ opening.points !== undefined &&
+ Array.isArray(opening.points) === true &&
+ opening.points.length === 2
+ ) {
+ const p1 = opening.points[0];
+ const p2 = opening.points[1];
+ const dx = Number(p2.x || 0) - Number(p1.x || 0);
+ const dy = Number(p2.y || 0) - Number(p1.y || 0);
+ return {
+ 'center_x': (Number(p1.x || 0) + Number(p2.x || 0)) / 2,
+ 'center_y': (Number(p1.y || 0) + Number(p2.y || 0)) / 2,
+ 'width': Math.max(12, Math.sqrt((dx * dx) + (dy * dy))),
+ 'rotation_radians': Math.atan2(dy, dx)
+ };
+ }
+
+ const width = Math.max(12, Number(opening.width || this.get_opening_default_width_(opening.type)));
+ return {
+ 'center_x': Number(opening.x || 0),
+ 'center_y': Number(opening.y || 0),
+ 'width': width,
+ 'rotation_radians': (Number(opening.rotation || 0) * Math.PI) / 180
+ };
+};
+
+/**
+ * Get the opening center Z. Opening elevation is measured from the bottom of
+ * the room reference plane, matching existing floor-plan behavior.
+ *
+ * @param {object} group The floor plan group.
+ * @param {object} opening The opening.
+ * @param {number} height The opening height.
+ *
+ * @return {number}
+ */
+beestat.component.scene.prototype.get_opening_center_z_ = function(group, opening, height) {
+ const group_elevation = Number(group.elevation || 0);
+ const floor_thickness = Number(beestat.component.scene.room_floor_thickness || 0);
+ const opening_elevation = Number(
+ opening.elevation !== undefined
+ ? opening.elevation
+ : this.get_opening_default_elevation_(opening.type)
+ );
+ return -group_elevation - floor_thickness - opening_elevation - (height / 2);
+};
+
/**
* Add a debug wireframe for an opening cutter.
*
@@ -2324,9 +2483,10 @@ beestat.component.scene.prototype.add_openings_debug_ = function(layer, group) {
return;
}
- const width = Math.max(12, Number(opening.width || 0));
- const height = Math.max(1, Number(opening.height || 0));
- const elevation = group.elevation || 0;
+ const opening_line = this.get_opening_line_params_(opening);
+ const width = opening_line.width;
+ const height = Math.max(1, Number(opening.height || this.get_opening_default_height_(opening.type)));
+ const center_z = this.get_opening_center_z_(group, opening, height);
const geometry = new THREE.BoxGeometry(
width,
@@ -2342,13 +2502,359 @@ beestat.component.scene.prototype.add_openings_debug_ = function(layer, group) {
})
);
- wireframe.position.x = Number(opening.x || 0);
- wireframe.position.y = Number(opening.y || 0);
- wireframe.position.z = -elevation - (height / 2);
+ wireframe.position.x = opening_line.center_x;
+ wireframe.position.y = opening_line.center_y;
+ wireframe.position.z = center_z;
+ wireframe.rotation.z = opening_line.rotation_radians;
wireframe.layers.set(beestat.component.scene.layer_visible);
layer.add(wireframe);
- });
+ }, this);
+};
+
+/**
+ * Add 3D opening fixtures.
+ *
+ * @param {THREE.Group} layer The layer to add fixtures to.
+ * @param {object} group The floor plan group.
+ */
+beestat.component.scene.prototype.add_opening_fixtures_ = function(layer, group) {
+ if (group.openings === undefined || group.openings.length === 0) {
+ return;
+ }
+
+ const wall_thickness = Number(beestat.component.scene.wall_thickness || 4);
+ const frame_thickness = Math.max(1, wall_thickness * 0.3);
+
+ if (this.opening_frame_material_ === undefined) {
+ this.opening_frame_material_ = new THREE.MeshStandardMaterial({
+ 'color': 0xf5f7fb,
+ 'roughness': 0.92,
+ 'metalness': 0.0
+ });
+ }
+ if (this.window_pane_material_ === undefined) {
+ this.window_pane_material_ = new THREE.MeshPhysicalMaterial({
+ 'color': 0xbfe6ff,
+ 'transparent': true,
+ 'opacity': 0.95,
+ 'roughness': 0.12,
+ 'metalness': 0.0,
+ 'reflectivity': 0.35,
+ 'clearcoat': 0.3,
+ 'clearcoatRoughness': 0.12,
+ 'transmission': 0.15
+ });
+ }
+
+ group.openings.forEach(function(opening) {
+ if (opening.editor_hidden === true) {
+ return;
+ }
+ if (opening.type !== 'door' && opening.type !== 'window' && opening.type !== 'glass') {
+ return;
+ }
+
+ const opening_line = this.get_opening_line_params_(opening);
+ const width = opening_line.width;
+ const height = Math.max(1, Number(opening.height || this.get_opening_default_height_(opening.type)));
+ const center_z = this.get_opening_center_z_(group, opening, height);
+
+ const fixture_group = new THREE.Group();
+ fixture_group.position.set(
+ opening_line.center_x,
+ opening_line.center_y,
+ center_z
+ );
+ fixture_group.rotation.z = opening_line.rotation_radians;
+
+ const side_geometry = new THREE.BoxGeometry(frame_thickness, wall_thickness, height);
+ const left_side = new THREE.Mesh(side_geometry, this.opening_frame_material_);
+ left_side.position.x = -(width / 2) + (frame_thickness / 2);
+ left_side.castShadow = true;
+ left_side.receiveShadow = true;
+ left_side.userData.is_opening = true;
+ fixture_group.add(left_side);
+
+ const right_side = new THREE.Mesh(side_geometry, this.opening_frame_material_);
+ right_side.position.x = (width / 2) - (frame_thickness / 2);
+ right_side.castShadow = true;
+ right_side.receiveShadow = true;
+ right_side.userData.is_opening = true;
+ fixture_group.add(right_side);
+
+ const top_geometry = new THREE.BoxGeometry(width, wall_thickness, frame_thickness);
+ const top_frame = new THREE.Mesh(top_geometry, this.opening_frame_material_);
+ top_frame.position.z = (height / 2) - (frame_thickness / 2);
+ top_frame.castShadow = true;
+ top_frame.receiveShadow = true;
+ top_frame.userData.is_opening = true;
+ fixture_group.add(top_frame);
+
+ if (opening.type === 'window' || opening.type === 'glass') {
+ const bottom_frame = new THREE.Mesh(top_geometry, this.opening_frame_material_);
+ bottom_frame.position.z = -(height / 2) + (frame_thickness / 2);
+ bottom_frame.castShadow = true;
+ bottom_frame.receiveShadow = true;
+ bottom_frame.userData.is_opening = true;
+ fixture_group.add(bottom_frame);
+
+ // Keep pane nearly flush to the inner frame; only leave a tiny inset to
+ // avoid edge z-fighting/flicker.
+ const pane_inset = 0.05;
+ const pane_width = Math.max(6, width - (frame_thickness * 2) - pane_inset);
+ const pane_height = Math.max(6, height - (frame_thickness * 2) - pane_inset);
+ const pane_depth = Math.max(0.25, wall_thickness * 0.18);
+ const pane = new THREE.Mesh(
+ new THREE.BoxGeometry(pane_width, pane_depth, pane_height),
+ this.window_pane_material_
+ );
+ pane.castShadow = false;
+ pane.receiveShadow = false;
+ pane.userData.is_opening = true;
+ fixture_group.add(pane);
+
+ if (opening.type === 'window') {
+ const divider_thickness = Math.max(1.4, frame_thickness * 0.7);
+ const divider_overhang = Math.max(0.35, wall_thickness * 0.12);
+ const divider_depth = wall_thickness + (divider_overhang * 2);
+ const divider = new THREE.Mesh(
+ new THREE.BoxGeometry(
+ pane_width,
+ divider_depth,
+ divider_thickness
+ ),
+ this.opening_frame_material_
+ );
+ divider.position.z = 0;
+ // Keep centered so the mullion protrudes equally from front/back faces.
+ divider.position.y = 0;
+ divider.castShadow = true;
+ divider.receiveShadow = true;
+ divider.userData.is_opening = true;
+ fixture_group.add(divider);
+ }
+ } else if (opening.type === 'door') {
+ const bottom_frame = new THREE.Mesh(top_geometry, this.opening_frame_material_);
+ bottom_frame.position.z = -(height / 2) + (frame_thickness / 2);
+ bottom_frame.castShadow = true;
+ bottom_frame.receiveShadow = true;
+ bottom_frame.userData.is_opening = true;
+ fixture_group.add(bottom_frame);
+
+ const door_clearance = 0.25;
+ const door_width = Math.max(10, width - (frame_thickness * 2) - door_clearance);
+ const door_height = Math.max(12, height - (frame_thickness * 2) - door_clearance);
+ const door_depth = Math.max(0.6, wall_thickness * 0.35);
+ const raw_door_color = String(opening.color || '#7a573b').toLowerCase();
+ const door_color = ['#2d2d2d', '#3f3f3f'].includes(raw_door_color)
+ ? '#4a4a4a'
+ : raw_door_color;
+ const door_material = new THREE.MeshStandardMaterial({
+ 'color': door_color,
+ 'roughness': 0.72,
+ 'metalness': 0.0
+ });
+ const door = new THREE.Mesh(
+ new THREE.BoxGeometry(door_width, door_depth, door_height),
+ door_material
+ );
+ door.position.z = 0;
+ door.castShadow = true;
+ door.receiveShadow = true;
+ door.userData.is_opening = true;
+ fixture_group.add(door);
+
+ }
+
+ layer.add(fixture_group);
+ }, this);
+};
+
+/**
+ * Convert color temperature in Kelvin to RGB color.
+ *
+ * @param {number} temperature_k
+ *
+ * @return {THREE.Color}
+ */
+beestat.component.scene.prototype.get_light_color_from_temperature_ = function(temperature_k) {
+ const kelvin = Math.max(1000, Math.min(12000, Number(temperature_k || 4000)));
+ const temp = kelvin / 100;
+
+ let red;
+ let green;
+ let blue;
+
+ if (temp <= 66) {
+ red = 255;
+ green = 99.4708025861 * Math.log(temp) - 161.1195681661;
+ blue = temp <= 19 ? 0 : (138.5177312231 * Math.log(temp - 10) - 305.0447927307);
+ } else {
+ red = 329.698727446 * Math.pow(temp - 60, -0.1332047592);
+ green = 288.1221695283 * Math.pow(temp - 60, -0.0755148492);
+ blue = 255;
+ }
+
+ const clamp_channel = function(value) {
+ return Math.max(0, Math.min(255, Number(value || 0)));
+ };
+
+ return new THREE.Color(
+ clamp_channel(red) / 255,
+ clamp_channel(green) / 255,
+ clamp_channel(blue) / 255
+ );
+};
+
+/**
+ * Add floor-plan light sources.
+ *
+ * @param {THREE.Group} layer The layer to add light sources to.
+ * @param {object} group The floor plan group.
+ */
+beestat.component.scene.prototype.add_light_sources_ = function(layer, group) {
+ if (Array.isArray(group.light_sources) !== true || group.light_sources.length === 0) {
+ return;
+ }
+ if (Array.isArray(this.light_sources_) !== true) {
+ this.light_sources_ = [];
+ }
+
+ if (this.debug_.light_source_orbs === true) {
+ if (this.light_source_marker_geometry_ === undefined) {
+ this.light_source_marker_geometry_ = new THREE.SphereGeometry(2.2, 12, 12);
+ }
+ if (this.light_source_glow_geometry_ === undefined) {
+ this.light_source_glow_geometry_ = new THREE.SphereGeometry(6, 16, 16);
+ }
+ if (this.light_source_marker_material_ === undefined) {
+ this.light_source_marker_material_ = new THREE.MeshStandardMaterial({
+ 'roughness': 0.2,
+ 'metalness': 0.05
+ });
+ }
+ if (this.light_source_glow_material_ === undefined) {
+ this.light_source_glow_material_ = new THREE.MeshBasicMaterial({
+ 'transparent': true,
+ 'opacity': 0.28,
+ 'depthWrite': false,
+ 'blending': THREE.AdditiveBlending
+ });
+ }
+ }
+
+ const group_elevation = Number(group.elevation || 0);
+ const floor_thickness = Number(beestat.component.scene.room_floor_thickness || 0);
+
+ group.light_sources.forEach(function(light_source) {
+ const x = Number(light_source.x || 0);
+ const y = Number(light_source.y || 0);
+ const elevation = Number(light_source.elevation !== undefined ? light_source.elevation : 84);
+ const z = -group_elevation - floor_thickness - elevation;
+ let intensity_level = 2;
+ if (light_source.intensity === 'dim') {
+ intensity_level = 1;
+ } else if (light_source.intensity === 'bright') {
+ intensity_level = 3;
+ }
+ const light_intensity = 0.9 * intensity_level;
+ const light_color = this.get_light_color_from_temperature_(light_source.temperature_k);
+
+ if (this.debug_.light_source_orbs === true) {
+ const marker = new THREE.Mesh(
+ this.light_source_marker_geometry_,
+ this.light_source_marker_material_.clone()
+ );
+ marker.material.color.copy(light_color);
+ marker.material.emissive.copy(light_color);
+ marker.material.emissiveIntensity = 0.9 + (intensity_level * 0.35);
+ marker.position.set(x, y, z);
+ marker.castShadow = false;
+ marker.receiveShadow = false;
+ marker.userData.is_light_source = true;
+ layer.add(marker);
+
+ const glow = new THREE.Mesh(
+ this.light_source_glow_geometry_,
+ this.light_source_glow_material_.clone()
+ );
+ glow.material.color.copy(light_color);
+ glow.material.opacity = 0.15 + (intensity_level * 0.08);
+ glow.position.set(x, y, z);
+ glow.castShadow = false;
+ glow.receiveShadow = false;
+ glow.userData.is_light_source = true;
+ layer.add(glow);
+ }
+
+ const light = new THREE.PointLight(light_color, light_intensity, 240, 2);
+ light.userData.base_intensity = light_intensity;
+ light.intensity = light_intensity * Number(this.target_light_source_intensity_multiplier_ || 0);
+ light.position.set(x, y, z);
+ light.castShadow = false;
+ light.userData.is_light_source = true;
+ layer.add(light);
+ this.light_sources_.push(light);
+ }, this);
+};
+
+/**
+ * Add warm interior point lights, one per room. Lights are invisible and their
+ * intensity is animated based on night/day state.
+ *
+ * @param {object} floor_plan The floor plan data.
+ */
+beestat.component.scene.prototype.add_interior_lights_ = function(floor_plan) {
+ this.interior_lights_ = [];
+ this.interior_light_group_ = new THREE.Group();
+ this.floor_plan_group_.add(this.interior_light_group_);
+ this.layers_['interior_lights'] = this.interior_light_group_;
+ let shadowed_light_count = 0;
+
+ floor_plan.data.groups.forEach(function(group) {
+ group.rooms.forEach((room) => {
+ if (room.points === undefined || room.points.length < 3) {
+ return;
+ }
+
+ const geojson_polygon = [];
+ room.points.forEach(function(point) {
+ geojson_polygon.push([
+ point.x,
+ point.y
+ ]);
+ });
+ const light_point = polylabel([geojson_polygon]);
+
+ const group_elevation = Number(group.elevation || 0);
+ const room_height = Number(room.height || group.height || 96);
+ const room_elevation = Number(room.elevation !== undefined ? room.elevation : group_elevation);
+
+ const light = new THREE.PointLight(0xffd79a, 0, 170, 2);
+ light.position.set(
+ Number(room.x || 0) + light_point[0],
+ Number(room.y || 0) + light_point[1],
+ -room_elevation - (room_height * 0.45)
+ );
+ if (shadowed_light_count < beestat.component.scene.interior_light_shadow_max) {
+ light.castShadow = true;
+ light.shadow.mapSize.width = 512;
+ light.shadow.mapSize.height = 512;
+ light.shadow.bias = -0.0012;
+ light.shadow.normalBias = 0.025;
+ light.shadow.radius = 2;
+ light.shadow.camera.near = 1;
+ light.shadow.camera.far = 220;
+ shadowed_light_count++;
+ } else {
+ light.castShadow = false;
+ }
+
+ this.interior_light_group_.add(light);
+ this.interior_lights_.push(light);
+ });
+ }, this);
};
/**
@@ -2484,6 +2990,21 @@ beestat.component.scene.prototype.add_floor_plan_ = function() {
opening_cutter_debug_layer
);
+ const openings_layer = new THREE.Group();
+ this.floor_plan_group_.add(openings_layer);
+ this.layers_['openings'] = openings_layer;
+ floor_plan.data.groups.forEach(function(group) {
+ self.add_opening_fixtures_(openings_layer, group);
+ });
+
+ this.light_sources_ = [];
+ const light_sources_layer = new THREE.Group();
+ this.floor_plan_group_.add(light_sources_layer);
+ this.layers_['light_sources'] = light_sources_layer;
+ floor_plan.data.groups.forEach(function(group) {
+ self.add_light_sources_(light_sources_layer, group);
+ });
+
if (this.debug_.openings === true) {
const openings_debug_layer = new THREE.Group();
this.floor_plan_group_.add(openings_debug_layer);
@@ -4177,7 +4698,7 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
if (foliage_enabled === true) {
this.tree_branch_groups_.push(branches);
}
- branches.visible = foliage_enabled !== true;
+ branches.visible = this.debug_.hide_tree_branches !== true && foliage_enabled !== true;
tree.add(branches);
if (foliage_enabled === true) {
tree.add(foliage);
@@ -4272,7 +4793,8 @@ beestat.component.scene.prototype.update_tree_foliage_season_ = function() {
const branch_group = this.tree_branch_groups_[i];
if (branch_group !== undefined) {
// Hide branches when canopy is visible; show them when canopy is not visible.
- branch_group.visible = state.visible !== true;
+ // Debug override can force branch meshes hidden at all times.
+ branch_group.visible = this.debug_.hide_tree_branches !== true && state.visible !== true;
}
}
}
@@ -4682,6 +5204,38 @@ beestat.component.scene.prototype.set_labels = function(labels) {
return this;
};
+/**
+ * Set whether room floor meshes can be hovered/selected.
+ *
+ * @param {boolean} enabled
+ *
+ * @return {beestat.component.scene}
+ */
+beestat.component.scene.prototype.set_room_interaction_enabled = function(enabled) {
+ this.room_interaction_enabled_ = enabled !== false;
+
+ if (this.room_interaction_enabled_ !== true) {
+ if (this.intersected_mesh_ !== undefined) {
+ if (
+ this.intersected_mesh_.material !== undefined &&
+ this.intersected_mesh_.material.emissive !== undefined
+ ) {
+ this.intersected_mesh_.material.emissive.setHex(0x000000);
+ }
+ delete this.intersected_mesh_;
+ }
+ if (this.active_mesh_ !== undefined) {
+ delete this.active_mesh_;
+ }
+ document.body.style.cursor = '';
+ if (this.rendered_ === true) {
+ this.update_();
+ }
+ }
+
+ return this;
+};
+
/**
* Set the gradient.
*
@@ -4875,6 +5429,24 @@ beestat.component.scene.prototype.dispose = function() {
if (this.csg_cutter_material_ !== undefined) {
this.csg_cutter_material_.dispose();
}
+ if (this.opening_frame_material_ !== undefined) {
+ this.opening_frame_material_.dispose();
+ }
+ if (this.window_pane_material_ !== undefined) {
+ this.window_pane_material_.dispose();
+ }
+ if (this.light_source_marker_material_ !== undefined) {
+ this.light_source_marker_material_.dispose();
+ }
+ if (this.light_source_glow_material_ !== undefined) {
+ this.light_source_glow_material_.dispose();
+ }
+ if (this.light_source_marker_geometry_ !== undefined) {
+ this.light_source_marker_geometry_.dispose();
+ }
+ if (this.light_source_glow_geometry_ !== undefined) {
+ this.light_source_glow_geometry_.dispose();
+ }
// Clean up THREE.js scene resources
if (this.scene_ !== undefined) {
diff --git a/js/js.php b/js/js.php
index 859d14a..e72bbc6 100755
--- a/js/js.php
+++ b/js/js.php
@@ -175,6 +175,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
+ echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;