├── EG.glb ├── OG.glb ├── README.md ├── T.directive.js ├── Three-HABPanel.webm └── main.css /EG.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpmeifyoucan/habpanel_threejs/9093a0a8e102e1418d4264a954016af8f85bcf95/EG.glb -------------------------------------------------------------------------------- /OG.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpmeifyoucan/habpanel_threejs/9093a0a8e102e1418d4264a954016af8f85bcf95/OG.glb -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # habpanel_threejs 2 | Example of a 3D floorplan live renderering in Openhab habpanel. 3 | 4 | This is just a quick example of how to implement it in Openhab 2.5 habpanel. Feel free to edit. 5 | 6 | ## Requires 7 | Put the following files and the included files of this repo in the subfolder \openhab\conf\html\three 8 | 9 | - [jquery-3.4.1.min.js](https://jquery.com/) -> https://code.jquery.com/jquery-3.4.1.js 10 | - [three.js](https://threejs.org/) -> https://unpkg.com/browse/three@0.115.0/ 11 | - [OrbitControls.js](https://threejs.org/docs/#examples/en/controls/OrbitControls) -> https://github.com/mrdoob/three.js/blob/master/examples/jsm/controls/OrbitControls.js 12 | - [GLTFLoader.js](https://threejs.org/docs/#examples/en/loaders/GLTFLoader) -> https://github.com/mrdoob/three.js/blob/master/examples/jsm/loaders/GLTFLoader.js 13 | - [dat.gui.js](https://github.com/dataarts/dat.gui) -> https://github.com/dataarts/dat.gui/blob/master/build/dat.gui.js 14 | - [Sky.js](https://github.com/loginov-rocks/three-sky) -> https://github.com/mrdoob/three.js/blob/master/examples/js/objects/Sky.js 15 | - [CSS2DRenderer.js](https://threejs.org/docs/#examples/en/renderers/CSS2DRenderer) -> https://github.com/mrdoob/three.js/blob/master/examples/jsm/renderers/CSS2DRenderer.js 16 | - [RectAreaLightUniformsLib.js](https://threejs.org/docs/#api/en/lights/RectAreaLight) -> https://github.com/mrdoob/three.js/blob/master/src/lights/RectAreaLight.js 17 | - round-slider.js -> https://github.com/thomasloven/round-slider 18 | - suncalc.js -> https://github.com/mourner/suncalc 19 | 20 | ## Example House 21 | Sweet Home 3D -> Example 6 22 | Export as Obj -> Import it in Blender 2.8 -> Export it as GLB 23 | 24 | ## Implementation 25 | Create a new dashboard in habpanel including a custom widget with the following definition 26 | 27 | ```html 28 | 29 |
30 | 33 | 34 |
35 | ``` 36 | ## Definition of items 37 | Adjust the list of items in the parameters p_lights, p_temp, p_windows 38 | - E.q. 'item1' equals a dimmer object. 39 | - E.q. 'item1_temp_xxx' equals a thermostat (in my case Max! eQ3) 40 | - E.q. 'item2_window_switch' equals a window sensor 41 | 42 | ## Definition of rooms / lights etc. 43 | Adjust the position and size of the rooms in p_rooms 44 | 45 | ## Floor 46 | You can move to the 1st Floor and back via p_room 'stairs' 47 | 48 | ## Demo 49 | You should be able to try a demo without customization of items by changing the values in the dat.gui controler. 50 | 51 | ## References 52 | - Three.js - https://threejs.org/ 53 | - SweetHome3D - http://www.sweethome3d.com/ 54 | - Switchbutton - https://codepen.io/vanderlanth/pen/BoNLvq/ 55 | - Round-Slider - https://github.com/thomasloven/round-slider 56 | - WebGL PoC - https://community.openhab.org/t/proof-of-concept-interactive-webgl-view-with-habpanel-sweet-home-3d/32995 57 | - suncalc - https://github.com/mourner/suncalc 58 | - Inspired by - https://github.com/ghys/habpanel-3dview/blob/master/README.md 59 | 60 | -------------------------------------------------------------------------------- /T.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.widgets') 6 | .directive('threeJs3d', TDirective); 7 | 8 | TDirective.$inject = ['OHService', '$ocLazyLoad', '$timeout', '$uibModal', '$templateCache']; 9 | function TDirective(OHService, $ocLazyLoad, $timeout, $uibModal, $templateCache) { 10 | 11 | var directive = { 12 | link: link, 13 | restrict: 'EA', 14 | template: 15 | '' + 32 | '
' + 33 | '
' + 34 | '' + 35 | '' + 36 | '' + 37 | '
' + 38 | '
', 39 | scope: {} 40 | }; 41 | return directive; 42 | 43 | function link($scope, element, attrs) { 44 | 45 | $ocLazyLoad.load([ 46 | '/static/three/jquery-3.4.1.min.js', 47 | '/static/three/three.js', 48 | '/static/three/OrbitControls.js', 49 | '/static/three/GLTFLoader.js', 50 | '/static/three/dat.gui.js', 51 | '/static/three/round-slider.js', 52 | '/static/three/suncalc.js', 53 | '/static/three/Sky.js', 54 | '/static/three/CSS2DRenderer.js', 55 | '/static/three/RectAreaLightUniformsLib.js', 56 | ], { serie: true }) 57 | .then (function () { 58 | $timeout( 59 | function (e) { 60 | showthree($scope); 61 | }, 200); 62 | }); 63 | ; 64 | } 65 | 66 | function showthree(scope){ 67 | 68 | THREE.Cache.enabled = true; 69 | var camera, scene, renderer, raycaster, controls, labelRenderer; 70 | 71 | var mouse = new THREE.Vector2(), INTERSECTED; 72 | var mousepos = new THREE.Vector2(); 73 | 74 | var box_lights = new THREE.Group(); 75 | var box_rooms = new THREE.Group(); 76 | var box_windows = new THREE.Group(); 77 | 78 | var bulbMat, ambilight, dirlight; 79 | 80 | var clicked; 81 | 82 | var params_man = { 83 | controls: true, 84 | time: 'xx:xx:xx', 85 | Licht_Room1_Status: -2, 86 | Licht_Room2_Status: -2, 87 | Licht_Room3_Status: -2, 88 | Licht_Room6_Status: -2, 89 | TempIst_Room1: 0, 90 | TempIst_Room2: 0, 91 | }; 92 | 93 | var p_rooms = { 94 | Room1: {geom: [ 4.76, 2.5, 2.6 ], pos: [ 2.38, 1.25, 1.3 ], layer: 0}, 95 | Room2: {geom: [ 5.3, 2.5, 3.0 ], pos: [ 7.35, 1.25, 3.5 ], layer: 0}, 96 | Room3: {geom: [ 4.76, 2.5, 2.4 ], pos: [ 2.38, 1.25, 3.8 ], layer: 0}, 97 | Stairs: {geom: [ 2.0, 2.5, 2.0 ], pos: [ 8.85, 1.25, 1.2 ], layer: 0}, 98 | Room4: {geom: [ 2.7, 2.5, 5.0 ], pos: [ 1.35, 3.75, 2.5 ], layer: 1}, 99 | Room5: {geom: [ 3.0, 2.5, 3.5 ], pos: [ 4.25, 3.75, 1.8 ], layer: 1}, 100 | Room6: {geom: [ 1.8, 2.5, 3.5 ], pos: [ 6.76, 3.75, 1.8 ], layer: 1}, 101 | Stairs2: {geom: [ 2.0, 2.5, 2.0 ], pos: [ 8.85, 3.75, 1.2 ], layer: 1}, 102 | 103 | }; 104 | 105 | var p_lights = { 106 | Room1: { color: 0xffee88, pos: [ 2.62, 2.2, 1.44 ], layer: 2, 107 | item: scope.$root.items.find(x => x.name === 'item1') || {state: 0}, 108 | switch: scope.$root.items.find(x => x.name === 'item1_switch' ) || {state: 'ONLINE'}}, 109 | Room2: { color: 0xffee88, pos: [ 7.5,2.2,3.0 ], layer: 2, 110 | item: scope.$root.items.find(x => x.name === 'item2') || {state: 0}, 111 | switch: scope.$root.items.find(x => x.name === 'item2_switch' ) || {state: 'ONLINE'}}, 112 | Room3: { color: 0xffee88, pos: [ 2.66,2.2,3.46 ], layer: 2, 113 | item: scope.$root.items.find(x => x.name === 'item3') || {state: 0}, 114 | switch: scope.$root.items.find(x => x.name === 'item3_switch' ) || {state: 'ONLINE'}}, 115 | Room6: { color: 0xffee88, pos: [ 6.76, 4.7, 1.8 ], layer: 3, 116 | item: scope.$root.items.find(x => x.name === 'item6') || {state: 0}, 117 | switch: scope.$root.items.find(x => x.name === 'item6_switch' ) || {state: 'ONLINE'}}, 118 | }; 119 | 120 | 121 | var p_temp = { 122 | Room1: { actual: scope.$root.items.find(x => x.name === 'item1_temp_ist' ) || {state: '0 °C'}, 123 | setting: scope.$root.items.find(x => x.name === 'item1_temp_soll' ) || {state: '12 °C'}, 124 | mode: scope.$root.items.find(x => x.name === 'item1_temp_mode' ) || {state: 'AUTOMATIC'}, 125 | layer: 0}, 126 | Room2: { actual: scope.$root.items.find(x => x.name === 'item2_temp_ist' ) || {state: '25 °C'}, 127 | setting: scope.$root.items.find(x => x.name === 'item2_temp_soll' ) || {state: '12 °C'}, 128 | mode: scope.$root.items.find(x => x.name === 'item2_temp_mode' ) || {state: 'AUTOMATIC'}, 129 | layer: 0}, 130 | }; 131 | 132 | 133 | var p_windows = { 134 | Room2: { contact: scope.$root.items.find(x => x.name === 'item2_window_switch' ) || {state: 'OPEN'}, 135 | pos: [ 3.62, 1.5, 4.97 ], rot: 0, dim: [0.91, 1.34], layer: 0}, 136 | }; 137 | 138 | 139 | var containerwidth = window.innerWidth-$(container).parent().offset().left; 140 | var containerheight = window.innerHeight-$(container).parent().offset().top; 141 | 142 | var activeElement = 'none'; 143 | var activeDropdown = 'none'; 144 | var activeDomain = 0; 145 | var activeFloor = 0; 146 | 147 | var switchButton = document.querySelector('.swibu'); 148 | var switchBtnRight = document.querySelector('.swibu-case.right'); 149 | var switchBtnLeft = document.querySelector('.swibu-case.left'); 150 | var activeSwitch = document.querySelector('.swibuactive'); 151 | $("#popup").hide(); 152 | $(".dropdown").hide(); 153 | 154 | var OG = new THREE.Group(); 155 | var EG = new THREE.Group(); 156 | var transplane; 157 | 158 | 159 | function switchLeft(){ 160 | switchBtnRight.classList.remove('swibuactive-case'); 161 | switchBtnLeft.classList.add('swibuactive-case'); 162 | activeSwitch.style.left = '0%'; 163 | activeDomain = 0; 164 | $('#rsl').attr({"min" : "0","max" : "100","step": "1"}); 165 | $('.annotation').css('visibility', 'hidden'); 166 | $(".dropdown").hide(); 167 | box_windows.visible = false; 168 | render(); 169 | } 170 | 171 | function switchRight(){ 172 | switchBtnRight.classList.add('swibuactive-case'); 173 | switchBtnLeft.classList.remove('swibuactive-case'); 174 | activeSwitch.style.left = '50%'; 175 | activeDomain = 1; 176 | $('#rsl').attr({"min" : "4.5","max" : "30","step": "0.5"}); 177 | $('.annotation').css('visibility', 'visible'); 178 | $(".dropdown").show(); 179 | box_windows.visible = true; 180 | render(); 181 | } 182 | 183 | switchBtnLeft.addEventListener('click', function(){ 184 | switchLeft(); 185 | }, false); 186 | 187 | switchBtnRight.addEventListener('click', function(){ 188 | switchRight(); 189 | }, false); 190 | 191 | 192 | document.querySelectorAll("round-slider").forEach(function (el) { 193 | el.addEventListener('value-changed', function(ev) { 194 | if(ev.detail.value !== undefined) 195 | if (ev.detail.value > ( ev.srcElement.max - ev.srcElement.min) * 0.9 + ev.srcElement.min ){ 196 | sendSlider(activeElement, ev.srcElement.max); 197 | setValue(ev.srcElement.max, false); 198 | } 199 | else if (ev.detail.value < ( ev.srcElement.max - ev.srcElement.min) * 0.1 + ev.srcElement.min ) { 200 | sendSlider(activeElement, ev.srcElement.min); 201 | setValue(ev.srcElement.min, false); 202 | } 203 | else { 204 | setValue(ev.detail.value, false); 205 | sendSlider(activeElement, ev.detail.value); 206 | } 207 | else if(ev.detail.low !== undefined) { 208 | setLow(ev.detail.low, false); 209 | sendSlider(activeElement, ev.detail.low); 210 | } 211 | else if(ev.detail.high !== undefined) { 212 | setHigh(ev.detail.high, false); 213 | sendSlider(activeElement, ev.detail.high); 214 | } 215 | }); 216 | 217 | el.addEventListener('value-changing', function(ev) { 218 | if(ev.detail.value !== undefined) 219 | setValue(ev.detail.value, true); 220 | else if(ev.detail.low !== undefined) 221 | setLow(ev.detail.low, true); 222 | else if(ev.detail.high !== undefined) 223 | setHigh(ev.detail.high, true); 224 | }); 225 | }); 226 | 227 | $(document).mouseup(function (e) { 228 | var popup = $("#popup"); 229 | if (!popup.is(e.target) && popup.has(e.target).length == 0) { 230 | popup.hide(); 231 | activeElement = 'none'; 232 | } 233 | }); 234 | 235 | 236 | $('.dropdown').on('click', function(e) { 237 | if (activeDropdown != 'none') { 238 | if (e.target.value == 'OFF') { 239 | OHService.sendCmd(activeDropdown, 'MANUAL') 240 | OHService.sendCmd(activeElement, '4.5'); 241 | } 242 | else 243 | OHService.sendCmd(activeDropdown, e.target.value); 244 | render(); 245 | } 246 | }); 247 | 248 | 249 | 250 | const setValue = function(value, active) { 251 | document.querySelectorAll("round-slider").forEach(function(el) { 252 | el.value = value; 253 | }); 254 | const span = document.querySelector("#value"); 255 | span.innerHTML = value; 256 | if(active) 257 | span.style.color = 'red'; 258 | else 259 | span.style.color = 'white'; 260 | } 261 | const setLow = function(value, active) { 262 | document.querySelectorAll("round-slider").forEach(function(el) { 263 | // if(!el.low) return; 264 | el.low = value; 265 | }); 266 | const span = document.querySelector("#low"); 267 | span.innerHTML = value; 268 | if(active) 269 | span.style.color = 'red'; 270 | else 271 | span.style.color = 'white'; 272 | } 273 | const setHigh = function(value, active) { 274 | document.querySelectorAll("round-slider").forEach(function(el) { 275 | // if(!el.high) return; 276 | el.high = value; 277 | }); 278 | const span = document.querySelector("#high"); 279 | span.innerHTML = value; 280 | if(active) 281 | span.style.color = 'red'; 282 | else 283 | span.style.color = 'white'; 284 | } 285 | 286 | function lerpColor(a, b, amount) { 287 | var ah = parseInt(a.replace(/#/g, ''), 16), 288 | ar = ah >> 16, ag = ah >> 8 & 0xff, ab = ah & 0xff, 289 | bh = parseInt(b.replace(/#/g, ''), 16), 290 | br = bh >> 16, bg = bh >> 8 & 0xff, bb = bh & 0xff, 291 | rr = ar + amount * (br - ar), 292 | rg = ag + amount * (bg - ag), 293 | rb = ab + amount * (bb - ab); 294 | return '#' + ((1 << 24) + (rr << 16) + (rg << 8) + rb | 0).toString(16).slice(1); 295 | } 296 | 297 | if($('#container').is(':visible')) { 298 | init(); 299 | animate(); 300 | } 301 | 302 | function init() { 303 | 304 | // CAMERA 305 | camera = new THREE.PerspectiveCamera( 60, containerwidth / containerheight, 0.1, 100 ); 306 | camera.position.set(4.34,16.5,9.3); 307 | camera.layers.enable( 2 ); 308 | 309 | var resetview = { add:function(){ camera.position.set(4.34,16.5,9.3) }}; 310 | 311 | 312 | // SCENE 313 | scene = new THREE.Scene(); 314 | 315 | // RAYCASTER 316 | raycaster = new THREE.Raycaster(); 317 | 318 | // RENDERER 319 | renderer = new THREE.WebGLRenderer({antialias: true, alpha: true}); 320 | renderer.physicallyCorrectLights = true; 321 | renderer.gammaFactor = 2.2; 322 | renderer.outputEncoding = THREE.sRGBEncoding; 323 | renderer.shadowMap.enabled = true; 324 | renderer.shadowMap.type = THREE.BasicShadowMap; 325 | renderer.toneMapping = THREE.Uncharted2ToneMapping; 326 | renderer.toneMappingExposure = 0.8; 327 | renderer.setPixelRatio( window.devicePixelRatio ); 328 | renderer.setSize( containerwidth, containerheight ); 329 | renderer.setClearColor(0x000000, 0.0); 330 | container.appendChild( renderer.domElement ); 331 | 332 | 333 | labelRenderer = new THREE.CSS2DRenderer(); 334 | labelRenderer.setSize( containerwidth, containerheight ); 335 | labelRenderer.domElement.style.position = 'absolute'; 336 | labelRenderer.domElement.style.top = 0; 337 | container.appendChild( labelRenderer.domElement ); 338 | 339 | // CONTROLS 340 | controls = new THREE.OrbitControls( camera, labelRenderer.domElement ); 341 | controls.target = (new THREE.Vector3(4.34,0,4.6)); 342 | controls.minDistance = 10; 343 | controls.maxDistance = 50; 344 | controls.maxPolarAngle = Math.PI*0.45; 345 | controls.update(); 346 | 347 | // Sky 348 | dirlight = new THREE.DirectionalLight(0xffffff, 1); //DirectionalLight( 0xffd28f, 1 ); 349 | scene.add( dirlight ); 350 | 351 | ambilight = new THREE.AmbientLight( 0xffffff, 0 ); 352 | scene.add( ambilight ); 353 | 354 | var light = new THREE.PointLight( 0xffffff, 2, 0 ); 355 | light.position.set(0, 1, 100 ); 356 | scene.add( light ); 357 | 358 | light = new THREE.PointLight( 0xffffff, 2, 0 ); 359 | light.position.set(0, 1, -100 ); 360 | scene.add( light ); 361 | 362 | light = new THREE.PointLight( 0xffffff, 2, 0 ); 363 | light.position.set( 100, 1, 0 ); 364 | scene.add( light ); 365 | 366 | light = new THREE.PointLight( 0xffffff, 2, 0 ); 367 | light.position.set(-100, 1, 0 ); 368 | scene.add( light ); 369 | 370 | initSky(); 371 | 372 | 373 | 374 | 375 | 376 | // LOADER 377 | const manager = new THREE.LoadingManager() 378 | manager.onLoad = function ( ) { 379 | console.log( "Loading complete!") 380 | render(); 381 | } 382 | manager.onProgress = function ( url, itemsLoaded, itemsTotal ) { 383 | console.log(`Items loaded: ${itemsLoaded}/${itemsTotal}`) 384 | } 385 | manager.onError = function ( url ) { 386 | console.log( 'There was an error loading ' + url ) 387 | } 388 | 389 | var loader = new THREE.GLTFLoader(manager); 390 | loader.load( '/static/three/EG.glb', function ( gltf ) { 391 | EG = gltf.scene; 392 | EG.traverse( function ( child ) { 393 | if ( child instanceof THREE.Mesh ) { 394 | child.castShadow = true; 395 | child.receiveShadow = true; 396 | } 397 | if (child.material == undefined) 398 | return; 399 | if (child.material.name.includes("window") || child.material.name.includes("glass") || child.material.name.includes("flltgrey") ) { 400 | child.castShadow = false; 401 | child.recieveShadow = false; 402 | } 403 | }); 404 | EG.scale.set(0.01,0.01,0.01); 405 | EG.name = 'EG'; 406 | scene.add( EG ); 407 | render(); 408 | 409 | } ); 410 | 411 | 412 | var loader = new THREE.GLTFLoader(manager); 413 | loader.load( '/static/three/OG.glb', function ( gltf ) { 414 | OG = gltf.scene; 415 | OG.traverse( function ( child ) { 416 | if ( child instanceof THREE.Mesh ) { 417 | child.castShadow = true; 418 | child.receiveShadow = true; 419 | child.layers.set( 1 ); 420 | } 421 | if (child.material == undefined) 422 | return; 423 | if (child.material.name.includes("window") || child.material.name.includes("glass") || child.material.name.includes("flltgrey") ) { 424 | child.castShadow = false; 425 | child.recieveShadow = false; 426 | } 427 | }); 428 | OG.scale.set(0.01,0.01,0.01); 429 | OG.name = 'OG'; 430 | scene.add( OG ); 431 | render(); 432 | 433 | } ); 434 | 435 | var geometry = new THREE.PlaneGeometry( 50, 50, 50 ); 436 | var material = new THREE.MeshBasicMaterial( { 437 | color: 0x000000, 438 | opacity: 0.5, 439 | transparent: true}); 440 | transplane = new THREE.Mesh( geometry, material ); 441 | transplane.rotation.x = -Math.PI/2; 442 | transplane.position.y = 2.51; 443 | transplane.layers.set( 1 ); 444 | scene.add( transplane ); 445 | 446 | 447 | // LIGHT 448 | box_lights.name = "box_lights"; 449 | Object.entries(p_lights).forEach(([p_light_name, p_light_val]) => { 450 | var light = new THREE.PointLight( p_light_val.color, 1, 8, 2 ); 451 | light.position.set( p_light_val.pos[0], p_light_val.pos[1], p_light_val.pos[2] ); 452 | light.castShadow = true; 453 | light.power = p_light_val.item.state * 2; 454 | light.name = p_light_name; 455 | light.layers.set( p_light_val.layer ); 456 | light.shadow.bias = -0.0001; 457 | 458 | box_lights.add( light ); 459 | }); 460 | scene.add( box_lights ); 461 | 462 | // ROOMS 463 | box_rooms.name = "box_rooms"; 464 | Object.entries(p_rooms).forEach(([p_room_name, p_room_val]) => { 465 | var geometry = new THREE.BoxBufferGeometry( p_room_val.geom[0], p_room_val.geom[1], p_room_val.geom[2] ); 466 | var material = new THREE.MeshPhongMaterial({ 467 | color: 0x711091, 468 | opacity: 0, 469 | transparent: true,}); 470 | var mesh = new THREE.Mesh( geometry, material ); 471 | mesh.position.set( p_room_val.pos[0], p_room_val.pos[1], p_room_val.pos[2] ); 472 | mesh.name = p_room_name; 473 | mesh.layers.set( p_room_val.layer ); 474 | box_rooms.add( mesh ); 475 | }); 476 | scene.add( box_rooms ); 477 | 478 | // TEMP 479 | Object.entries(p_temp).forEach(([p_temp_name, p_temp_val]) => { 480 | var room = box_rooms.children.find( x => x.name === p_temp_name); 481 | if (room == undefined) 482 | return; 483 | var divlabel = document.createElement( 'div' ); 484 | divlabel.className = 'annotation'; 485 | divlabel.id = 'templabel_' + p_temp_name; 486 | divlabel.textContent = p_temp_val.actual.state; 487 | divlabel.style.marginTop = '-1em'; 488 | var templabel = new THREE.CSS2DObject( divlabel ); 489 | templabel.layers.set( p_temp_val.layer ); 490 | room.add( templabel ); 491 | }); 492 | 493 | // WINDOWS 494 | THREE.RectAreaLightUniformsLib.init(); 495 | box_windows.name = "box_windows"; 496 | Object.entries(p_windows).forEach(([p_window_name, p_window_val]) => { 497 | var rectLight; 498 | rectLight = new THREE.RectAreaLight( 0xf5ff00, 2, p_window_val.dim[0], p_window_val.dim[1] ); //0x00ff00 499 | rectLight.position.set( p_window_val.pos[0], p_window_val.pos[1], p_window_val.pos[2] ); 500 | rectLight.rotation.y = p_window_val.rot; 501 | rectLight.name = p_window_name; 502 | rectLight.visible = false; 503 | rectLight.layers.set( p_window_val.layer); 504 | 505 | var rectLightMesh = new THREE.Mesh( new THREE.PlaneBufferGeometry(), new THREE.MeshBasicMaterial( { side: THREE.DoubleSide } ) ); 506 | rectLightMesh.scale.x = rectLight.width+.5; 507 | rectLightMesh.scale.y = rectLight.height+.5; 508 | rectLightMesh.material.opacity = 0.3; 509 | rectLightMesh.material.transparent = true; 510 | rectLightMesh.material.color.setHex(0xf5ff00); 511 | rectLight.add( rectLightMesh ); 512 | 513 | box_windows.add( rectLight ); 514 | }); 515 | scene.add( box_windows ); 516 | 517 | // LISTENER 518 | window.addEventListener( 'resize', onWindowResize, false ); 519 | document.addEventListener( 'mousemove', onDocumentMouseMove, false ); 520 | document.addEventListener( 'touchstart', onDocumentTouchStart, false ); 521 | window.addEventListener( 'mousedown', onWindowMouseDown, false); 522 | 523 | // GUT 524 | var gui = new dat.GUI(); 525 | gui.add( resetview, 'add' ).name('Reset Camera'); 526 | gui.add( params_man, 'controls'); 527 | gui.add( params_man, 'time').onChange(initSky); 528 | gui.add( params_man, 'Licht_Room1_Status', -2, 100 ); 529 | gui.add( params_man, 'Licht_Room2_Status', -2, 100 ); 530 | gui.add( params_man, 'Licht_Room3_Status', -2, 100 ); 531 | gui.add( params_man, 'Licht_Room6_Status', -2, 100 ); 532 | gui.add( params_man, 'TempIst_Room1', 0, 35 ); 533 | gui.add( params_man, 'TempIst_Room2', 0, 35 ); 534 | gui.close(); 535 | 536 | } 537 | 538 | 539 | 540 | function initSky() { 541 | var lat = 47.642676; 542 | var long = 9.393985; 543 | var ele = 100; 544 | 545 | var d = Date.now(); 546 | if (params_man.time != 'xx:xx:xx') { 547 | d = new Date('04/19/2020 ' + params_man.time); 548 | d = d.getTime(); 549 | } 550 | 551 | var sc = SunCalc.getTimes(d, lat, long); 552 | 553 | var sunPos = SunCalc.getPosition(d, lat, long, ele); 554 | var inclination = sunPos.altitude-Math.PI/2; 555 | var azimuth = sunPos.azimuth; 556 | 557 | // Add Sky 558 | var sky = new THREE.Sky(); 559 | sky.scale.setScalar( 450000 ); 560 | scene.add( sky ); 561 | 562 | // Add Sun Helper 563 | var sunSphere = new THREE.Mesh( 564 | new THREE.SphereBufferGeometry( 20000, 16, 8 ), 565 | new THREE.MeshBasicMaterial( { color: 0xffffff } ) 566 | ); 567 | sunSphere.position.y = - 700000; 568 | scene.add( sunSphere ); 569 | 570 | var distance = 400000; 571 | var uniforms = sky.material.uniforms; 572 | uniforms.turbidity.value = 10; 573 | uniforms.rayleigh.value = 0.642; 574 | uniforms.luminance.value = 0.9; 575 | uniforms.mieCoefficient.value = 0.005; 576 | uniforms.mieDirectionalG.value = 0.9; 577 | 578 | sunSphere.position.setFromSphericalCoords(distance, inclination, azimuth); 579 | uniforms.sunPosition.value.copy( sunSphere.position ); 580 | 581 | if ((d > sc.sunrise & d < sc.goldenHourEnd) | (d > sc.goldenHour & d < sc.sunset)) { 582 | dirlight.position.copy( sunSphere.position ); 583 | dirlight.color.setHex(0xe9ce5d); 584 | dirlight.intensity = 1; 585 | ambilight.intensity = 0.2; 586 | } 587 | else if ((d > sc.sunset | d < sc.sunrise)) { 588 | dirlight.position.set(0,1,0) 589 | dirlight.color.setHex(0xffd28f); 590 | dirlight.intensity = 0.05; 591 | ambilight.intensity = 0; 592 | } 593 | else { 594 | switchRight(); 595 | dirlight.position.copy( sunSphere.position ); 596 | dirlight.color.setHex(0xffffff); 597 | dirlight.intensity = 1; 598 | ambilight.intensity = 0.1; 599 | } 600 | } 601 | 602 | 603 | function onWindowResize() { 604 | containerwidth = window.innerWidth-$(container).parent().offset().left; 605 | containerheight = window.innerHeight-$(container).parent().offset().top; 606 | 607 | camera.aspect = containerwidth / containerheight; 608 | camera.updateProjectionMatrix(); 609 | renderer.setSize(containerwidth, containerheight); 610 | } 611 | 612 | function onWindowMouseDown() { 613 | clicked = true; 614 | } 615 | 616 | function onDocumentMouseMove( event ) { 617 | if($('#container').is(':visible')) { 618 | event.preventDefault(); 619 | var parentOffset = $(container).parent().offset(); 620 | mouse.x = ( (event.clientX - parentOffset.left) / containerwidth ) * 2 - 1; 621 | mouse.y = - ( (event.clientY - parentOffset.top) / containerheight ) * 2 + 1; 622 | mousepos.x = event.clientX; 623 | mousepos.y = event.clientY; 624 | } 625 | } 626 | 627 | function onDocumentTouchStart( event ) { 628 | if($('#container').is(':visible')) { 629 | clicked = true; 630 | var parentOffset = $(container).parent().offset(); 631 | mouse.x = ( (event.clientX - parentOffset.left) / containerwidth ) * 2 - 1; 632 | mouse.y = - ( (event.clientY - parentOffset.top) / containerheight ) * 2 + 1; 633 | mousepos.x = event.clientX; 634 | mousepos.y = event.clientY; 635 | } 636 | } 637 | 638 | function movePopup() { 639 | var x = Math.max(mousepos.x,100); 640 | x = Math.min(x, $(window).width()-100); 641 | var y = Math.max(mousepos.y,100); 642 | y = Math.min(y, $(window).width()) 643 | $(".popup").css("left", x); 644 | $(".popup").css("top", y); 645 | }; 646 | 647 | 648 | // ANIMATE 649 | function animate() { 650 | requestAnimationFrame(animate); 651 | controls.update(); 652 | if (activeDomain == 0){ 653 | updateLights(); 654 | } 655 | else { 656 | updateTemp(); 657 | } 658 | updateRaycaster(); 659 | if (!$('.popup').is(':visible')) { 660 | render(); 661 | } 662 | } 663 | 664 | function render() { 665 | renderer.render(scene, camera); 666 | labelRenderer.render( scene, camera ); 667 | } 668 | 669 | 670 | 671 | 672 | function updateLights() { 673 | $('.annotation').css('visibility', 'hidden'); 674 | box_lights.children.forEach(function(light) { 675 | light.color.setHex(p_lights[light.name].color); 676 | light.power = p_lights[light.name].item.state *2; 677 | }); 678 | 679 | 680 | box_rooms.children.forEach(function(room) { 681 | var light = box_lights.children.find( x => x.name === room.name); 682 | 683 | 684 | if (light == undefined) { 685 | if ( INTERSECTED != room) 686 | room.material.opacity = 0; 687 | return; 688 | } 689 | 690 | if (INTERSECTED == room & !$('.popup').is(':visible')) { 691 | 692 | movePopup(); 693 | document.getElementById("popupcontent").innerHTML = "

" + room.name + "

Licht

"; 694 | if (p_lights[room.name].switch.state == 'ONLINE') 695 | setValue(p_lights[room.name].item.state, false); 696 | else { 697 | setValue(0, false); 698 | document.getElementById("popupcontent").innerHTML = "

" + room.name + "

OFFLINE

Licht

"; 699 | } 700 | 701 | $('#popup').show(); 702 | activeElement = room.name +'_Licht'; 703 | } 704 | 705 | 706 | if ( INTERSECTED != room) { 707 | var manual = params_man['Licht_' + room.name + '_Status']; 708 | 709 | if (p_lights[room.name].switch.state == 'OFFLINE' & manual == -2) { 710 | light.power = 0; 711 | room.material.emissive.setHex( 0x303030 ); // 712 | room.material.opacity = 0.5; 713 | } 714 | else if ( manual == -1) { 715 | light.power = 0; 716 | room.material.emissive.setHex( 0x303030 ); 717 | room.material.opacity = 0.2; 718 | } 719 | else { 720 | if (manual == -2) { 721 | light.power = p_lights[light.name].item.state *2; 722 | } 723 | else { 724 | light.power = manual *2; 725 | } 726 | room.material.emissive.setHex( 0x711091 ); 727 | room.material.opacity = 0; 728 | } 729 | 730 | } 731 | }); 732 | } 733 | 734 | function updateTemp() { 735 | 736 | box_rooms.children.forEach(function(room) { 737 | var light = box_lights.children.find( x => x.name === room.name); 738 | var TempIst_man = params_man['TempIst_' + room.name]; 739 | 740 | if (p_temp[room.name] == undefined) 741 | return; 742 | 743 | if (INTERSECTED == room & !$('.popup').is(':visible')) { 744 | movePopup(); 745 | document.getElementById("popupcontent").innerHTML = "

" + room.name + "

" + p_temp[room.name].actual.state + "

"; 746 | $(".dropbtn").text(p_temp[room.name].mode.state); 747 | if (Number(p_temp[room.name].setting.state.match(/^\d*\.?\d*/)[0]) != 'NaN') 748 | setValue(Number(p_temp[room.name].setting.state.match(/^\d*\.?\d*/)[0]), false); 749 | else { 750 | setValue(0, false); 751 | document.getElementById("popupcontent").innerHTML = "

" + room.name + "

OFFLINE

Heizung

"; 752 | } 753 | $('#popup').show(); 754 | activeElement = room.name +'_Thermostat_Soll'; 755 | activeDropdown = room.name + '_Thermostat_Modus'; 756 | } 757 | 758 | if ( INTERSECTED != room) { 759 | if (TempIst_man == 0) { 760 | var percCol = Math.min(Math.max((p_temp[room.name].actual.state.match(/^\d*\.?\d*/)[0] - 15) / 12,0),1); 761 | var col = lerpColor('#0d2261', '#ff0000', percCol); 762 | } 763 | else { 764 | var percCol = Math.min(Math.max((TempIst_man - 15) / 12,0),1); 765 | var col = lerpColor('#0d2261', '#ff0000', percCol); 766 | } 767 | room.material.emissive.setStyle(col); 768 | room.material.opacity = .5; 769 | 770 | if (light != undefined) { 771 | light.power = 100; 772 | light.color.setStyle(col) 773 | } 774 | } 775 | }); 776 | 777 | Object.entries(p_temp).forEach(([p_temp_name, p_temp_val]) => { 778 | var room = box_rooms.children.find( x => x.name === p_temp_name); 779 | if (p_temp_val.layer == activeFloor) 780 | $ ( "#templabel_" + p_temp_name ).css('visibility', 'visible'); 781 | else { 782 | $ ( "#templabel_" + p_temp_name ).css('visibility', 'hidden'); 783 | return; 784 | } 785 | 786 | $( "#templabel_" + p_temp_name ).html('

' + p_temp_val.actual.state + '
' + p_temp_val.setting.state + '

' ); 787 | if (p_temp_val.mode.state == "AUTOMATIC") 788 | $ ( "#templabel_" + p_temp_name ).css('border-style', 'solid'); 789 | else 790 | $ ( "#templabel_" + p_temp_name ).css('border-style', 'hidden'); 791 | }); 792 | 793 | box_windows.children.forEach(function(wind) { 794 | if (p_windows[wind.name].contact.state == 'OPEN' & p_windows[wind.name].layer == activeFloor) 795 | wind.visible = true; 796 | else 797 | wind.visible = false; 798 | }); 799 | 800 | } 801 | 802 | 803 | function updateRaycaster() { 804 | if (clicked){ 805 | raycaster.setFromCamera( mouse, camera ); 806 | var intersects = raycaster.intersectObjects( scene.getObjectByName('box_rooms').children, true ); 807 | if ( intersects.length > 0 ) { 808 | if ( INTERSECTED != intersects[ 0 ].object ) { 809 | if ( INTERSECTED ) { 810 | INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex ); 811 | INTERSECTED.material.opacity = INTERSECTED.currentOpacity; 812 | } 813 | INTERSECTED = intersects[ 0 ].object; 814 | INTERSECTED.currentHex = INTERSECTED.material.emissive.getHex(); 815 | INTERSECTED.currentOpacity = INTERSECTED.material.opacity; 816 | INTERSECTED.material.emissive.setStyle( '#556B2F' ); 817 | INTERSECTED.material.opacity = .5; 818 | } 819 | } else { 820 | if ( INTERSECTED ) { 821 | INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex ); 822 | INTERSECTED.material.opacity = INTERSECTED.currentOpacity; 823 | } 824 | INTERSECTED = null; 825 | } 826 | clicked = false; 827 | 828 | if (INTERSECTED == box_rooms.children.find(x => x.name === 'Stairs' ) | INTERSECTED == box_rooms.children.find(x => x.name === 'Stairs2' )) { 829 | if (activeFloor == 0) { 830 | activeFloor = 1; 831 | camera.layers.enable( 1 ); 832 | camera.layers.enable( 3 ); 833 | 834 | camera.layers.disable( 2 ); 835 | raycaster.layers.set( 1 ); 836 | 837 | } 838 | else { 839 | activeFloor = 0; 840 | camera.layers.disable( 1 ); 841 | camera.layers.disable( 3 ); 842 | 843 | camera.layers.enable( 2 ); 844 | 845 | raycaster.layers.set( 0 ); 846 | 847 | } 848 | } 849 | render(); 850 | } 851 | } 852 | 853 | function sendSlider(actElement, actValue){ 854 | if (actElement != 'none') 855 | OHService.sendCmd(actElement, actValue); 856 | render(); 857 | } 858 | 859 | 860 | } 861 | } 862 | })(); -------------------------------------------------------------------------------- /Three-HABPanel.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpmeifyoucan/habpanel_threejs/9093a0a8e102e1418d4264a954016af8f85bcf95/Three-HABPanel.webm -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: Monospace; 4 | font-size: 13px; 5 | line-height: 24px; 6 | overscroll-behavior: none; 7 | } 8 | 9 | button { 10 | cursor: pointer; 11 | text-transform: uppercase; 12 | } 13 | 14 | canvas { 15 | display: block; 16 | /* background: linear-gradient(to bottom, #2473ab 0%,#1e528e 20%,#5b7983 33%) */ 17 | background: linear-gradient(to bottom, #071B26 0%,#071B26 10%,#8A3B12 25%,#240E03 33%) 18 | /* background: linear-gradient(to bottom, #11e8bb 0%, #8200c9 33%); */ 19 | } 20 | 21 | #info { 22 | position: absolute; 23 | top: 0px; 24 | width: 100%; 25 | padding: 10px; 26 | box-sizing: border-box; 27 | text-align: center; 28 | user-select: none; 29 | pointer-events: none; 30 | z-index: 1; /* TODO Solve this in HTML */ 31 | } 32 | 33 | a, button, input, select { 34 | pointer-events: auto; 35 | } 36 | 37 | .dg.ac { 38 | -moz-user-select: none; 39 | -webkit-user-select: none; 40 | -ms-user-select: none; 41 | user-select: none; 42 | z-index: 2 !important; /* TODO Solve this in HTML */ 43 | } 44 | 45 | #overlay { 46 | position: absolute; 47 | z-index: 2; 48 | top: 0; 49 | left: 0; 50 | width: 100%; 51 | height:100%; 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | opacity: 1; 56 | background-color: #000000; 57 | color: #ffffff; 58 | } 59 | 60 | #overlay > div { 61 | text-align: center; 62 | } 63 | 64 | #overlay > div > button { 65 | height: 20px; 66 | background: transparent; 67 | color: #ffffff; 68 | outline: 1px solid #ffffff; 69 | border: 0px; 70 | cursor: pointer; 71 | } 72 | 73 | #overlay > div > p { 74 | color: #777777; 75 | font-size: 12px; 76 | } 77 | 78 | .popup { 79 | position:fixed; 80 | padding: 10px; 81 | max-width: 500px; 82 | border-radius: 25px; 83 | left: -50%; 84 | top: 50%; 85 | transform:translate(-50%,-50%); 86 | background: var(--header-bg,#000); 87 | outline: none; 88 | display: flex; 89 | flex-direction:row; 90 | align-items:center; 91 | text-align: center; 92 | z-index: 100; 93 | font-size: x-large; 94 | opacity: 0.95; 95 | } 96 | 97 | #debug { 98 | position: absolute; 99 | left: 1em; 100 | top: 3em; 101 | padding: 1em; 102 | background: rgba(0, 0, 0, 0.8); 103 | color: white; 104 | font-family: monospace; 105 | visibility: hidden; 106 | } 107 | 108 | 109 | .annotation { 110 | position: absolute; 111 | top: 0; 112 | left: 0; 113 | z-index: 997; 114 | color: var(--primary-color); 115 | background: var(--body-bg,#000); 116 | transition: opacity .5s; 117 | width: 5em; 118 | height: 4em; 119 | border-radius: 30%; 120 | text-align: center; 121 | border-color: var(--primary-color); 122 | border-width: solid; 123 | border-style: solid; 124 | opacity: 0.6; 125 | display: table; 126 | } 127 | 128 | .annotation p { 129 | display: table-cell; 130 | vertical-align: middle; 131 | text-align: center; 132 | line-height: 1em; 133 | } 134 | 135 | .swibu { 136 | width: 200px; 137 | height: 25px; 138 | text-align: center; 139 | position: absolute; 140 | will-change: transform; 141 | transition: 0.3s ease all; 142 | border: 1px solid #012; 143 | background: var(--body-bg,#000); 144 | z-index: 998; 145 | } 146 | .swibu-case { 147 | display: inline-block; 148 | background: none; 149 | width: 49%; 150 | height: 100%; 151 | color: var(--widget-text-color,#def); 152 | position: relative; 153 | border: none; 154 | transition: 0.3s ease all; 155 | padding-bottom: 1px; 156 | z-index: 999; 157 | } 158 | .swibu-case:hover { 159 | color: var(--primary-color,#fff); 160 | } 161 | .swibu-case:focus { 162 | outline: none; 163 | } 164 | .swibu .swibuactive { 165 | color: var(--widget-text-color,#def); 166 | background-color: var(--primary-color,#fff); 167 | position: absolute; 168 | left: 0; 169 | top: 0; 170 | width: 50%; 171 | height: 100%; 172 | z-index: 998; 173 | transition: 0.3s ease-out all; 174 | } 175 | .swibu .swibuactive-case { 176 | color: #000/*var(--widget-text-color,#def);*/ 177 | } 178 | 179 | round-slider { 180 | float: left; 181 | width: 75px; 182 | --round-slider-path-width: 7; 183 | --round-slider-path-color: var(--widget-design-bg,#def); 184 | --round-slider-bar-color: var(--primary-color); 185 | --round-slider-handle-color: var(--primary-color); 186 | } 187 | 188 | 189 | 190 | 191 | .dropbtn { 192 | background-color: var(--primary-color); 193 | color: #000; 194 | border: none; 195 | cursor: pointer; 196 | } 197 | 198 | .dropdown { 199 | position: relative; 200 | display: inline-block; 201 | } 202 | 203 | .dropdown-content { 204 | display: none; 205 | position: absolute; 206 | background-color: var(--widget-design-bg,#def); 207 | /* min-width: 160px;*/ 208 | box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); 209 | z-index: 999; 210 | text-align: left; 211 | } 212 | 213 | .dropdown-content option { 214 | color: var(--widget-text-color,#def); 215 | text-decoration: none; 216 | display: block; 217 | } 218 | 219 | .dropdown-content option:hover {background-color: var(--primary-color)} 220 | 221 | .dropdown:hover .dropdown-content { 222 | display: block; 223 | } 224 | --------------------------------------------------------------------------------