1
0
mirror of https://github.com/beestat/app.git synced 2026-02-26 05:00:21 -05:00
This commit is contained in:
Jon Ziebell 2026-02-19 22:02:40 -05:00
parent 42be5e4d7d
commit c46f5dbb9e
11 changed files with 2251 additions and 202 deletions

View File

@ -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"; }

View File

@ -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);
}
});
};
/**

View File

@ -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) {

View File

@ -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 =

View File

@ -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;
}
};

View File

@ -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:

View File

@ -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) {

View File

@ -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) {

View File

@ -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;
};

View File

@ -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) {

View File

@ -175,6 +175,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '<script src="/js/component/floor_plan_entity/surface.js"></script>' . PHP_EOL;
echo '<script src="/js/component/floor_plan_entity/opening.js"></script>' . PHP_EOL;
echo '<script src="/js/component/floor_plan_entity/tree.js"></script>' . PHP_EOL;
echo '<script src="/js/component/floor_plan_entity/light_source.js"></script>' . PHP_EOL;
echo '<script src="/js/component/floor_plan_entity/point.js"></script>' . PHP_EOL;
echo '<script src="/js/component/floor_plan_entity/wall.js"></script>' . PHP_EOL;
echo '<script src="/js/component/metric.js"></script>' . PHP_EOL;