1
0
mirror of https://github.com/beestat/app.git synced 2026-04-12 20:22:14 -04:00
This commit is contained in:
Jon Ziebell 2026-02-21 19:14:53 -05:00
parent 80dce469c7
commit 855c1ec794
6 changed files with 715 additions and 77 deletions

View File

@ -40,6 +40,9 @@ beestat.component.card.three_d = function() {
change_function
);
this.scene_settings_menu_open_ = false;
this.scene_settings_values_ = undefined;
beestat.component.card.apply(this, arguments);
};
beestat.extend(beestat.component.card.three_d, beestat.component.card);
@ -117,6 +120,7 @@ beestat.component.card.three_d.prototype.decorate_contents_ = function(parent) {
});
parent.appendChild(fps_container);
this.decorate_fps_ticker_(fps_container);
this.update_fps_visibility_();
// Toolbar
const toolbar_container = document.createElement('div');
@ -129,6 +133,19 @@ beestat.component.card.three_d.prototype.decorate_contents_ = function(parent) {
parent.appendChild(toolbar_container);
this.decorate_toolbar_(toolbar_container);
// Scene settings panel
const scene_settings_container = document.createElement('div');
Object.assign(scene_settings_container.style, {
'position': 'absolute',
'top': `${beestat.style.size.gutter + 72}px`,
'right': `${beestat.style.size.gutter}px`,
'min-width': '220px',
'max-width': '250px',
'z-index': 2
});
parent.appendChild(scene_settings_container);
this.decorate_scene_settings_panel_(scene_settings_container);
// Environment date slider (shown only in environment view)
const environment_date_container = document.createElement('div');
Object.assign(environment_date_container.style, {
@ -374,11 +391,14 @@ beestat.component.card.three_d.prototype.decorate_drawing_pane_ = function(paren
if (this.scene_ !== undefined) {
this.scene_.dispose();
}
this.ensure_scene_settings_values_();
this.scene_ = new beestat.component.scene(
beestat.setting('visualize.floor_plan_id'),
this.get_data_()
);
this.apply_weather_setting_to_scene_();
this.scene_.set_scene_settings(this.scene_settings_values_, {
'rerender': false
});
this.scene_.addEventListener('change_active_room', function() {
self.update_hud_();
@ -515,23 +535,39 @@ beestat.component.card.three_d.prototype.get_weather_mode_ = function() {
};
/**
* Map UI weather mode to scene weather effect.
* Map weather mode to weather property values.
*
* @param {string} weather_mode
*
* @return {string} none|cloudy|rain|snow
* @return {{cloud_density: number, rain_density: number, snow_density: number}}
*/
beestat.component.card.three_d.prototype.get_weather_from_mode_ = function(weather_mode) {
beestat.component.card.three_d.prototype.get_weather_settings_from_mode_ = function(weather_mode) {
switch (weather_mode) {
case 'cloudy':
return 'cloudy';
return {
'cloud_density': 1,
'rain_density': 0,
'snow_density': 0
};
case 'raining':
return 'rain';
return {
'cloud_density': 1,
'rain_density': 1,
'snow_density': 0
};
case 'snowing':
return 'snow';
return {
'cloud_density': 1,
'rain_density': 0,
'snow_density': 1
};
case 'sunny':
default:
return 'none';
return {
'cloud_density': 0,
'rain_density': 0,
'snow_density': 0
};
}
};
@ -543,8 +579,220 @@ beestat.component.card.three_d.prototype.apply_weather_setting_to_scene_ = funct
return;
}
const weather = this.get_weather_from_mode_(this.get_weather_mode_());
this.scene_.set_weather(weather);
this.ensure_scene_settings_values_();
const weather_settings = this.get_weather_settings_from_mode_(this.get_weather_mode_());
Object.assign(this.scene_settings_values_, weather_settings);
this.scene_.set_scene_settings(weather_settings, {
'rerender': false
});
if (this.scene_settings_container_ !== undefined) {
this.decorate_scene_settings_panel_();
}
};
/**
* Get whether or not this user can access scene settings controls.
*
* @return {boolean}
*/
beestat.component.card.three_d.prototype.can_access_scene_settings_ = function() {
return (
beestat.user.get() !== undefined &&
Number(beestat.user.get().user_id) === 1
);
};
/**
* Ensure local scene settings state exists.
*/
beestat.component.card.three_d.prototype.ensure_scene_settings_values_ = function() {
if (this.scene_settings_values_ !== undefined) {
return;
}
this.scene_settings_values_ = Object.assign({}, beestat.component.scene.default_settings);
if (
this.scene_settings_values_.tree_branch_depth === undefined &&
this.scene_settings_values_.tree_branch_recursion_depth !== undefined
) {
this.scene_settings_values_.tree_branch_depth = this.scene_settings_values_.tree_branch_recursion_depth;
}
if (
Number.isFinite(Number(this.scene_settings_values_.random_seed)) !== true ||
Number(this.scene_settings_values_.random_seed) <= 0
) {
this.scene_settings_values_.random_seed = Math.floor(Math.random() * 2147483646) + 1;
}
Object.assign(
this.scene_settings_values_,
this.get_weather_settings_from_mode_(this.get_weather_mode_())
);
};
/**
* Set one scene setting from the settings panel and force rerender.
*
* @param {string} key
* @param {*} value
*/
beestat.component.card.three_d.prototype.set_scene_setting_from_panel_ = function(key, value) {
this.ensure_scene_settings_values_();
this.scene_settings_values_[key] = value;
if (this.scene_ !== undefined) {
this.scene_.set_scene_settings({
[key]: value
}, {
'rerender': true,
'source': 'panel'
});
}
};
/**
* Decorate scene settings panel.
*
* @param {HTMLDivElement=} parent
*/
beestat.component.card.three_d.prototype.decorate_scene_settings_panel_ = function(parent) {
if (parent !== undefined) {
this.scene_settings_container_ = parent;
}
if (this.scene_settings_container_ === undefined) {
return;
}
this.scene_settings_container_.innerHTML = '';
if (this.can_access_scene_settings_() !== true || this.scene_settings_menu_open_ !== true) {
this.scene_settings_container_.style.display = 'none';
this.update_fps_visibility_();
return;
}
this.scene_settings_container_.style.display = 'block';
this.ensure_scene_settings_values_();
const panel = document.createElement('div');
Object.assign(panel.style, {
'background': 'rgba(32, 42, 48, 0.94)',
'border': '1px solid rgba(255,255,255,0.16)',
'border-radius': '8px',
'padding': '10px',
'color': '#fff',
'font-size': beestat.style.font_size.small,
'display': 'flex',
'flex-direction': 'column',
'grid-gap': '8px'
});
this.scene_settings_container_.appendChild(panel);
const get_title_case_label = (key) => {
return key
.split('_')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
};
const add_boolean_setting = (label, key) => {
const row = document.createElement('label');
Object.assign(row.style, {
'display': 'flex',
'justify-content': 'space-between',
'align-items': 'center',
'grid-gap': '10px'
});
const text = document.createElement('span');
text.innerText = label;
row.appendChild(text);
const input = document.createElement('input');
input.type = 'checkbox';
input.checked = this.scene_settings_values_[key] === true;
Object.assign(input.style, {
'visibility': 'visible',
'appearance': 'auto',
'-webkit-appearance': 'checkbox',
'accent-color': beestat.style.color.lightblue.base,
'width': '16px',
'height': '16px',
'margin': '0'
});
input.addEventListener('change', () => {
this.set_scene_setting_from_panel_(key, input.checked === true);
});
row.appendChild(input);
panel.appendChild(row);
};
const add_number_setting = (label, key, min, max, step) => {
const row = document.createElement('label');
Object.assign(row.style, {
'display': 'flex',
'justify-content': 'space-between',
'align-items': 'center',
'grid-gap': '10px'
});
const text = document.createElement('span');
text.innerText = label;
row.appendChild(text);
const input = document.createElement('input');
input.type = 'number';
input.value = String(this.scene_settings_values_[key]);
input.min = String(min);
input.max = String(max);
input.step = String(step);
Object.assign(input.style, {
'width': '62px',
'background': '#1a242a',
'color': '#fff',
'border': '1px solid rgba(255,255,255,0.2)',
'border-radius': '4px',
'padding': '2px 4px'
});
input.addEventListener('change', () => {
const parsed = Number(input.value);
if (Number.isFinite(parsed) !== true) {
input.value = String(this.scene_settings_values_[key]);
return;
}
const clamped = Math.max(min, Math.min(max, parsed));
const normalized = step >= 1 ? Math.round(clamped) : clamped;
input.value = String(normalized);
this.set_scene_setting_from_panel_(key, normalized);
});
row.appendChild(input);
panel.appendChild(row);
};
const add_separator = () => {
const separator = document.createElement('div');
Object.assign(separator.style, {
'height': '1px',
'background': 'rgba(255,255,255,0.16)',
'margin': '2px 0'
});
panel.appendChild(separator);
};
// Weather
add_number_setting(get_title_case_label('cloud_density'), 'cloud_density', 0, 2, 0.1);
add_number_setting(get_title_case_label('rain_density'), 'rain_density', 0, 2, 0.1);
add_number_setting(get_title_case_label('snow_density'), 'snow_density', 0, 2, 0.1);
add_separator();
// Tree
add_boolean_setting(get_title_case_label('tree_enabled'), 'tree_enabled');
add_number_setting(get_title_case_label('tree_branch_depth'), 'tree_branch_depth', 0, 4, 1);
add_separator();
// Light / Sky
add_number_setting(get_title_case_label('star_density'), 'star_density', 0, 2, 0.1);
add_boolean_setting(get_title_case_label('light_user_enabled'), 'light_user_enabled');
this.update_fps_visibility_();
};
/**
@ -1112,6 +1360,21 @@ beestat.component.card.three_d.prototype.decorate_fps_ticker_ = function(parent)
this.fps_interval_ = window.setInterval(set_text, 250);
};
/**
* Show FPS only while scene settings are open.
*/
beestat.component.card.three_d.prototype.update_fps_visibility_ = function() {
if (this.fps_container_ === undefined) {
return;
}
const show = (
this.can_access_scene_settings_() === true &&
this.scene_settings_menu_open_ === true
);
this.fps_container_.style.display = show ? 'block' : 'none';
};
/**
* Toolbar.
*
@ -1200,6 +1463,23 @@ beestat.component.card.three_d.prototype.decorate_toolbar_ = function(parent) {
);
}
if (this.can_access_scene_settings_() === true) {
tile_group.add_tile(new beestat.component.tile()
.set_icon('tune')
.set_title('Scene Settings')
.set_text_color(beestat.style.color.gray.light)
.set_background_color(this.scene_settings_menu_open_ === true ? beestat.style.color.lightblue.base : beestat.style.color.bluegray.base)
.set_background_hover_color(this.scene_settings_menu_open_ === true ? beestat.style.color.lightblue.light : beestat.style.color.bluegray.light)
.addEventListener('click', function(e) {
e.stopPropagation();
self.scene_settings_menu_open_ = self.scene_settings_menu_open_ !== true;
self.decorate_toolbar_();
self.decorate_scene_settings_panel_();
self.update_fps_visibility_();
})
);
}
// Labels (hidden while environment view is on)
if (show_environment === false) {
tile_group.add_tile(new beestat.component.tile()

View File

@ -263,15 +263,150 @@ beestat.component.scene.sidereal_day_seconds = 86164.0905;
*/
beestat.component.scene.star_drift_visual_factor = 0.12;
/**
* Runtime scene settings exposed through the scene settings panel.
*
* @type {{
* cloud_density: number,
* rain_density: number,
* snow_density: number,
* tree_enabled: boolean,
* tree_branch_depth: number,
* star_density: number,
* light_user_enabled: boolean,
* random_seed: number
* }}
*/
beestat.component.scene.default_settings = {
'cloud_density': 1,
'rain_density': 1,
'snow_density': 1,
'tree_enabled': true,
'tree_branch_depth': 1,
'star_density': 1,
'light_user_enabled': true,
'random_seed': 1
};
/**
* Normalization area used to convert weather density to particle counts.
*
* @type {number}
*/
beestat.component.scene.weather_density_unit_area = 2500000;
/**
* Build deterministic PRNG from a numeric seed.
*
* @param {number} seed
*
* @return {function(): number}
*/
beestat.component.scene.prototype.create_seeded_random_generator_ = function(seed) {
let state = (seed >>> 0);
if (state === 0) {
state = 0x6d2b79f5;
}
return function() {
state += 0x6d2b79f5;
let t = state;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
};
/**
* Reset random generator for this scene from current seed setting.
*/
beestat.component.scene.prototype.reset_random_generator_ = function() {
const raw_seed = Number(this.get_scene_setting_('random_seed'));
const normalized_seed = Number.isFinite(raw_seed)
? Math.max(1, Math.round(raw_seed))
: 1;
this.random_seed_ = normalized_seed;
this.random_generator_ = this.create_seeded_random_generator_(normalized_seed);
};
/**
* Get deterministic random number in [0, 1).
*
* @return {number}
*/
beestat.component.scene.prototype.random_ = function() {
if (typeof this.random_generator_ !== 'function') {
this.reset_random_generator_();
}
return this.random_generator_();
};
/**
* Run scene-generation logic using deterministic Math.random.
*
* @param {function()} callback
*/
beestat.component.scene.prototype.with_seeded_random_ = function(callback) {
const original_random = Math.random;
this.reset_random_generator_();
Math.random = this.random_.bind(this);
try {
callback();
} finally {
Math.random = original_random;
}
};
/**
* Build deterministic unsigned seed from string parts.
*
* @param {Array<*>} parts
*
* @return {number}
*/
beestat.component.scene.prototype.get_seed_from_parts_ = function(parts) {
const input = parts.map((part) => String(part)).join('|');
let hash = 2166136261;
for (let i = 0; i < input.length; i++) {
hash ^= input.charCodeAt(i);
hash = Math.imul(hash, 16777619);
}
hash >>>= 0;
return hash === 0 ? 1 : hash;
};
/**
* Run callback with a temporary deterministic random source.
*
* @param {number} seed
* @param {function()} callback
*
* @return {*}
*/
beestat.component.scene.prototype.with_random_seed_ = function(seed, callback) {
const normalized_seed = Math.max(1, Number(seed || 1) >>> 0);
const original_random = Math.random;
const local_random = this.create_seeded_random_generator_(normalized_seed);
Math.random = local_random;
try {
return callback();
} finally {
Math.random = original_random;
}
};
/**
* Rerender the scene by removing the primary group, then re-adding it and the
* floor plan. This avoids having to reconstruct everything and then also
* having to manually save camera info etc.
*/
beestat.component.scene.prototype.rerender = function() {
this.reset_celestial_lights_for_rerender_();
this.scene_.remove(this.main_group_);
this.add_main_group_();
this.add_floor_plan_();
this.with_seeded_random_(function() {
this.add_main_group_();
this.add_floor_plan_();
}.bind(this));
this.apply_appearance_rotation_to_lights_();
// Ensure everything gets updated with the latest info.
@ -280,6 +415,35 @@ beestat.component.scene.prototype.rerender = function() {
}
};
/**
* Reset celestial objects so rerender can rebuild stars/lights from settings.
*/
beestat.component.scene.prototype.reset_celestial_lights_for_rerender_ = function() {
if (this.sun_light_ !== undefined && this.sun_light_.target !== undefined && this.sun_light_.target.parent !== null) {
this.sun_light_.target.parent.remove(this.sun_light_.target);
}
if (this.moon_light_ !== undefined && this.moon_light_.target !== undefined && this.moon_light_.target.parent !== null) {
this.moon_light_.target.parent.remove(this.moon_light_.target);
}
if (this.celestial_light_group_ !== undefined && this.celestial_light_group_.parent !== null) {
this.celestial_light_group_.parent.remove(this.celestial_light_group_);
}
delete this.celestial_light_group_;
delete this.sun_light_;
delete this.moon_light_;
delete this.sun_light_helper_;
delete this.moon_light_helper_;
delete this.sun_path_line_;
delete this.sun_visual_group_;
delete this.sun_core_mesh_;
delete this.sun_glow_sprite_;
delete this.moon_visual_group_;
delete this.moon_sprite_;
delete this.star_group_;
delete this.stars_;
};
/**
* Get an appearance value with fallback to default if not set.
*
@ -295,6 +459,65 @@ beestat.component.scene.prototype.get_appearance_value_ = function(key) {
return beestat.component.scene.default_appearance[key];
};
/**
* Get a scene setting value with default fallback.
*
* @param {string} key
*
* @return {*}
*/
beestat.component.scene.prototype.get_scene_setting_ = function(key) {
if (this.scene_settings_ !== undefined && this.scene_settings_[key] !== undefined) {
return this.scene_settings_[key];
}
return beestat.component.scene.default_settings[key];
};
/**
* Get all currently effective scene settings.
*
* @return {object}
*/
beestat.component.scene.prototype.get_scene_settings = function() {
const current_settings = Object.assign({}, beestat.component.scene.default_settings);
if (this.scene_settings_ !== undefined) {
Object.assign(current_settings, this.scene_settings_);
}
return current_settings;
};
/**
* Update scene settings.
*
* @param {object} scene_settings
* @param {object=} options
*
* @return {beestat.component.scene}
*/
beestat.component.scene.prototype.set_scene_settings = function(scene_settings, options) {
if (scene_settings === undefined || scene_settings === null) {
return this;
}
if (this.scene_settings_ === undefined) {
this.scene_settings_ = {};
}
Object.assign(this.scene_settings_, scene_settings);
const rerender = options !== undefined && options.rerender === true;
if (this.rendered_ === true) {
if (rerender === true) {
this.rerender();
} else {
this.update_weather_targets_();
this.update_tree_foliage_season_();
this.update_weather_();
}
}
return this;
};
/**
* Set the width of this component.
*
@ -325,6 +548,10 @@ beestat.component.scene.prototype.decorate_ = function(parent) {
// Dark background to help reduce apparant flicker when resizing
parent.style('background', '#202a30');
if (this.scene_settings_ === undefined) {
this.scene_settings_ = {};
}
this.debug_ = {
'axes': false,
'directional_light_helpers': false,
@ -351,8 +578,10 @@ beestat.component.scene.prototype.decorate_ = function(parent) {
this.add_skybox_(parent);
this.add_static_lights_();
this.add_main_group_();
this.add_floor_plan_();
this.with_seeded_random_(function() {
this.add_main_group_();
this.add_floor_plan_();
}.bind(this));
this.fps_ = 0;
this.fps_frame_count_ = 0;

View File

@ -56,6 +56,8 @@ beestat.component.scene.prototype.update_tree_foliage_season_ = function() {
}
const state = this.get_tree_foliage_state_();
const tree_foliage_enabled = state.visible === true;
const tree_branch_enabled = state.visible !== true;
if (has_foliage_meshes === true) {
for (let i = 0; i < this.tree_foliage_meshes_.length; i++) {
const mesh = this.tree_foliage_meshes_[i];
@ -68,7 +70,7 @@ beestat.component.scene.prototype.update_tree_foliage_season_ = function() {
mesh.material.transparent = beestat.component.scene.debug_tree_canopy_opacity < 1;
mesh.material.depthWrite = beestat.component.scene.debug_tree_canopy_opacity >= 1;
mesh.material.needsUpdate = true;
mesh.visible = state.visible;
mesh.visible = tree_foliage_enabled === true;
}
}
@ -78,7 +80,9 @@ beestat.component.scene.prototype.update_tree_foliage_season_ = function() {
if (branch_group !== undefined) {
// Hide branches when canopy is visible; show them when canopy is not visible.
// Debug override can force branch meshes hidden at all times.
branch_group.visible = this.debug_.hide_tree_branches !== true && state.visible !== true;
branch_group.visible =
this.debug_.hide_tree_branches !== true &&
tree_branch_enabled === true;
}
}
}
@ -91,6 +95,10 @@ beestat.component.scene.prototype.update_tree_foliage_season_ = function() {
* @param {number} ground_surface_z
*/
beestat.component.scene.prototype.add_trees_ = function(ground_surface_z) {
if (this.get_scene_setting_('tree_enabled') !== true) {
return;
}
const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
const tree_group = new THREE.Group();
tree_group.userData.is_environment = true;
@ -98,8 +106,6 @@ beestat.component.scene.prototype.add_trees_ = function(ground_surface_z) {
this.tree_foliage_meshes_ = [];
this.tree_branch_groups_ = [];
const foliage_enabled = beestat.component.scene.environment_tree_foliage_enabled;
const trees = [];
floor_plan.data.groups.forEach(function(group) {
if (Array.isArray(group.trees) === true) {
@ -109,7 +115,7 @@ beestat.component.scene.prototype.add_trees_ = function(ground_surface_z) {
}
});
trees.forEach(function(tree_data) {
trees.forEach(function(tree_data, tree_index) {
const tree_type = ['conical', 'round', 'oval'].includes(tree_data.type)
? tree_data.type
: 'round';
@ -118,14 +124,27 @@ beestat.component.scene.prototype.add_trees_ = function(ground_surface_z) {
const tree_x = Number(tree_data.x || 0);
const tree_y = Number(tree_data.y || 0);
let tree;
if (tree_type === 'conical') {
tree = this.create_conical_tree_(tree_height, tree_diameter, foliage_enabled);
} else if (tree_type === 'oval') {
tree = this.create_oval_tree_(tree_height, tree_diameter, foliage_enabled);
} else {
tree = this.create_round_tree_(tree_height, tree_diameter, foliage_enabled);
}
const tree_seed = this.get_seed_from_parts_([
this.get_scene_setting_('random_seed'),
'tree',
tree_index,
tree_type,
tree_x,
tree_y,
tree_height,
tree_diameter
]);
const tree = this.with_random_seed_(tree_seed, function() {
this.active_tree_seed_ = tree_seed;
if (tree_type === 'conical') {
return this.create_conical_tree_(tree_height, tree_diameter, true);
} else if (tree_type === 'oval') {
return this.create_oval_tree_(tree_height, tree_diameter, true);
}
return this.create_round_tree_(tree_height, tree_diameter, true);
}.bind(this));
tree.position.set(tree_x, tree_y, ground_surface_z);
tree.rotation.z = 0;

View File

@ -259,7 +259,9 @@ beestat.component.scene.prototype.add_stars_ = function() {
this.stars_ = [];
const radius = 4200;
for (let i = 0; i < beestat.component.scene.star_count; i++) {
const star_density = Math.max(0, Number(this.get_scene_setting_('star_density') || 0));
const star_count = Math.max(0, Math.round(1000 * star_density));
for (let i = 0; i < star_count; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos((Math.random() * 2) - 1);
@ -667,6 +669,10 @@ beestat.component.scene.prototype.get_light_color_from_temperature_ = function(t
* @param {object} group The floor plan group.
*/
beestat.component.scene.prototype.add_light_sources_ = function(layer, group) {
if (this.get_scene_setting_('light_user_enabled') !== true) {
return;
}
if (Array.isArray(group.light_sources) !== true || group.light_sources.length === 0) {
return;
}

View File

@ -319,7 +319,7 @@ beestat.component.scene.prototype.create_stick_mesh_ = function(config) {
: (radius_bottom * resolved_top_ratio)
);
const radial_segments = Math.max(3, config.radial_segments || 7);
const height_segments = Math.max(1, config.height_segments || 6);
const segments = Math.max(1, Math.round(height / 12));
const control_count = Math.max(2, config.control_count || 5);
const max_drift = Math.max(0, config.max_drift || 0);
const direction_jitter = config.direction_jitter || (radius_bottom * 0.15);
@ -353,7 +353,7 @@ beestat.component.scene.prototype.create_stick_mesh_ = function(config) {
radius_bottom,
height,
radial_segments,
height_segments
segments
);
geometry.rotateX(-Math.PI / 2);
@ -492,7 +492,6 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
'height': trunk_height,
'radius_bottom': trunk_radius_bottom,
'radial_segments': 7,
'height_segments': 8,
'control_count': 6,
'max_drift': 8,
'direction_jitter': 3,
@ -627,7 +626,10 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
};
};
const branch_height_samples = [];
const recursive_depth_limit = 1;
const recursive_depth_limit = Math.max(
0,
Math.round(Number(this.get_scene_setting_('tree_branch_depth') || 0))
);
const children_per_branch = 2;
if (foliage_enabled === true && this.tree_foliage_meshes_ === undefined) {
this.tree_foliage_meshes_ = [];
@ -696,7 +698,6 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
'height': length,
'radius_bottom': radius_bottom,
'radial_segments': 7,
'height_segments': 6,
'control_count': 6,
'max_drift': length * 0.24,
'direction_jitter': length * 0.12,
@ -787,7 +788,13 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
}
if (foliage_enabled === true) {
const canopy_result = create_canopy_from_branch_function_();
const canopy_seed = this.get_seed_from_parts_([
this.active_tree_seed_ === undefined ? this.get_scene_setting_('random_seed') : this.active_tree_seed_,
'canopy'
]);
const canopy_result = this.with_random_seed_(canopy_seed, function() {
return create_canopy_from_branch_function_();
});
const canopy_mesh = canopy_result.mesh;
canopy_mesh.castShadow = true;
canopy_mesh.receiveShadow = true;
@ -799,7 +806,8 @@ beestat.component.scene.prototype.create_round_tree_ = function(height, max_diam
if (foliage_enabled === true) {
this.tree_branch_groups_.push(branches);
}
branches.visible = this.debug_.hide_tree_branches !== true && foliage_enabled !== true;
branches.visible =
this.debug_.hide_tree_branches !== true;
tree.add(branches);
if (foliage_enabled === true) {
tree.add(foliage);

View File

@ -16,7 +16,44 @@ beestat.component.scene.prototype.set_weather = function(weather) {
floor_plan.data.appearance = {};
}
floor_plan.data.appearance.weather = weather;
this.update_weather_targets_();
// Backward-compatible weather mode support by translating to density values.
let weather_settings;
switch (weather) {
case 'snow':
weather_settings = {
'cloud_density': 1,
'rain_density': 0,
'snow_density': 1
};
break;
case 'rain':
weather_settings = {
'cloud_density': 1,
'rain_density': 1,
'snow_density': 0
};
break;
case 'cloudy':
weather_settings = {
'cloud_density': 1,
'rain_density': 0,
'snow_density': 0
};
break;
case 'sunny':
case 'none':
default:
weather_settings = {
'cloud_density': 0,
'rain_density': 0,
'snow_density': 0
};
break;
}
this.set_scene_settings(weather_settings, {
'rerender': false
});
if (this.rendered_ === true) {
this.update_();
@ -27,41 +64,74 @@ beestat.component.scene.prototype.set_weather = function(weather) {
/**
* Get weather transition profile for visuals.
* Get design count at density 1 for a weather channel.
*
* @param {string} weather
* @param {string} density_key
*
* @return {number}
*/
beestat.component.scene.prototype.get_weather_design_count_ = function(density_key) {
switch (density_key) {
case 'cloud_density':
return Math.max(1, Number(beestat.component.scene.weather_cloud_max_count || 1));
case 'rain_density':
return Math.max(1, Number(beestat.component.scene.weather_rain_max_count || 1));
case 'snow_density':
return Math.max(1, Number(beestat.component.scene.weather_snow_max_count || 1));
default:
return 1;
}
};
/**
* Get design capacity count (density 1) for a weather channel and area.
*
* @param {string} density_key
* @param {number=} opt_area
*
* @return {number}
*/
beestat.component.scene.prototype.get_weather_design_capacity_count_ = function(density_key, opt_area) {
const design_count = this.get_weather_design_count_(density_key);
const area = Math.max(
1,
Number(opt_area || this.weather_area_ || beestat.component.scene.weather_density_unit_area)
);
const unit_area = Math.max(1, Number(beestat.component.scene.weather_density_unit_area || 1));
return Math.max(0, Math.round(design_count * (area / unit_area)));
};
/**
* Convert density setting to particle count using scene area.
*
* @param {string} density_key
* @param {number=} opt_area
*
* @return {number}
*/
beestat.component.scene.prototype.get_weather_count_from_density_ = function(density_key, opt_area) {
const density = Math.max(0, Number(this.get_scene_setting_(density_key) || 0));
const design_count = this.get_weather_design_count_(density_key);
const area = Math.max(
1,
Number(opt_area || this.weather_area_ || beestat.component.scene.weather_density_unit_area)
);
const unit_area = Math.max(1, Number(beestat.component.scene.weather_density_unit_area || 1));
return Math.max(0, Math.round(design_count * density * (area / unit_area)));
};
/**
* Get weather transition profile for visuals.
*
* @return {object}
*/
beestat.component.scene.prototype.get_weather_profile_ = function(weather) {
switch (weather) {
case 'snow':
return {
'cloud_count': beestat.component.scene.weather_cloud_max_count,
'rain_count': 0,
'snow_count': beestat.component.scene.weather_snow_max_count
};
case 'rain':
return {
'cloud_count': Math.round(beestat.component.scene.weather_cloud_max_count * 0.92),
'rain_count': beestat.component.scene.weather_rain_max_count,
'snow_count': 0
};
case 'cloudy':
return {
'cloud_count': Math.round(beestat.component.scene.weather_cloud_max_count * 0.72),
'rain_count': 0,
'snow_count': 0
};
case 'sunny':
case 'none':
default:
return {
'cloud_count': 0,
'rain_count': 0,
'snow_count': 0
};
}
beestat.component.scene.prototype.get_weather_profile_ = function() {
return {
'cloud_count': this.get_weather_count_from_density_('cloud_density'),
'rain_count': this.get_weather_count_from_density_('rain_density'),
'snow_count': this.get_weather_count_from_density_('snow_density')
};
};
@ -71,6 +141,10 @@ beestat.component.scene.prototype.get_weather_profile_ = function(weather) {
* @return {number}
*/
beestat.component.scene.prototype.get_cloud_dimming_factor_ = function() {
const configured_cloud_count = Math.max(
1,
this.get_weather_design_capacity_count_('cloud_density')
);
const current_cloud_count = this.current_cloud_count_ === undefined
? 0
: this.current_cloud_count_;
@ -78,7 +152,7 @@ beestat.component.scene.prototype.get_cloud_dimming_factor_ = function() {
0,
Math.min(
1,
current_cloud_count / beestat.component.scene.weather_cloud_max_count
current_cloud_count / configured_cloud_count
)
);
@ -90,7 +164,7 @@ beestat.component.scene.prototype.get_cloud_dimming_factor_ = function() {
* Update weather transition targets based on appearance weather.
*/
beestat.component.scene.prototype.update_weather_targets_ = function() {
this.weather_profile_target_ = this.get_weather_profile_(this.get_appearance_value_('weather'));
this.weather_profile_target_ = this.get_weather_profile_();
this.weather_transition_start_profile_ = {
'cloud_count': this.current_cloud_count_ === undefined ? 0 : this.current_cloud_count_,
@ -107,9 +181,10 @@ beestat.component.scene.prototype.update_weather_targets_ = function() {
* @return {number}
*/
beestat.component.scene.prototype.get_snow_cover_blend_ = function() {
const configured_snow_count = this.get_weather_design_capacity_count_('snow_density');
if (
this.current_snow_count_ === undefined ||
beestat.component.scene.weather_snow_max_count <= 0
configured_snow_count <= 0
) {
return 0;
}
@ -118,7 +193,7 @@ beestat.component.scene.prototype.get_snow_cover_blend_ = function() {
0,
Math.min(
1,
this.current_snow_count_ / beestat.component.scene.weather_snow_max_count
this.current_snow_count_ / configured_snow_count
)
);
};
@ -340,6 +415,10 @@ beestat.component.scene.prototype.add_weather_ = function(center_x, center_y, pl
'min_z': -780,
'max_z': 140
};
this.weather_area_ = Math.max(
1,
(bounds.max_x - bounds.min_x) * (bounds.max_y - bounds.min_y)
);
this.weather_group_ = new THREE.Group();
this.weather_group_.userData.is_environment = true;
@ -356,7 +435,14 @@ beestat.component.scene.prototype.add_weather_ = function(center_x, center_y, pl
this.rain_particle_texture_ = this.create_rain_particle_texture_();
}
const cloud_count = beestat.component.scene.weather_cloud_max_count;
const configured_cloud_count = this.get_weather_count_from_density_(
'cloud_density',
this.weather_area_
);
const cloud_capacity = Math.max(
this.get_weather_design_capacity_count_('cloud_density', this.weather_area_),
configured_cloud_count
);
const cloud_opacity = 0.2;
const cloud_bounds = {
'min_x': bounds.min_x - 260,
@ -370,7 +456,7 @@ beestat.component.scene.prototype.add_weather_ = function(center_x, center_y, pl
this.cloud_sprites_ = [];
this.cloud_motion_ = [];
for (let i = 0; i < cloud_count; i++) {
for (let i = 0; i < cloud_capacity; i++) {
const cloud_material = new THREE.SpriteMaterial({
'map': this.cloud_texture_,
'color': 0xdce3ee,
@ -415,7 +501,10 @@ beestat.component.scene.prototype.add_weather_ = function(center_x, center_y, pl
this.rain_particles_ = this.create_precipitation_system_(
bounds,
beestat.component.scene.weather_rain_max_count,
Math.max(
this.get_weather_design_capacity_count_('rain_density', this.weather_area_),
this.get_weather_count_from_density_('rain_density', this.weather_area_)
),
{
'size': 11,
'color': 0xa8c7ff,
@ -430,7 +519,10 @@ beestat.component.scene.prototype.add_weather_ = function(center_x, center_y, pl
this.snow_particles_ = this.create_precipitation_system_(
bounds,
beestat.component.scene.weather_snow_max_count,
Math.max(
this.get_weather_design_capacity_count_('snow_density', this.weather_area_),
this.get_weather_count_from_density_('snow_density', this.weather_area_)
),
{
'size': 10,
'color': 0xffffff,
@ -445,7 +537,7 @@ beestat.component.scene.prototype.add_weather_ = function(center_x, center_y, pl
this.weather_last_update_ms_ = window.performance.now();
const initial_weather_profile = this.get_weather_profile_(this.get_appearance_value_('weather'));
const initial_weather_profile = this.get_weather_profile_();
this.weather_profile_target_ = initial_weather_profile;
this.current_cloud_count_ = initial_weather_profile.cloud_count;
this.current_rain_count_ = initial_weather_profile.rain_count;
@ -517,11 +609,15 @@ beestat.component.scene.prototype.update_weather_ = function() {
if (this.cloud_sprites_ !== undefined && this.cloud_motion_ !== undefined) {
const now_seconds = now_ms / 1000;
const cloud_normalization_count = Math.max(
1,
this.get_weather_design_capacity_count_('cloud_density')
);
const cloud_density = Math.max(
0,
Math.min(
1,
this.current_cloud_count_ / beestat.component.scene.weather_cloud_max_count
this.current_cloud_count_ / cloud_normalization_count
)
);
for (let i = 0; i < this.cloud_sprites_.length; i++) {