127 lines
14 KiB
JavaScript
127 lines
14 KiB
JavaScript
import {floors,walls,outline,pointInPolygon} from './plan.js?v=1850';
|
||
export function buildHouse(ctx){
|
||
const {THREE,houseGroup,modelRoot,roofGroup,groundGroup,boundaryGroup,parkingGroup,planPoint,box,mesh,group,siteData}=ctx;
|
||
const mat=(color,extra={})=>new THREE.MeshStandardMaterial({color,roughness:.86,...extra});
|
||
const m={wall:mat('#e5e0d5'),trim:mat('#cfbfa5'),plinth:mat('#99998f'),roof:mat('#786e63',{side:THREE.DoubleSide}),frame:mat('#e3dfd3'),glass:mat('#a5c6cc',{transparent:true,opacity:.36,roughness:.2,side:THREE.DoubleSide}),oak:mat('#c5a77e'),tile:mat('#c8c6bc'),brick:mat('#ae9180'),deck:mat('#b29c7d'),ceiling:mat('#f1eee6',{side:THREE.DoubleSide}),metal:mat('#606964',{metalness:.3}),grass:mat('#a3b091'),road:mat('#969c94')};
|
||
const block=(p,n,x,z,w,d,b,t,material)=>{const [X,Z]=planPoint(x,z);return box(p,n,X,(b+t)/2,Z,w,t-b,d,material);};
|
||
function surface(p,n,polygon,height,material,holes=[]){
|
||
const pts=polygon.map(q=>new THREE.Vector2(...q)),hs=holes.map(h=>h.map(q=>new THREE.Vector2(...q)));
|
||
const all=[...polygon,...holes.flat()];const g=new THREE.BufferGeometry();
|
||
g.setAttribute('position',new THREE.Float32BufferAttribute(all.flatMap(([x,y])=>{const [X,Z]=planPoint(x,y);return [X,typeof height==='function'?height(x,y):height,Z];}),3));
|
||
g.setIndex(THREE.ShapeUtils.triangulateShape(pts,hs).flatMap(([a,b,c])=>[c,b,a]));g.computeVertexNormals();return mesh(g,material,p,n);
|
||
}
|
||
function face(p,n,pts,material){const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(pts.flatMap(([x,h,z])=>{const [X,Z]=planPoint(x,z);return[X,h,Z];}),3));const ix=[];for(let i=1;i<pts.length-1;i++)ix.push(0,i,i+1);g.setIndex(ix);g.computeVertexNormals();return mesh(g,material,p,n);}
|
||
const levels=floors.map(f=>({...f,root:group(houseGroup,f.title),colliders:[]}));
|
||
const wallBox=(p,axis,fixed,a,b,t,bottom,top,ma,n)=>block(p,n,axis==='x'?fixed:(a+b)/2,axis==='y'?fixed:(a+b)/2,axis==='x'?t:b-a,axis==='y'?t:b-a,bottom,top,ma);
|
||
for(const f of levels){
|
||
f.full=group(f.root,'Stěny a otvory');f.cut=group(f.root,'Stěny v řezu');f.ground=group(f.root,'Podlahy');f.ceiling=group(f.root,'Podhledy');f.furniture=group(f.root,'Orientační vybavení');f.stairs=group(f.root,'Schodiště');
|
||
const b=f.base;
|
||
if(f.id==='ground')surface(f.ground,'Prahy 1NP',outline,-.055,m.tile,[[[5.07,1.0],[6.15,1.0],[6.15,4.27],[5.07,4.27]]]);
|
||
if(f.id==='upper'){
|
||
surface(f.ground,'Nosná deska hlavní části',[[4.19,5.45],[10.44,5.45],[10.44,12.36],[4.19,12.36]],3.54,m.tile);
|
||
surface(f.ground,'Podlaha podkroví',[[1.22,1.67],[4.72,1.67],[4.72,11.09],[1.22,11.09]],3.24,m.oak);
|
||
}
|
||
for(const r of f.rooms){
|
||
if(!r.id.endsWith('.01')||r.id==='1.01')surface(f.ground,r.id+' '+r.name+' · '+r.area+' m²',r.polygon,r.level+.007,m[r.floor]||m.oak);
|
||
if(r.height && r.name!=='Schodiště')surface(f.ceiling,'Podhled '+r.id,r.polygon,r.level+r.height,m.ceiling);
|
||
// Floor pattern is clipped to each polygon.
|
||
const x0=Math.min(...r.polygon.map(v=>v[0])),x1=Math.max(...r.polygon.map(v=>v[0])),y0=Math.min(...r.polygon.map(v=>v[1])),y1=Math.max(...r.polygon.map(v=>v[1]));
|
||
const pos=[],step=['wood','deck'].includes(r.floor)?.22:.6;
|
||
for(let y=Math.ceil(y0/step)*step;y<y1;y+=step){let a=null;for(let x=x0+.006;x<x1+.025;x+=.025){const ok=pointInPolygon(r.polygon,x,y);if(ok&&a===null)a=x;if(!ok&&a!==null){const A=planPoint(a,y),B=planPoint(x-.025,y);pos.push(A[0],r.level+.012,A[1],B[0],r.level+.012,B[1]);a=null;}}if(a!==null){const A=planPoint(a,y),B=planPoint(x1-.005,y);pos.push(A[0],r.level+.012,A[1],B[0],r.level+.012,B[1]);}}
|
||
const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(pos,3));const lines=new THREE.LineSegments(g,new THREE.LineBasicMaterial({color:'#706b5f',transparent:true,opacity:.16}));f.ground.add(lines);
|
||
}
|
||
for(const [axis,fixed,start,end,t,bottom,top,openings] of walls[f.id]){
|
||
const cutTop=Math.min(top,bottom+1.12);
|
||
function solid(a,b,lo=bottom,hi=top,collide=true){if(b-a<.002||hi-lo<.002)return;wallBox(f.full,axis,fixed,a,b,t,lo,hi,m.wall,'Zdivo');if(lo<cutTop)wallBox(f.cut,axis,fixed,a,b,t,lo,Math.min(hi,cutTop),m.wall,'Zdivo v řezu');
|
||
if(collide&&lo<bottom+.4){const x=axis==='x'?fixed:(a+b)/2,y=axis==='y'?fixed:(a+b)/2;const [X,Z]=planPoint(x,y),sx=axis==='x'?t:b-a,sz=axis==='y'?t:b-a;f.colliders.push({minX:X-sx/2,maxX:X+sx/2,minZ:Z-sz/2,maxZ:Z+sz/2});}}
|
||
let cursor=start;
|
||
for(const [a,b,sill,head,kind] of openings){solid(cursor,a);solid(a,b,bottom,sill,kind==='window');solid(a,b,head,top,false);
|
||
const window=kind==='window';for(const parent of [f.full,f.cut]){const cap=parent===f.cut?cutTop:head;
|
||
for(const edge of [a,b])if(cap>sill)wallBox(parent,axis,fixed,edge-.025,edge+.025,t+.025,sill,cap,m.frame,'Rám otvoru');
|
||
if(head<=cap)wallBox(parent,axis,fixed,a,b,t+.035,head-.05,head,m.frame,'Horní rám');
|
||
if(window&&sill<cap)wallBox(parent,axis,fixed,a,b,t+.08,sill-.035,sill+.015,m.frame,'Parapet');
|
||
if(window&&cap>sill){wallBox(parent,axis,fixed,a+.025,b-.025,.025,sill+.02,cap-.025,m.glass,'Zasklení').castShadow=false;if(b-a>.8)wallBox(parent,axis,fixed,(a+b)/2-.025,(a+b)/2+.025,.09,sill,cap,m.frame,'Sloupek okna');}
|
||
}
|
||
// Exterior doors remain open for the walkthrough; balcony doors have a light side frame.
|
||
cursor=b;
|
||
}solid(cursor,end);
|
||
}
|
||
f.cut.visible=false;
|
||
}
|
||
// Sloping podkroví ceilings: ruled surfaces, matching the lower roof and the stated maximum clear height.
|
||
const upper=levels[2];
|
||
const lowCeiling=(x,y)=>Math.min(5.71,3.43+.492*(x+.3)-.13,3.43+.598*(y-.25)-.13,3.43+.598*(12.36-y)-.13);
|
||
upper.ceiling.children.filter(o=>/2.04|2.05/.test(o.name)).forEach(o=>upper.ceiling.remove(o));
|
||
for(const r of upper.rooms.filter(r=>['2.04','2.05'].includes(r.id))){
|
||
const xs=r.polygon.map(p=>p[0]),ys=r.polygon.map(p=>p[1]),pos=[],indices=[];
|
||
for(let x=Math.min(...xs);x<Math.max(...xs);x+=.15)for(let y=Math.min(...ys);y<Math.max(...ys);y+=.15){
|
||
const x1=Math.min(x+.15,Math.max(...xs)),y1=Math.min(y+.15,Math.max(...ys)),k=pos.length/3;
|
||
const cx=(x+x1)/2,cy=(y+y1)/2;if([[1.65,6.4,.72,.8],[1.65,8.2,.72,.8],[3.25,2.25,.8,.64]].some(([a,b,w,d])=>Math.abs(cx-a)<w/2-.1&&Math.abs(cy-b)<d/2-.1))continue;
|
||
for(const [a,b]of [[x,y],[x1,y],[x1,y1],[x,y1]]){const [X,Z]=planPoint(a,b);pos.push(X,lowCeiling(a,b),Z);}indices.push(k,k+2,k+1,k,k+3,k+2);
|
||
}
|
||
const geom=new THREE.BufferGeometry();geom.setAttribute('position',new THREE.Float32BufferAttribute(pos,3));geom.setIndex(indices);geom.computeVertexNormals();mesh(geom,m.ceiling,upper.ceiling,'Šikmý podhled '+r.id);
|
||
}
|
||
// Clip attic partitions to the roof envelope so walls never pierce its planes.
|
||
for(const parent of [upper.full,upper.cut])for(const o of parent.children){if(!o.isMesh||!o.geometry.attributes.position)continue;const x=o.position.x+5.465,y=o.position.z+6.18;if(x>4.5)continue;
|
||
const a=o.geometry.attributes.position;for(let i=0;i<a.count;i++){const X=a.getX(i)+x,Y=a.getZ(i)+y;const cap=lowCeiling(X,Y);if(a.getY(i)+o.position.y>cap)a.setY(i,cap-o.position.y);}a.needsUpdate=true;o.geometry.computeVertexNormals();}
|
||
// Stair is a schematic 17 × 200 / 275 flight with a turn at its upper end.
|
||
for(let i=0;i<13;i++)block(levels[1].stairs,'Stupeň 1NP → 2NP',5.61,4.15-i*.245,1.08,.25,-.05,(i+1)*.2,m.oak);
|
||
for(let i=0;i<4;i++)block(levels[1].stairs,'Zatáčka schodiště',5.3+i*.32,1.09,.34,.88,2.6,2.8+i*.2,m.oak);
|
||
surface(upper.ground,'Podesta 2NP',[[5.07,4.27],[6.27,4.27],[6.27,5.57],[5.07,5.57]],3.4,m.oak);
|
||
for(let i=0;i<10;i++)block(levels[0].stairs,'Stupeň 1PP → 1NP',5.57,1.8+i*.265,1,.267,-2.2,-1.9+(i+1)*.19,m.plinth);
|
||
surface(levels[0].ground,'Podesta sklepa',[[5.07,1.02],[6.73,1.02],[6.73,1.72],[5.07,1.72]],-1.9,m.tile);
|
||
block(levels[0].full,'Pilíř ve sklepě',8.89,1.9,.32,.32,-2.2,-.14,m.wall);block(levels[0].cut,'Pilíř ve sklepě v řezu',8.89,1.9,.32,.32,-2.2,-1.08,m.wall);
|
||
block(upper.full,'Horní stěna nad napojením nižší střechy',5.36,5.685,2.28,.47,5.78,6.8,m.wall);
|
||
// Balcony slab and open metal guardrail.
|
||
const balcony=upper.rooms.find(r=>r.id==='2.06');surface(upper.ground,'Balkón – spodní líc',balcony.polygon,3.14,m.plinth);
|
||
const rail=group(upper.full,'Balkónové zábradlí');for(let i=0;i<balcony.polygon.length-1;i++){if(i===2)continue;const a=balcony.polygon[i],b=balcony.polygon[i+1],d=Math.hypot(b[0]-a[0],b[1]-a[1]),count=Math.ceil(d/.9);for(let j=0;j<=count;j++){const x=a[0]+(b[0]-a[0])*j/count,z=a[1]+(b[1]-a[1])*j/count;block(rail,'Sloupek',x,z,.035,.035,3.45,4.5,m.metal);}const [X,Z]=planPoint((a[0]+b[0])/2,(a[1]+b[1])/2);box(rail,'Madlo',X,4.5,Z,d,.035,.035,m.metal,-Math.atan2(b[1]-a[1],b[0]-a[0]));}
|
||
upper.cut.add(rail.clone());
|
||
for(let i=0;i<3;i++)block(levels[1].ground,'Vstupní schod',11.1+i*.29,1.17,.3,1.4,-.6,-.15*(i+1),m.plinth);
|
||
function hip(x0,y0,x1,y1,eave,ridge){
|
||
const cx=(x0+x1)/2,cy=(y0+y1)/2,dx=x1-x0,dy=y1-y0;const h=(ridge-eave)/Math.tan(31*Math.PI/180);const A=[x0,eave,y0],B=[x1,eave,y0],C=[x1,eave,y1],D=[x0,eave,y1];
|
||
const R1=dx<dy?[cx,ridge,y0+h]:[x0+h,ridge,cy],R2=dx<dy?[cx,ridge,y1-h]:[x1-h,ridge,cy];
|
||
if(dx<dy){face(roofGroup,'Valba',[A,B,R1],m.roof);face(roofGroup,'Střešní rovina',[B,C,R2,R1],m.roof);face(roofGroup,'Valba',[C,D,R2],m.roof);face(roofGroup,'Střešní rovina',[D,A,R1,R2],m.roof);}else{face(roofGroup,'Střecha',[A,B,R2,R1],m.roof);face(roofGroup,'Valba',[B,C,R2],m.roof);face(roofGroup,'Střecha',[C,D,R1,R2],m.roof);face(roofGroup,'Valba',[D,A,R1],m.roof);}
|
||
for(const [a,b]of [[A,B],[B,C],[C,D],[D,A]]){const [X,Z]=planPoint((a[0]+b[0])/2,(a[2]+b[2])/2);box(roofGroup,'Okap a římsa',X,eave-.06,Z,Math.hypot(a[0]-b[0],a[2]-b[2]),.13,.13,m.metal,-Math.atan2(b[2]-a[2],b[0]-a[0]));}
|
||
}
|
||
hip(3.89,5.15,10.74,12.66,6.8,8.85);
|
||
hip(4.42,.25,6.8,5.75,5.9,6.53);
|
||
const A=[-.3,3.43,.25],B=[4.72,5.9,4.38],C=[4.72,5.9,8.23],D=[-.3,3.43,12.36];
|
||
const hole=(x,y,w,d)=>[[x-w/2,y-d/2],[x+w/2,y-d/2],[x+w/2,y+d/2],[x-w/2,y+d/2]];
|
||
surface(roofGroup,'Severní střešní rovina',[A,B,C,D].map(v=>[v[0],v[2]]),(x,y)=>3.43+.492*(x+.3),m.roof,[hole(1.65,6.4,.72,.8),hole(1.65,8.2,.72,.8)]);
|
||
surface(roofGroup,'Východní valba nižší střechy',[[A[0],A[2]],[4.72,.25],[B[0],B[2]]],(x,y)=>3.43+.598*(y-.25),m.roof,[hole(3.25,2.25,.8,.64)]);
|
||
face(roofGroup,'Západní valba nižší střechy',[D,C,[4.72,3.43,12.36]],m.roof);
|
||
block(roofGroup,'Komín',4.92,5.25,.65,.65,5.4,8.72,m.wall);block(roofGroup,'Hlava komína',4.92,5.25,.78,.78,8.72,8.84,m.plinth);
|
||
// Roof windows are schematic; their plan positions follow the dashed openings.
|
||
for(const [x,y]of [[1.65,6.4],[1.65,8.2],[3.25,2.25]]){
|
||
const w=x<2?.72:.8,d=x<2?.8:.64;
|
||
const height=(a,b)=>x<2?3.43+.492*(a+.3)+.025:3.43+.598*(b-.25)+.025;
|
||
surface(roofGroup,'Zasklení střešního okna',hole(x,y,w,d),height,m.glass).castShadow=false;
|
||
surface(roofGroup,'Rám střešního okna',hole(x,y,w+.12,d+.12),height,m.frame,[hole(x,y,w,d)]);
|
||
const outer=hole(x,y,w,d),inner=hole(x,y,w-.20,d-.20);
|
||
for(let i=0;i<4;i++){const j=(i+1)%4;face(upper.ceiling,'Ostění střešního okna',[[...inner[i]],[...inner[j]],[...outer[j]],[...outer[i]]].map(([a,b],k)=>[a,k<2?lowCeiling(a,b):height(a,b),b]),m.ceiling);}
|
||
}
|
||
const foundation=group(houseGroup,'Sokl');for(let i=0;i<outline.length;i++){const a=outline[i],b=outline[(i+1)%outline.length];wallBox(foundation,a[0]===b[0]?'x':'y',a[0]===b[0]?a[0]:a[1],a[0]===b[0]?Math.min(a[1],b[1]):Math.min(a[0],b[0]),a[0]===b[0]?Math.max(a[1],b[1]):Math.max(a[0],b[0]),.24,-1.1,-.05,m.plinth,'Sokl');}
|
||
return {levels,materials:m,block,surface,face,foundation,wallMaterial:m.wall};
|
||
}
|
||
export function buildSite(ctx,data){
|
||
const {THREE,groundGroup,boundaryGroup,parkingGroup,planPoint,mesh,box,siteData:s}=ctx;const {surface,materials:m}=data;
|
||
let datum=372.45;
|
||
const raw=(x,y)=>{const e=s.origin[0]+s.ux[0]*x+s.uy[0]*y,n=s.origin[1]+s.ux[1]*x+s.uy[1]*y;const ix=Math.max(0,Math.min(s.width-1.001,e-s.bbox[0]-.5)),iy=Math.max(0,Math.min(s.height-1.001,s.bbox[3]-n-.5));const i=Math.floor(ix),j=Math.floor(iy),u=ix-i,v=iy-j;return (1-v)*((1-u)*s.heights[j][i]+u*s.heights[j][i+1])+v*((1-u)*s.heights[j+1][i]+u*s.heights[j+1][i+1]);};
|
||
const height=(x,y)=>raw(x,y)-datum;
|
||
const terrainMeshes=[],boundaries=[];
|
||
for(const parcel of s.parcels){
|
||
const polygon=parcel.polygon.slice(0,-1),pos=[],indices=[];
|
||
const triangles=THREE.ShapeUtils.triangulateShape(polygon.map(p=>new THREE.Vector2(...p)),[]);
|
||
for(const tri of triangles){const [a,b,c]=tri.map(i=>polygon[i]);const n=Math.ceil(Math.max(Math.hypot(a[0]-b[0],a[1]-b[1]),Math.hypot(a[0]-c[0],a[1]-c[1]),Math.hypot(b[0]-c[0],b[1]-c[1]))/1.2);const rows=[];
|
||
for(let i=0;i<=n;i++){rows[i]=[];for(let j=0;j<=n-i;j++){const x=a[0]+(b[0]-a[0])*i/n+(c[0]-a[0])*j/n,y=a[1]+(b[1]-a[1])*i/n+(c[1]-a[1])*j/n;rows[i][j]=pos.length/3;const [X,Z]=planPoint(x,y);pos.push(X,height(x,y),Z);}}
|
||
for(let i=0;i<n;i++)for(let j=0;j<n-i;j++){indices.push(rows[i][j],rows[i][j+1],rows[i+1][j]);if(j<n-i-1)indices.push(rows[i+1][j],rows[i][j+1],rows[i+1][j+1]);}
|
||
}
|
||
const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(pos,3));g.setIndex(indices);g.computeVertexNormals();const o=mesh(g,m.grass,groundGroup,'Terén parcely '+parcel.id);terrainMeshes.push(o);
|
||
const points=[...polygon,polygon[0]].map(([x,y])=>{const [X,Z]=planPoint(x,y);return new THREE.Vector3(X,height(x,y)+.06,Z);});const line=new THREE.Line(new THREE.BufferGeometry().setFromPoints(points),new THREE.LineBasicMaterial({color:'#4d6a4b'}));line.name='Hranice '+parcel.id;boundaryGroup.add(line);boundaries.push(line);
|
||
}
|
||
surface(parkingGroup,'Orientační poloha ulice Kovářova',[[-8,15],[19,15],[19,19],[-8,19]],height,m.road);
|
||
// Editable datum is explicit: the drawings do not tie ±0.000 to a Bpv elevation.
|
||
function setDatum(value){const delta=datum-value;datum=value;groundGroup.position.y+=delta;boundaryGroup.position.y+=delta;parkingGroup.position.y+=delta;}
|
||
return {setDatum,get datum(){return datum;},raw};
|
||
}
|