diff --git a/css/dashboard.css b/css/dashboard.css index 0e1b2f5..c592152 100644 --- a/css/dashboard.css +++ b/css/dashboard.css @@ -553,6 +553,8 @@ input[type=range]::-moz-range-thumb { .icon.wifi_strength_1_alert:before { content: "\F0920"; } .icon.wifi_strength_4:before { content: "\F0928"; } .icon.zigbee:before { content: "\F0D41"; } +.icon.label:before { content: "\F0315"; } +.icon.label_off:before { content: "\F0ACB"; } .icon.f16:before { font-size: 16px; } .icon.f24:before { font-size: 24px; } diff --git a/js/.eslintrc.json b/js/.eslintrc.json index afca6fe..0a19574 100644 --- a/js/.eslintrc.json +++ b/js/.eslintrc.json @@ -12,7 +12,8 @@ "Sentry": true, "THREE": true, "SunCalc": true, - "ClipperLib": true + "ClipperLib": true, + "polylabel": true }, "extends": "eslint:all", "rules": { diff --git a/js/component/card/three_d.js b/js/component/card/three_d.js index eaae174..dc844d3 100644 --- a/js/component/card/three_d.js +++ b/js/component/card/three_d.js @@ -547,6 +547,24 @@ beestat.component.card.three_d.prototype.decorate_toolbar_ = function(parent) { .set_text_color(beestat.style.color.lightblue.base) ); + let labels = false; + + // Add room + tile_group.add_tile(new beestat.component.tile() + .set_icon('label_off') + .set_title('Auto-Rotate') + .set_text_color(beestat.style.color.gray.light) + .set_background_color(beestat.style.color.bluegray.base) + .set_background_hover_color(beestat.style.color.bluegray.light) + .addEventListener('click', function() { + labels = !labels; + this.set_icon( + 'label' + (labels === false ? '_off' : '') + ); + self.scene_.set_labels(labels); + }) + ); + let auto_rotate = false; // Add room diff --git a/js/component/scene.js b/js/component/scene.js index bcc7afd..aa24cc3 100644 --- a/js/component/scene.js +++ b/js/component/scene.js @@ -8,6 +8,8 @@ beestat.component.scene = function(floor_plan_id, data) { this.floor_plan_id_ = floor_plan_id; this.data_ = data; + this.label_material_memo_ = []; + beestat.component.apply(this, arguments); }; beestat.extend(beestat.component.scene, beestat.component); @@ -108,6 +110,13 @@ beestat.component.scene.prototype.decorate_ = function(parent) { this.add_main_group_(); this.add_floor_plan_(); + + + + + + + /** * Example of how to do a roof * @@ -450,11 +459,26 @@ beestat.component.scene.prototype.update_ = function() { color = beestat.style.rgb_to_hex( self.gradient_[Math.floor((self.gradient_.length - 1) * percentage)] ); + + // Labels + const label_sprite = self.rooms_[room.room_id].userData.label_sprite; + if (self.labels_ === true) { + label_sprite.material = self.get_label_material_( + beestat.temperature({ + 'temperature': value, + 'units': true + }) + ); + } else { + label_sprite.material = self.get_label_material_(); + } } else { color = beestat.style.color.gray.dark; } self.rooms_[room.room_id].material.color.setHex(color.replace('#', '0x')); + + }); }); @@ -815,6 +839,48 @@ beestat.component.scene.prototype.add_room_ = function(layer, group, room) { this.rooms_[room.room_id] = mesh; layer.add(mesh); + + /** + * LABEL + */ + const label_material = this.get_label_material_(); + const label_sprite = new THREE.Sprite(label_material); + + // Scale to an appropriate-looking size. + const scale_x = 0.16; + const scale_y = scale_x * label_material.map.source.data.height / label_material.map.source.data.width; + label_sprite.scale.set(scale_x, scale_y, 1); + + // Set center of sprite to bottom middle. + label_sprite.center.set(0.5, 0); + + // Determine where the sprite will go. + const geojson_polygon = []; + room.points.forEach(function(point) { + geojson_polygon.push([ + point.x, + point.y + ]); + }); + const label_point = polylabel([geojson_polygon]); + + /** + * Some arbitrary small number so the sprite is *just* above the room or + * when you view from directly above sometimes they disappear. + */ + const z_offset = 1; + + label_sprite.position.set( + room.x + label_point[0], + room.y + label_point[1], + mesh.position.z - z_offset + ); + layer.add(label_sprite); + + mesh.userData.label_sprite = label_sprite; + /** + * LABEL + */ }; /** @@ -985,6 +1051,19 @@ beestat.component.scene.prototype.set_auto_rotate = function(auto_rotate) { return this; }; +/** + * Set whether or not labels are enabled. + * + * @param {boolean} labels + * + * @return {beestat.component.scene} + */ +beestat.component.scene.prototype.set_labels = function(labels) { + this.labels_ = labels; + + return this; +}; + /** * Set the gradient. * @@ -1020,3 +1099,51 @@ beestat.component.scene.prototype.set_camera_state = function(camera_state) { this.camera_.scale ); }; + +/** + * Get a material representing a label. Memoizes the result so the material + * can be re-used. + * + * @param {string} text + * + * @return {THREE.Material} + */ +beestat.component.scene.prototype.get_label_material_ = function(text = '') { + if (this.label_material_memo_[text] === undefined) { + /** + * Increasing the size of the canvas increases the resolution of the texture + * and thus makes it less blurry. + */ + const canvas = document.createElement('canvas'); + canvas.width = 120; + canvas.height = 30; + + const context = canvas.getContext('2d'); + + // Debug red background + // context.fillStyle = 'rgba(255, 0, 0, 0.5)'; + // context.fillRect(0, 0, canvas.width, canvas.height); + + const font_size = canvas.height; + context.font = 'bold ' + font_size + 'px Montserrat'; + context.fillStyle = '#fff'; + context.textAlign = 'center'; + context.fillText( + text, + canvas.width / 2, + canvas.height + ); + + const texture = new THREE.Texture(canvas); + texture.needsUpdate = true; + + const material = new THREE.SpriteMaterial({ + 'map': texture, + 'sizeAttenuation': false + }); + + this.label_material_memo_[text] = material; + } + + return this.label_material_memo_[text]; +};