1
0
mirror of https://github.com/beestat/app.git synced 2026-02-26 05:00:21 -05:00
This commit is contained in:
Jon Ziebell 2026-02-21 21:46:27 -05:00
parent f7f5252ea9
commit 118b67e1d3
4 changed files with 305 additions and 26 deletions

View File

@ -4,6 +4,25 @@
beestat.component.card.three_d = function() {
const self = this;
if (
beestat.component.card.three_d.active_instance_ !== undefined &&
beestat.component.card.three_d.active_instance_ !== null &&
beestat.component.card.three_d.active_instance_ !== this
) {
beestat.component.card.three_d.active_instance_.force_dispose_stale_instance_();
}
beestat.component.card.three_d.active_instance_ = this;
this.disposed_ = false;
this.handle_scene_settings_change_ = function() {
if (self.disposed_ === true || self.scene_ === undefined) {
return;
}
self.update_scene_();
self.update_hud_();
};
// Things that update the scene that don't require a rerender.
// TODO these probably need moved to the layer instead of here
beestat.dispatcher.addEventListener(
@ -14,20 +33,27 @@ beestat.component.card.three_d = function() {
'setting.visualize.heat_map_static.temperature.max',
'setting.visualize.heat_map_static.occupancy.min',
'setting.visualize.heat_map_static.occupancy.max'
], function() {
self.update_scene_();
self.update_hud_();
});
],
this.handle_scene_settings_change_
);
// Rerender the scene when the floor plan changes.
beestat.dispatcher.addEventListener('cache.floor_plan', function() {
this.handle_floor_plan_cache_change_ = function() {
if (self.disposed_ === true || self.scene_ === undefined) {
return;
}
self.scene_.rerender();
self.apply_layer_visibility_();
self.update_scene_();
self.update_hud_();
});
};
const change_function = beestat.debounce(function() {
// Rerender the scene when the floor plan changes.
beestat.dispatcher.addEventListener('cache.floor_plan', this.handle_floor_plan_cache_change_);
this.handle_runtime_data_change_ = beestat.debounce(function() {
if (self.disposed_ === true || self.scene_ === undefined) {
return;
}
self.state_.scene_camera_state = self.scene_.get_camera_state();
self.rerender();
}, 10);
@ -37,7 +63,7 @@ beestat.component.card.three_d = function() {
'cache.data.three_d__runtime_sensor',
'cache.data.three_d__runtime_thermostat'
],
change_function
this.handle_runtime_data_change_
);
this.scene_settings_menu_open_ = false;
@ -396,6 +422,18 @@ beestat.component.card.three_d.prototype.decorate_drawing_pane_ = function(paren
beestat.setting('visualize.floor_plan_id'),
this.get_data_()
);
const initial_width = parent.getBoundingClientRect().width;
if (this.state_.width === undefined && initial_width > 0) {
this.state_.width = initial_width;
}
if (this.state_.width !== undefined && this.state_.width > 0) {
this.scene_.set_initial_width(this.state_.width);
}
if (this.state_.scene_camera_state !== undefined) {
this.scene_.set_initial_camera_state(this.state_.scene_camera_state);
}
this.scene_.set_scene_settings(this.scene_settings_values_, {
'rerender': false
});
@ -483,10 +521,6 @@ beestat.component.card.three_d.prototype.decorate_drawing_pane_ = function(paren
this.scene_.set_width(this.state_.width);
}
if (this.state_.scene_camera_state !== undefined) {
this.scene_.set_camera_state(this.state_.scene_camera_state);
}
beestat.dispatcher.removeEventListener('resize.three_d');
beestat.dispatcher.addEventListener('resize.three_d', function() {
self.state_.width = parent.getBoundingClientRect().width;
@ -545,7 +579,7 @@ beestat.component.card.three_d.prototype.get_weather_settings_from_mode_ = funct
switch (weather_mode) {
case 'cloudy':
return {
'cloud_density': 1,
'cloud_density': 0.5,
'rain_density': 0,
'snow_density': 0
};
@ -2011,8 +2045,82 @@ beestat.component.card.three_d.prototype.get_most_recent_time_with_data_ = funct
return null;
};
beestat.component.card.three_d.prototype.dispose = function() {
/**
* Remove global listeners registered by this card instance.
*/
beestat.component.card.three_d.prototype.remove_global_listeners_ = function() {
beestat.dispatcher.removeEventListener(
'setting.visualize.data_type',
this.handle_scene_settings_change_
);
beestat.dispatcher.removeEventListener(
'setting.visualize.heat_map_values',
this.handle_scene_settings_change_
);
beestat.dispatcher.removeEventListener(
'setting.visualize.heat_map_static.temperature.min',
this.handle_scene_settings_change_
);
beestat.dispatcher.removeEventListener(
'setting.visualize.heat_map_static.temperature.max',
this.handle_scene_settings_change_
);
beestat.dispatcher.removeEventListener(
'setting.visualize.heat_map_static.occupancy.min',
this.handle_scene_settings_change_
);
beestat.dispatcher.removeEventListener(
'setting.visualize.heat_map_static.occupancy.max',
this.handle_scene_settings_change_
);
beestat.dispatcher.removeEventListener(
'cache.floor_plan',
this.handle_floor_plan_cache_change_
);
beestat.dispatcher.removeEventListener(
'cache.data.three_d__runtime_sensor',
this.handle_runtime_data_change_
);
beestat.dispatcher.removeEventListener(
'cache.data.three_d__runtime_thermostat',
this.handle_runtime_data_change_
);
beestat.dispatcher.removeEventListener('resize.three_d');
};
/**
* Force teardown for stale card instances that were not formally disposed.
*/
beestat.component.card.three_d.prototype.force_dispose_stale_instance_ = function() {
if (this.disposed_ === true) {
return;
}
this.disposed_ = true;
window.clearInterval(this.fps_interval_);
delete this.fps_interval_;
this.remove_global_listeners_();
if (this.scene_ !== undefined) {
this.scene_.dispose();
delete this.scene_;
}
};
beestat.component.card.three_d.prototype.dispose = function() {
this.disposed_ = true;
window.clearInterval(this.fps_interval_);
delete this.fps_interval_;
this.remove_global_listeners_();
if (this.scene_ !== undefined) {
this.scene_.dispose();
delete this.scene_;
}
if (beestat.component.card.three_d.active_instance_ === this) {
delete beestat.component.card.three_d.active_instance_;
}
beestat.component.card.prototype.dispose.apply(this, arguments);
};

View File

@ -402,7 +402,11 @@ beestat.component.scene.prototype.with_random_seed_ = function(seed, callback) {
*/
beestat.component.scene.prototype.rerender = function() {
this.reset_celestial_lights_for_rerender_();
this.scene_.remove(this.main_group_);
if (this.main_group_ !== undefined) {
this.dispose_object3d_resources_(this.main_group_);
this.scene_.remove(this.main_group_);
}
this.reset_runtime_scene_references_for_rerender_();
this.with_seeded_random_(function() {
this.add_main_group_();
this.add_floor_plan_();
@ -415,6 +419,91 @@ beestat.component.scene.prototype.rerender = function() {
}
};
/**
* Dispose geometries/materials/textures under an Object3D subtree.
*
* @param {THREE.Object3D} root
*/
beestat.component.scene.prototype.dispose_object3d_resources_ = function(root) {
if (root === undefined || root === null || typeof root.traverse !== 'function') {
return;
}
const disposed_textures = new Set();
const disposed_materials = new Set();
const dispose_texture = function(texture) {
if (
texture !== undefined &&
texture !== null &&
texture.isTexture === true &&
disposed_textures.has(texture) !== true
) {
disposed_textures.add(texture);
texture.dispose();
}
};
const dispose_material = function(material) {
if (material === undefined || material === null) {
return;
}
if (disposed_materials.has(material) === true) {
return;
}
disposed_materials.add(material);
for (const key in material) {
if (Object.prototype.hasOwnProperty.call(material, key) !== true) {
continue;
}
const value = material[key];
if (value !== undefined && value !== null && value.isTexture === true) {
dispose_texture(value);
}
}
material.dispose();
};
root.traverse(function(object) {
if (object.geometry !== undefined && object.geometry !== null) {
object.geometry.dispose();
}
if (object.material !== undefined && object.material !== null) {
if (Array.isArray(object.material) === true) {
object.material.forEach(function(material) {
dispose_material(material);
});
} else {
dispose_material(object.material);
}
}
});
};
/**
* Clear references that are recreated each rerender.
*/
beestat.component.scene.prototype.reset_runtime_scene_references_for_rerender_ = function() {
this.meshes_ = {};
this.layers_ = {};
this.light_sources_ = [];
this.tree_foliage_meshes_ = [];
this.tree_branch_groups_ = [];
delete this.floor_plan_group_;
delete this.environment_group_;
delete this.environment_surface_group_;
delete this.weather_group_;
delete this.rain_particles_;
delete this.snow_particles_;
delete this.cloud_sprites_;
delete this.cloud_motion_;
delete this.weather_profile_target_;
delete this.weather_transition_start_profile_;
delete this.active_mesh_;
delete this.intersected_mesh_;
delete this.tree_ground_contact_material_;
};
/**
* Reset celestial objects so rerender can rebuild stars/lights from settings.
*/
@ -567,13 +656,16 @@ beestat.component.scene.prototype.decorate_ = function(parent) {
};
this.room_interaction_enabled_ = true;
this.width_ = this.state_.scene_width || 800;
this.width_ = this.initial_width_ || this.state_.scene_width || 800;
this.height_ = 500;
this.add_scene_(parent);
this.add_renderer_(parent);
this.add_camera_();
this.add_controls_(parent);
if (this.initial_camera_state_ !== undefined) {
this.set_camera_state(this.initial_camera_state_);
}
this.add_raycaster_(parent);
this.add_skybox_(parent);
this.add_static_lights_();
@ -606,6 +698,34 @@ beestat.component.scene.prototype.decorate_ = function(parent) {
animate();
};
/**
* Set width to use when scene first decorates/renders.
*
* @param {number} width
*
* @return {beestat.component.scene}
*/
beestat.component.scene.prototype.set_initial_width = function(width) {
if (Number.isFinite(width) === true && width > 0) {
this.initial_width_ = width;
}
return this;
};
/**
* Set camera state to apply before first rendered frame.
*
* @param {object} camera_state
*
* @return {beestat.component.scene}
*/
beestat.component.scene.prototype.set_initial_camera_state = function(camera_state) {
if (camera_state !== undefined) {
this.initial_camera_state_ = camera_state;
}
return this;
};
/**
* Add the scene. Everything gets added to the scene.
*
@ -1246,7 +1366,13 @@ beestat.component.scene.prototype.get_fps = function() {
* @return {object}
*/
beestat.component.scene.prototype.get_camera_state = function() {
return this.camera_.matrix.toArray();
const state = {
'matrix': this.camera_.matrix.toArray()
};
if (this.controls_ !== undefined && this.controls_.target !== undefined) {
state.target = this.controls_.target.toArray();
}
return state;
};
/**
@ -1255,12 +1381,40 @@ beestat.component.scene.prototype.get_camera_state = function() {
* @param {object} camera_state
*/
beestat.component.scene.prototype.set_camera_state = function(camera_state) {
this.camera_.matrix.fromArray(camera_state);
let matrix_state = camera_state;
let target_state;
if (
camera_state !== undefined &&
camera_state !== null &&
Array.isArray(camera_state) !== true
) {
matrix_state = camera_state.matrix;
target_state = camera_state.target;
}
if (Array.isArray(matrix_state) !== true) {
return;
}
this.camera_.matrix.fromArray(matrix_state);
this.camera_.matrix.decompose(
this.camera_.position,
this.camera_.quaternion,
this.camera_.scale
);
if (
Array.isArray(target_state) === true &&
target_state.length >= 3 &&
this.controls_ !== undefined
) {
this.controls_.target.set(
Number(target_state[0]) || 0,
Number(target_state[1]) || 0,
Number(target_state[2]) || 0
);
this.controls_.update();
}
};
/**
@ -1438,6 +1592,20 @@ beestat.component.scene.prototype.dispose = function() {
if (this.light_source_glow_geometry_ !== undefined) {
this.light_source_glow_geometry_.dispose();
}
if (this.raycaster_document_mousemove_handler_ !== undefined) {
document.removeEventListener('mousemove', this.raycaster_document_mousemove_handler_);
delete this.raycaster_document_mousemove_handler_;
}
if (
this.raycaster_dom_element_ !== undefined &&
this.raycaster_dom_mousedown_handler_ !== undefined
) {
this.raycaster_dom_element_.removeEventListener('mousedown', this.raycaster_dom_mousedown_handler_);
this.raycaster_dom_element_.removeEventListener('touchstart', this.raycaster_dom_touchstart_handler_);
delete this.raycaster_dom_mousedown_handler_;
delete this.raycaster_dom_touchstart_handler_;
delete this.raycaster_dom_element_;
}
// Clean up THREE.js scene resources
if (this.scene_ !== undefined) {

View File

@ -67,15 +67,18 @@ beestat.component.scene.prototype.add_raycaster_ = function() {
*/
this.raycaster_pointer_ = new THREE.Vector2(10000, 10000);
// TODO remove event listener on dispose
document.addEventListener('mousemove', function(e) {
this.raycaster_document_mousemove_handler_ = function(e) {
const rect = self.renderer_.domElement.getBoundingClientRect();
self.raycaster_pointer_.x = ( ( e.clientX - rect.left ) / ( rect.right - rect.left ) ) * 2 - 1;
self.raycaster_pointer_.y = - ( ( e.clientY - rect.top ) / ( rect.bottom - rect.top) ) * 2 + 1;
});
// TODO remove event listener on dispose
this.renderer_.domElement.addEventListener('mousedown', this.mousedown_handler_.bind(this));
this.renderer_.domElement.addEventListener('touchstart', this.mousedown_handler_.bind(this));
};
document.addEventListener('mousemove', this.raycaster_document_mousemove_handler_);
this.raycaster_dom_element_ = this.renderer_.domElement;
this.raycaster_dom_mousedown_handler_ = this.mousedown_handler_.bind(this);
this.raycaster_dom_touchstart_handler_ = this.mousedown_handler_.bind(this);
this.raycaster_dom_element_.addEventListener('mousedown', this.raycaster_dom_mousedown_handler_);
this.raycaster_dom_element_.addEventListener('touchstart', this.raycaster_dom_touchstart_handler_);
};

View File

@ -36,7 +36,7 @@ beestat.component.scene.prototype.set_weather = function(weather) {
break;
case 'cloudy':
weather_settings = {
'cloud_density': 1,
'cloud_density': 0.5,
'rain_density': 0,
'snow_density': 0
};