├── 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 | 'Licht ' +
36 | 'Heizung ' +
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 |
--------------------------------------------------------------------------------