Files
martin.fencl f0a6480c86
continuous-integration/drone/push Build is passing
feat: add BufferGeometryUtils for advanced geometry manipulation
- Implemented computeMikkTSpaceTangents for consistent tangent generation.
- Added mergeGeometries to combine multiple BufferGeometries.
- Introduced mergeAttributes for merging BufferAttributes.
- Created deepCloneAttribute for cloning BufferAttributes.
- Developed interleaveAttributes and deinterleaveAttribute for attribute management.
- Added estimateBytesUsed to calculate memory usage of geometries.
- Implemented mergeVertices to optimize vertex data.
- Created toTrianglesDrawMode for converting geometries to triangle mode.
- Added computeMorphedAttributes for handling morph targets in geometries.
- Implemented mergeGroups to consolidate geometry groups.
- Developed toCreasedNormals for generating smooth normals with crease angles.
2026-11-02 13:38:01 +01:00

128 lines
8.8 KiB
JavaScript
Raw Permalink 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.
// Plan coordinates in metres: px garden -> road; py west -> east.
// Areas are the printed PDF values; names (except zádveří) follow the drawn fixtures.
export const rooms = [
{id:'living',name:'Obývací pokoj + kuchyň',short:'Obývák + kuchyň',area:49.88,polygon:[[.295,.295],[7.955,.295],[7.955,6.791],[.295,6.791]],label:[4.0,3.7],spawn:[3.7,3.7],look:[1.2,1.8],floor:'wood'},
{id:'spare',name:'Místnost bez určení',short:'Místnost',area:7.05,polygon:[[8.1,.295],[11.255,.295],[11.255,2.531],[8.1,2.531]],label:[9.7,1.4],spawn:[9.7,1.4],look:[8.5,2.3],floor:'tile'},
{id:'wardrobe',name:'Šatna',short:'Šatna',area:6.15,polygon:[[8.1,2.656],[11.255,2.656],[11.255,4.605],[8.1,4.605]],label:[9.7,3.63],spawn:[8.8,3.65],look:[10.5,3.65],floor:'wood'},
{id:'entry',name:'Zádveří',short:'Zádveří',area:8.58,polygon:[[8.1,4.75],[11.255,4.75],[11.255,7.432],[8.1,7.432]],label:[9.8,6.1],spawn:[10.3,6.1],look:[8.3,5.8],floor:'tile'},
{id:'hall',name:'Chodba',short:'Chodba',area:7.59,polygon:[[8.1,7.557],[9.421,7.557],[9.421,13.406],[8.1,13.406]],label:[8.76,10.5],spawn:[8.76,8.15],look:[8.76,12.6],floor:'wood'},
{id:'technical',name:'Technická místnost',short:'Technická',area:7.10,polygon:[[9.546,7.557],[11.38,7.557],[11.38,6.821],[12.755,6.821],[12.755,9.456],[9.546,9.456]],label:[11.15,8.35],spawn:[10.5,8.4],look:[12.3,8.1],floor:'tile'},
{id:'bath',name:'Koupelna',short:'Koupelna',area:8.66,polygon:[[9.546,9.581],[12.755,9.581],[12.755,12.282],[9.546,12.282]],label:[10.6,10.9],spawn:[10.25,10.95],look:[12.15,11.0],floor:'tile'},
{id:'front',name:'Pokoj u příjezdu',short:'Pokoj',area:15.08,polygon:[[9.546,12.407],[12.755,12.407],[12.755,17.105],[9.546,17.105]],label:[11.0,14.6],spawn:[10.9,14.3],look:[12.0,16.0],floor:'wood'},
{id:'garden',name:'Pokoj u zahrady',short:'Pokoj',area:15.62,polygon:[[4.045,7.015],[7.955,7.015],[7.955,10.981],[4.045,10.981]],label:[5.7,8.8],spawn:[5.4,8.7],look:[7.0,7.5],floor:'wood'},
{id:'master',name:'Ložnice se šatnou',short:'Ložnice + šatna',area:22.35,polygon:[[4.045,11.106],[7.955,11.106],[7.955,13.425],[5.344,13.425],[5.344,13.55],[7.673,13.55],[7.673,17.105],[4.045,17.105]],label:[4.65,14.2],spawn:[4.6,14.5],look:[5.95,16.2],floor:'wood'},
{id:'ensuite',name:'Koupelna u ložnice',short:'Koupelna',area:5.68,polygon:[[7.798,13.55],[9.421,13.55],[9.421,17.105],[7.798,17.105]],label:[8.6,15.2],spawn:[8.35,14.55],look:[8.6,16.65],floor:'tile'}
];
// axis=px: fixed px, runs along py; axis=py: fixed py, runs along px.
export const partitions = [
['px',8.0275,.295,13.425,.145,[[5.19,6.09],[9.257,10.156],[11.805,12.708]]],
['py',2.5935,8.1,11.255,.125,[[8.349,9.248]]],
['py',4.6775,8.1,11.255,.145,[[8.349,9.248]]],
['py',7.4945,8.1,11.255,.125,[[8.349,9.248]]],
['py',6.903,3.775,7.955,.224,[]],
['py',11.0435,4.045,7.955,.125,[]],
['px',9.4835,7.557,13.406,.125,[[8.006,8.905],[10.656,11.457],[12.457,13.357]]],
['py',9.5185,9.546,12.755,.125,[]],
['py',12.3445,9.546,12.755,.125,[]],
['py',13.4785,8.1,9.421,.145,[]],
['py',13.4875,5.344,7.955,.125,[]],
['px',7.7355,13.55,17.105,.125,[[14.152,14.953]]],
['px',9.4835,13.406,17.105,.125,[]],
['py',10.63,11.255,12.755,.10,[]]
];
export function buildInterior({THREE,houseGroup,planPoint,box,mesh,group}) {
const root=group(houseGroup,'Interiér podle půdorysu');
const floors=group(root,'Podlahy místností'),fullWalls=group(root,'Příčky – plná výška'),cutWalls=group(root,'Příčky – řez 1,12 m');
const ceiling=group(root,'Podhled +2,600 m'),furniture=group(root,'Orientační vybavení');
const height=2.6,cutHeight=1.12,doorHead=2.1;
const wallMaterial=new THREE.MeshStandardMaterial({color:0xeeeae1,roughness:.92});
const wood=new THREE.MeshStandardMaterial({color:0xc7ac87,roughness:.85});
const tile=new THREE.MeshStandardMaterial({color:0xc8c6be,roughness:.83});
const trim=new THREE.MeshStandardMaterial({color:0xe4d5bb,roughness:.78});
const grout=new THREE.LineBasicMaterial({color:0x8c8373,transparent:true,opacity:.16});
const wallColliders=[];
function horizontal(parent,name,polygon,y,material,down=false) {
const points=polygon.map(p=>planPoint(...p));
const triangles=THREE.ShapeUtils.triangulateShape(points.map(([x,z])=>new THREE.Vector2(x,z)),[]);
const geom=new THREE.BufferGeometry();
geom.setAttribute('position',new THREE.Float32BufferAttribute(points.flatMap(([x,z])=>[x,y,z]),3));
const indices=triangles.flatMap(([a,b,c])=>{
const A=points[a],B=points[b],C=points[c];
const normalY=(B[1]-A[1])*(C[0]-A[0])-(B[0]-A[0])*(C[1]-A[1]);
return (normalY>0)!==down?[a,b,c]:[c,b,a];
});
geom.setIndex(indices);geom.computeVertexNormals();
const m=mesh(geom,material,parent,name);m.castShadow=false;return m;
}
// A continuous surface also fills thresholds, so doors have no visible cracks.
const outline=[[0,0],[11.55,0],[11.55,6.525],[13.05,6.525],[13.05,17.4],[3.75,17.4],[3.75,7.125],[0,7.125]];
horizontal(floors,'Podlaha a prahy',outline,.004,tile);
horizontal(ceiling,'Souvislý rovný podhled',outline,height,new THREE.MeshStandardMaterial({color:0xf5f3eb,roughness:1,side:THREE.DoubleSide}),true);
for(const room of rooms) {
const m=horizontal(floors,`${room.name} · ${room.area.toFixed(2)} m²`,room.polygon,.01,room.floor==='wood'?wood:tile);
m.userData.roomId=room.id;
// Thin joints: plank rhythm in dry rooms, large tiles in service rooms.
const loX=Math.min(...room.polygon.map(p=>p[0])),hiX=Math.max(...room.polygon.map(p=>p[0]));
const loY=Math.min(...room.polygon.map(p=>p[1])),hiY=Math.max(...room.polygon.map(p=>p[1]));
const pts=[];
const inside=(px,py)=>pointInPolygon(room.polygon,px,py);
function joint(a,b) { const [x,z]=planPoint(...a),[xx,zz]=planPoint(...b);pts.push(x,.012,z,xx,.012,zz); }
for(let py=Math.ceil(loY/(room.floor==='wood'?.20:.6))*(room.floor==='wood'?.20:.6);py<hiY;py+=room.floor==='wood'?.20:.6) {
let start=null;
for(let px=loX+.002;px<=hiX+.03;px+=.025) {
const ok=px<hiX&&inside(px,py);
if(ok&&start===null)start=px;
if(!ok&&start!==null){joint([start,py],[Math.min(px,hiX-.002),py]);start=null;}
}
}
if(room.floor==='tile')for(let px=Math.ceil(loX/.6)*.6;px<hiX;px+=.6){
let start=null;for(let py=loY+.002;py<=hiY+.03;py+=.025){
const ok=py<hiY&&inside(px,py);if(ok&&start===null)start=py;
if(!ok&&start!==null){joint([px,start],[px,Math.min(py,hiY-.002)]);start=null;}
}
}
const geometry=new THREE.BufferGeometry();geometry.setAttribute('position',new THREE.Float32BufferAttribute(pts,3));
const joints=new THREE.LineSegments(geometry,grout);joints.name='Spáry podlahy';floors.add(joints);
}
function wallBox(parent,axis,fixed,start,end,thickness,bottom,top,material,name) {
const px=axis==='px'?fixed:(start+end)/2,py=axis==='py'?fixed:(start+end)/2;
const [x,z]=planPoint(px,py);
return box(parent,name,x,(bottom+top)/2,z,axis==='px'?end-start:thickness,top-bottom,axis==='px'?thickness:end-start,material);
}
for(const [axis,fixed,start,end,thickness,holes] of partitions) {
let cursor=start;
const solid=(a,b)=>{
if(b-a<.001)return;
const m=wallBox(fullWalls,axis,fixed,a,b,thickness,0,height,wallMaterial,'Vnitřní příčka');
wallBox(cutWalls,axis,fixed,a,b,thickness,0,cutHeight,wallMaterial,'Vnitřní příčka v řezu');
const sx=axis==='px'?b-a:thickness,sz=axis==='px'?thickness:b-a;
wallColliders.push({minX:m.position.x-sx/2,maxX:m.position.x+sx/2,minZ:m.position.z-sz/2,maxZ:m.position.z+sz/2});
for(const parent of [fullWalls,cutWalls])wallBox(parent,axis,fixed,a,b,thickness+.016,.015,.09,trim,'Soklová lišta');
};
for(const [a,b] of holes){
solid(cursor,a);
wallBox(fullWalls,axis,fixed,a,b,thickness,doorHead,height,wallMaterial,'Nadpraží vnitřních dveří');
for(const parent of [fullWalls,cutWalls])for(const edge of [a,b])wallBox(parent,axis,fixed,edge-.018,edge+.018,thickness+.035,.01,parent===fullWalls?doorHead:cutHeight,trim,'Obložková zárubeň');
wallBox(fullWalls,axis,fixed,a-.018,b+.018,thickness+.035,doorHead-.04,doorHead,trim,'Horní zárubeň');
cursor=b;
}
solid(cursor,end);
}
// Attic access as drawn in vestibule; a flush hatch, not a freestanding object.
const [hx,hz]=planPoint(8.9,6.3);
box(ceiling,'Půdní výlez 1000 × 700',hx,2.586,hz,.70,.018,1.0,trim);
fullWalls.visible=false;ceiling.visible=false;root.visible=false;
return {root,floors,fullWalls,cutWalls,ceiling,furniture,rooms,wallColliders,wallMaterial,cutHeight,height};
}
export function pointInPolygon(polygon,x,z) {
let inside=false;
for(let i=0,j=polygon.length-1;i<polygon.length;j=i++){
const a=polygon[i],b=polygon[j];
if((a[1]>z)!==(b[1]>z)&&x<(b[0]-a[0])*(z-a[1])/(b[1]-a[1])+a[0])inside=!inside;
}
return inside;
}