1
0
mirror of https://github.com/beestat/app.git synced 2026-02-26 13:10:23 -05:00
This commit is contained in:
Jon Ziebell 2026-02-16 23:40:31 -05:00
parent ef2ba14202
commit e630b61c01
6 changed files with 391 additions and 109 deletions

View File

@ -433,8 +433,9 @@ input[type=range]::-moz-range-thumb {
.icon.battery_10:before { content: "\F007A"; }
.icon.bell:before { content: "\F009A"; }
.icon.bell_off:before { content: "\F009B"; }
.icon.border_none_variant:before { content: "\F08A4"; }
.icon.bullhorn:before { content: "\F00E6"; }
.icon.border_none_variant:before { content: "\F08A4"; }
.icon.texture_box:before { content: "\F0FE6"; }
.icon.bullhorn:before { content: "\F00E6"; }
.icon.calendar:before { content: "\F00ED"; }
.icon.calendar_alert:before { content: "\F0A31"; }
.icon.calendar_edit:before { content: "\F08A7"; }

View File

@ -176,6 +176,7 @@ beestat.component.card.floor_plan_editor.prototype.decorate_contents_ = function
*/
beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = function(parent) {
const self = this;
const has_early_access = beestat.user.has_early_access() === true;
// Dispose existing SVG to remove any global listeners.
if (this.floor_plan_ !== undefined) {
@ -195,6 +196,11 @@ beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = func
this.floor_plan_.render(parent);
if (has_early_access !== true) {
delete this.state_.active_surface_entity;
delete this.state_.active_tree_entity;
}
// Create and render the compass for setting orientation (early access only)
if (beestat.user.has_early_access() === true) {
this.compass_ = new beestat.component.compass(
@ -343,6 +349,7 @@ beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = func
let active_surface_entity;
this.state_.active_group.surfaces.forEach(function(surface) {
const surface_entity = new beestat.component.floor_plan_entity.surface(self.floor_plan_, self.state_)
.set_enabled(has_early_access)
.set_surface(surface)
.set_group(self.state_.active_group);
@ -396,6 +403,7 @@ beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = func
let active_tree_entity;
tree_group.trees.forEach(function(tree) {
const tree_entity = new beestat.component.floor_plan_entity.tree(self.floor_plan_, self.state_)
.set_enabled(has_early_access)
.set_tree(tree)
.set_group(tree_group);
@ -602,8 +610,9 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_tree_ = fu
const grid = $.createElement('div')
.style({
'display': 'grid',
'grid-template-columns': 'repeat(auto-fit, minmax(150px, 1fr))',
'column-gap': beestat.style.size.gutter
'grid-template-columns': 'repeat(4, minmax(150px, 1fr))',
'column-gap': beestat.style.size.gutter,
'width': '100%'
});
parent.appendChild(grid);
@ -694,8 +703,9 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_surface_ =
const grid = $.createElement('div')
.style({
'display': 'grid',
'grid-template-columns': 'repeat(auto-fit, minmax(150px, 1fr))',
'column-gap': beestat.style.size.gutter
'grid-template-columns': 'repeat(4, minmax(150px, 1fr))',
'column-gap': beestat.style.size.gutter,
'width': '100%'
});
parent.appendChild(grid);
@ -708,29 +718,116 @@ beestat.component.card.floor_plan_editor.prototype.decorate_info_pane_surface_ =
.set_label('Color')
.set_width('100%');
const surface_colors = [
{'label': 'Concrete', 'value': '#9e9e9e'},
{'label': 'Asphalt', 'value': '#2f2f2f'},
{'label': 'Mulch - Brown', 'value': '#6f4e37'},
{'label': 'Mulch - Black', 'value': '#1f1b1a'},
{'label': 'Gravel', 'value': '#b3aea3'},
{'label': 'Pavers', 'value': '#8c6d5a'},
{'label': 'Deck - Wood', 'value': '#8b5a2b'},
{'label': 'Grass', 'value': '#4a7c3f'}
];
const normalize_hex_color = function(value) {
if (value === undefined || value === null) {
return undefined;
}
let normalized = String(value).trim();
if (normalized === '') {
return undefined;
}
if (normalized.charAt(0) !== '#') {
normalized = '#' + normalized;
}
if (/^#[0-9a-fA-F]{6}$/.test(normalized) !== true) {
return undefined;
}
return normalized.toLowerCase();
};
const apply_surface_color = function(color) {
surface.color = color;
self.floor_plan_.update_infobox();
self.update_floor_plan_();
self.rerender();
};
const surface_colors = [
{'label': 'Pavement - Concrete', 'value': '#9a9a96'},
{'label': 'Pavement - Asphalt', 'value': '#1f2328'},
{'label': 'Pavers - Brick', 'value': '#7a2f2a'},
{'label': 'Pavers - Stone', 'value': '#8f877e'},
{'label': 'Wood - Light', 'value': '#c79a6b'},
{'label': 'Wood - Dark', 'value': '#4b2f1f'},
{'label': 'Mulch - Brown', 'value': '#6b4a2f'},
{'label': 'Mulch - Red', 'value': '#7a3f32'},
{'label': 'Mulch - Black', 'value': '#2e3136'},
{'label': 'Water - Pool', 'value': '#3e89b8'},
{'label': 'Water - Natural', 'value': '#3f6f5b'}
];
surface_colors.sort(function(a, b) {
return a.label.localeCompare(b.label, 'en', {'sensitivity': 'base'});
});
surface_colors.push({'label': 'Custom', 'value': '__custom__'});
const preset_color_map = {};
surface_colors.forEach(function(surface_color) {
if (surface_color.value !== '__custom__') {
preset_color_map[surface_color.value] = true;
}
color_input.add_option(surface_color);
});
color_input.render(div);
color_input.set_value(surface.color || '#9e9e9e');
const custom_color_container = $.createElement('div');
custom_color_container.style('display', 'none');
grid.appendChild(custom_color_container);
const custom_color_input = new beestat.component.input.text()
.set_label('Custom Hex')
.set_placeholder('#RRGGBB')
.set_width('100%')
.set_maxlength(7)
.render(custom_color_container);
const current_surface_color = normalize_hex_color(surface.color) || '#9a9a96';
const is_preset_color = preset_color_map[current_surface_color] === true;
if (is_preset_color === true) {
color_input.set_value(current_surface_color);
custom_color_input.set_value('', false);
custom_color_container.style('display', 'none');
} else {
color_input.set_value('__custom__');
custom_color_input.set_value(current_surface_color, false);
custom_color_container.style('display', 'block');
}
color_input.addEventListener('change', function() {
surface.color = color_input.get_value();
self.floor_plan_.update_infobox();
self.update_floor_plan_();
self.rerender();
const selected_value = color_input.get_value();
if (selected_value === '__custom__') {
const custom_color = normalize_hex_color(custom_color_input.get_value()) ||
normalize_hex_color(surface.color) ||
'#9a9a96';
custom_color_input.set_value(custom_color, false);
custom_color_container.style('display', 'block');
custom_color_input.input_.focus();
return;
}
custom_color_input.set_value('', false);
custom_color_container.style('display', 'none');
apply_surface_color(selected_value);
});
custom_color_input.addEventListener('change', function() {
if (color_input.get_value() !== '__custom__') {
return;
}
const custom_color = normalize_hex_color(custom_color_input.get_value());
if (custom_color === undefined) {
custom_color_input.set_value(surface.color || '#9a9a96', false);
return;
}
custom_color_input.set_value(custom_color, false);
apply_surface_color(custom_color);
});
// Elevation

View File

@ -66,14 +66,27 @@ beestat.component.floor_plan.prototype.render = function(parent) {
this.update_view_box_();
this.toolbar_container_ = $.createElement('div');
const toolbar_left = beestat.style.size.gutter + (beestat.style.size.gutter / 2);
const toolbar_column_width = 40;
const toolbar_column_gap = beestat.style.size.gutter / 2;
const toolbar_row_offset = toolbar_column_width;
this.toolbar_container_.style({
'position': 'absolute',
'top': beestat.style.size.gutter,
'left': beestat.style.size.gutter + (beestat.style.size.gutter / 2),
'left': toolbar_left,
'width': '40px'
});
parent.appendChild(this.toolbar_container_);
this.toolbar_container_secondary_ = $.createElement('div');
this.toolbar_container_secondary_.style({
'position': 'absolute',
'top': beestat.style.size.gutter + toolbar_row_offset,
'left': toolbar_left + toolbar_column_width + toolbar_column_gap,
'width': '40px'
});
parent.appendChild(this.toolbar_container_secondary_);
this.floors_container_ = $.createElement('div');
this.floors_container_.style({
'position': 'absolute',
@ -112,22 +125,27 @@ beestat.component.floor_plan.prototype.render = function(parent) {
} else if (e.key === 'Delete') {
if (self.state_.active_point_entity !== undefined) {
self.remove_point_();
} else if (self.state_.active_surface_entity !== undefined) {
self.remove_surface_();
} else if (self.state_.active_room_entity !== undefined) {
self.remove_room_();
} else if (self.state_.active_tree_entity !== undefined) {
self.remove_tree_();
} else {
self.remove_active_entity_();
}
} else if (e.key.toLowerCase() === 'r') {
if (e.ctrlKey === false) {
self.add_room_();
}
} else if (e.key.toLowerCase() === 'f') {
if (e.ctrlKey === false && beestat.user.has_early_access() === true) {
self.add_surface_();
}
} else if (e.key.toLowerCase() === 't') {
if (e.ctrlKey === false && beestat.user.has_early_access() === true) {
self.add_tree_();
}
} else if (e.key.toLowerCase() === 's') {
self.toggle_snapping_();
} else if (
e.key.toLowerCase() === 'c' &&
e.ctrlKey === true &&
beestat.user.has_early_access() === true &&
self.state_.active_surface_entity !== undefined
) {
self.state_.copied_object = {
@ -146,6 +164,7 @@ beestat.component.floor_plan.prototype.render = function(parent) {
} else if (
e.key.toLowerCase() === 'c' &&
e.ctrlKey === true &&
beestat.user.has_early_access() === true &&
self.state_.active_tree_entity !== undefined
) {
self.state_.copied_object = {
@ -155,6 +174,7 @@ beestat.component.floor_plan.prototype.render = function(parent) {
} else if (
e.key.toLowerCase() === 'v' &&
e.ctrlKey === true &&
beestat.user.has_early_access() === true &&
self.state_.copied_object !== undefined &&
self.state_.copied_object.type === 'surface'
) {
@ -162,6 +182,7 @@ beestat.component.floor_plan.prototype.render = function(parent) {
} else if (
e.key.toLowerCase() === 'v' &&
e.ctrlKey === true &&
beestat.user.has_early_access() === true &&
self.state_.copied_object !== undefined &&
self.state_.copied_object.type === 'tree'
) {
@ -450,6 +471,7 @@ beestat.component.floor_plan.prototype.dispose = function() {
beestat.component.floor_plan.prototype.update_toolbar = function() {
const self = this;
const tree_group = this.get_tree_group_();
const has_early_access = beestat.user.has_early_access() === true;
if (this.tile_group_ !== undefined) {
this.tile_group_.dispose();
@ -458,8 +480,12 @@ beestat.component.floor_plan.prototype.update_toolbar = function() {
if (this.tile_group_floors_ !== undefined) {
this.tile_group_floors_.dispose();
}
if (this.tile_group_secondary_ !== undefined) {
this.tile_group_secondary_.dispose();
}
this.tile_group_ = new beestat.component.tile_group();
this.tile_group_secondary_ = new beestat.component.tile_group();
// Add floor
this.tile_group_.add_tile(new beestat.component.tile()
@ -480,49 +506,55 @@ beestat.component.floor_plan.prototype.update_toolbar = function() {
})
);
// Add surface
this.tile_group_.add_tile(new beestat.component.tile()
.set_icon('border_none_variant')
.set_title('Add Surface')
.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_surface_();
})
);
// Remove room
const remove_room_button = new beestat.component.tile()
.set_icon('card_remove_outline')
.set_title('Remove Room [Delete]')
.set_background_color(beestat.style.color.bluegray.base);
this.tile_group_.add_tile(remove_room_button);
if (this.state_.active_room_entity !== undefined) {
remove_room_button
if (has_early_access === true) {
// Add surface
this.tile_group_.add_tile(new beestat.component.tile()
.set_icon('texture_box')
.set_title('Add Surface [F]')
.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)
.set_text_color(beestat.style.color.red.base)
.addEventListener('click', this.remove_room_.bind(this));
} else {
remove_room_button
.set_text_color(beestat.style.color.bluegray.dark);
.addEventListener('click', function() {
self.add_surface_();
})
);
// Add tree (first floor only)
const add_tree_button = new beestat.component.tile()
.set_icon('tree')
.set_title('Add Tree [T]')
.set_background_color(beestat.style.color.bluegray.base);
this.tile_group_.add_tile(add_tree_button);
if (this.state_.active_group === tree_group) {
add_tree_button
.set_background_hover_color(beestat.style.color.bluegray.light)
.set_text_color(beestat.style.color.gray.light)
.addEventListener('click', this.add_tree_.bind(this));
} else {
add_tree_button
.set_text_color(beestat.style.color.bluegray.dark);
}
}
// Remove surface
const remove_surface_button = new beestat.component.tile()
// Remove selected room, surface, or tree
const remove_button = new beestat.component.tile()
.set_icon('card_remove_outline')
.set_title('Remove Surface [Delete]')
.set_title('Remove [Delete]')
.set_background_color(beestat.style.color.bluegray.base);
this.tile_group_.add_tile(remove_surface_button);
this.tile_group_.add_tile(remove_button);
if (this.state_.active_surface_entity !== undefined) {
remove_surface_button
if (
this.state_.active_room_entity !== undefined ||
this.state_.active_surface_entity !== undefined ||
this.state_.active_tree_entity !== undefined
) {
remove_button
.set_background_hover_color(beestat.style.color.bluegray.light)
.set_text_color(beestat.style.color.red.base)
.addEventListener('click', this.remove_surface_.bind(this));
.addEventListener('click', this.remove_active_entity_.bind(this));
} else {
remove_surface_button
remove_button
.set_text_color(beestat.style.color.bluegray.dark);
}
@ -589,7 +621,7 @@ beestat.component.floor_plan.prototype.update_toolbar = function() {
.set_icon('undo')
.set_title('Undo [Ctrl+Z]')
.set_background_color(beestat.style.color.bluegray.base);
this.tile_group_.add_tile(undo_button);
this.tile_group_secondary_.add_tile(undo_button);
if (
this.can_undo_() === true
@ -610,7 +642,7 @@ beestat.component.floor_plan.prototype.update_toolbar = function() {
.set_icon('redo')
.set_title('Redo [Ctrl+Y]')
.set_background_color(beestat.style.color.bluegray.base);
this.tile_group_.add_tile(redo_button);
this.tile_group_secondary_.add_tile(redo_button);
if (
this.can_redo_() === true
@ -631,7 +663,7 @@ beestat.component.floor_plan.prototype.update_toolbar = function() {
.set_icon('magnify_plus_outline')
.set_title('Zoom In')
.set_background_color(beestat.style.color.bluegray.base);
this.tile_group_.add_tile(zoom_in_button);
this.tile_group_secondary_.add_tile(zoom_in_button);
if (
this.can_zoom_in_() === true
@ -652,7 +684,7 @@ beestat.component.floor_plan.prototype.update_toolbar = function() {
.set_icon('magnify_minus_outline')
.set_title('Zoom out')
.set_background_color(beestat.style.color.bluegray.base);
this.tile_group_.add_tile(zoom_out_button);
this.tile_group_secondary_.add_tile(zoom_out_button);
if (
this.can_zoom_out_() === true
@ -668,25 +700,9 @@ beestat.component.floor_plan.prototype.update_toolbar = function() {
.set_text_color(beestat.style.color.bluegray.dark);
}
// Add tree (first floor only)
const add_tree_button = new beestat.component.tile()
.set_icon('tree')
.set_title('Add Tree')
.set_background_color(beestat.style.color.bluegray.base);
this.tile_group_.add_tile(add_tree_button);
if (this.state_.active_group === tree_group) {
add_tree_button
.set_background_hover_color(beestat.style.color.bluegray.light)
.set_text_color(beestat.style.color.gray.light)
.addEventListener('click', this.add_tree_.bind(this));
} else {
add_tree_button
.set_text_color(beestat.style.color.bluegray.dark);
}
// Render
this.tile_group_.render(this.toolbar_container_);
this.tile_group_secondary_.render(this.toolbar_container_secondary_);
// FLOORS
this.tile_group_floors_ = new beestat.component.tile_group();
@ -810,6 +826,10 @@ beestat.component.floor_plan.prototype.toggle_snapping_ = function() {
* @param {object} surface Optional surface to copy from.
*/
beestat.component.floor_plan.prototype.add_surface_ = function(surface) {
if (beestat.user.has_early_access() !== true) {
return;
}
this.save_buffer();
if (this.state_.active_group.surfaces === undefined) {
@ -825,7 +845,7 @@ beestat.component.floor_plan.prototype.add_surface_ = function(surface) {
'surface_id': window.crypto.randomUUID(),
'x': svg_view_box.x + (svg_view_box.width / 2) - (new_surface_size / 2),
'y': svg_view_box.y + (svg_view_box.height / 2) - (new_surface_size / 2),
'color': '#9e9e9e',
'color': '#9a9a96',
'height': 0,
'points': [
{
@ -863,7 +883,7 @@ beestat.component.floor_plan.prototype.add_surface_ = function(surface) {
'surface_id': window.crypto.randomUUID(),
'x': svg_view_box.x + (svg_view_box.width / 2) - ((max_x - min_x) / 2),
'y': svg_view_box.y + (svg_view_box.height / 2) - ((max_y - min_y) / 2),
'color': surface.color || '#9e9e9e',
'color': surface.color || '#9a9a96',
'height': surface.height || 0,
'points': beestat.clone(surface.points)
};
@ -984,6 +1004,25 @@ beestat.component.floor_plan.prototype.remove_room_ = function() {
this.dispatchEvent('remove_room');
};
/**
* Remove the currently active selectable entity (surface, room, or tree).
*/
beestat.component.floor_plan.prototype.remove_active_entity_ = function() {
if (this.state_.active_surface_entity !== undefined) {
this.remove_surface_();
return;
}
if (this.state_.active_room_entity !== undefined) {
this.remove_room_();
return;
}
if (this.state_.active_tree_entity !== undefined) {
this.remove_tree_();
}
};
/**
* Remove the currently active surface.
*/
@ -1026,6 +1065,10 @@ beestat.component.floor_plan.prototype.remove_surface_ = function() {
* @param {object} tree Optional tree to copy from.
*/
beestat.component.floor_plan.prototype.add_tree_ = function(tree) {
if (beestat.user.has_early_access() !== true) {
return;
}
const tree_group = this.get_tree_group_();
if (tree_group === undefined || this.state_.active_group !== tree_group) {
return;

View File

@ -30,7 +30,7 @@ beestat.component.floor_plan_entity.surface.prototype.decorate_polygon_ = functi
} else if (this.enabled_ === true) {
this.polygon_.style.cursor = 'pointer';
this.polygon_.style.fillOpacity = '0.5';
this.polygon_.style.fill = this.surface_.color || '#9e9e9e';
this.polygon_.style.fill = this.surface_.color || '#9a9a96';
this.polygon_.style.stroke = beestat.style.color.gray.base;
} else {
this.polygon_.style.cursor = 'default';
@ -82,7 +82,7 @@ beestat.component.floor_plan_entity.surface.prototype.set_surface = function(sur
}
if (this.surface_.color === undefined) {
this.surface_.color = '#9e9e9e';
this.surface_.color = '#9a9a96';
}
if (this.surface_.height === undefined) {
this.surface_.height = 0;
@ -108,6 +108,10 @@ beestat.component.floor_plan_entity.surface.prototype.get_surface = function() {
* @return {beestat.component.floor_plan_entity.surface} This.
*/
beestat.component.floor_plan_entity.surface.prototype.set_active = function(active) {
if (active === true && this.enabled_ !== true) {
return this;
}
if (this.state_.active_point_entity !== undefined) {
this.state_.active_point_entity.set_active(false);
this.floor_plan_.update_toolbar();

View File

@ -161,6 +161,19 @@ beestat.component.floor_plan_entity.tree.prototype.set_group = function(group) {
return this;
};
/**
* Set enabled (different than active).
*
* @param {boolean} enabled
*
* @return {beestat.component.floor_plan_entity.tree} This.
*/
beestat.component.floor_plan_entity.tree.prototype.set_enabled = function(enabled) {
this.enabled_ = enabled;
return this;
};
/**
* Get the tree.
*
@ -178,6 +191,10 @@ beestat.component.floor_plan_entity.tree.prototype.get_tree = function() {
* @return {beestat.component.floor_plan_entity.tree} This.
*/
beestat.component.floor_plan_entity.tree.prototype.set_active = function(active) {
if (active === true && this.enabled_ !== true) {
return this;
}
if (active !== this.active_) {
this.active_ = active;

View File

@ -377,7 +377,7 @@ beestat.component.scene.prototype.get_snow_cover_blend_ = function() {
};
/**
* Blend roof and ground surface materials toward snow white.
* Blend roof, ground, and floor-plan surface materials toward snow white.
*
* @param {number} snow_blend
*/
@ -386,7 +386,10 @@ beestat.component.scene.prototype.update_snow_surface_colors_ = function(snow_bl
return;
}
const blend = Math.max(0, Math.min(1, snow_blend));
// Keep a small amount of base color visible at peak snow for definition.
const normalized_blend = Math.max(0, Math.min(1, snow_blend));
const blend = normalized_blend * 0.9;
const foliage_blend = normalized_blend * 0.75;
const snow_color = new THREE.Color(beestat.component.scene.snow_surface_color);
const base_roof_color = new THREE.Color(this.get_appearance_value_('roof_color'));
const base_ground_color = new THREE.Color(this.get_appearance_value_('ground_color'));
@ -411,12 +414,38 @@ beestat.component.scene.prototype.update_snow_surface_colors_ = function(snow_bl
this.layers_.environment.traverse(function(object) {
if (
object.userData !== undefined &&
object.userData.is_ground_surface === true &&
object.userData.is_ground === true &&
object.material !== undefined &&
object.material.color !== undefined
) {
object.material.color.copy(ground_color);
}
if (
object.userData !== undefined &&
object.userData.is_surface === true &&
object.material !== undefined &&
object.material.color !== undefined
) {
const base_surface_color = new THREE.Color(
object.userData.base_surface_color || object.material.color.getHex()
);
const surface_color = base_surface_color.clone().lerp(snow_color, blend);
object.material.color.copy(surface_color);
}
if (
object.userData !== undefined &&
object.userData.is_tree_foliage === true &&
object.material !== undefined &&
object.material.color !== undefined
) {
const base_foliage_color = new THREE.Color(
object.userData.base_tree_foliage_color || object.material.color.getHex()
);
const foliage_color = base_foliage_color.clone().lerp(snow_color, foliage_blend);
object.material.color.copy(foliage_color);
}
});
}
};
@ -1879,7 +1908,7 @@ beestat.component.scene.prototype.add_surface_ = function(layer, group, surface)
}
shape.closePath();
const color = surface.color || '#9e9e9e';
const color = surface.color || '#9a9a96';
const height = Math.max(0, Number(surface.height || 0));
const elevation = surface.elevation || group.elevation || 0;
const z_lift = beestat.component.scene.surface_z_lift;
@ -1917,6 +1946,7 @@ beestat.component.scene.prototype.add_surface_ = function(layer, group, surface)
mesh.castShadow = true;
mesh.userData.is_environment = true;
mesh.userData.is_surface = true;
mesh.userData.base_surface_color = color;
layer.add(mesh);
};
@ -2130,12 +2160,44 @@ beestat.component.scene.prototype.update_debug_ = function() {
}
};
/**
* Get a finite bounding box for scene layout. Empty floor plans can report
* Infinity bounds; clamp those to a reasonable fallback around origin.
*
* @return {{left:number,right:number,top:number,bottom:number,width:number,height:number,x:number,y:number}}
*/
beestat.component.scene.prototype.get_scene_bounding_box_ = function() {
const bounding_box = beestat.floor_plan.get_bounding_box(this.floor_plan_id_);
const is_finite_box =
Number.isFinite(bounding_box.left) &&
Number.isFinite(bounding_box.right) &&
Number.isFinite(bounding_box.top) &&
Number.isFinite(bounding_box.bottom);
if (is_finite_box === true) {
return bounding_box;
}
const fallback_half_size = 180;
return {
'left': -fallback_half_size,
'right': fallback_half_size,
'top': -fallback_half_size,
'bottom': fallback_half_size,
'width': fallback_half_size * 2,
'height': fallback_half_size * 2,
'x': -fallback_half_size,
'y': -fallback_half_size
};
};
/**
* Add a group containing all of the extruded geometry that can be transformed
* all together.
*/
beestat.component.scene.prototype.add_main_group_ = function() {
const bounding_box = beestat.floor_plan.get_bounding_box(this.floor_plan_id_);
const bounding_box = this.get_scene_bounding_box_();
// Main group handles orientation and centering
this.main_group_ = new THREE.Group();
@ -3317,6 +3379,8 @@ beestat.component.scene.prototype.create_pine_tree_ = function(height, max_diame
foliage_mesh.castShadow = true;
foliage_mesh.receiveShadow = true;
foliage_mesh.userData.is_environment = true;
foliage_mesh.userData.is_tree_foliage = true;
foliage_mesh.userData.base_tree_foliage_color = foliage_mesh.material.color.getHex();
tree.add(foliage_mesh);
previous_apex_height = segment_base_height + segment_height;
@ -3483,6 +3547,7 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
const tree = new THREE.Group();
tree.userData.is_environment = true;
tree.userData.is_tree = true;
const max_canopy_radius = Math.max(0.5, max_diameter / 2);
const wood_material = new THREE.MeshStandardMaterial({
'color': 0x6a4d2f,
@ -3492,7 +3557,6 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
});
const trunk_height = height * 0.75;
const size_scale = trunk_height / Math.max(1, height);
const trunk_radius_bottom = Math.max(1.5, trunk_height * 0.03);
const trunk_stick = this.create_stick_mesh_({
'height': trunk_height,
@ -3551,7 +3615,10 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
positions.needsUpdate = true;
geometry.computeVertexNormals();
return new THREE.Mesh(geometry, foliage_material.clone());
const foliage_mesh = new THREE.Mesh(geometry, foliage_material.clone());
foliage_mesh.userData.is_tree_foliage = true;
foliage_mesh.userData.base_tree_foliage_color = foliage_mesh.material.color.getHex();
return foliage_mesh;
};
const branch_height_samples = [];
const branch_tips = [];
@ -3559,6 +3626,9 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
if (has_foliage === true && this.tree_foliage_meshes_ === undefined) {
this.tree_foliage_meshes_ = [];
}
if (has_foliage === true && this.tree_branch_groups_ === undefined) {
this.tree_branch_groups_ = [];
}
for (let i = 0; i < branch_count; i++) {
const stratified = (i + 0.5) / branch_count;
@ -3601,6 +3671,22 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
};
const create_branch = function(base, direction, length, radius_bottom, depth) {
const horizontal_direction_length = Math.sqrt(
(direction.x * direction.x) + (direction.y * direction.y)
);
if (horizontal_direction_length > 0) {
const base_horizontal_radius = Math.sqrt((base.x * base.x) + (base.y * base.y));
const max_length_from_diameter =
(max_canopy_radius - base_horizontal_radius) / horizontal_direction_length;
if (Number.isFinite(max_length_from_diameter) === true) {
length = Math.max(0, Math.min(length, max_length_from_diameter));
}
}
length = Math.max(0, length);
if (length < 1) {
return null;
}
const branch_stick = self.create_stick_mesh_({
'height': length,
'radius_bottom': radius_bottom,
@ -3657,6 +3743,9 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
child_radius_bottom,
depth + 1
);
if (child_branch === null) {
continue;
}
branch_tips.push(get_stick_point_world(child_branch, 1));
add_sub_branches(child_branch, depth + 1);
}
@ -3666,7 +3755,16 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
const t = branch_height_samples[i];
const base_height = trunk_height * (0.5 + (t * 0.45));
const base_offset = this.sample_stick_curve_offset_(trunk_stick.curve, base_height);
const branch_length = Math.max(8, (max_diameter * (0.75 - (t * 0.34))) * size_scale);
// Scale branch length by both canopy diameter and total tree height so
// taller trees do not end up with disproportionately short limbs.
const height_to_diameter_ratio = height / Math.max(1, max_diameter);
const branch_height_scale = Math.max(0.75, Math.min(1.9, height_to_diameter_ratio / 1.4));
// Stronger nonlinear taper so upper branches are visibly shorter.
const vertical_taper = Math.pow(1 - t, 1.35);
const branch_length = Math.max(
4,
(max_diameter * (0.2 + (0.8 * vertical_taper))) * branch_height_scale
);
const branch_radius_bottom = Math.max(0.35, trunk_radius_bottom * (0.42 - (t * 0.26)));
const azimuth = ((i / branch_count) * Math.PI * 2) + ((Math.random() - 0.5) * 0.35);
const elevation = (Math.PI / 180) * (16 + (Math.random() * 10));
@ -3678,11 +3776,14 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
const base = new THREE.Vector3(base_offset.x, base_offset.y, -base_height);
const primary_branch = create_branch(base, direction, branch_length, branch_radius_bottom, 0);
if (primary_branch === null) {
continue;
}
branch_tips.push(get_stick_point_world(primary_branch, 1));
add_sub_branches(primary_branch, 0);
}
if (has_foliage === true) {
if (has_foliage === true) {
const core_height = trunk_height * 0.75;
const core_offset = this.sample_stick_curve_offset_(trunk_stick.curve, core_height);
const core_center = new THREE.Vector3(core_offset.x, core_offset.y, -core_height);
@ -3693,7 +3794,7 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
coverage_radius = distance;
}
}
const core_radius = Math.max(20, coverage_radius * 1.03);
const core_radius = Math.min(max_canopy_radius, Math.max(4, coverage_radius * 1.03));
const core_blob = create_foliage_blob(core_radius, 0.18);
core_blob.position.copy(core_center);
core_blob.scale.set(
@ -3711,6 +3812,10 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
this.tree_foliage_meshes_.push(core_blob);
}
if (has_foliage === true) {
this.tree_branch_groups_.push(branches);
}
branches.visible = has_foliage !== true;
tree.add(branches);
if (has_foliage === true) {
tree.add(foliage);
@ -3764,21 +3869,35 @@ beestat.component.scene.prototype.get_tree_foliage_state_ = function() {
* Apply seasonal foliage appearance to deciduous canopy meshes.
*/
beestat.component.scene.prototype.update_tree_foliage_season_ = function() {
if (this.tree_foliage_meshes_ === undefined || this.tree_foliage_meshes_.length === 0) {
const has_foliage_meshes = this.tree_foliage_meshes_ !== undefined && this.tree_foliage_meshes_.length > 0;
const has_branch_groups = this.tree_branch_groups_ !== undefined && this.tree_branch_groups_.length > 0;
if (has_foliage_meshes === false && has_branch_groups === false) {
return;
}
const state = this.get_tree_foliage_state_();
for (let i = 0; i < this.tree_foliage_meshes_.length; i++) {
const mesh = this.tree_foliage_meshes_[i];
if (mesh === undefined || mesh.material === undefined) {
continue;
if (has_foliage_meshes === true) {
for (let i = 0; i < this.tree_foliage_meshes_.length; i++) {
const mesh = this.tree_foliage_meshes_[i];
if (mesh === undefined || mesh.material === undefined) {
continue;
}
mesh.material.color.copy(state.color);
mesh.userData.base_tree_foliage_color = state.color.getHex();
mesh.material.opacity = 1;
mesh.material.transparent = false;
mesh.material.needsUpdate = true;
mesh.visible = state.visible;
}
}
if (has_branch_groups === true) {
for (let i = 0; i < this.tree_branch_groups_.length; i++) {
const branch_group = this.tree_branch_groups_[i];
if (branch_group !== undefined) {
branch_group.visible = state.visible !== true;
}
}
mesh.material.color.copy(state.color);
mesh.material.opacity = 1;
mesh.material.transparent = false;
mesh.material.needsUpdate = true;
mesh.visible = state.visible;
}
};
@ -3793,6 +3912,7 @@ beestat.component.scene.prototype.add_trees_ = function(ground_surface_z) {
tree_group.userData.is_environment = true;
this.environment_group_.add(tree_group);
this.tree_foliage_meshes_ = [];
this.tree_branch_groups_ = [];
const foliage_enabled = beestat.component.scene.environment_tree_foliage_enabled;
@ -3831,7 +3951,7 @@ beestat.component.scene.prototype.add_trees_ = function(ground_surface_z) {
*/
beestat.component.scene.prototype.add_environment_ = function() {
const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
const bounding_box = beestat.floor_plan.get_bounding_box(this.floor_plan_id_);
const bounding_box = this.get_scene_bounding_box_();
const center_x = (bounding_box.right + bounding_box.left) / 2;
const center_y = (bounding_box.bottom + bounding_box.top) / 2;
const plan_width = bounding_box.right - bounding_box.left;
@ -3886,7 +4006,7 @@ beestat.component.scene.prototype.add_environment_ = function() {
mesh.position.z = current_z + stratum.thickness / 2;
mesh.userData.is_environment = true;
if (index === 0) {
mesh.userData.is_ground_surface = true;
mesh.userData.is_ground = true;
}
mesh.receiveShadow = true;