This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import {pointInPolygon} from './plan.js?v=1850';
|
||||
export function installInterior(ctx){
|
||||
const {THREE,house,houseGroup,modelRoot,roofGroup,roofTimber,controls,scene,renderer,planPoint,groundGroup,parkingGroup,boundaryGroup,resize,saveBlob,exportGlb,solar,setSolarPlaying,toggleInfo,toggleSun}=ctx;
|
||||
const $=s=>document.querySelector(s),canvas=renderer.domElement,perspective=ctx.getCamera(),ortho=new THREE.OrthographicCamera(-10,10,10,-10,.035,180);
|
||||
const state={enabled:false,mode:'axon',floor:'ground',panel:true,furniture:true,labels:true,fill:true,room:'1.08',position:new THREE.Vector3(),yaw:0,pitch:0,keys:new Set(),pad:new Set(),pointer:null,saved:null,userCamera:false};
|
||||
let data=house.levels[1],labels=[];
|
||||
const light=new THREE.AmbientLight(0xfff7e8,1.35);light.visible=false;scene.add(light);
|
||||
const toWorld=(p,y=0)=>{houseGroup.updateWorldMatrix(true,false);const [x,z]=planPoint(...p);return houseGroup.localToWorld(new THREE.Vector3(x,y,z));};
|
||||
const localToWorld=v=>{houseGroup.updateWorldMatrix(true,false);return houseGroup.localToWorld(v.clone());};
|
||||
const area=v=>v.toFixed(2).replace('.',',')+' m²';
|
||||
function rebuildMap(){
|
||||
$('#interiorRoom').innerHTML='<option value="">Vyberte místnost…</option>';$('#interiorMap').innerHTML='';$('#interiorLabels').innerHTML='';labels=[];
|
||||
$('#floorSummary').textContent=data.rooms.length+' místností · '+area(data.area)+(state.floor==='upper'?' + balkón 27,88 m²':'');
|
||||
$('.interior-heading .eyebrow').textContent='KOVÁŘOVA / '+data.title;
|
||||
const file=state.floor==='ground'?'1-D.1.1.3.2_Půdorys 1NP_420x525_20.10.2026.pdf':state.floor==='upper'?'2-D.1.1.3.3_Půdorys 2NP_420x525_20.10.2026.pdf':'3-D.1.1.3.1_Půdorys 1PP_420x525_20.10.2026.pdf';$('#floorSource').href='./references/'+encodeURIComponent(file);
|
||||
for(const room of data.rooms){
|
||||
const option=document.createElement('option');option.value=room.id;option.textContent=room.id+' '+room.name+' · '+area(room.area);$('#interiorRoom').append(option);
|
||||
const ns='http://www.w3.org/2000/svg',g=document.createElementNS(ns,'g'),p=document.createElementNS(ns,'polygon');p.setAttribute('points',room.polygon.map(([x,y])=>`${26+x*22},${10+y*22}`).join(' '));p.setAttribute('class','map-room');p.dataset.room=room.id;p.setAttribute('tabindex','0');p.setAttribute('role','button');p.setAttribute('aria-label',option.textContent);p.addEventListener('click',()=>selectRoom(room.id));p.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();selectRoom(room.id);}});g.append(p);
|
||||
const text=document.createElementNS(ns,'text');text.setAttribute('x',26+room.label[0]*22);text.setAttribute('y',13+room.label[1]*22);text.setAttribute('text-anchor','middle');text.textContent=room.id;g.append(text);$('#interiorMap').append(g);
|
||||
const tag=document.createElement('button');tag.type='button';tag.className='int-room-tag';tag.innerHTML=`<b>${room.short}</b><small>${area(room.area)}</small>`;tag.addEventListener('click',()=>selectRoom(room.id));$('#interiorLabels').append(tag);labels.push({room,tag});
|
||||
}
|
||||
}
|
||||
function selectRoom(id){const room=data.rooms.find(r=>r.id===id)||data.rooms[0];state.room=room.id;const [x,z]=planPoint(...room.spawn),[lx,lz]=planPoint(...room.look);state.position.set(x,room.level+1.62,z);state.yaw=Math.atan2(lx-x,lz-z);state.pitch=-.04;mode('walk');$('#interiorRoom').value=room.id;$('#interiorViewTitle').textContent=room.id+' '+room.name;updateWalkCamera();}
|
||||
function selectFloor(id){state.floor=id;data=house.levels.find(f=>f.id===id);state.room=data.rooms.find(r=>!r.name.includes('Schodiště'))?.id||data.rooms[0].id;state.position.set(0,0,0);rebuildMap();document.querySelectorAll('[data-floor]').forEach(b=>{b.classList.toggle('active',b.dataset.floor===id);b.setAttribute('aria-pressed',String(b.dataset.floor===id));});if(state.mode==='walk')selectRoom(state.room);else{sync();resize();fit();}}
|
||||
function switchCamera(c){ctx.setCamera(c);controls.object=c;}
|
||||
function sync(){
|
||||
const walking=state.enabled&&state.mode==='walk',cut=state.enabled&&!walking;
|
||||
for(const f of house.levels){f.root.visible=state.enabled?f===data:f.id!=='basement';f.full.visible=!cut;f.cut.visible=cut;f.ceiling.visible=!cut;f.furniture.visible=state.furniture;f.stairs.visible=true;}
|
||||
house.foundation.visible=!state.enabled;roofGroup.visible=!cut;roofTimber.visible=!cut;light.visible=state.enabled&&state.fill;house.materials.glass.opacity=walking?.15:.36;
|
||||
document.body.classList.toggle('interior-active',state.enabled);document.body.classList.toggle('interior-walk',walking);document.body.classList.toggle('interior-panel-closed',!state.panel);
|
||||
$('#interiorLaunch').setAttribute('aria-expanded',String(state.enabled));$('#interiorPanelToggle').setAttribute('aria-expanded',String(state.panel));$('#interiorRoomLabels').disabled=walking;
|
||||
document.querySelectorAll('[data-interior-mode]').forEach(b=>{const on=b.dataset.interiorMode===state.mode;b.classList.toggle('active',on);b.setAttribute('aria-pressed',String(on));});
|
||||
$('#interiorHint').textContent=walking?'W A S D: chůze · tažením se rozhlížejte · podlaží přepněte v panelu':'Tažením otáčejte · kolečkem přibližujte · vyberte místnost';
|
||||
if(!walking)$('#interiorViewTitle').textContent=(state.mode==='plan'?'Půdorys · ':'3D řez · ')+data.title;
|
||||
if(state.enabled){groundGroup.visible=walking&&data.id!=='basement'&&state.saved.ground;parkingGroup.visible=groundGroup.visible;boundaryGroup.visible=false;}
|
||||
labels.forEach(({tag})=>tag.hidden=!state.enabled||walking||!state.labels);
|
||||
}
|
||||
function enter(){if(state.enabled)return;state.saved={position:perspective.position.clone(),target:controls.target.clone(),fov:perspective.fov,near:perspective.near,ground:groundGroup.visible,parking:parkingGroup.visible,boundary:boundaryGroup.visible,playing:solar.playing};setSolarPlaying(false);toggleInfo(false);toggleSun(false);state.enabled=true;state.mode='axon';state.panel=true;state.userCamera=false;controls.minDistance=2;controls.maxDistance=95;sync();resize();fit();}
|
||||
function clearKeys(){state.keys.clear();state.pad.clear();document.querySelectorAll('#walkPad .held').forEach(b=>b.classList.remove('held'));}
|
||||
function exit(){if(!state.enabled)return;state.enabled=false;clearKeys();state.pointer=null;sync();const s=state.saved;switchCamera(perspective);controls.enabled=true;controls.enableRotate=true;controls.minDistance=15;controls.minPolarAngle=.08;controls.maxPolarAngle=1.52;perspective.up.set(0,1,0);perspective.fov=s.fov;perspective.near=s.near;perspective.position.copy(s.position);controls.target.copy(s.target);groundGroup.visible=s.ground;parkingGroup.visible=s.parking;boundaryGroup.visible=s.boundary;controls.update();resize();setSolarPlaying(s.playing);}
|
||||
function mode(value){if(!state.enabled)enter();state.mode=value;state.userCamera=false;clearKeys();controls.enabled=value!=='walk';controls.enableRotate=value!=='plan';controls.minPolarAngle=.001;controls.maxPolarAngle=1.52;switchCamera(value==='plan'?ortho:perspective);perspective.near=value==='walk'?.035:.1;perspective.fov=value==='walk'?65:36;perspective.updateProjectionMatrix();sync();resize();if(value==='walk'){if(state.position.lengthSq()===0)selectRoom(state.room);else updateWalkCamera();}else fit();}
|
||||
function fit(){if(!state.enabled)return;state.userCamera=false;if(state.mode==='walk'){selectRoom(state.room);return;}const rect=canvas.getBoundingClientRect(),aspect=rect.width/rect.height;const center=state.floor==='basement'?[7.9,2.3]:[5.4,6.15];const target=toWorld(center,data.base+.4);controls.target.copy(target);if(state.mode==='plan'){const h=Math.max(state.floor==='basement'?7:15,(state.floor==='basement'?8:14)/aspect);ortho.left=-h*aspect/2;ortho.right=-ortho.left;ortho.top=h/2;ortho.bottom=-h/2;ortho.zoom=1;const up=new THREE.Vector3(0,0,-1).applyQuaternion(modelRoot.quaternion);ortho.up.copy(up);ortho.position.copy(target).add(new THREE.Vector3(0,45,0));ortho.lookAt(target);ortho.updateProjectionMatrix();}else{perspective.up.set(0,1,0);const dist=Math.max(state.floor==='basement'?15:29,(state.floor==='basement'?12:27)/aspect);const offset=new THREE.Vector3(9,20,22).normalize().multiplyScalar(dist).applyQuaternion(modelRoot.quaternion);perspective.position.copy(target).add(offset);perspective.lookAt(target);}controls.update();}
|
||||
function resizeView(){if(!state.enabled)return;if(state.mode==='plan'||state.mode==='axon'&&!state.userCamera)fit();}
|
||||
function canStand(x,z){const px=x+5.465,py=z+6.18,r=.12;if(!pointInPolygon(data.outline,px,py))return false;const attic=data.id==='upper'&&data.rooms.find(r=>['2.04','2.05'].includes(r.id)&&pointInPolygon(r.polygon,px,py));if(attic&&Math.min(5.71,3.43+.492*(px+.3)-.13,3.43+.598*(py-.25)-.13,3.43+.598*(12.36-py)-.13)<attic.level+1.72)return false;for(const b of [...data.colliders,...(state.furniture?data.furnitureColliders:[])])if(x>b.minX-r&&x<b.maxX+r&&z>b.minZ-r&&z<b.maxZ+r)return false;return true;}
|
||||
function move(dx,dz){const n=Math.max(1,Math.ceil(Math.hypot(dx,dz)/.035));for(let i=0;i<n;i++){if(canStand(state.position.x+dx/n,state.position.z))state.position.x+=dx/n;if(canStand(state.position.x,state.position.z+dz/n))state.position.z+=dz/n;}const x=state.position.x+5.465,y=state.position.z+6.18;const r=data.rooms.find(r=>pointInPolygon(r.polygon,x,y));if(r){state.position.y=r.level+1.62;state.room=r.id;}}
|
||||
function updateWalkCamera(){const dir=new THREE.Vector3(Math.sin(state.yaw)*Math.cos(state.pitch),Math.sin(state.pitch),Math.cos(state.yaw)*Math.cos(state.pitch));perspective.position.copy(localToWorld(state.position));perspective.up.set(0,1,0);perspective.lookAt(localToWorld(state.position.clone().add(dir)));}
|
||||
function updateLabels(){if(!state.enabled||state.mode==='walk'||!state.labels)return;const c=ctx.getCamera(),r=canvas.getBoundingClientRect();c.updateMatrixWorld();const occupied=[];for(const {room,tag}of labels){const p=toWorld(room.label,room.level+.08).project(c),x=(p.x+1)*r.width/2,y=(1-p.y)*r.height/2;tag.style.left=x+'px';tag.style.top=y+'px';tag.hidden=p.z<-1||p.z>1||x<20||x>r.width-20||y<85||y>r.height-78;if(tag.hidden)continue;const w=tag.offsetWidth,h=tag.offsetHeight,b={x:x-w/2,y:y-h/2,w,h};if(occupied.some(a=>b.x<a.x+a.w+3&&b.x+b.w+3>a.x&&b.y<a.y+a.h+3&&b.y+b.h+3>a.y))tag.hidden=true;else occupied.push(b);}}
|
||||
function frame(dt){if(!state.enabled)return;if(state.mode==='walk'){const k=state.keys,p=state.pad;const forward=Number(k.has('KeyW')||k.has('ArrowUp')||p.has('forward'))-Number(k.has('KeyS')||k.has('ArrowDown')||p.has('back')),side=Number(k.has('KeyD')||p.has('right'))-Number(k.has('KeyA')||p.has('left'));state.yaw+=(Number(k.has('ArrowLeft'))-Number(k.has('ArrowRight')))*dt*1.4;const d=dt*1.4/Math.max(1,Math.hypot(forward,side));move((Math.sin(state.yaw)*forward-Math.cos(state.yaw)*side)*d,(Math.cos(state.yaw)*forward+Math.sin(state.yaw)*side)*d);updateWalkCamera();}updateLabels();}
|
||||
function keydown(e){if(e.key==='Escape'){exit();return;}if(/^(INPUT|SELECT|TEXTAREA|BUTTON|SUMMARY)$/.test(e.target.tagName))return;if(state.mode==='walk'&&['KeyW','KeyA','KeyS','KeyD','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)){e.preventDefault();state.keys.add(e.code);}}
|
||||
window.addEventListener('keyup',e=>state.keys.delete(e.code));window.addEventListener('blur',clearKeys);document.addEventListener('visibilitychange',clearKeys);
|
||||
canvas.addEventListener('pointerdown',e=>{if(!state.enabled)return;state.userCamera=true;if(state.mode==='walk'){document.activeElement?.blur();canvas.setPointerCapture(e.pointerId);state.pointer={id:e.pointerId,x:e.clientX,y:e.clientY};}});
|
||||
canvas.addEventListener('pointermove',e=>{const p=state.pointer;if(!p||p.id!==e.pointerId||state.mode!=='walk')return;state.yaw-=(e.clientX-p.x)*.004;state.pitch=Math.max(-1.15,Math.min(1.15,state.pitch-(e.clientY-p.y)*.003));p.x=e.clientX;p.y=e.clientY;});
|
||||
for(const event of ['pointerup','pointercancel','lostpointercapture'])canvas.addEventListener(event,()=>state.pointer=null);
|
||||
for(const b of document.querySelectorAll('[data-walk]')){b.addEventListener('pointerdown',e=>{e.preventDefault();b.setPointerCapture(e.pointerId);state.pad.add(b.dataset.walk);b.classList.add('held');});for(const event of ['pointerup','pointercancel','lostpointercapture'])b.addEventListener(event,()=>{state.pad.delete(b.dataset.walk);b.classList.remove('held');});}
|
||||
$('#interiorLaunch').addEventListener('click',enter);$('#interiorExit').addEventListener('click',exit);$('#interiorRoom').addEventListener('change',e=>e.target.value&&selectRoom(e.target.value));document.querySelectorAll('[data-floor]').forEach(b=>b.addEventListener('click',()=>selectFloor(b.dataset.floor)));document.querySelectorAll('[data-interior-mode]').forEach(b=>b.addEventListener('click',()=>{mode(b.dataset.interiorMode);b.blur();}));
|
||||
$('#interiorFit').addEventListener('click',fit);$('#interiorPanelToggle').addEventListener('click',()=>{state.panel=!state.panel;sync();resize();});$('#interiorFurniture').addEventListener('change',e=>{state.furniture=e.target.checked;sync();});$('#interiorRoomLabels').addEventListener('change',e=>{state.labels=e.target.checked;sync();});$('#interiorFill').addEventListener('change',e=>{state.fill=e.target.checked;sync();});$('#interiorWallColor').addEventListener('input',e=>house.wallMaterial.color.set(e.target.value));$('#interiorSaveGlb').addEventListener('click',exportGlb);
|
||||
function savePng(){renderer.render(scene,ctx.getCamera());updateLabels();const out=document.createElement('canvas'),scale=canvas.width/canvas.clientWidth;out.width=canvas.width;out.height=canvas.height+62*scale;const c=out.getContext('2d');c.fillStyle='#f8f9f3';c.fillRect(0,0,out.width,out.height);c.drawImage(canvas,0,0);if(state.labels&&state.mode!=='walk')for(const {room,tag}of labels){if(tag.hidden)continue;const x=parseFloat(tag.style.left)*scale,y=parseFloat(tag.style.top)*scale;c.font=`${10*scale}px Arial`;const text=room.short+' · '+area(room.area),w=c.measureText(text).width;c.fillStyle='#fafcf4eb';c.fillRect(x-w/2-6*scale,y-10*scale,w+12*scale,20*scale);c.fillStyle='#334c35';c.textAlign='center';c.fillText(text,x,y+3*scale);}c.textAlign='left';c.fillStyle='#2d3c31';c.font=`${12*scale}px Arial`;c.fillText('Kovářova · '+$('#interiorViewTitle').textContent,22*scale,canvas.height+26*scale);c.font=`${9*scale}px Arial`;c.fillText('Stávající stav · vybavení je pouze orientační',22*scale,canvas.height+44*scale);out.toBlob(blob=>blob&&saveBlob(blob,'kovarova-'+state.floor+'-'+state.mode+'.png'));}
|
||||
$('#interiorSavePng').addEventListener('click',savePng);rebuildMap();sync();
|
||||
window.KOVAROVA_INTERIOR={enter,exit,setMode:mode,selectRoom,selectFloor,canStand:(x,y)=>canStand(...planPoint(x,y)),getState:()=>({enabled:state.enabled,mode:state.mode,floor:state.floor,room:state.room,position:state.position.toArray(),canStand:canStand(state.position.x,state.position.z)})};
|
||||
return {get enabled(){return state.enabled;},get walking(){return state.enabled&&state.mode==='walk';},frame,resize:resizeView,keydown,exit,enter,savePng};
|
||||
}
|
||||
Reference in New Issue
Block a user