Files
martinfencl.eu/kovarova/js/model.js
T
martin.fencl 16804546e9
continuous-integration/drone/push Build is passing
Add .DS_Store file to locenice directory
2026-11-06 20:46:55 +01:00

127 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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};
}