feat: add BufferGeometryUtils for advanced geometry manipulation
continuous-integration/drone/push Build is passing

- 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.
This commit is contained in:
martin.fencl
2026-11-02 13:38:01 +01:00
parent d01bd43f6b
commit f0a6480c86
18 changed files with 84615 additions and 47 deletions
+145
View File
@@ -0,0 +1,145 @@
// Schematic furnishing positions follow the supplied floor plan. Finishes are provisional.
export function furnishInterior({THREE,parent,planPoint,box,mesh,group}) {
const material=(color,roughness=.8)=>new THREE.MeshStandardMaterial({color,roughness});
const m={oak:material(0xb99b70),oakLight:material(0xd2ba95),white:material(0xf0eee5),stone:material(0xe2ded2),sage:material(0x9ca894),cloth:material(0xd3c7b5),linen:material(0xeae5d9),pillow:material(0xb9bca9),dark:material(0x323a38),metal:new THREE.MeshStandardMaterial({color:0x8c9390,metalness:.65,roughness:.25}),water:material(0xc8d8d7),glass:new THREE.MeshStandardMaterial({color:0xb6d4d3,transparent:true,opacity:.23,roughness:.15,side:THREE.DoubleSide}),mirror:new THREE.MeshStandardMaterial({color:0xbdcaca,metalness:.65,roughness:.08})};
const colliders=[];
function block(p,name,px,py,w,d,h,y,mat){const [x,z]=planPoint(px,py);return box(p,name,x,y+h/2,z,d,h,w,mat);}
function collide(px,py,w,d){const [x,z]=planPoint(px,py);colliders.push({minX:x-d/2,maxX:x+d/2,minZ:z-w/2,maxZ:z+w/2});}
function cylinder(p,name,px,py,r,h,y,mat){const [x,z]=planPoint(px,py);const o=mesh(new THREE.CylinderGeometry(r,r,h,24),mat,p,name);o.position.set(x,y+h/2,z);return o;}
function oval(p,name,px,py,w,d,h,y,mat){const [x,z]=planPoint(px,py);const o=mesh(new THREE.SphereGeometry(1,24,12),mat,p,name);o.scale.set(d/2,h/2,w/2);o.position.set(x,y+h/2,z);return o;}
function cabinet(p,name,px,py,w,d,h=2.35,axis='py',mat=m.white){
const g=group(p,name);block(g,'Korpus',px,py,w,d,h,.02,mat);block(g,'Sokl',px,py,w-.06,d-.06,.09,.02,m.dark);
const span=axis==='py'?w:d,n=Math.max(1,Math.round(span/.55));
for(let i=1;i<n;i++){
const a=-span/2+i*span/n;
block(g,'Spára dvířek',px+(axis==='py'?a:0),py+(axis==='px'?a:0),axis==='py'?.008:w+.002,axis==='px'?.008:d+.002,h-.16,.12,m.oakLight);
}
collide(px,py,w,d);return g;
}
function legs(p,px,py,w,d,h){for(const a of [-1,1])for(const b of [-1,1])block(p,'Noha',px+a*(w/2-.08),py+b*(d/2-.08),.045,.045,h,.02,m.oak);}
function table(p,name,px,py,w,d,h=.75){const g=group(p,name);block(g,'Deska',px,py,w,d,.05,h-.05,m.oakLight);legs(g,px,py,w,d,h-.05);collide(px,py,w,d);return g;}
function chair(p,px,py,rotation=0){
const g=group(p,'Židle');const [x,z]=planPoint(px,py);g.position.set(x,0,z);g.rotation.y=rotation;
box(g,'Sedák',0,.47,0,.43,.08,.43,m.cloth);box(g,'Opěradlo',0,.71,.19,.43,.45,.06,m.cloth);
for(const a of [-1,1])for(const b of [-1,1])box(g,'Noha',a*.16,.22,b*.16,.035,.44,.035,m.oak);
}
function bed(p,name,px,py,w,d,head='py'){
const g=group(p,name);block(g,'Rám postele',px,py,w,d,.29,.08,m.oak);block(g,'Matrace',px,py,w-.06,d-.06,.2,.37,m.linen);
const across=head==='py'?w:d,along=head==='py'?d:w;
block(g,'Čelo',px+(head==='px'?w/2-.04:0),py+(head==='py'?d/2-.04:0),head==='px'?.08:w,head==='py'?.08:d,.9,.07,m.oakLight);
block(g,'Přikrývka',px-(head==='px'?.25:0),py-(head==='py'?.25:0),head==='px'?w-.55:w-.03,head==='py'?d-.55:d-.03,.055,.58,m.pillow);
const count=across>1.3?2:1;
for(let i=0;i<count;i++){
const side=(i-(count-1)/2)*.8;
oval(g,'Polštář',px+(head==='px'?along/2-.32:side),py+(head==='py'?along/2-.32:side),head==='py'?.64:.40,head==='py'?.40:.64,.16,.58,m.linen);
}
collide(px,py,w,d);return g;
}
function sink(p,name,px,py,w,d){
const g=group(p,name);block(g,'Umyvadlová skříňka',px,py,w,d,.52,.3,m.oakLight);block(g,'Umyvadlová deska',px,py,w+.03,d+.03,.06,.82,m.white);
const alongPx=w>d;const n=Math.max(w,d)>1.1?2:1;
for(let i=0;i<n;i++){
const off=(i-(n-1)/2)*.62,cx=px+(alongPx?off:0),cy=py+(alongPx?0:off);
oval(g,'Umyvadlo',cx,cy,alongPx?.5:.36,alongPx?.36:.5,.05,.88,m.white);
oval(g,'Dno mísy',cx,cy,alongPx?.37:.25,alongPx?.25:.37,.014,.912,m.water);
cylinder(g,'Baterie',cx+(alongPx?0:.18),cy+(alongPx?.18:0),.018,.20,.86,m.metal);
}
collide(px,py,w,d);return g;
}
function wc(p,px,py,axis='px'){
const g=group(p,'Závěsné WC');const w=axis==='px'?.65:.38,d=axis==='px'?.38:.65;
oval(g,'Keramická mísa',px,py,w,d,.34,.15,m.white);oval(g,'Sedátko',px,py,w,d,.045,.49,m.white);
oval(g,'Vnitřek mísy',px-.035,py, w*.59,d*.57,.016,.526,m.water);collide(px,py,w,d);
}
function shower(p,px,py,w,d){
const g=group(p,'Sprchový kout');block(g,'Vanička v úrovni podlahy',px,py,w,d,.025,.016,m.stone);
block(g,'Odtok',px+w/2-.1,py,.035,d*.55,.005,.043,m.metal);
const [x,z]=planPoint(px+w/2-.08,py);const o=mesh(new THREE.CylinderGeometry(.014,.014,1.35,10),m.metal,g,'Sprchová tyč');o.position.set(x,1.45,z);
cylinder(g,'Sprchová hlavice',px+w/2-.2,py,.11,.018,2.08,m.metal);
// Open entry to the shower; only the side screen is solid.
block(g,'Skleněná zástěna',px,py+d/2,w,.014,2,.03,m.glass);
}
const kitchen=group(parent,'Kuchyň do U s poloostrovem');
// Run at garden wall, then west side and peninsula; clear aisle 1.30 m.
for(let i=0;i<6;i++){
const py=.625+i*.60;cabinet(kitchen,'Spodní kuchyňská skříňka',.605,py,.60,.59,.82,'px',m.sage);
block(kitchen,'Pracovní deska',.605,py,.64,.60,.045,.84,m.stone);
}
cabinet(kitchen,'Dřezová část',1.545,.605,1.30,.60,.82,'py',m.sage);
block(kitchen,'Pracovní deska',1.545,.605,1.30,.64,.045,.84,m.stone);
cabinet(kitchen,'Poloostrov',2.645,1.745,.90,2.30,.82,'px',m.sage);
block(kitchen,'Deska poloostrova',2.645,1.745,.96,2.34,.045,.84,m.oakLight);
block(kitchen,'Dřez',1.45,.61,.60,.44,.012,.89,m.metal);block(kitchen,'Dřez – vnitřek',1.45,.61,.50,.34,.013,.90,m.dark);
cylinder(kitchen,'Dřezová baterie',1.45,.39,.018,.30,.89,m.metal);
block(kitchen,'Varná deska',.61,2.45,.49,.60,.015,.89,m.dark);
for(const a of [-.13,.13])for(const b of [-.16,.16])cylinder(kitchen,'Varná zóna',.61+a,2.45+b,.083,.003,.908,m.metal);
block(kitchen,'Vestavná trouba',.91,3.04,.016,.48,.42,.27,m.dark);block(kitchen,'Madlo trouby',.93,3.04,.025,.36,.025,.65,m.metal);
cabinet(kitchen,'Vysoká potravinová skříň',.62,3.92,.64,.60,2.30,'px',m.sage);
cabinet(kitchen,'Americká lednice',.66,4.78,.72,.92,1.88,'px',m.metal);
block(kitchen,'Lednice – madla',1.035,4.78,.025,.045,.62,.78,m.dark);
const living=group(parent,'Obývací pokoj a jídelna');
block(living,'Koberec',5.55,2.1,2.6,3.1,.012,.016,m.linen);
block(living,'Pohovka – sedací část',5.0,1.86,.95,2.80,.40,.12,m.cloth);
block(living,'Pohovka – lenoška',5.65,.91,1.7,.90,.40,.12,m.cloth);
block(living,'Pohovka – opěradlo',4.51,1.86,.18,2.80,.76,.12,m.cloth);
block(living,'Pohovka – bočnice',5.0,3.23,1.0,.15,.60,.12,m.cloth);
for(let py=.8;py<3;py+=.75)block(living,'Sedací polštář',5.05,py,.78,.68,.12,.51,m.linen);
collide(5.0,1.86,1.1,2.8);collide(5.65,.91,1.7,.9);
table(living,'Konferenční stolek',6.25,2.25,.70,1.15,.40);
cabinet(living,'Nízká TV skříňka',7.65,1.95,.45,2.30,.42,'px',m.oak);
block(living,'Televize',7.89,1.95,.05,1.45,.82,1.05,m.dark);
cylinder(living,'Kamna',7.582,4.763,.24,1.02,.02,m.dark);cylinder(living,'Kouřovod',7.582,4.763,.075,1.56,1.04,m.dark);
collide(7.582,4.763,.52,.52);
table(living,'Jídelní stůl pro šest',3.95,5.30,2.1,.86);
for(const px of [3.25,3.95,4.65]){chair(living,px,4.57,Math.PI/2);chair(living,px,6.03,-Math.PI/2);}
const storage=group(parent,'Šatny a zádveří');
for(const py of [2.956,4.306])cabinet(storage,'Šatní skříň',10.26,py,1.96,.60,2.35,'py');
cabinet(storage,'Šatní skříň v zádveří',10.2,7.13,1.90,.60,2.35,'py');
cabinet(storage,'Botník',10.20,4.98,1.90,.44,.48,'py',m.oakLight);
block(storage,'Zrcadlo v zádveří',10.20,4.76,1.20,.012,1.25,.92,m.mirror);
for(const py of [11.406,13.125])cabinet(storage,'Šatní skříň u ložnice',6.68,py,2.55,.60,2.35,'py');
const bedrooms=group(parent,'Pokoje a ložnice');
bed(bedrooms,'Lůžko pokoje u zahrady',6.96,7.53,1.95,1.0,'px');
cabinet(bedrooms,'Šatní skříň pokoje',6.65,10.65,2.50,.65,2.35,'py');
table(bedrooms,'Pracovní stůl u zahrady',4.7,10.64,1.15,.62);chair(bedrooms,4.7,9.85,Math.PI/2);
bed(bedrooms,'Lůžko pokoje u příjezdu',12.25,16.08,.92,2.0,'py');
cabinet(bedrooms,'Šatní skříň pokoje u příjezdu',9.85,15.8,.60,2.50,2.35,'px');
table(bedrooms,'Pracovní stůl u příjezdu',11.7,12.75,1.45,.65);chair(bedrooms,11.7,13.55,-Math.PI/2);
bed(bedrooms,'Manželská postel',5.95,16.06,1.80,2.05,'py');
for(const px of [4.77,7.13])cabinet(bedrooms,'Noční stolek',px,16.75,.48,.50,.46,'py',m.oakLight);
const bath=group(parent,'Sanita – společná koupelna');
shower(bath,12.005,10.08,1.50,1.0);
// Quarter-round corner tub, bounded by the 1600 x 1600 footprint in the PDF.
const tubShape=new THREE.Shape();tubShape.moveTo(0,0);tubShape.lineTo(1.6,0);tubShape.absarc(0,0,1.6,0,Math.PI/2,false);tubShape.lineTo(0,0);
const tubGeometry=new THREE.ExtrudeGeometry(tubShape,{depth:.53,bevelEnabled:true,bevelSize:.025,bevelThickness:.025,bevelSegments:2,steps:1,curveSegments:24});
tubGeometry.rotateX(-Math.PI/2);
const tub=mesh(tubGeometry,m.white,bath,'Rohová vana 1600');const [tx,tz]=planPoint(12.73,12.26);tub.position.set(tx,.055,tz);
// Local x increases towards room, local -z towards garden; oval basin is schematic.
oval(bath,'Vana – vnitřní mísa',12.05,11.53,1.15,.83,.035,.62,m.water);
collide(12.0,11.5,1.55,1.55);
sink(bath,'Dvojumyvadlo',10.65,12.01,1.25,.50);
block(bath,'Zrcadlo',10.65,12.27,1.28,.012,.82,1.08,m.mirror);wc(bath,10.0,9.91,'py');
const ensuite=group(parent,'Sanita – koupelna u ložnice');
shower(ensuite,8.60,16.60,1.55,1.0);wc(ensuite,9.06,13.95,'px');
sink(ensuite,'Dvojumyvadlo u ložnice',9.15,15.30,.48,1.35);
block(ensuite,'Zrcadlo',9.41,15.30,.012,1.40,.80,1.1,m.mirror);
for(let i=0;i<7;i++)block(ensuite,'Topný žebřík',7.83,15.35,.04,.55,.025,.60+i*.10,m.white);
const technical=group(parent,'Technické vybavení – orientační');
cabinet(technical,'Rekuperace / tepelné čerpadlo',12.10,7.15,1.0,.60,1.9,'py',m.white);
for(let i=0;i<2;i++){
block(technical,i?'Sušička':'Pračka',12.39,9.13,.62,.62,.82,.04+i*.84,m.white);
const o=cylinder(technical,'Kruhová dvířka spotřebiče',12.39,8.805,.20,.025,.44+i*.84,m.dark);o.rotation.x=Math.PI/2;o.position.y=.44+i*.84;
const [x,z]=planPoint(12.39,8.80);o.position.x=x;o.position.z=z;
}
collide(12.39,9.13,.62,.62);
cabinet(technical,'Mrazák',11.65,9.13,.63,.62,1.45,'py',m.white);
block(technical,'Elektroměnič a baterie',10.20,7.72,.65,.30,1.1,.6,m.white);
collide(10.20,7.72,.65,.30);
return {colliders,materials:m};
}