mirror of
https://github.com/beestat/app.git
synced 2026-04-12 20:22:14 -04:00
Weather
This commit is contained in:
parent
6d780e67a6
commit
e940451d0b
@ -83,8 +83,9 @@ beestat.setting = function(argument_1, opt_value, opt_callback) {
|
||||
'visualize.three_d.show_labels': false,
|
||||
'visualize.three_d.auto_rotate': false,
|
||||
'visualize.three_d.show_walls': false,
|
||||
'visualize.three_d.show_roof': false,
|
||||
'visualize.three_d.show_environment': true,
|
||||
'visualize.three_d.show_roof': false,
|
||||
'visualize.three_d.show_environment': true,
|
||||
'visualize.three_d.weather_mode': 'current',
|
||||
|
||||
'date_format': 'M/D/YYYY',
|
||||
|
||||
|
||||
@ -361,6 +361,7 @@ beestat.component.card.three_d.prototype.decorate_drawing_pane_ = function(paren
|
||||
beestat.setting('visualize.floor_plan_id'),
|
||||
this.get_data_()
|
||||
);
|
||||
this.apply_weather_setting_to_scene_();
|
||||
|
||||
this.scene_.addEventListener('change_active_room', function() {
|
||||
self.update_hud_();
|
||||
@ -472,6 +473,50 @@ beestat.component.card.three_d.prototype.get_show_environment_ = function() {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get selected weather mode.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
beestat.component.card.three_d.prototype.get_weather_mode_ = function() {
|
||||
return beestat.setting('visualize.three_d.weather_mode') || 'current';
|
||||
};
|
||||
|
||||
/**
|
||||
* Map UI weather mode to scene weather effect.
|
||||
*
|
||||
* @param {string} weather_mode
|
||||
*
|
||||
* @return {string} none|cloudy|rain|snow
|
||||
*/
|
||||
beestat.component.card.three_d.prototype.get_weather_effect_from_mode_ = function(weather_mode) {
|
||||
switch (weather_mode) {
|
||||
case 'cloudy':
|
||||
return 'cloudy';
|
||||
case 'raining':
|
||||
return 'rain';
|
||||
case 'snowing':
|
||||
return 'snow';
|
||||
case 'current':
|
||||
case 'sunny':
|
||||
default:
|
||||
// Placeholder mappings for now.
|
||||
return 'none';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply current weather settings to the scene.
|
||||
*/
|
||||
beestat.component.card.three_d.prototype.apply_weather_setting_to_scene_ = function() {
|
||||
if (this.scene_ === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const weather_effect = this.get_weather_effect_from_mode_(this.get_weather_mode_());
|
||||
this.scene_.set_weather(weather_effect);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set environment view state and mirror to legacy key for compatibility.
|
||||
*
|
||||
@ -497,6 +542,9 @@ beestat.component.card.three_d.prototype.apply_layer_visibility_ = function() {
|
||||
}
|
||||
|
||||
const show_environment = this.get_show_environment_();
|
||||
if (show_environment === false) {
|
||||
this.weather_menu_open_ = false;
|
||||
}
|
||||
|
||||
this.scene_.set_layer_visible('walls', show_environment);
|
||||
this.scene_.set_layer_visible('roof', show_environment);
|
||||
@ -1064,6 +1112,32 @@ beestat.component.card.three_d.prototype.decorate_toolbar_ = function(parent) {
|
||||
})
|
||||
);
|
||||
|
||||
// Weather controls (environment view only)
|
||||
if (show_environment === true) {
|
||||
const selected_mode = this.get_weather_mode_();
|
||||
const weather_modes = [
|
||||
{'value': 'current', 'icon': 'weather_partly_cloudy', 'title': 'Weather: Current'},
|
||||
{'value': 'sunny', 'icon': 'weather_sunny', 'title': 'Weather: Sunny'},
|
||||
{'value': 'cloudy', 'icon': 'weather_cloudy', 'title': 'Weather: Cloudy'},
|
||||
{'value': 'raining', 'icon': 'weather_pouring', 'title': 'Weather: Raining'},
|
||||
{'value': 'snowing', 'icon': 'weather_snowy', 'title': 'Weather: Snowing'}
|
||||
];
|
||||
const selected_weather_mode = weather_modes.find((mode) => mode.value === selected_mode) || weather_modes[0];
|
||||
|
||||
tile_group.add_tile(new beestat.component.tile()
|
||||
.set_icon(selected_weather_mode.icon)
|
||||
.set_title('Weather Menu')
|
||||
.set_text_color(beestat.style.color.gray.light)
|
||||
.set_background_color(this.weather_menu_open_ === true ? beestat.style.color.lightblue.base : beestat.style.color.bluegray.base)
|
||||
.set_background_hover_color(this.weather_menu_open_ === true ? beestat.style.color.lightblue.light : beestat.style.color.bluegray.light)
|
||||
.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
self.weather_menu_open_ = self.weather_menu_open_ !== true;
|
||||
self.decorate_toolbar_();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Labels (hidden while environment view is on)
|
||||
if (show_environment === false) {
|
||||
tile_group.add_tile(new beestat.component.tile()
|
||||
@ -1087,6 +1161,56 @@ beestat.component.card.three_d.prototype.decorate_toolbar_ = function(parent) {
|
||||
}
|
||||
|
||||
tile_group.render($(this.toolbar_container_));
|
||||
|
||||
if (show_environment === true && this.weather_menu_open_ === true) {
|
||||
const weather_tile_element = this.toolbar_container_.querySelector('[title=\"Weather Menu\"]');
|
||||
if (weather_tile_element !== null) {
|
||||
const toolbar_rect = this.toolbar_container_.getBoundingClientRect();
|
||||
const weather_tile_rect = weather_tile_element.getBoundingClientRect();
|
||||
const selected_mode = this.get_weather_mode_();
|
||||
const weather_modes = [
|
||||
{'value': 'sunny', 'icon': 'weather_sunny', 'title': 'Weather: Sunny'},
|
||||
{'value': 'cloudy', 'icon': 'weather_cloudy', 'title': 'Weather: Cloudy'},
|
||||
{'value': 'raining', 'icon': 'weather_pouring', 'title': 'Weather: Raining'},
|
||||
{'value': 'snowing', 'icon': 'weather_snowy', 'title': 'Weather: Snowing'}
|
||||
];
|
||||
|
||||
const popup = document.createElement('div');
|
||||
Object.assign(popup.style, {
|
||||
'position': 'absolute',
|
||||
'left': `${Math.round(weather_tile_rect.right - toolbar_rect.left + 6)}px`,
|
||||
'top': `${Math.round(weather_tile_rect.top - toolbar_rect.top - 2)}px`,
|
||||
'display': 'flex',
|
||||
'flex-direction': 'row',
|
||||
'align-items': 'center',
|
||||
'grid-gap': '4px',
|
||||
'padding': '2px'
|
||||
});
|
||||
this.toolbar_container_.appendChild(popup);
|
||||
|
||||
weather_modes.forEach((mode) => {
|
||||
const is_selected = mode.value === selected_mode;
|
||||
const tile = new beestat.component.tile()
|
||||
.set_icon(mode.icon)
|
||||
.set_title(mode.title)
|
||||
.set_text_color(is_selected ? beestat.style.color.gray.dark : beestat.style.color.gray.light)
|
||||
.set_background_color(is_selected ? beestat.style.color.bluegray.light : beestat.style.color.bluegray.base)
|
||||
.set_background_hover_color(is_selected ? beestat.style.color.bluegray.light : beestat.style.color.bluegray.light);
|
||||
|
||||
if (is_selected === false) {
|
||||
tile.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
beestat.setting('visualize.three_d.weather_mode', mode.value);
|
||||
this.apply_weather_setting_to_scene_();
|
||||
this.weather_menu_open_ = false;
|
||||
this.decorate_toolbar_();
|
||||
});
|
||||
}
|
||||
|
||||
tile.render($(popup));
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -226,6 +226,7 @@ beestat.component.modal.update_floor_plan.prototype.decorate_contents_ = functio
|
||||
? self.state_.appearance.ground_color
|
||||
: (floor_plan.data.appearance?.ground_color || '#4a7c3f');
|
||||
ground_color_select.set_value(current_ground_color);
|
||||
|
||||
}
|
||||
|
||||
// Address
|
||||
|
||||
@ -21,7 +21,7 @@ beestat.component.scene.layer_outline = 2;
|
||||
beestat.component.scene.roof_pitch = 0.5; // Rise over run (0.5 = 6:12 pitch)
|
||||
beestat.component.scene.roof_overhang = 12; // Roof overhang beyond walls
|
||||
beestat.component.scene.wall_thickness = 4;
|
||||
beestat.component.scene.environment_padding = 200; // Padding around floor plan
|
||||
beestat.component.scene.environment_padding = 400; // Padding around floor plan
|
||||
beestat.component.scene.room_floor_thickness = 6;
|
||||
beestat.component.scene.room_wall_inset = 1.5;
|
||||
|
||||
@ -33,8 +33,10 @@ beestat.component.scene.default_appearance = {
|
||||
'roof_color': '#3a3a3a',
|
||||
'roof_style': 'hip',
|
||||
'siding_color': '#889aaa',
|
||||
'ground_color': '#4a7c3f'
|
||||
'ground_color': '#4a7c3f',
|
||||
'weather': 'none'
|
||||
};
|
||||
beestat.component.scene.snow_surface_color = '#f0f0f0';
|
||||
|
||||
/**
|
||||
* Light intensity constants
|
||||
@ -54,6 +56,12 @@ beestat.component.scene.prototype.rerender = function() {
|
||||
this.add_main_group_();
|
||||
this.add_floor_plan_();
|
||||
this.apply_appearance_rotation_to_lights_();
|
||||
|
||||
// Ensure weather/date-driven celestial targets are recalculated after
|
||||
// rerendered environment changes (e.g., cloudy/rain/snow dimming).
|
||||
if (this.rendered_ === true) {
|
||||
this.update_();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -71,6 +79,57 @@ beestat.component.scene.prototype.get_appearance_value_ = function(key) {
|
||||
return beestat.component.scene.default_appearance[key];
|
||||
};
|
||||
|
||||
/**
|
||||
* Set weather effect override for this scene instance.
|
||||
*
|
||||
* @param {string} weather none|rain|snow
|
||||
*
|
||||
* @return {beestat.component.scene}
|
||||
*/
|
||||
beestat.component.scene.prototype.set_weather = function(weather) {
|
||||
this.weather_ = weather;
|
||||
|
||||
if (this.rendered_ === true) {
|
||||
this.rerender();
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the effective weather effect for this scene.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
beestat.component.scene.prototype.get_weather_effect_ = function() {
|
||||
if (this.weather_ !== undefined) {
|
||||
return this.weather_;
|
||||
}
|
||||
|
||||
const appearance_weather = this.get_appearance_value_('weather');
|
||||
return appearance_weather || 'none';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get effective surface colors, applying snow overrides when snow weather is
|
||||
* active.
|
||||
*
|
||||
* @return {{ground_color: string, roof_color: string}}
|
||||
*/
|
||||
beestat.component.scene.prototype.get_surface_colors_ = function() {
|
||||
if (this.get_weather_effect_() === 'snow') {
|
||||
return {
|
||||
'ground_color': beestat.component.scene.snow_surface_color,
|
||||
'roof_color': beestat.component.scene.snow_surface_color
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
'ground_color': this.get_appearance_value_('ground_color'),
|
||||
'roof_color': this.get_appearance_value_('roof_color')
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the width of this component.
|
||||
*
|
||||
@ -130,6 +189,7 @@ beestat.component.scene.prototype.decorate_ = function(parent) {
|
||||
self.controls_.update();
|
||||
self.update_raycaster_();
|
||||
self.update_celestial_light_intensities_();
|
||||
self.update_weather_();
|
||||
self.renderer_.render(self.scene_, self.camera_);
|
||||
};
|
||||
animate();
|
||||
@ -228,7 +288,7 @@ beestat.component.scene.prototype.add_controls_ = function(parent) {
|
||||
this.controls_ = new THREE.OrbitControls(this.camera_, parent[0]);
|
||||
this.controls_.enableDamping = true;
|
||||
this.controls_.enablePan = false;
|
||||
this.controls_.maxDistance = 2000;
|
||||
this.controls_.maxDistance = 1500;
|
||||
this.controls_.minDistance = 400;
|
||||
this.controls_.maxPolarAngle = Math.PI / 2.1;
|
||||
};
|
||||
@ -416,6 +476,126 @@ beestat.component.scene.prototype.create_sun_glow_texture_ = function() {
|
||||
return texture;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a circular particle texture for snow.
|
||||
*
|
||||
* @return {THREE.Texture}
|
||||
*/
|
||||
beestat.component.scene.prototype.create_snow_particle_texture_ = function() {
|
||||
const size = 64;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
const gradient = context.createRadialGradient(
|
||||
size / 2,
|
||||
size / 2,
|
||||
0,
|
||||
size / 2,
|
||||
size / 2,
|
||||
size / 2
|
||||
);
|
||||
gradient.addColorStop(0.0, 'rgba(255, 255, 255, 1.0)');
|
||||
gradient.addColorStop(0.4, 'rgba(245, 250, 255, 0.9)');
|
||||
gradient.addColorStop(1.0, 'rgba(240, 245, 255, 0.0)');
|
||||
|
||||
context.fillStyle = gradient;
|
||||
context.fillRect(0, 0, size, size);
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
texture.needsUpdate = true;
|
||||
|
||||
return texture;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a streak-like particle texture for rain.
|
||||
*
|
||||
* @return {THREE.Texture}
|
||||
*/
|
||||
beestat.component.scene.prototype.create_rain_particle_texture_ = function() {
|
||||
const width = 24;
|
||||
const height = 64;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
const gradient = context.createLinearGradient(0, 0, 0, height);
|
||||
gradient.addColorStop(0.0, 'rgba(170, 200, 255, 0.0)');
|
||||
gradient.addColorStop(0.25, 'rgba(185, 210, 255, 0.85)');
|
||||
gradient.addColorStop(1.0, 'rgba(170, 200, 255, 0.0)');
|
||||
|
||||
context.fillStyle = gradient;
|
||||
context.fillRect(width / 2 - 2, 0, 4, height);
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
texture.needsUpdate = true;
|
||||
|
||||
return texture;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a soft cloud texture used for weather cloud sprites.
|
||||
*
|
||||
* @return {THREE.Texture}
|
||||
*/
|
||||
beestat.component.scene.prototype.create_cloud_texture_ = function() {
|
||||
const size = 256;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
const circles = [
|
||||
{'x': 0.36, 'y': 0.56, 'r': 0.2},
|
||||
{'x': 0.5, 'y': 0.5, 'r': 0.24},
|
||||
{'x': 0.64, 'y': 0.56, 'r': 0.2},
|
||||
{'x': 0.5, 'y': 0.64, 'r': 0.22}
|
||||
];
|
||||
|
||||
circles.forEach(function(circle) {
|
||||
const gradient = context.createRadialGradient(
|
||||
size * circle.x,
|
||||
size * circle.y,
|
||||
0,
|
||||
size * circle.x,
|
||||
size * circle.y,
|
||||
size * circle.r
|
||||
);
|
||||
gradient.addColorStop(0.0, 'rgba(255,255,255,0.9)');
|
||||
gradient.addColorStop(0.55, 'rgba(240,245,255,0.55)');
|
||||
gradient.addColorStop(1.0, 'rgba(240,245,255,0.0)');
|
||||
context.fillStyle = gradient;
|
||||
context.beginPath();
|
||||
context.arc(size * circle.x, size * circle.y, size * circle.r, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
});
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get dimming multiplier from weather for sun/moon brightness.
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
beestat.component.scene.prototype.get_cloud_dimming_factor_ = function() {
|
||||
switch (this.get_weather_effect_()) {
|
||||
case 'cloudy':
|
||||
return 0.18;
|
||||
case 'rain':
|
||||
return 0.08;
|
||||
case 'snow':
|
||||
return 0.12;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw the moon phase into the reusable moon canvas texture.
|
||||
*
|
||||
@ -820,10 +1000,13 @@ beestat.component.scene.prototype.update_celestial_lights_ = function(date, lati
|
||||
this.sun_visual_horizon_fade_ = Math.max(0, Math.min(1, (sun_pos.altitude + 0.15) / 0.3));
|
||||
}
|
||||
|
||||
const cloud_dimming = this.get_cloud_dimming_factor_();
|
||||
|
||||
// Calculate target intensity for smooth transitions
|
||||
this.target_sun_intensity_ = sun_pos.altitude < 0
|
||||
? Math.max(0, beestat.component.scene.sun_light_intensity * (1 + sun_pos.altitude / (Math.PI / 6)))
|
||||
: beestat.component.scene.sun_light_intensity;
|
||||
this.target_sun_intensity_ *= cloud_dimming;
|
||||
|
||||
// Moon
|
||||
const moon_pos = SunCalc.getMoonPosition(js_date, latitude, longitude);
|
||||
@ -859,6 +1042,7 @@ beestat.component.scene.prototype.update_celestial_lights_ = function(date, lati
|
||||
? Math.max(0, moon_intensity * (1 + moon_pos.altitude / (Math.PI / 6)))
|
||||
: moon_intensity;
|
||||
}
|
||||
this.target_moon_intensity_ *= cloud_dimming;
|
||||
|
||||
// Update helpers
|
||||
if (this.debug_.sun_light_helper) {
|
||||
@ -1677,9 +1861,10 @@ beestat.component.scene.prototype.add_roofs_ = function() {
|
||||
* Add hip roofs using the straight skeleton algorithm.
|
||||
*/
|
||||
beestat.component.scene.prototype.add_hip_roofs_ = function() {
|
||||
const self = this;
|
||||
const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
|
||||
const exposed_areas = this.compute_exposed_ceiling_areas_(floor_plan);
|
||||
const surface_colors = this.get_surface_colors_();
|
||||
const roof_color = surface_colors.roof_color;
|
||||
|
||||
// Create layer for roofs
|
||||
const roofs_layer = new THREE.Group();
|
||||
@ -1734,7 +1919,6 @@ beestat.component.scene.prototype.add_hip_roofs_ = function() {
|
||||
'depth': hip_roof_base_thickness,
|
||||
'bevelEnabled': false
|
||||
});
|
||||
const roof_color = self.get_appearance_value_('roof_color');
|
||||
const base_material = new THREE.MeshStandardMaterial({
|
||||
'color': roof_color,
|
||||
'side': THREE.DoubleSide,
|
||||
@ -1877,13 +2061,91 @@ beestat.component.scene.prototype.add_hip_roofs_ = function() {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Animate weather particles (snow/rain) each frame.
|
||||
*/
|
||||
beestat.component.scene.prototype.update_weather_ = function() {
|
||||
if (this.cloud_sprites_ !== undefined && this.cloud_bounds_ !== undefined) {
|
||||
const cloud_span_x = this.cloud_bounds_.max_x - this.cloud_bounds_.min_x;
|
||||
const cloud_span_y = this.cloud_bounds_.max_y - this.cloud_bounds_.min_y;
|
||||
for (let i = 0; i < this.cloud_sprites_.length; i++) {
|
||||
const sprite = this.cloud_sprites_[i];
|
||||
sprite.position.x += this.cloud_speeds_x_[i] * 0.016;
|
||||
sprite.position.y += this.cloud_speeds_y_[i] * 0.016;
|
||||
if (sprite.position.x > this.cloud_bounds_.max_x) {
|
||||
sprite.position.x = this.cloud_bounds_.min_x;
|
||||
} else if (sprite.position.x < this.cloud_bounds_.min_x) {
|
||||
sprite.position.x = this.cloud_bounds_.max_x;
|
||||
}
|
||||
if (sprite.position.y > this.cloud_bounds_.max_y) {
|
||||
sprite.position.y = this.cloud_bounds_.min_y;
|
||||
} else if (sprite.position.y < this.cloud_bounds_.min_y) {
|
||||
sprite.position.y = this.cloud_bounds_.max_y;
|
||||
}
|
||||
if (sprite.position.x === this.cloud_bounds_.min_x || sprite.position.x === this.cloud_bounds_.max_x) {
|
||||
sprite.position.y = this.cloud_bounds_.min_y + Math.random() * cloud_span_y;
|
||||
}
|
||||
if (sprite.position.y === this.cloud_bounds_.min_y || sprite.position.y === this.cloud_bounds_.max_y) {
|
||||
sprite.position.x = this.cloud_bounds_.min_x + Math.random() * cloud_span_x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.weather_points_ === undefined ||
|
||||
this.weather_particle_speeds_ === undefined ||
|
||||
this.weather_bounds_ === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now_ms = window.performance.now();
|
||||
if (this.weather_last_update_ms_ === undefined) {
|
||||
this.weather_last_update_ms_ = now_ms;
|
||||
return;
|
||||
}
|
||||
|
||||
const delta_seconds = Math.min(0.05, (now_ms - this.weather_last_update_ms_) / 1000);
|
||||
this.weather_last_update_ms_ = now_ms;
|
||||
if (delta_seconds <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const positions = this.weather_points_.geometry.attributes.position.array;
|
||||
const bounds = this.weather_bounds_;
|
||||
const span_x = bounds.max_x - bounds.min_x;
|
||||
const span_y = bounds.max_y - bounds.min_y;
|
||||
const span_z = bounds.max_z - bounds.min_z;
|
||||
|
||||
for (let i = 0; i < this.weather_particle_speeds_.length; i++) {
|
||||
const offset = i * 3;
|
||||
|
||||
positions[offset + 2] += this.weather_particle_speeds_[i] * delta_seconds;
|
||||
positions[offset] += this.weather_particle_drift_x_[i] * delta_seconds;
|
||||
positions[offset + 1] += this.weather_particle_drift_y_[i] * delta_seconds;
|
||||
|
||||
if (
|
||||
positions[offset] < bounds.min_x || positions[offset] > bounds.max_x ||
|
||||
positions[offset + 1] < bounds.min_y || positions[offset + 1] > bounds.max_y ||
|
||||
positions[offset + 2] > bounds.max_z
|
||||
) {
|
||||
positions[offset] = bounds.min_x + Math.random() * span_x;
|
||||
positions[offset + 1] = bounds.min_y + Math.random() * span_y;
|
||||
positions[offset + 2] = bounds.min_z + Math.random() * span_z;
|
||||
}
|
||||
}
|
||||
|
||||
this.weather_points_.geometry.attributes.position.needsUpdate = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add flat roofs to the scene.
|
||||
*/
|
||||
beestat.component.scene.prototype.add_flat_roofs_ = function() {
|
||||
const self = this;
|
||||
const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
|
||||
const exposed_areas = this.compute_exposed_ceiling_areas_(floor_plan);
|
||||
const surface_colors = this.get_surface_colors_();
|
||||
const roof_color = surface_colors.roof_color;
|
||||
|
||||
// Create layer for roofs
|
||||
const roofs_layer = new THREE.Group();
|
||||
@ -1939,7 +2201,6 @@ beestat.component.scene.prototype.add_flat_roofs_ = function() {
|
||||
});
|
||||
|
||||
// Create material - use appearance roof color
|
||||
const roof_color = self.get_appearance_value_('roof_color');
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
'color': roof_color,
|
||||
'side': THREE.DoubleSide,
|
||||
@ -2121,7 +2382,8 @@ beestat.component.scene.prototype.add_environment_ = function() {
|
||||
let current_z = 0;
|
||||
|
||||
const padding = beestat.component.scene.environment_padding;
|
||||
const ground_color = this.get_appearance_value_('ground_color');
|
||||
const surface_colors = this.get_surface_colors_();
|
||||
const ground_color = surface_colors.ground_color;
|
||||
const strata = [
|
||||
{'color': ground_color, 'thickness': 10, 'roughness': 0.95},
|
||||
{'color': 0x4a3f35, 'thickness': 60, 'roughness': 0.85},
|
||||
@ -2159,6 +2421,162 @@ beestat.component.scene.prototype.add_environment_ = function() {
|
||||
|
||||
// Add celestial lights (sun and moon) - toggled with environment visibility
|
||||
this.add_celestial_lights_();
|
||||
this.add_weather_effect_(center_x, center_y, plan_width, plan_height);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add procedural weather particles based on floor plan appearance.
|
||||
*
|
||||
* @param {number} center_x
|
||||
* @param {number} center_y
|
||||
* @param {number} plan_width
|
||||
* @param {number} plan_height
|
||||
*/
|
||||
beestat.component.scene.prototype.add_weather_effect_ = function(center_x, center_y, plan_width, plan_height) {
|
||||
const weather = this.get_weather_effect_();
|
||||
const has_clouds = weather === 'cloudy' || weather === 'rain' || weather === 'snow';
|
||||
const has_precipitation = weather === 'rain' || weather === 'snow';
|
||||
|
||||
if (has_clouds === false && has_precipitation === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const padding = beestat.component.scene.environment_padding + 120;
|
||||
const bounds = {
|
||||
'min_x': center_x - ((plan_width + padding * 2) / 2),
|
||||
'max_x': center_x + ((plan_width + padding * 2) / 2),
|
||||
'min_y': center_y - ((plan_height + padding * 2) / 2),
|
||||
'max_y': center_y + ((plan_height + padding * 2) / 2),
|
||||
'min_z': -780,
|
||||
'max_z': 140
|
||||
};
|
||||
|
||||
const config = weather === 'snow'
|
||||
? {
|
||||
'count': 1500,
|
||||
'size': 10,
|
||||
'opacity': 0.75,
|
||||
'color': 0xffffff,
|
||||
'speed_min': 18,
|
||||
'speed_max': 44,
|
||||
'drift': 12
|
||||
}
|
||||
: {
|
||||
'count': 2200,
|
||||
'size': 11,
|
||||
'opacity': 0.7,
|
||||
'color': 0xa8c7ff,
|
||||
'speed_min': 280,
|
||||
'speed_max': 430,
|
||||
'drift': 28
|
||||
};
|
||||
|
||||
this.weather_group_ = new THREE.Group();
|
||||
this.weather_group_.userData.is_environment = true;
|
||||
this.environment_group_.add(this.weather_group_);
|
||||
|
||||
if (has_clouds === true) {
|
||||
if (this.cloud_texture_ === undefined) {
|
||||
this.cloud_texture_ = this.create_cloud_texture_();
|
||||
}
|
||||
|
||||
// const cloud_count = weather === 'cloudy' ? 140 : 180;
|
||||
const cloud_count = 140;
|
||||
// const cloud_opacity = weather === 'cloudy' ? 0.55 : 0.62;
|
||||
const cloud_opacity = 0.2;
|
||||
const cloud_bounds = {
|
||||
'min_x': bounds.min_x - 260,
|
||||
'max_x': bounds.max_x + 260,
|
||||
'min_y': bounds.min_y - 260,
|
||||
'max_y': bounds.max_y + 260,
|
||||
'z': -760
|
||||
};
|
||||
|
||||
this.cloud_bounds_ = cloud_bounds;
|
||||
this.cloud_sprites_ = [];
|
||||
this.cloud_speeds_x_ = new Float32Array(cloud_count);
|
||||
this.cloud_speeds_y_ = new Float32Array(cloud_count);
|
||||
|
||||
for (let i = 0; i < cloud_count; i++) {
|
||||
const cloud_material = new THREE.SpriteMaterial({
|
||||
'map': this.cloud_texture_,
|
||||
'color': 0xdce3ee,
|
||||
'transparent': true,
|
||||
'opacity': cloud_opacity,
|
||||
'depthWrite': false,
|
||||
'depthTest': true
|
||||
});
|
||||
|
||||
const cloud = new THREE.Sprite(cloud_material);
|
||||
cloud.position.set(
|
||||
cloud_bounds.min_x + Math.random() * (cloud_bounds.max_x - cloud_bounds.min_x),
|
||||
cloud_bounds.min_y + Math.random() * (cloud_bounds.max_y - cloud_bounds.min_y),
|
||||
cloud_bounds.z + (Math.random() * 130)
|
||||
);
|
||||
const cloud_size = 520 + Math.random() * 560;
|
||||
cloud.scale.set(cloud_size, cloud_size * 0.6, 1);
|
||||
cloud.layers.set(beestat.component.scene.layer_visible);
|
||||
cloud.userData.is_environment = true;
|
||||
this.weather_group_.add(cloud);
|
||||
this.cloud_sprites_.push(cloud);
|
||||
this.cloud_speeds_x_[i] = (Math.random() - 0.5) * 0.8;
|
||||
this.cloud_speeds_y_[i] = (Math.random() - 0.5) * 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_precipitation === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (weather === 'snow' && this.snow_particle_texture_ === undefined) {
|
||||
this.snow_particle_texture_ = this.create_snow_particle_texture_();
|
||||
}
|
||||
if (weather === 'rain' && this.rain_particle_texture_ === undefined) {
|
||||
this.rain_particle_texture_ = this.create_rain_particle_texture_();
|
||||
}
|
||||
|
||||
const positions = new Float32Array(config.count * 3);
|
||||
const particle_speeds = new Float32Array(config.count);
|
||||
const particle_drift_x = new Float32Array(config.count);
|
||||
const particle_drift_y = new Float32Array(config.count);
|
||||
|
||||
const span_x = bounds.max_x - bounds.min_x;
|
||||
const span_y = bounds.max_y - bounds.min_y;
|
||||
const span_z = bounds.max_z - bounds.min_z;
|
||||
|
||||
for (let i = 0; i < config.count; i++) {
|
||||
const offset = i * 3;
|
||||
positions[offset] = bounds.min_x + Math.random() * span_x;
|
||||
positions[offset + 1] = bounds.min_y + Math.random() * span_y;
|
||||
positions[offset + 2] = bounds.min_z + Math.random() * span_z;
|
||||
|
||||
particle_speeds[i] = config.speed_min + Math.random() * (config.speed_max - config.speed_min);
|
||||
particle_drift_x[i] = (Math.random() - 0.5) * config.drift;
|
||||
particle_drift_y[i] = (Math.random() - 0.5) * config.drift;
|
||||
}
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||
|
||||
const material = new THREE.PointsMaterial({
|
||||
'size': config.size,
|
||||
'color': config.color,
|
||||
'transparent': true,
|
||||
'opacity': config.opacity,
|
||||
'depthWrite': false,
|
||||
'blending': THREE.NormalBlending,
|
||||
'map': weather === 'snow' ? this.snow_particle_texture_ : this.rain_particle_texture_
|
||||
});
|
||||
|
||||
this.weather_points_ = new THREE.Points(geometry, material);
|
||||
this.weather_points_.layers.set(beestat.component.scene.layer_visible);
|
||||
this.weather_group_.add(this.weather_points_);
|
||||
|
||||
this.weather_bounds_ = bounds;
|
||||
this.weather_particle_speeds_ = particle_speeds;
|
||||
this.weather_particle_drift_x_ = particle_drift_x;
|
||||
this.weather_particle_drift_y_ = particle_drift_y;
|
||||
this.weather_last_update_ms_ = window.performance.now();
|
||||
};
|
||||
|
||||
/**
|
||||
@ -2477,6 +2895,15 @@ beestat.component.scene.prototype.dispose = function() {
|
||||
if (this.sun_glow_texture_ !== undefined) {
|
||||
this.sun_glow_texture_.dispose();
|
||||
}
|
||||
if (this.snow_particle_texture_ !== undefined) {
|
||||
this.snow_particle_texture_.dispose();
|
||||
}
|
||||
if (this.rain_particle_texture_ !== undefined) {
|
||||
this.rain_particle_texture_.dispose();
|
||||
}
|
||||
if (this.cloud_texture_ !== undefined) {
|
||||
this.cloud_texture_.dispose();
|
||||
}
|
||||
if (this.moon_phase_texture_ !== undefined) {
|
||||
this.moon_phase_texture_.dispose();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user