diff --git a/js/component/card/floor_plan_editor.js b/js/component/card/floor_plan_editor.js
index be6d841..7a17721 100644
--- a/js/component/card/floor_plan_editor.js
+++ b/js/component/card/floor_plan_editor.js
@@ -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);
diff --git a/js/component/card/three_d.js b/js/component/card/three_d.js
index bf36a44..26cf326 100644
--- a/js/component/card/three_d.js
+++ b/js/component/card/three_d.js
@@ -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.
diff --git a/js/component/compass.js b/js/component/compass.js
new file mode 100644
index 0000000..8ec9337
--- /dev/null
+++ b/js/component/compass.js
@@ -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;
+};
diff --git a/js/component/modal/update_floor_plan.js b/js/component/modal/update_floor_plan.js
index 0424564..1a4ce4a 100644
--- a/js/component/modal/update_floor_plan.js
+++ b/js/component/modal/update_floor_plan.js
@@ -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
diff --git a/js/component/scene.js b/js/component/scene.js
index cc16899..c5432b7 100644
--- a/js/component/scene.js
+++ b/js/component/scene.js
@@ -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) {
diff --git a/js/js.php b/js/js.php
index c3bd3d0..996e1b1 100755
--- a/js/js.php
+++ b/js/js.php
@@ -131,6 +131,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
+ echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;
echo '' . PHP_EOL;