1
0
mirror of https://github.com/beestat/app.git synced 2026-03-20 08:27:59 -04:00
This commit is contained in:
Jon Ziebell 2026-02-13 14:01:25 -05:00
parent fa8e2e862a
commit f3877f9103
6 changed files with 316 additions and 64 deletions

View File

@ -176,6 +176,11 @@ beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = func
this.floor_plan_.dispose();
}
// Dispose existing compass
if (this.compass_ !== undefined) {
this.compass_.dispose();
}
// Create and render a new SVG component.
this.floor_plan_ = new beestat.component.floor_plan(
beestat.setting('visualize.floor_plan_id'),
@ -184,6 +189,17 @@ beestat.component.card.floor_plan_editor.prototype.decorate_drawing_pane_ = func
this.floor_plan_.render(parent);
// Create and render the compass for setting orientation
this.compass_ = new beestat.component.compass(
beestat.setting('visualize.floor_plan_id')
);
this.compass_.render(parent);
// Update floor plan when rotation changes
this.compass_.addEventListener('rotation_change', function() {
self.update_floor_plan_();
});
setTimeout(function() {
if (parent.getBoundingClientRect().width > 0) {
self.floor_plan_.set_width(parent.getBoundingClientRect().width);

View File

@ -359,29 +359,28 @@ beestat.component.card.three_d.prototype.decorate_drawing_pane_ = function(paren
this.update_scene_();
this.scene_.render($(parent));
// Get current time of day
const now = moment();
const current_hour = now.hour();
const current_minute = now.minute();
if (beestat.setting('visualize.range_type') === 'dynamic') {
const sensor_ids = Object.keys(
beestat.floor_plan.get_sensor_ids_map(this.floor_plan_id_)
);
if (
beestat.setting('visualize.range_dynamic') === 0 &&
sensor_ids.length > 0
) {
this.date_m_ = this.get_most_recent_time_with_data_();
} else {
this.date_m_ = moment()
.subtract(
beestat.setting('visualize.range_dynamic'),
'day'
)
.hour(0)
.minute(0)
.second(0);
}
// Set the date, then apply current time of day
this.date_m_ = moment()
.subtract(
beestat.setting('visualize.range_dynamic'),
'day'
)
.hour(current_hour)
.minute(current_minute)
.second(0);
} else {
// Set the static date, then apply current time of day
this.date_m_ = moment(
beestat.setting('visualize.range_static.begin') + ' 00:00:00'
);
)
.hour(current_hour)
.minute(current_minute);
}
// Set some defaults on the scene.

258
js/component/compass.js Normal file
View File

@ -0,0 +1,258 @@
/**
* Interactive compass component for setting floor plan rotation.
* Allows users to visually set which direction is "North" for their home.
*
* @param {number} floor_plan_id The floor plan ID
*/
beestat.component.compass = function(floor_plan_id) {
this.floor_plan_id_ = floor_plan_id;
beestat.component.apply(this, arguments);
};
beestat.extend(beestat.component.compass, beestat.component);
/**
* Render the compass.
*
* @param {HTMLElement} parent
*
* @return {beestat.component.compass} This
*/
beestat.component.compass.prototype.render = function(parent) {
const self = this;
// Container for the compass
this.container_ = document.createElement('div');
Object.assign(this.container_.style, {
'position': 'absolute',
'bottom': `${beestat.style.size.gutter}px`,
'right': `${beestat.style.size.gutter}px`,
'width': '80px',
'height': '80px',
'user-select': 'none',
'cursor': 'grab',
'z-index': '10'
});
this.container_.title = 'Drag to set which direction is North for sun/moon positioning';
// Create SVG
this.svg_ = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.svg_.setAttribute('width', '80');
this.svg_.setAttribute('height', '80');
this.svg_.setAttribute('viewBox', '0 0 80 80');
this.container_.appendChild(this.svg_);
// Background circle
const bg_circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
bg_circle.setAttribute('cx', '40');
bg_circle.setAttribute('cy', '40');
bg_circle.setAttribute('r', '38');
bg_circle.setAttribute('fill', beestat.style.color.bluegray.base);
bg_circle.setAttribute('stroke', '#fff');
bg_circle.setAttribute('stroke-width', '2');
bg_circle.setAttribute('opacity', '0.9');
this.svg_.appendChild(bg_circle);
// Create rotating group for compass rose
this.compass_group_ = document.createElementNS('http://www.w3.org/2000/svg', 'g');
this.compass_group_.setAttribute('transform', 'translate(40, 40)');
this.svg_.appendChild(this.compass_group_);
// Cardinal direction markers
const directions = [
{label: 'N', angle: 0, color: beestat.style.color.red.base, size: 14},
{label: 'E', angle: 90, color: '#fff', size: 10},
{label: 'S', angle: 180, color: '#fff', size: 10},
{label: 'W', angle: 270, color: '#fff', size: 10}
];
directions.forEach(function(dir) {
const angle_rad = (dir.angle - 90) * Math.PI / 180; // -90 to start at top
const radius = 28;
const x = radius * Math.cos(angle_rad);
const y = radius * Math.sin(angle_rad);
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', x);
text.setAttribute('y', y);
text.setAttribute('text-anchor', 'middle');
text.setAttribute('dominant-baseline', 'middle');
text.setAttribute('fill', dir.color);
text.setAttribute('font-size', dir.size);
text.setAttribute('font-weight', dir.label === 'N' ? 'bold' : 'normal');
text.setAttribute('pointer-events', 'none');
text.textContent = dir.label;
self.compass_group_.appendChild(text);
});
// North arrow indicator
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
arrow.setAttribute('d', 'M 0,-22 L 4,-10 L 0,-12 L -4,-10 Z');
arrow.setAttribute('fill', beestat.style.color.red.base);
arrow.setAttribute('stroke', '#fff');
arrow.setAttribute('stroke-width', '1');
arrow.setAttribute('pointer-events', 'none');
this.compass_group_.appendChild(arrow);
// Center dot
const center_dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
center_dot.setAttribute('cx', '0');
center_dot.setAttribute('cy', '0');
center_dot.setAttribute('r', '3');
center_dot.setAttribute('fill', '#fff');
center_dot.setAttribute('pointer-events', 'none');
this.compass_group_.appendChild(center_dot);
// Set initial rotation
this.update_rotation_(this.get_rotation_());
// Make it draggable
this.set_draggable_();
parent.appendChild(this.container_);
this.rendered_ = true;
return this;
};
/**
* Get the current rotation value from the floor plan data.
*
* @return {number} Rotation in degrees (0-359)
*/
beestat.component.compass.prototype.get_rotation_ = function() {
const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
return floor_plan.data.appearance?.rotation || 0;
};
/**
* Set the rotation value in the floor plan data.
*
* @param {number} degrees Rotation in degrees (0-359)
*/
beestat.component.compass.prototype.set_rotation_ = function(degrees) {
const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
if (floor_plan.data.appearance === undefined) {
floor_plan.data.appearance = {};
}
// Normalize to 0-359 and store as integer
const normalized = Math.round(((degrees % 360) + 360) % 360);
floor_plan.data.appearance.rotation = normalized;
this.update_rotation_(normalized);
// Dispatch event so other components know rotation changed
this.dispatchEvent('rotation_change', normalized);
};
/**
* Update the visual rotation of the compass.
*
* @param {number} degrees Rotation in degrees
*/
beestat.component.compass.prototype.update_rotation_ = function(degrees) {
if (this.compass_group_) {
this.compass_group_.setAttribute(
'transform',
`translate(40, 40) rotate(${degrees})`
);
}
};
/**
* Make the compass draggable to change rotation.
*/
beestat.component.compass.prototype.set_draggable_ = function() {
const self = this;
let is_dragging = false;
const get_angle = function(e) {
const rect = self.svg_.getBoundingClientRect();
const center_x = rect.left + rect.width / 2;
const center_y = rect.top + rect.height / 2;
const client_x = e.clientX || (e.touches && e.touches[0].clientX);
const client_y = e.clientY || (e.touches && e.touches[0].clientY);
const dx = client_x - center_x;
const dy = client_y - center_y;
// Calculate angle in degrees (0° = North/up, clockwise)
let angle = Math.atan2(dy, dx) * 180 / Math.PI + 90;
if (angle < 0) {
angle += 360;
}
return angle;
};
const handle_start = function(e) {
is_dragging = true;
self.container_.style.cursor = 'grabbing';
e.preventDefault();
};
const handle_move = function(e) {
if (!is_dragging) {
return;
}
const angle = get_angle(e);
self.set_rotation_(angle);
e.preventDefault();
};
const handle_end = function(e) {
if (is_dragging) {
is_dragging = false;
self.container_.style.cursor = 'grab';
e.preventDefault();
}
};
// Store handlers for cleanup
this.handlers_ = {
'mouse_start': handle_start,
'mouse_move': handle_move,
'mouse_end': handle_end,
'touch_start': handle_start,
'touch_move': handle_move,
'touch_end': handle_end
};
// Mouse events
this.container_.addEventListener('mousedown', this.handlers_.mouse_start);
window.addEventListener('mousemove', this.handlers_.mouse_move);
window.addEventListener('mouseup', this.handlers_.mouse_end);
// Touch events
this.container_.addEventListener('touchstart', this.handlers_.touch_start);
window.addEventListener('touchmove', this.handlers_.touch_move);
window.addEventListener('touchend', this.handlers_.touch_end);
};
/**
* Dispose of the compass.
*/
beestat.component.compass.prototype.dispose = function() {
// Remove event listeners
if (this.handlers_) {
if (this.container_) {
this.container_.removeEventListener('mousedown', this.handlers_.mouse_start);
this.container_.removeEventListener('touchstart', this.handlers_.touch_start);
}
window.removeEventListener('mousemove', this.handlers_.mouse_move);
window.removeEventListener('mouseup', this.handlers_.mouse_end);
window.removeEventListener('touchmove', this.handlers_.touch_move);
window.removeEventListener('touchend', this.handlers_.touch_end);
}
// Remove from DOM
if (this.container_ && this.container_.parentNode) {
this.container_.parentNode.removeChild(this.container_);
}
// Mark as not rendered
this.rendered_ = false;
};

View File

@ -79,10 +79,6 @@ beestat.component.modal.update_floor_plan.prototype.decorate_contents_ = functio
(new beestat.component.title('Appearance')).render(parent);
parent.appendChild($.createElement('p').innerHTML('Customize how your floor plan looks in 3D view.'));
// Debug: Log current appearance data
console.log('Floor plan appearance data:', floor_plan.data.appearance);
console.log('State appearance:', self.state_.appearance);
const appearance_grid = document.createElement('div');
Object.assign(appearance_grid.style, {
'display': 'grid',
@ -93,29 +89,6 @@ beestat.component.modal.update_floor_plan.prototype.decorate_contents_ = functio
});
parent.appendChild(appearance_grid);
// Rotation input
const rotation_input = new beestat.component.input.text()
.set_label('Rotation (°)')
.set_width('100%')
.set_icon('rotate-right');
const current_rotation = self.state_.appearance?.rotation !== undefined
? self.state_.appearance.rotation
: (floor_plan.data.appearance?.rotation || 0);
rotation_input.set_value(String(current_rotation));
rotation_input.addEventListener('change', function() {
if (self.state_.appearance === undefined) {
self.state_.appearance = {};
}
const value = parseInt(rotation_input.get_value(), 10);
if (!isNaN(value) && value >= 0 && value <= 359) {
self.state_.appearance.rotation = value;
}
});
rotation_input.render($(appearance_grid));
// Roof Style dropdown
const roof_style_select = new beestat.component.input.select()
.set_label('Roof Style')
@ -172,7 +145,6 @@ beestat.component.modal.update_floor_plan.prototype.decorate_contents_ = functio
const current_roof_color = self.state_.appearance?.roof_color !== undefined
? self.state_.appearance.roof_color
: (floor_plan.data.appearance?.roof_color || '#3a3a3a');
console.log('Setting roof color to:', current_roof_color);
roof_color_select.set_value(current_roof_color);
// Siding Color dropdown
@ -209,7 +181,6 @@ beestat.component.modal.update_floor_plan.prototype.decorate_contents_ = functio
const current_siding_color = self.state_.appearance?.siding_color !== undefined
? self.state_.appearance.siding_color
: (floor_plan.data.appearance?.siding_color || '#889aaa');
console.log('Setting siding color to:', current_siding_color);
siding_color_select.set_value(current_siding_color);
// Ground Color dropdown
@ -243,7 +214,6 @@ beestat.component.modal.update_floor_plan.prototype.decorate_contents_ = functio
const current_ground_color = self.state_.appearance?.ground_color !== undefined
? self.state_.appearance.ground_color
: (floor_plan.data.appearance?.ground_color || '#4a7c3f');
console.log('Setting ground color to:', current_ground_color);
ground_color_select.set_value(current_ground_color);
// Address

View File

@ -533,14 +533,10 @@ beestat.component.scene.prototype.update_celestial_lights_ = function(date, lati
const js_date = date.toDate();
// Get user rotation offset (0 = North, clockwise)
const rotation_degrees = this.get_appearance_value_('rotation');
const rotation_offset = (rotation_degrees * Math.PI) / 180;
// === SUN ===
const sun_position = SunCalc.getPosition(js_date, latitude, longitude);
const sun_altitude = sun_position.altitude;
const sun_azimuth = sun_position.azimuth + rotation_offset;
const sun_azimuth = sun_position.azimuth;
// Convert spherical coordinates to Cartesian
// SunCalc: azimuth 0=south, π/2=west, π/-π=north, -π/2=east
@ -565,7 +561,7 @@ beestat.component.scene.prototype.update_celestial_lights_ = function(date, lati
// === MOON ===
const moon_position = SunCalc.getMoonPosition(js_date, latitude, longitude);
const moon_altitude = moon_position.altitude;
const moon_azimuth = moon_position.azimuth + rotation_offset;
const moon_azimuth = moon_position.azimuth;
// Get moon illumination (phase)
const moon_illumination = SunCalc.getMoonIllumination(js_date);
@ -1112,10 +1108,22 @@ beestat.component.scene.prototype.update_debug_ = function() {
beestat.component.scene.prototype.add_main_group_ = function() {
const bounding_box = beestat.floor_plan.get_bounding_box(this.floor_plan_id_);
// Main group handles rotation and orientation
this.main_group_ = new THREE.Group();
this.main_group_.translateX((bounding_box.right + bounding_box.left) / -2);
this.main_group_.translateZ((bounding_box.bottom + bounding_box.top) / -2);
// Apply X rotation to orient the floor plan
this.main_group_.rotation.x = Math.PI / 2;
// Apply user-defined rotation around Z axis (vertical axis after X rotation)
const rotation_degrees = this.get_appearance_value_('rotation');
this.main_group_.rotation.z = (rotation_degrees * Math.PI) / 180;
// Content group is offset to center the geometry at the rotation point
this.content_group_ = new THREE.Group();
this.content_group_.translateX((bounding_box.right + bounding_box.left) / -2);
this.content_group_.translateZ((bounding_box.bottom + bounding_box.top) / -2);
this.main_group_.add(this.content_group_);
this.scene_.add(this.main_group_);
};
@ -1129,12 +1137,12 @@ beestat.component.scene.prototype.add_floor_plan_ = function() {
this.layers_ = {};
const walls_layer = new THREE.Group();
self.main_group_.add(walls_layer);
self.content_group_.add(walls_layer);
self.layers_['walls'] = walls_layer;
floor_plan.data.groups.forEach(function(group) {
const layer = new THREE.Group();
self.main_group_.add(layer);
self.content_group_.add(layer);
self.layers_[group.group_id] = layer;
group.rooms.forEach(function(room) {
self.add_room_(layer, group, room);
@ -1343,7 +1351,7 @@ beestat.component.scene.prototype.add_hip_roofs_ = function() {
// Create layer for roofs
const roofs_layer = new THREE.Group();
this.main_group_.add(roofs_layer);
this.content_group_.add(roofs_layer);
this.layers_['roof'] = roofs_layer;
const roof_pitch = beestat.component.scene.roof_pitch;
@ -1518,7 +1526,7 @@ beestat.component.scene.prototype.add_flat_roofs_ = function() {
// Create layer for roofs
const roofs_layer = new THREE.Group();
this.main_group_.add(roofs_layer);
this.content_group_.add(roofs_layer);
this.layers_['roof'] = roofs_layer;
// Process each exposed area
@ -1605,7 +1613,7 @@ beestat.component.scene.prototype.add_roof_outlines_ = function() {
// Create layer for roof outlines
const roof_outlines_layer = new THREE.Group();
this.main_group_.add(roof_outlines_layer);
this.content_group_.add(roof_outlines_layer);
this.layers_['roof_outlines'] = roof_outlines_layer;
// Render each exposed area as red outline
@ -1651,7 +1659,7 @@ beestat.component.scene.prototype.add_roof_skeleton_debug_ = function() {
// Create layer for skeleton debug lines
const skeleton_debug_layer = new THREE.Group();
this.main_group_.add(skeleton_debug_layer);
this.content_group_.add(skeleton_debug_layer);
this.layers_['roof_skeleton_debug'] = skeleton_debug_layer;
let total_polygons = 0;
@ -1808,7 +1816,7 @@ beestat.component.scene.prototype.add_environment_ = function() {
];
const environment_layer = new THREE.Group();
this.main_group_.add(environment_layer);
this.content_group_.add(environment_layer);
this.layers_['environment'] = environment_layer;
strata.forEach(function(stratum) {

View File

@ -131,6 +131,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '<script src="/js/component/menu_item.js"></script>' . PHP_EOL;
echo '<script src="/js/component/scene.js"></script>' . PHP_EOL;
echo '<script src="/js/component/floor_plan.js"></script>' . PHP_EOL;
echo '<script src="/js/component/compass.js"></script>' . PHP_EOL;
echo '<script src="/js/component/radio_group.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/runtime_thermostat_summary_custom.js"></script>' . PHP_EOL;