From 855c1ec794e8842aed1c8b0528685184bf6c0eab Mon Sep 17 00:00:00 2001 From: Jon Ziebell Date: Sat, 21 Feb 2026 19:14:53 -0500 Subject: [PATCH] Debug --- js/component/card/three_d.js | 300 +++++++++++++++++++++++++++++- js/component/scene.js | 237 ++++++++++++++++++++++- js/component/scene/environment.js | 45 +++-- js/component/scene/light.js | 8 +- js/component/scene/tree.js | 22 ++- js/component/scene/weather.js | 180 +++++++++++++----- 6 files changed, 715 insertions(+), 77 deletions(-) diff --git a/js/component/card/three_d.js b/js/component/card/three_d.js index 8bc2813..44ada58 100644 --- a/js/component/card/three_d.js +++ b/js/component/card/three_d.js @@ -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() diff --git a/js/component/scene.js b/js/component/scene.js index b824bbb..bc76bb1 100644 --- a/js/component/scene.js +++ b/js/component/scene.js @@ -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; diff --git a/js/component/scene/environment.js b/js/component/scene/environment.js index 055b301..cc5dc44 100644 --- a/js/component/scene/environment.js +++ b/js/component/scene/environment.js @@ -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; diff --git a/js/component/scene/light.js b/js/component/scene/light.js index 1ed8617..996c20c 100644 --- a/js/component/scene/light.js +++ b/js/component/scene/light.js @@ -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; } diff --git a/js/component/scene/tree.js b/js/component/scene/tree.js index b20d27b..e32d80c 100644 --- a/js/component/scene/tree.js +++ b/js/component/scene/tree.js @@ -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); diff --git a/js/component/scene/weather.js b/js/component/scene/weather.js index 6fe64bc..35bd7b7 100644 --- a/js/component/scene/weather.js +++ b/js/component/scene/weather.js @@ -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++) {