/** * Home Scene * * @param {number} floor_plan_id The floor plan to render. * @param {object} data Sensor data. */ 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); beestat.component.scene.layer_visible = 0; beestat.component.scene.layer_hidden = 1; beestat.component.scene.layer_outline = 2; /** * Brightness of the top-down light. This gives definition to the sides of * meshes by lighting the tops. Increase this for more edge definition. */ beestat.component.scene.directional_light_top_intensity = 0.25; /** * Brightness of the ambient light. Works with the top light to provide a base * level of light to the scene. */ beestat.component.scene.ambient_light_intensity = 0.3; /** * Rerender the scene by removing the primary group, then re-adding it and the * floor plan. This avoids having to reconstruct everything and then also * having to manually save camera info etc. */ beestat.component.scene.prototype.rerender = function() { this.scene_.remove(this.main_group_); this.add_main_group_(); this.add_floor_plan_(); }; /** * Set the width of this component. * * @param {number} width */ beestat.component.scene.prototype.set_width = function(width) { this.width_ = width; this.camera_.aspect = this.width_ / this.height_; this.camera_.updateProjectionMatrix(); this.renderer_.setSize(this.width_, this.height_); }; /** * Decorate * * @param {rocket.Elements} parent */ beestat.component.scene.prototype.decorate_ = function(parent) { const self = this; // Dark background to help reduce apparant flicker when resizing parent.style('background', '#202a30'); this.debug_ = { 'axes': false, 'directional_light_top_helper': false, // 'grid': false, 'watcher': false }; this.width_ = this.state_.scene_width || 800; this.height_ = 500; this.add_scene_(parent); this.add_renderer_(parent); this.add_camera_(); this.add_controls_(parent); this.add_raycaster_(parent); this.add_skybox_(parent); this.add_ambient_light_(); this.add_directional_light_top_(); this.add_main_group_(); this.add_floor_plan_(); const animate = function() { self.animation_frame_ = window.requestAnimationFrame(animate); self.controls_.update(); self.update_raycaster_(); self.renderer_.render(self.scene_, self.camera_); }; animate(); }; /** * Add the scene. Everything gets added to the scene. * * @param {rocket.Elements} parent Parent */ beestat.component.scene.prototype.add_scene_ = function(parent) { this.scene_ = new THREE.Scene(); if (this.debug_.axes === true) { this.scene_.add( new THREE.AxesHelper(800) .setColors( 0xff0000, 0x00ff00, 0x0000ff ) ); } if (this.debug_.watcher === true) { this.debug_info_ = {}; this.debug_container_ = $.createElement('div').style({ 'position': 'absolute', 'top': (beestat.style.size.gutter / 2), 'left': (beestat.style.size.gutter / 2), 'padding': (beestat.style.size.gutter / 2), 'background': 'rgba(0, 0, 0, 0.5)', 'color': '#fff', 'font-family': 'Consolas, Courier, Monospace', 'white-space': 'pre' }); parent.appendChild(this.debug_container_); } }; /** * Add the renderer. * * @param {rocket.Elements} parent */ beestat.component.scene.prototype.add_renderer_ = function(parent) { this.renderer_ = new THREE.WebGLRenderer({ 'antialias': true }); this.renderer_.setPixelRatio(window.devicePixelRatio); this.renderer_.setSize(this.width_, this.height_); parent[0].appendChild(this.renderer_.domElement); }; /** * Add a camera and point it at the scene. */ beestat.component.scene.prototype.add_camera_ = function() { const field_of_view = 75; const aspect_ratio = window.innerWidth / window.innerHeight; const near_plane = 1; const far_plane = 100000; this.camera_ = new THREE.PerspectiveCamera( field_of_view, aspect_ratio, near_plane, far_plane ); this.camera_.layers.enable(beestat.component.scene.layer_visible); this.camera_.layers.enable(beestat.component.scene.layer_outline); this.camera_.position.x = 400; this.camera_.position.y = 400; this.camera_.position.z = 400; }; /** * Add camera controls. * * @param {rocket.Elements} parent */ 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 = 1000; this.controls_.minDistance = 400; this.controls_.maxPolarAngle = Math.PI / 2.5; }; /** * Initialize a click. * * @param {Event} e */ beestat.component.scene.prototype.mousedown_handler_ = function(e) { // Don't propagate to things under me. e.stopPropagation(); this.mousemove_handler_ = this.mousemove_handler_.bind(this); window.addEventListener('mousemove', this.mousemove_handler_); window.addEventListener('touchmove', this.mousemove_handler_); this.mouseup_handler_ = this.mouseup_handler_.bind(this); window.addEventListener('mouseup', this.mouseup_handler_); window.addEventListener('touchend', this.mouseup_handler_); this.dragged_ = false; }; /** * Added after mousedown, so when the mouse moves just set dragged = true. */ beestat.component.scene.prototype.mousemove_handler_ = function() { this.dragged_ = true; }; /** * Set an active mesh if it wasn't a drag. */ beestat.component.scene.prototype.mouseup_handler_ = function() { window.removeEventListener('mousemove', this.mousemove_handler_); window.removeEventListener('touchmove', this.mousemove_handler_); window.removeEventListener('mouseup', this.mouseup_handler_); window.removeEventListener('touchend', this.mouseup_handler_); if (this.dragged_ === false) { this.active_mesh_ = this.intersected_mesh_; this.dispatchEvent('change_active_room'); this.update_(); } }; /** * Add the raycaster. * * @param {rocket.Elements} parent */ beestat.component.scene.prototype.add_raycaster_ = function() { const self = this; this.raycaster_ = new THREE.Raycaster(); this.raycaster_.layers.set(beestat.component.scene.layer_visible); /** * Initialize a pointer representing the raycaster. Initialize it pointing * way off screen instead of 0,0 so nothing starts thinking the mouse is * over it. */ this.raycaster_pointer_ = new THREE.Vector2(10000, 10000); // TODO remove event listener on dispose document.addEventListener('mousemove', 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)); }; /** * Update the raycaster. * * @param {rocket.Elements} parent */ beestat.component.scene.prototype.update_raycaster_ = function() { if (this.raycaster_ !== undefined) { this.raycaster_.setFromCamera(this.raycaster_pointer_, this.camera_); const intersects = this.raycaster_.intersectObject(this.scene_); // Clear any existing intersects. if (this.intersected_mesh_ !== undefined) { document.body.style.cursor = ''; this.intersected_mesh_.material.emissive.setHex(0x000000); delete this.intersected_mesh_; } // Set intersect. for (let i = 0; i < intersects.length; i++) { if (intersects[i].object.type === 'Mesh') { this.intersected_mesh_ = intersects[i].object; break; } } // Style intersect. if (this.intersected_mesh_ !== undefined) { this.intersected_mesh_.material.emissive.setHex(0xffffff); this.intersected_mesh_.material.emissiveIntensity = 0.1; document.body.style.cursor = 'pointer'; } } }; /** * Add a skybox background. Generated using Spacescape with the Unity export * settings. After export: bottom is rotated CW 90°; top is roted 90°CCW. * * nx -> bk * ny -> dn * nz -> lf * px -> ft * py -> up * pz -> rt * * @link https://www.mapcore.org/topic/24535-online-tools-to-convert-cubemaps-to-panoramas-and-vice-versa/ * @link https://jaxry.github.io/panorama-to-cubemap/ * @link http://alexcpeterson.com/spacescape/ */ beestat.component.scene.prototype.add_skybox_ = function() { const loader = new THREE.CubeTextureLoader(); loader.setPath('img/visualize/skybox/'); const texture = loader.load([ 'front.png', 'back.png', 'up.png', 'down.png', 'right.png', 'left.png' ]); this.scene_.background = texture; }; /** * Consistent directional light that provides definition to the edge of meshes * by lighting the top. */ beestat.component.scene.prototype.add_directional_light_top_ = function() { this.directional_light_top_ = new THREE.DirectionalLight( 0xffffff, beestat.component.scene.directional_light_top_intensity ); this.directional_light_top_.position.set(0, 1000, 0); this.scene_.add(this.directional_light_top_); if (this.debug_.directional_light_top_helper === true) { this.directional_light_top_helper_ = new THREE.DirectionalLightHelper( this.directional_light_top_, 500 ); this.scene_.add(this.directional_light_top_helper_); } }; /** * Ambient lighting so nothing is shrouded in darkness. */ beestat.component.scene.prototype.add_ambient_light_ = function() { this.scene_.add(new THREE.AmbientLight( 0xffffff, beestat.component.scene.ambient_light_intensity )); }; /** * Update the scene based on the currently set date. */ beestat.component.scene.prototype.update_ = function() { const self = this; const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_]; const time = this.date_.format('HH:mm'); // Set the color of each room floor_plan.data.groups.forEach(function(group) { group.rooms.forEach(function(room) { const value_sprite = self.meshes_[room.room_id].userData.sprites.value; const icon_sprite = self.meshes_[room.room_id].userData.sprites.icon; // Room outline if (self.meshes_[room.room_id] === self.active_mesh_) { self.meshes_[room.room_id].userData.outline.visible = true; } else { self.meshes_[room.room_id].userData.outline.visible = false; } let color; if ( room.sensor_id !== undefined && self.data_.series[self.data_type_][room.sensor_id] !== undefined && self.data_.series[self.data_type_][room.sensor_id][time] !== undefined ) { const value = self.data_.series[self.data_type_][room.sensor_id][time]; /** * Set the percentage between the min and max. Special case for if min * and max are equal to avoid math issues. */ let percentage; if ( self.heat_map_min_ === self.heat_map_max_ && value === self.heat_map_min_ ) { percentage = 0.5; } else { percentage = Math.min( 1, Math.max( 0, (value - self.heat_map_min_) / (self.heat_map_max_ - self.heat_map_min_) ) ); } color = beestat.style.rgb_to_hex( self.gradient_[Math.floor((self.gradient_.length - 1) * percentage)] ); // TODO this technically doesn't handle if both heating and cooling is active in a range const sensor = beestat.cache.sensor[room.sensor_id]; let icon; let icon_opacity; if (sensor !== undefined) { if ( self.data_.series.compressor_cool_1[sensor.thermostat_id][time] !== undefined && self.data_.series.compressor_cool_1[sensor.thermostat_id][time] > 0 ) { icon = 'snowflake'; icon_opacity = self.data_.series.compressor_cool_1[sensor.thermostat_id][time]; } else if ( self.data_.series.compressor_cool_2[sensor.thermostat_id][time] !== undefined && self.data_.series.compressor_cool_2[sensor.thermostat_id][time] > 0 ) { icon = 'snowflake'; icon_opacity = self.data_.series.compressor_cool_2[sensor.thermostat_id][time]; } else if ( self.data_.series.compressor_heat_1[sensor.thermostat_id][time] !== undefined && self.data_.series.compressor_heat_1[sensor.thermostat_id][time] > 0 ) { icon = 'fire'; icon_opacity = self.data_.series.compressor_heat_1[sensor.thermostat_id][time]; } else if ( self.data_.series.compressor_heat_2[sensor.thermostat_id][time] !== undefined && self.data_.series.compressor_heat_2[sensor.thermostat_id][time] > 0 ) { icon = 'fire'; icon_opacity = self.data_.series.compressor_heat_2[sensor.thermostat_id][time]; } else if ( self.data_.series.auxiliary_heat_1[sensor.thermostat_id][time] !== undefined && self.data_.series.auxiliary_heat_1[sensor.thermostat_id][time] > 0 ) { icon = 'fire'; icon_opacity = self.data_.series.auxiliary_heat_1[sensor.thermostat_id][time]; } else if ( self.data_.series.auxiliary_heat_2[sensor.thermostat_id][time] !== undefined && self.data_.series.auxiliary_heat_2[sensor.thermostat_id][time] > 0 ) { icon = 'fire'; icon_opacity = self.data_.series.auxiliary_heat_2[sensor.thermostat_id][time]; } else if ( self.data_.series.fan[sensor.thermostat_id][time] !== undefined && self.data_.series.fan[sensor.thermostat_id][time] > 0 ) { icon = 'fan'; icon_opacity = self.data_.series.fan[sensor.thermostat_id][time]; } icon_opacity = Math.round(icon_opacity * 10) / 10; } // Labels if ( self.labels_ === true || self.meshes_[room.room_id] === self.active_mesh_ ) { switch (self.data_type_) { case 'temperature': value_sprite.material = self.get_label_material_({ 'type': 'value', 'value': beestat.temperature({ 'temperature': value, 'type': 'string', 'units': true }) }); icon_sprite.material = self.get_label_material_({ 'type': 'icon', 'icon': icon, 'color': 'rgba(255, 255, 255, ' + icon_opacity + ')' }); break; case 'occupancy': value_sprite.material = self.get_label_material_({ 'type': 'value', 'value': Math.round(value) + '%' }); icon_sprite.material = self.get_blank_label_material_(); break; } } else { value_sprite.material = self.get_blank_label_material_(); icon_sprite.material = self.get_blank_label_material_(); } } else { color = beestat.style.color.gray.dark; value_sprite.material = self.get_blank_label_material_(); icon_sprite.material = self.get_blank_label_material_(); } self.meshes_[room.room_id].material.color.setHex(color.replace('#', '0x')); }); }); if (this.debug_.directional_light_top_helper === true) { this.directional_light_top_helper_.update(); } // Update debug watcher if (this.debug_.watcher === true) { this.update_debug_(); } }; /** * Add a room. Room coordinates are absolute. * * @param {THREE.Group} layer The layer the room belongs to. * @param {object} group The group the room belongs to. * @param {object} room The room to add. */ beestat.component.scene.prototype.add_room_ = function(layer, group, room) { const self = this; const color = beestat.style.color.gray.dark; var clipper_offset = new ClipperLib.ClipperOffset(); clipper_offset.AddPath( room.points, ClipperLib.JoinType.jtSquare, ClipperLib.EndType.etClosedPolygon ); var clipper_hole = new ClipperLib.Path(); clipper_offset.Execute(clipper_hole, -1.5); // Full height // const extrude_height = (room.height || group.height) - 3; // Just the floor plan const extrude_height = 6; // Create a shape using the points of the room. const shape = new THREE.Shape(); const first_point = clipper_hole[0].shift(); shape.moveTo(first_point.x, first_point.y); clipper_hole[0].forEach(function(point) { shape.lineTo(point.x, point.y); }); // Extrude the shape and create the mesh. const extrude_settings = { 'depth': extrude_height, 'bevelEnabled': false }; const geometry = new THREE.ExtrudeGeometry( shape, extrude_settings ); const material = new THREE.MeshPhongMaterial({ 'color': color }); if ( room.sensor_id === undefined || beestat.cache.sensor[room.sensor_id] === undefined ) { const loader = new THREE.TextureLoader(); loader.load( 'img/visualize/stripe.png', function(texture) { texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; texture.repeat.set(0.005, 0.005); material.map = texture; material.needsUpdate = true; } ); } const mesh = new THREE.Mesh(geometry, material); mesh.position.z = -extrude_height - (room.elevation || group.elevation); // Translate the mesh to the room x/y position. mesh.translateX(room.x); mesh.translateY(room.y); // Store a reference to the mesh representing each room. if (this.meshes_ === undefined) { this.meshes_ = {}; } // Allow me to go from room -> mesh and mesh -> room this.meshes_[room.room_id] = mesh; // mesh.userData.room_id = room.room_id; mesh.userData.room = room; layer.add(mesh); // Label mesh.userData.sprites = {}; // Outline const edges_geometry = new THREE.EdgesGeometry(geometry); const outline = new THREE.LineSegments( edges_geometry, new THREE.LineBasicMaterial({ 'color': '#ffffff' }) ); outline.translateX(room.x); outline.translateY(room.y); outline.position.z = -extrude_height - (room.elevation || group.elevation); outline.visible = false; outline.layers.set(beestat.component.scene.layer_outline); mesh.userData.outline = outline; layer.add(outline); // Determine where the sprites will go. const geojson_polygon = []; room.points.forEach(function(point) { geojson_polygon.push([ point.x, point.y ]); }); const label_point = polylabel([geojson_polygon]); [ 'value', 'icon' ].forEach(function(sprite_type) { const sprite_material = self.get_blank_label_material_(); const sprite = new THREE.Sprite(sprite_material); // Scale to an appropriate-looking size. const scale_x = 0.14; const scale_y = scale_x * sprite_material.map.source.data.height / sprite_material.map.source.data.width; sprite.scale.set(scale_x, scale_y, 1); // Set center of sprite to bottom middle. sprite.center.set(0.5, 0); /** * 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; sprite.position.set( room.x + label_point[0], room.y + label_point[1], mesh.position.z - z_offset ); layer.add(sprite); mesh.userData.sprites[sprite_type] = sprite; }); }; /** * Add a helpful debug window that can be refreshed with the contents of * this.debug_info_. * * @param {rocket.Elements} parent */ beestat.component.scene.prototype.add_debug_ = function(parent) { if (this.debug_.watcher === true) { this.debug_info_ = {}; this.debug_container_ = $.createElement('div').style({ 'position': 'absolute', 'top': (beestat.style.size.gutter / 2), 'left': (beestat.style.size.gutter / 2), 'padding': (beestat.style.size.gutter / 2), 'background': 'rgba(0, 0, 0, 0.5)', 'color': '#fff', 'font-family': 'Consolas, Courier, Monospace', 'white-space': 'pre' }); parent.appendChild(this.debug_container_); } }; /** * Update the debug window. */ beestat.component.scene.prototype.update_debug_ = function() { if (this.debug_.watcher === true) { this.debug_container_.innerHTML( JSON.stringify(this.debug_info_, null, 2) ); } }; /** * Add a group containing all of the extruded geometry that can be transformed * all together. */ beestat.component.scene.prototype.add_main_group_ = function() { const bounding_box = beestat.floor_plan.get_bounding_box(this.floor_plan_id_); 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); this.main_group_.rotation.x = Math.PI / 2; this.scene_.add(this.main_group_); }; /** * Add the floor plan to the scene. */ beestat.component.scene.prototype.add_floor_plan_ = function() { const self = this; const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_]; this.layers_ = {}; floor_plan.data.groups.forEach(function(group) { const layer = new THREE.Group(); self.main_group_.add(layer); self.layers_[group.group_id] = layer; group.rooms.forEach(function(room) { self.add_room_(layer, group, room); }); }); }; /** * Set the current date. * * @param {moment} date * * @return {beestat.component.scene} */ beestat.component.scene.prototype.set_date = function(date) { this.date_ = date; if (this.rendered_ === true) { this.update_(); } return this; }; /** * Set the type of data this scene is visualizing. * * @param {string} data_type temperature|occupancy * * @return {beestat.component.scene} */ beestat.component.scene.prototype.set_data_type = function(data_type) { this.data_type_ = data_type; if (this.rendered_ === true) { this.update_(); } return this; }; /** * Set the min value of the heat map. * * @param {string} heat_map_min * * @return {beestat.component.scene} */ beestat.component.scene.prototype.set_heat_map_min = function(heat_map_min) { this.heat_map_min_ = heat_map_min; if (this.rendered_ === true) { this.update_(); } return this; }; /** * Set the max value of the heat map. * * @param {string} heat_map_max * * @return {beestat.component.scene} */ beestat.component.scene.prototype.set_heat_map_max = function(heat_map_max) { this.heat_map_max_ = heat_map_max; if (this.rendered_ === true) { this.update_(); } return this; }; /** * Set the visibility of a layer. * * @param {string} layer_name * @param {boolean} visible * * @return {beestat.component.scene} */ beestat.component.scene.prototype.set_layer_visible = function(layer_name, visible) { this.layers_[layer_name].traverse(function(child) { child.layers.set( visible === true ? beestat.component.scene.layer_visible : beestat.component.scene.layer_hidden ); }); return this; }; /** * Set whether or not auto-rotate is enabled. * * @param {boolean} auto_rotate * * @return {beestat.component.scene} */ beestat.component.scene.prototype.set_auto_rotate = function(auto_rotate) { this.controls_.autoRotate = 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; this.update_(); return this; }; /** * Set the gradient. * * @param {boolean} gradient * * @return {beestat.component.scene} */ beestat.component.scene.prototype.set_gradient = function(gradient) { this.gradient_ = gradient; return this; }; /** * Get the state of the camera. * * @return {object} */ beestat.component.scene.prototype.get_camera_state = function() { return this.camera_.matrix.toArray(); }; /** * Restore the state of the camera. * * @param {object} camera_state */ beestat.component.scene.prototype.set_camera_state = function(camera_state) { this.camera_.matrix.fromArray(camera_state); this.camera_.matrix.decompose( this.camera_.position, this.camera_.quaternion, this.camera_.scale ); }; /** * Get a material representing a label. Memoizes the result so the material * can be re-used. * * @param {object} args * * @return {THREE.Material} */ beestat.component.scene.prototype.get_label_material_ = function(args) { let memo_key; switch (args.type) { case 'value': memo_key = [ args.type, args.value ].join('|'); break; case 'icon': memo_key = [ args.type, args.icon, args.color ].join('|'); break; } if (this.label_material_memo_[memo_key] === undefined) { /** * Increasing the size of the canvas increases the resolution of the texture * and thus makes it less blurry. */ const scale = 2; const canvas = document.createElement('canvas'); canvas.width = 100 * scale; canvas.height = 55 * scale; const context = canvas.getContext('2d'); // Debug red background // context.fillStyle = 'rgba(255, 0, 0, 0.2)'; // context.fillRect(0, 0, canvas.width, canvas.height); const font_size = canvas.height / 2; switch (args.type) { case 'value': { context.font = '700 ' + font_size + 'px Montserrat'; context.fillStyle = '#fff'; context.textAlign = 'center'; context.fillText( args.value, canvas.width / 2, canvas.height ); break; } case 'icon': { const icon_scale = 2.5; const icon_size = 24 * icon_scale; context.fillStyle = args.color; context.translate((canvas.width / 2) - (icon_size / 2), 0); context.fill(this.get_icon_path_(args.icon, icon_scale)); break; } } const texture = new THREE.Texture(canvas); texture.needsUpdate = true; texture.anisotropy = this.renderer_.capabilities.getMaxAnisotropy(); const material = new THREE.SpriteMaterial({ 'map': texture, 'sizeAttenuation': false }); this.label_material_memo_[memo_key] = material; } return this.label_material_memo_[memo_key]; }; /** * Get a blank label. * * @return {THREE.Material} */ beestat.component.scene.prototype.get_blank_label_material_ = function() { return this.get_label_material_({ 'type': 'value', 'value': '' }); }; /** * Get an icon path for placing on a canvas texture. * * @param {string} icon * @param {int} scale * * @return {Path2D} */ beestat.component.scene.prototype.get_icon_path_ = function(icon, scale = 4) { const icons = { 'snowflake': 'M20.79,13.95L18.46,14.57L16.46,13.44V10.56L18.46,9.43L20.79,10.05L21.31,8.12L19.54,7.65L20,5.88L18.07,5.36L17.45,7.69L15.45,8.82L13,7.38V5.12L14.71,3.41L13.29,2L12,3.29L10.71,2L9.29,3.41L11,5.12V7.38L8.5,8.82L6.5,7.69L5.92,5.36L4,5.88L4.47,7.65L2.7,8.12L3.22,10.05L5.55,9.43L7.55,10.56V13.45L5.55,14.58L3.22,13.96L2.7,15.89L4.47,16.36L4,18.12L5.93,18.64L6.55,16.31L8.55,15.18L11,16.62V18.88L9.29,20.59L10.71,22L12,20.71L13.29,22L14.7,20.59L13,18.88V16.62L15.5,15.17L17.5,16.3L18.12,18.63L20,18.12L19.53,16.35L21.3,15.88L20.79,13.95M9.5,10.56L12,9.11L14.5,10.56V13.44L12,14.89L9.5,13.44V10.56Z', 'fire': 'M17.66 11.2C17.43 10.9 17.15 10.64 16.89 10.38C16.22 9.78 15.46 9.35 14.82 8.72C13.33 7.26 13 4.85 13.95 3C13 3.23 12.17 3.75 11.46 4.32C8.87 6.4 7.85 10.07 9.07 13.22C9.11 13.32 9.15 13.42 9.15 13.55C9.15 13.77 9 13.97 8.8 14.05C8.57 14.15 8.33 14.09 8.14 13.93C8.08 13.88 8.04 13.83 8 13.76C6.87 12.33 6.69 10.28 7.45 8.64C5.78 10 4.87 12.3 5 14.47C5.06 14.97 5.12 15.47 5.29 15.97C5.43 16.57 5.7 17.17 6 17.7C7.08 19.43 8.95 20.67 10.96 20.92C13.1 21.19 15.39 20.8 17.03 19.32C18.86 17.66 19.5 15 18.56 12.72L18.43 12.46C18.22 12 17.66 11.2 17.66 11.2M14.5 17.5C14.22 17.74 13.76 18 13.4 18.1C12.28 18.5 11.16 17.94 10.5 17.28C11.69 17 12.4 16.12 12.61 15.23C12.78 14.43 12.46 13.77 12.33 13C12.21 12.26 12.23 11.63 12.5 10.94C12.69 11.32 12.89 11.7 13.13 12C13.9 13 15.11 13.44 15.37 14.8C15.41 14.94 15.43 15.08 15.43 15.23C15.46 16.05 15.1 16.95 14.5 17.5H14.5Z', 'fan': 'M12,11A1,1 0 0,0 11,12A1,1 0 0,0 12,13A1,1 0 0,0 13,12A1,1 0 0,0 12,11M12.5,2C17,2 17.11,5.57 14.75,6.75C13.76,7.24 13.32,8.29 13.13,9.22C13.61,9.42 14.03,9.73 14.35,10.13C18.05,8.13 22.03,8.92 22.03,12.5C22.03,17 18.46,17.1 17.28,14.73C16.78,13.74 15.72,13.3 14.79,13.11C14.59,13.59 14.28,14 13.88,14.34C15.87,18.03 15.08,22 11.5,22C7,22 6.91,18.42 9.27,17.24C10.25,16.75 10.69,15.71 10.89,14.79C10.4,14.59 9.97,14.27 9.65,13.87C5.96,15.85 2,15.07 2,11.5C2,7 5.56,6.89 6.74,9.26C7.24,10.25 8.29,10.68 9.22,10.87C9.41,10.39 9.73,9.97 10.14,9.65C8.15,5.96 8.94,2 12.5,2Z' }; const icon_path = new Path2D(icons[icon]); const svg_matrix = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ).createSVGMatrix(); const transform = svg_matrix.scale(scale); const scaled_path = new Path2D(); scaled_path.addPath(icon_path, transform); return scaled_path; }; beestat.component.scene.prototype.dispose = function() { window.cancelAnimationFrame(this.animation_frame_); beestat.component.prototype.dispose.apply(this, arguments); }; /** * Get the currently active room. * * @return {object} */ beestat.component.scene.prototype.get_active_room_ = function() { if (this.active_mesh_ !== undefined) { return this.active_mesh_.userData.room; } return null; };