Skip to content
Kiln1.5Get Kiln

An open timber armchair with shaped legs, broad armrests and separate upholstered seat and back cushions.

An open timber armchair with shaped legs, broad armrests and separate upholstered seat and back cushions.

Drag to orbit after opening. The model starts stationary.

For your engine. The download is a standard glTF 2.0 binary (GLB) with PBR metallic-roughness materials. glTF stores materials, not lighting or tone mapping, so your engine decides how they read. This 3D view tone-maps with Review Neutral, the Khronos PBR Neutral construction with a smaller glare offset (0.015 instead of 0.04), at exposure 0.9. Its Tone mapping control also shows ACES and Linear, for comparison.

Triangles
7,336
Meshes
2
Materials
2
Dimensions (m)
0.67 × 0.90 × 0.69

Runtime assets

Models, textures and animations prepared for use in a scene or application.

495,060 bytes (0.50 MB)

Download runtime assets

Full Kiln assets (remixable)

Models plus Kiln source, editing metadata, materials and included revisions, so you can reopen and continue editing.

898,227 bytes (0.90 MB)

Download full kiln assets (remixable)

Runtime includes the GLB and its Kiln metadata sidecar. The editable ZIP is this final saved revision: model, exact source, editing manifest and preview. Earlier revisions are left out; the parent revision identifier is kept.

Import the editable ZIP into Kiln’s Library, or run node kiln.mjs import asset.zip in an authoring workspace. Use Kiln 1.4.0 or later.

Collection / asset / revision
project / a_0cc64d69fec0441884617672724738c2 / r_63ad6298f8c24d7fa220eeb8b4bfd300
Parent revision
r_09f8f88ff7c9445b8221f6dd0de521dc
Source SHA-256
e08f77cd417155915e12d3950175086a621461b7c046b0250e767a90a1acd134
Oak armchair: exact saved source
const meta={"name": "wmc-armchair-oak", "units": "meters"};
const CFG={"id": "wmc-armchair-oak", "kind": "chair"};
function rng(seed){let s=seed>>>0;return()=>{s^=s<<13;s^=s>>>17;s^=s<<5;return(s>>>0)/4294967296;};}
function bucket(){return{p:[],n:[],uv:[],c:[]};}
const boxCache=new Map();
async function bevel(w,h,d,r=.006,soft=false){const key=[w,h,d,r,soft].join(',');if(!boxCache.has(key))boxCache.set(key,await roundedBoxGeo(w,h,d,Math.min(r,w*.22,h*.22,d*.22),{style:soft?'round':'chamfer',segments:soft?5:1}));return boxCache.get(key).clone();}
function put(b,geo,pos=[0,0,0],rot=[0,0,0],scale=[1,1,1],tint=1,mode='metric',grain=1,shift=0){
 let g=geo.index?geo.toNonIndexed():geo.clone();const p=g.getAttribute('position'),n=g.getAttribute('normal'),old=g.getAttribute('uv'),uv=[];
 for(let i=0;i<p.count;i++){
  const v=[p.getX(i)*scale[0],p.getY(i)*scale[1],p.getZ(i)*scale[2]],nn=[n.getX(i),n.getY(i),n.getZ(i)],axis=nn.map(Math.abs).indexOf(Math.max(...nn.map(Math.abs)));let u=0,w=0;
  if(mode==='keep'){u=old?old.getX(i):0;w=old?old.getY(i):0;}
  else if(mode==='wood'){
   if(axis===grain){const a=[0,1,2].filter(x=>x!==grain);u=Math.hypot(v[a[0]],v[a[1]])*6;w=Math.atan2(v[a[1]],v[a[0]])/6.283;}
   else{const a=[0,1,2].find(x=>x!==grain&&x!==axis);u=v[a]/.135+shift+pos[a]*.138;w=v[grain]/.65+shift*.37+pos[grain]*.173;}
  }else{const a=axis===0?[2,1]:axis===1?[0,2]:[0,1],rep=mode==='cloth'?.13:mode==='rug'?.09:mode==='stone'?.65:1;u=(v[a[0]]+pos[a[0]])/rep+shift;w=(v[a[1]]+pos[a[1]])/rep;}
  uv.push(u,w);
 }
 const q=rot.isQuaternion?rot:new THREE.Quaternion().setFromEuler(new THREE.Euler(...rot));g.applyMatrix4(new THREE.Matrix4().compose(new THREE.Vector3(...pos),q,new THREE.Vector3(...scale)));
 const pp=g.getAttribute('position'),nn=g.getAttribute('normal');const c=typeof tint==='number'?[tint,tint,tint]:tint;
 const cc=g.getAttribute('color');for(let i=0;i<pp.count;i++){b.p.push(pp.getX(i),pp.getY(i),pp.getZ(i));b.n.push(nn.getX(i),nn.getY(i),nn.getZ(i));b.uv.push(uv[i*2],uv[i*2+1]);b.c.push(c[0]*(cc?cc.getX(i):1),c[1]*(cc?cc.getY(i):1),c[2]*(cc?cc.getZ(i):1));}
}
function finish(root,name,b,mat){if(!b.p.length)return;const g=new THREE.BufferGeometry();for(const[k,a,s]of[['position',b.p,3],['normal',b.n,3],['uv',b.uv,2],['color',b.c,3]])g.setAttribute(k,new THREE.Float32BufferAttribute(a,s));mat.vertexColors=true;return createPart(name,g,mat,{parent:root});}
function bins(){return{warm:bucket(),dark:bucket(),stone:bucket(),iron:bucket(),snow:bucket(),linen:bucket(),throw:bucket(),ember:bucket(),glass:bucket()};}
async function mats(){
 const warm=await materialRecipe('kiln.material.wood.v1',{baseColor:'#bdc9c2',roughness:.62,textureResources:{baseColor:'kiln.texture.oak-veneer-albedo.v1',normal:'kiln.texture.oak-veneer-normal.v1'}});warm.normalScale.set(.16,.16);
 const dark=await materialRecipe('kiln.material.wood.v1',{baseColor:'#7f8580',roughness:.70,textureResources:{baseColor:'kiln.texture.oak-veneer-albedo.v1',normal:'kiln.texture.oak-veneer-normal.v1'}});dark.normalScale.set(.17,.17);
 const stone=await materialRecipe('kiln.material.stone.v1',{baseColor:'#babbb6',roughness:.96,textureResources:{baseColor:'kiln.texture.rock-face-albedo.v1',normal:'kiln.texture.rock-face-normal.v1'}});stone.normalScale.set(.55,.55);
 const snowT=proceduralTexture({schemaVersion:2,size:128,name:'NeutralFineSnow',layers:[{op:'noise',colorA:0xe5e9e9,colorB:0xf9faf7,scale:48,octaves:2,seed:149}]});
 const clothT=proceduralTexture({schemaVersion:2,size:256,name:'OatmealWoolWeaveM4B',layers:[{op:'noise',colorA:0xb5ad9e,colorB:0xcdc4b3,scale:54,octaves:3,seed:43},{op:'stripes',count:128,angleDeg:0,colorA:0xa9a293,colorB:0xffffff,opacity:.12,blend:'multiply'},{op:'stripes',count:128,angleDeg:90,colorA:0xb6ac96,colorB:0xffffff,opacity:.14,blend:'multiply'}]});
 const throwT=proceduralTexture({schemaVersion:2,size:256,name:'CharcoalTweedWeave',layers:[{op:'noise',colorA:0x4b5050,colorB:0x6c7170,scale:46,octaves:3,seed:84},{op:'stripes',count:96,angleDeg:45,colorA:0x77756d,colorB:0xe9e0cb,opacity:.22,blend:'multiply'}]});
 const linen=pbrMaterial({albedo:clothT,normal:normalMapFromHeight(clothT,{strength:4}),roughness:.94,metalness:0});linen.normalScale.set(.28,.28);if(CFG.kind==='lamp')linen.side=THREE.DoubleSide;
 const thr=pbrMaterial({albedo:throwT,normal:normalMapFromHeight(throwT,{strength:2}),roughness:.96,metalness:0});thr.normalScale.set(.28,.28);thr.side=THREE.DoubleSide;
 return{warm,dark,stone,iron:new THREE.MeshStandardMaterial({color:0x303737,roughness:.49,metalness:.68}),snow:pbrMaterial({albedo:snowT,roughness:.97,metalness:0}),linen,throw:thr,ember:new THREE.MeshStandardMaterial({color:0xb45a1d,emissive:0xff641e,emissiveIntensity:2.5,roughness:1}),glass:new THREE.MeshStandardMaterial({color:0xe6eef1,transparent:true,opacity:.075,roughness:.10,metalness:0,side:THREE.DoubleSide,depthWrite:false})};
}
function weather(g,seed,amount){const p=g.getAttribute('position');for(let i=0;i<p.count;i++){const x=p.getX(i),y=p.getY(i),z=p.getZ(i);p.setXYZ(i,x+amount*Math.sin(y*27+z*19+seed)*Math.cos(x*17),y+amount*.5*Math.sin(x*24+z*22+seed),z+amount*Math.cos(x*31+y*17+seed));}g.computeVertexNormals();return g;}
async function block(B,key,size,pos,grain=1,tint=1,r=.006,rot=[0,0,0],shift=0){put(B[key],await bevel(...size,r),pos,rot,[1,1,1],tint,key==='stone'?'stone':key==='warm'||key==='dark'?'wood':'metric',grain,shift);}
async function beam(B,key,a,b,w=.16,d=.16,tint=1){const va=new THREE.Vector3(...a),vb=new THREE.Vector3(...b),delta=vb.clone().sub(va),q=new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,1,0),delta.clone().normalize());put(B[key],await bevel(w,delta.length(),d,.005),va.add(vb).multiplyScalar(.5).toArray(),q,[1,1,1],tint,'wood',1);}
function rod(B,key,a,b,r=.008,radial=6,tint=1){const va=new THREE.Vector3(...a),vb=new THREE.Vector3(...b),delta=vb.clone().sub(va),q=new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,1,0),delta.clone().normalize());put(B[key],new THREE.CylinderGeometry(r,r,delta.length(),radial),va.add(vb).multiplyScalar(.5).toArray(),q,[1,1,1],tint);}
function polygon(points,depth=0){const sh=new THREE.Shape();points.forEach((p,i)=>i?sh.lineTo(...p):sh.moveTo(...p));sh.closePath();if(depth){const g=new THREE.ExtrudeGeometry(sh,{depth,bevelEnabled:false,steps:1});g.translate(0,0,-depth/2);return g;}return new THREE.ShapeGeometry(sh);}
function snowBlob(B,p,s,seed){put(B.snow,weather(new THREE.SphereGeometry(1,10,6),seed,.10),p,[0,seed,0],s,.98);}
async function stoneBlock(B,size,pos,seed){const g=weather(await bevel(...size,.015),seed,.008);put(B.stone,g,pos,[0,seed%3*.005,0],[1,1,1],.78+(seed%7)*.035,'stone');}
async function wallRect(B,size,center,normal,seed){
 const axis=Math.abs(normal[0])?0:2,across=axis===0?2:0,depth=size[axis],width=size[across],H=size[1],face=normal[axis];
 const core=[...size];core[axis]=depth-.054;await block(B,'dark',core,center,1,.58,.002);
 const n=Math.max(1,Math.ceil(width/.18)),bw=width/n;
 for(let k=0;k<n;k++){
  const p=[...center];p[across]+=-width/2+(k+.5)*bw;
  const ss=[...size];ss[across]=bw-.003;ss[axis]=.030;p[axis]=center[axis]+face*(depth/2-.015);
  await block(B,'dark',ss,p,1,.86+.025*((k+seed)%6),.003,[0,0,0],(k+seed)*.137);
  ss[axis]=.024;p[axis]=center[axis]-face*(depth/2-.012);await block(B,'warm',ss,p,1,.92+.025*((k+seed)%4),.002,[0,0,0],(k+seed)*.173);
 }
 // Flush sill and head finish the cladding ends without narrowing the opening.
 for(const y of [-H/2+.04,H/2-.04]){const ss=[...size];ss[1]=.08;const p=[...center];p[1]+=y;await block(B,'dark',ss,p,across,.92,.003);}
}
async function floorBuild(B,cfg){
 const W=cfg.width,D=cfg.depth,H=cfg.height,deck=cfg.kind==='deck',key=deck?'dark':'warm';
 if(!deck)await block(B,'dark',[W,H-.045,D],[0,-.0225,0],2,.78,.003);
 else for(const x of [-W/2+.07,W/2-.07])await block(B,'dark',[.12,H-.04,D],[x,-.02,0],2,.87,.004);
 const n=Math.round(W/(deck?.15:.18)),bw=W/n;
 for(let i=0;i<n;i++){
  const x=-W/2+(i+.5)*bw;let z=-D/2,j=0;
  while(z<D/2-.001){const len=Math.min(D/2-z,deck?D:(j===0?.83+(i%3)*.47:1.82));
   await block(B,key,[bw-.003,.045,len-.002],[x,H/2-.0225,z+len/2],2,.87+.025*((i*7+j*3)%6),.0025,[0,0,0],i*.137+j*.07);
   if(deck)for(const zz of [z+.10,z+len-.10])for(const xx of [x-bw*.25,x+bw*.25]){const g=new THREE.CylinderGeometry(.0022,.0022,.0008,6);put(B.iron,g,[xx,H/2-.0004,zz]);}
   z+=len;j++;
  }
 }
 if(cfg.loft)await block(B,'dark',[3.9,.3,.22],[1.3,0,2.085],0,.92,.006);
}
async function wallBuild(B,cfg){for(let i=0;i<cfg.rects.length;i++){const r=cfg.rects[i];await wallRect(B,r.size,r.pos,cfg.normal,i*3);}
 if(cfg.gable==='rear'){
  put(B.dark,polygon([[-3.6,4.5],[3.6,4.5],[0,7.52076]],.20),[0,0,0],[0,0,0],[1,1,1],.90,'wood',1);
  const n=40,bw=7.2/n;for(let i=0;i<n;i++){const x=-3.6+(i+.5)*bw,h=3.02076-Math.abs(x)*Math.tan(40*Math.PI/180)-.015;if(h>.05){await block(B,'dark',[bw-.004,h,.035],[x,4.5+h/2,-.1075],1,.89+.025*(i%5),.002,[0,0,0],i*.13);await block(B,'warm',[bw-.004,h,.024],[x,4.5+h/2,.112],1,.95,.002,[0,0,0],i*.15);}}
 }
 if(cfg.gable==='front'){
  // Fitted triangular timber frame; the panes are a separately batchable asset.
  await beam(B,'dark',[-3.6,4.5,0],[0,7.52076,0],.18,.25);await beam(B,'dark',[0,7.52076,0],[3.6,4.5,0],.18,.25);
  await block(B,'dark',[7.2,.18,.25],[0,4.5,0],0,.94,.006);
  for(const x of [-2.4,-1.2,0,1.2,2.4]){const h=3.02076-Math.abs(x)*Math.tan(40*Math.PI/180)-.07;await block(B,'dark',[x===0?.16:.10,h,.18],[x,4.5+h/2,0],1,.96,.005);}
  for(const x of [-2.4,-1.2,0,1.2,2.4])for(const z of [-.13,.13]){const g=new THREE.CylinderGeometry(.012,.012,.005,8);g.rotateX(Math.PI/2);put(B.warm,g,[x,4.55,z],[0,0,0],[1,1,1],.62);}
 }
}
async function plinthBuild(B){await block(B,'stone',[2.4,1.6,.32],[0,0,0],1,.5,.002);let y=-.8;const hs=[.25,.29,.24,.28,.26,.28];for(let row=0;row<6;row++){let x=-1.2,i=0;while(x<1.2-.001){const w=Math.min(1.2-x,.38+((row*7+i*11)%5)*.09);await stoneBlock(B,[w-.014,hs[row]-.014,.34],[x+w/2,y+hs[row]/2,0],row*17+i*7);x+=w;i++;}y+=hs[row];}}
async function postBuild(B){await block(B,'dark',[.22,4.5,.22],[0,2.25,0],1,.96,.008);for(const y of [.12,4.30]){await block(B,'iron',[.225,.055,.225],[0,y,0],1,1,.002);for(const z of [-.114,.114]){const g=new THREE.CylinderGeometry(.014,.014,.005,8);g.rotateX(Math.PI/2);put(B.iron,g,[0,y,z]);}}}
async function rafterBuild(B){const slope=40*Math.PI/180,L=5.482710615195571;for(const side of [-1,1]){await block(B,'dark',[L,.20,.20],[side*2.1,5.537890774527711,0],0,.98,.006,[0,0,-side*slope]);}
 await block(B,'iron',[.25,.38,.22],[0,7.18,0],1,1,.004);for(const x of [-.065,.065])for(const y of [7.08,7.29]){const g=new THREE.CylinderGeometry(.012,.012,.006,8);g.rotateX(Math.PI/2);put(B.iron,g,[x,y,.114]);}}
async function roofBuild(B,cfg){
 const W=5.482710615195571,D=12.1,side=cfg.side,q=new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,0,1),-side*40*Math.PI/180),center=[side*2.1,5.757890774527711,0];
 const local=bins(),hole=side===1?[-2.48,-1.56,-.84,.04]:null;
 const pieces=hole?[[-W/2,hole[0],-D/2,D/2],[hole[1],W/2,-D/2,D/2],[hole[0],hole[1],-D/2,hole[2]],[hole[0],hole[1],hole[3],D/2]]:[[-W/2,W/2,-D/2,D/2]];
 for(const p of pieces){await block(local,'iron',[p[1]-p[0],.025,p[3]-p[2]],[(p[0]+p[1])/2,.0975,(p[2]+p[3])/2],0,1,.002);}
 const n=61,bw=D/n;for(let k=0;k<n;k++){const z=-D/2+(k+.5)*bw;const ranges=hole&&z>hole[2]-.1&&z<hole[3]+.1?[[-W/2,hole[0]],[hole[1],W/2]]:[[-W/2,W/2]];for(const r of ranges)await block(local,'warm',[r[1]-r[0]-.002,.195,bw-.003],[(r[0]+r[1])/2,-.0125,z],0,.85+.02*(k%7),.002,[0,0,0],k*.127);}
 for(let z=-D/2+.04;z<D/2;z+=.46){const ranges=hole&&z>hole[2]&&z<hole[3]?[[-W/2,hole[0]],[hole[1],W/2]]:[[-W/2,W/2]];for(const r of ranges)await block(local,'iron',[r[1]-r[0],.033,.018],[(r[0]+r[1])/2,.122,z],0,.92,.004);}
 for(const x of [-W/2,W/2])await block(local,'dark',[.06,.27,D],[x,-.015,0],2,.83,.004);
 // Closed variable-thickness snow skin, with wavy exposed eaves and a scoured seam.
 const nx=28,nz=64,verts=[],uv=[],ix=[],at=(i,j)=>j*(nx+1)+i;
 for(let j=0;j<=nz;j++)for(let i=0;i<=nx;i++){const u=i/nx,v=j/nz,z=-D/2+.03+v*(D-.06),edge=.025*Math.sin(j*.97)+.022*Math.sin(j*2.41),x=-W/2+.045+u*(W-.09)+edge*(2*u-1),h=.235+.045*Math.sin(x*2.3+z*.85+side)+.025*Math.sin(z*3.7+x*4.5)+.018*Math.cos(z*8.1+x*6.7);verts.push(x,h,z);uv.push(x,z);}
 const addQuad=(a,b,c,d)=>ix.push(a,c,b,b,c,d);
 for(let j=0;j<nz;j++)for(let i=0;i<nx;i++){const x=-W/2+(i+.5)/nx*W,z=-D/2+(j+.5)/nz*D;if(hole&&x>hole[0]-.05&&x<hole[1]+.05&&z>hole[2]-.08&&z<hole[3]+.08)continue;if(i>nx-5&&Math.sin(z*1.3+side)>.89)continue;addQuad(at(i,j),at(i+1,j),at(i,j+1),at(i+1,j+1));}
 const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(verts,3));g.setAttribute('uv',new THREE.Float32BufferAttribute(uv,2));g.setIndex(ix);g.computeVertexNormals();put(local.snow,g,[0,0,0],[0,0,0],[1,1,1],1,'keep');
 // Eave/end skirts visibly carry snow thickness instead of a paper-thin plane.
 for(const ii of [0,nx])for(let j=0;j<nz;j++){const a=at(ii,j)*3,b=at(ii,j+1)*3,points=[verts[a],.111,verts[a+2],verts[b],.111,verts[b+2],verts[a],verts[a+1],verts[a+2],verts[b],verts[b+1],verts[b+2]];const sk=new THREE.BufferGeometry();sk.setAttribute('position',new THREE.Float32BufferAttribute(points,3));sk.setAttribute('uv',new THREE.Float32BufferAttribute([0,0,1,0,0,1,1,1],2));sk.setIndex(ii===0?[0,1,2,1,3,2]:[0,2,1,1,2,3]);sk.computeVertexNormals();put(local.snow,sk);}
 for(const key of ['warm','dark','iron','snow']){const bb=local[key],geo=new THREE.BufferGeometry();for(const[k,a,s]of[['position',bb.p,3],['normal',bb.n,3],['uv',bb.uv,2],['color',bb.c,3]])geo.setAttribute(k,new THREE.Float32BufferAttribute(a,s));put(B[key],geo,center,q,[1,1,1],1,'keep');}
 if(side===-1){await block(B,'iron',[.30,.065,D],[0,7.55,0],2,1,.02);put(B.snow,weather(await bevel(.46,.15,D-.05,.05,true),4,.027),[0,7.72,0],[0,0,0],[1,1,1],.99);}
}
async function paneBuild(B){const xs=[-3.49,-2.45,-1.25,-.08,.08,1.25,2.45,3.49];for(let j=0;j<6;j++){let a,b;if(j<3){a=xs[j]+.025;b=xs[j+1]-.025;}else{a=xs[j+1]+.025;b=xs[j+2]-.025;}const top=x=>3.02076-Math.abs(x)*Math.tan(40*Math.PI/180)-.12;const p=[[a,.10],[b,.10],[b,top(b)],[a,top(a)]];put(B.glass,polygon(p),[0,0,.018]);}}
async function doorBuild(B,cfg){const L=1.24,H=2.24,local=bins();await block(local,'dark',[L,H,.055],[L/2,H/2,0],1,.90,.005);
 for(let i=0;i<7;i++){const w=(L-.18)/7;await block(local,'warm',[w-.003,H-.30,.025],[.09+(i+.5)*w,H/2,-.028],1,.9+.025*(i%4),.003,[0,0,0],i*.19);}
 for(const x of [.055,L-.055])await block(local,'dark',[.11,H,.09],[x,H/2,0],1,.98,.005);
 for(const y of [.07,H-.07,.74])await block(local,'dark',[L-.20,.14,.09],[L/2,y,0],0,.95,.005);
 for(const y of [.32,1.87]){await block(local,'iron',[.38,.043,.008],[.20,y,.049],0,1,.002);rod(local,'iron',[.014,y-.085,0],[.014,y+.085,0],.018,10);for(const x of [.08,.28,.36]){const g=new THREE.CylinderGeometry(.009,.009,.005,8);g.rotateX(Math.PI/2);put(local.iron,g,[x,y,.055]);}}
 for(const z of [-.051,.051]){await block(local,'iron',[.032,.20,.012],[L-.16,1.04,z],1,1,.003);rod(local,'iron',[L-.16,1.03,z],[L-.28,1.03,z],.012,10);}
 // Both leaves keep their original hinge origin. Entry width runs +Z; deck width +X.
 const q=cfg.entry?new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0),-Math.PI/2):new THREE.Quaternion();
 for(const key of ['dark','warm','iron']){const bb=local[key],geo=new THREE.BufferGeometry();for(const[k,a,s]of[['position',bb.p,3],['normal',bb.n,3],['uv',bb.uv,2]])geo.setAttribute(k,new THREE.Float32BufferAttribute(a,s));put(B[key],geo,[0,0,0],q,[1,1,1],1,'keep');}}
async function treadBuild(B){await block(B,'warm',[1.2,.12,.28],[0,0,0],0,.95,.006);for(const x of [-.49,.49])for(const z of [-.085,.085]){const g=new THREE.CylinderGeometry(.006,.006,.001,8);put(B.iron,g,[x,.0595,z]);}for(const z of [-.11,.11])await block(B,'dark',[1.18,.012,.009],[0,-.048,z],0,.8,.002);}
async function stairRailBuild(B,cfg){const n=cfg.steps,run=cfg.run,rise=cfg.rise,D=n*run,ext=cfg.exterior,first=ext?rise:0,last=ext?(n+1)*rise:n*rise;
 await beam(B,'dark',[0,first-.12,0],[0,last-.12,-D],.12,.20,.93);
 await beam(B,'dark',[0,first+1.015,0],[0,last+1.015,-D],.10,.09,.98);
 const count=ext?5:7;for(let k=0;k<count;k++){const t=k/(count-1),z=-D*t,y=first+(last-first)*t;await block(B,'dark',[.10,1.06,.10],[0,y+.50,z],1,.92,.005);for(const yy of [y+.12,y+.92]){const g=new THREE.CylinderGeometry(.011,.011,.004,8);g.rotateZ(Math.PI/2);put(B.iron,g,[.052,yy,z]);}}
 for(const y of [.25,.49,.73])rod(B,'iron',[0,first+y,0],[0,last+y,-D],.006,6);
}
async function railBuild(B){const L=1.3;for(const x of [-L/2+.05,L/2-.05]){await block(B,'dark',[.10,1.04,.10],[x,.52,0],1,.96,.005);await block(B,'iron',[.13,.025,.13],[x,.015,0],1,1,.003);for(const y of [.24,.49,.74]){const g=new THREE.CylinderGeometry(.011,.011,.006,8);g.rotateX(Math.PI/2);put(B.iron,g,[x,y,.054]);}}
 await block(B,'dark',[L,.10,.10],[0,1.025,0],0,.98,.006);for(const y of [.24,.49,.74])rod(B,'iron',[-.6,y,0],[.6,y,0],.006,6);}
async function storageBuild(B){await block(B,'dark',[1,1.2,3.24],[0,.6,0],2,.90,.004);for(let i=0;i<6;i++){const z=-1.62+(i+.5)*.54;await block(B,'warm',[.028,1.04,.522],[.494,.59,z],1,.95,.004,[0,0,0],i*.16);await block(B,'dark',[.032,.05,.49],[.499,.16,z],2,.9,.004);rod(B,'iron',[.516,.65,z+.14],[.516,.77,z+.14],.009,8);}await block(B,'warm',[1,.055,3.24],[0,1.1725,0],2,.95,.005);}
async function pillow(B,key,size,pos,rot=[0,0,0],seed=0){let g=await bevel(...size,Math.min(...size)*.24,true);const p=g.getAttribute('position');for(let i=0;i<p.count;i++){let x=p.getX(i),y=p.getY(i),z=p.getZ(i);p.setXYZ(i,x,y+.003*Math.sin(x*35+seed)*Math.sin(z*38+seed),z+.004*Math.sin(x*27+y*24+seed));}g.computeVertexNormals();put(B[key],g,pos,rot,[1,1,1],1,'cloth');}
function clothSheet(B,key,fn,n=24,m=32){const p=[],uv=[],ix=[];for(let j=0;j<=m;j++)for(let i=0;i<=n;i++){const u=i/n,v=j/m;p.push(...fn(u,v));uv.push(u*7,v*9);}for(let j=0;j<m;j++)for(let i=0;i<n;i++){const a=j*(n+1)+i,b=a+1,c=a+n+1,d=c+1;ix.push(a,b,c,b,d,c);}const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(p,3));g.setAttribute('uv',new THREE.Float32BufferAttribute(uv,2));g.setIndex(ix);g.computeVertexNormals();put(B[key],g,[0,0,0],[0,0,0],[1,1,1],1,'keep');}
// M4B: bounded, supported upholstery; no cloth simulation or footprint expansion.
function settlePoint(v,size,seed=0,press=.008){
 const half=size.map(x=>x/2),r=Math.min(...size)*.42,core=half.map(x=>x-r),p=v.map((x,i)=>Math.max(-core[i],Math.min(core[i],x))),d=v.map((x,i)=>x-p[i]),L=Math.hypot(...d);
 if(L>0)for(let i=0;i<3;i++)p[i]+=d[i]/L*r;
 const thin=size.indexOf(Math.min(...size)),axes=[0,1,2].filter(x=>x!==thin),a=p[axes[0]]/half[axes[0]],b=p[axes[1]]/half[axes[1]],side=thin===2?-1:1,t=Math.max(0,p[thin]*side/half[thin]);
 const edge=.010*(.6*a*a+.4*b*b),dent=press*Math.exp(-(((a-.13*Math.sin(seed))/.55)**2)-(((b+.18*Math.cos(seed))/.58)**2));
 p[thin]-=side*t*(edge+dent);
 // Small gathering near the seam, not high-frequency noise across the face.
 p[thin]+=side*t*.0015*Math.max(0,(Math.abs(a)-.65)/.35)*Math.sin(b*18+seed);
 return p;
}
function settleGeo(size,seed=0,press=.008){
 const seg=size.map(x=>Math.max(6,Math.min(28,Math.ceil(x/.028)))),g=new THREE.BoxGeometry(...size,...seg),p=g.getAttribute('position');
 for(let i=0;i<p.count;i++)p.setXYZ(i,...settlePoint([p.getX(i),p.getY(i),p.getZ(i)],size,seed,press));
 g.computeVertexNormals();const n=g.getAttribute('normal'),shared=new Map();
 for(let i=0;i<p.count;i++){const k=[p.getX(i),p.getY(i),p.getZ(i)].map(v=>Math.round(v*1e6)).join(','),a=shared.get(k)||[0,0,0];a[0]+=n.getX(i);a[1]+=n.getY(i);a[2]+=n.getZ(i);shared.set(k,a);}
 for(let i=0;i<p.count;i++){const a=shared.get([p.getX(i),p.getY(i),p.getZ(i)].map(v=>Math.round(v*1e6)).join(',')),L=Math.hypot(...a);n.setXYZ(i,a[0]/L,a[1]/L,a[2]/L);}return g;
}
function settled(B,key,size,pos,rot=[0,0,0],seed=0,press=.008,seam=true){
 put(B[key],settleGeo(size,seed,press),pos,rot,[1,1,1],1,'cloth');
 if(!seam)return;
 const thin=size.indexOf(Math.min(...size)),axes=[0,1,2].filter(x=>x!==thin),pts=[];
 for(let i=0;i<=80;i++){const t=i/80*4,j=Math.min(3,Math.floor(t)),a=t-j,uv=j===0?[1,-1+2*a]:j===1?[1-2*a,1]:j===2?[-1,1-2*a]:[-1+2*a,-1],v=[0,0,0];v[axes[0]]=uv[0]*size[axes[0]]/2;v[axes[1]]=uv[1]*size[axes[1]]/2;v[thin]=(thin===2?-1:1)*size[thin]*.02;pts.push(settlePoint(v,size,seed,press));}
 put(B[key],curveToMesh(pts,.00125,100,5),pos,rot,[1,1,1],.83,'cloth');
}
function axisSurface(B,axis,a,b,side=1,min=-10,max=10){
 const axes=[0,1,2].filter(x=>x!==axis),p=B.p;let hit=side>0?-Infinity:Infinity;
 for(let i=0;i<p.length;i+=9){const ax=p[i+axes[0]],ay=p[i+axes[1]],bx=p[i+3+axes[0]],by=p[i+3+axes[1]],cx=p[i+6+axes[0]],cy=p[i+6+axes[1]],den=(by-cy)*(ax-cx)+(cx-bx)*(ay-cy);if(Math.abs(den)<1e-12)continue;
  const u=((by-cy)*(a-cx)+(cx-bx)*(b-cy))/den,v=((cy-ay)*(a-cx)+(ax-cx)*(b-cy))/den,w=1-u-v;if(u<-.000001||v<-.000001||w<-.000001)continue;
  const h=u*p[i+axis]+v*p[i+3+axis]+w*p[i+6+axis];if(h<min||h>max)continue;hit=side>0?Math.max(hit,h):Math.min(hit,h);
 }return Number.isFinite(hit)?hit:null;
}
function thickCloth(B,key,fn,n,m,thick=.005,flip=false,repeat=[5,10]){
 const p=[],uv=[],ix=[];for(let j=0;j<=m;j++)for(let i=0;i<=n;i++){p.push(...fn(i/n,j/m));uv.push(i/n*repeat[0],j/m*repeat[1]);}
 for(let j=0;j<m;j++)for(let i=0;i<n;i++){const a=j*(n+1)+i,b=a+1,c=a+n+1,d=c+1;ix.push(...(flip?[a,c,b,b,c,d]:[a,b,c,b,d,c]));}
 const top=new THREE.BufferGeometry();top.setAttribute('position',new THREE.Float32BufferAttribute(p,3));top.setIndex(ix);top.computeVertexNormals();const normals=top.getAttribute('normal'),N=p.length/3,topIndices=ix.slice();
 for(let i=0;i<N;i++){p.push(p[i*3]-normals.getX(i)*thick,p[i*3+1]-normals.getY(i)*thick,p[i*3+2]-normals.getZ(i)*thick);uv.push(uv[i*2],uv[i*2+1]);}
 for(let i=0;i<topIndices.length;i+=3)ix.push(topIndices[i]+N,topIndices[i+2]+N,topIndices[i+1]+N);
 const edge=[];for(let i=0;i<=n;i++)edge.push(i);for(let j=1;j<=m;j++)edge.push(j*(n+1)+n);for(let i=n-1;i>=0;i--)edge.push(m*(n+1)+i);for(let j=m-1;j>0;j--)edge.push(j*(n+1));
 for(let i=0;i<edge.length;i++){const a=edge[i],b=edge[(i+1)%edge.length];ix.push(...(flip?[a,b,a+N,b,b+N,a+N]:[a,a+N,b,b,a+N,b+N]));}
 const g=new THREE.BufferGeometry();g.setAttribute('position',new THREE.Float32BufferAttribute(p,3));g.setAttribute('uv',new THREE.Float32BufferAttribute(uv,2));g.setIndex(ix);g.computeVertexNormals();put(B[key],g,[0,0,0],[0,0,0],[1,1,1],1,'keep');
}

async function sofaBuild(B){
 for(const x of [-.66,.66])for(const z of [-.29,.29])await beam(B,'warm',[x,.015,z],[x*.94,.25,z*.94],.075,.075,.92);
 await pillow(B,'linen',[1.65,.22,.80],[0,.30,0]);
 for(const x of [-.76,.76])await pillow(B,'linen',[.13,.43,.82],[x,.49,0]);
 for(const x of [-.35,.35]){await pillow(B,'linen',[.68,.14,.59],[x,.435,-.07],[0,0,0],x*7);await pillow(B,'linen',[.68,.43,.16],[x,.70,.29],[.10,0,0],x*9);}
 for(const [x,z,r]of[[-.45,.18,-.16],[.40,.16,.13]])await pillow(B,'throw',[.33,.34,.095],[x,.66,z],[.16,0,r],x*17);
 clothSheet(B,'throw',(u,v)=>{const x=.47+u*.34,z=.30-v*.69,y=v<.35?.88-v*.62:.66-(v-.35)*.73;return[x+.008*Math.sin(v*23+u*12),y+.018*Math.sin(u*35+v*11),z];},16,26);
 for(let i=0;i<12;i++)rod(B,'throw',[.48+i*.027,.18,-.39],[.48+i*.027,.12,-.40],.003,4,.92);
}
async function chairBuild(B){
 for(const x of [-.26,.26])for(const z of [-.27,.27])await beam(B,'warm',[x,.015,z],[x*.87,.47,z*.94],.055,.055,.92);
 await block(B,'warm',[.65,.06,.68],[0,.35,0],2,.95,.005);
 for(const x of [-.30,.30]){await beam(B,'warm',[x,.36,.27],[x,.88,.29],.06,.06);await block(B,'warm',[.065,.055,.67],[x,.62,0],2,.96,.005);}
 settled(B,'linen',[.54,.14,.58],[0,.45,-.015],[0,0,0],4,.014);settled(B,'linen',[.55,.43,.13],[0,.70,.27],[.12,0,0],9,.009);
 await block(B,'warm',[.65,.065,.06],[0,.86,.31],0,.95,.005);
 for(const x of [-.30,.30])await block(B,'warm',[.05,.26,.05],[x,.49,-.27],1,.96,.004);
}
async function tableBuild(B){for(const x of [-.45,.45])for(const z of [-.23,.23])await block(B,'warm',[.09,.39,.09],[x,.195,z],1,.93,.006);for(const z of [-.23,.23])await block(B,'dark',[.99,.10,.045],[0,.28,z],0,.9,.004);
 for(let i=0;i<3;i++)await block(B,'warm',[1.15,.065,.203],[0,.425,-.206+i*.206],0,.89+.04*i,.008,[0,0,0],i*.17);
 // Restrained books and one ceramic bowl add scale without obstructing the walk.
 await block(B,'linen',[.29,.045,.22],[-.25,.48,.03],0,.92,.003,[0,.13,0]);await block(B,'dark',[.31,.009,.23],[-.25,.506,.03],0,.80,.002,[0,.13,0]);
 const bowl=new THREE.SphereGeometry(.105,16,10,0,Math.PI*2,Math.PI*.35,Math.PI*.58);put(B.stone,bowl,[.25,.52,.02],[Math.PI,0,0],[1,.55,1],.80,'stone');
}
async function rugBuild(B){const W=2,D=3.3;const g=await bevel(W,.018,D,.007,true);put(B.linen,g,[0,.009,0],[0,0,0],[1,1,1],.78,'rug');for(const x of [-.965,.965])put(B.throw,await bevel(.04,.007,D-.02,.003),[x,.021,0],[0,0,0],[1,1,1],.82,'rug');for(const z of [-1.61,1.61])put(B.throw,await bevel(W-.08,.007,.04,.003),[0,.021,z],[0,0,0],[1,1,1],.82,'rug');for(const z of [-1.66,1.66])for(let i=0;i<48;i++)rod(B,'linen',[-.94+i*.04,.009,z],[-.937+i*.04,.007,z+Math.sign(z)*.065],.0023,4,.88);}
async function hearthBuild(B){
 await stoneBlock(B,[1.45,.24,1.1],[0,.12,0],31);
 for(const x of [-.53,.53])for(let j=0;j<6;j++)await stoneBlock(B,[.27,.21,.72],[x,.355+j*.23,-.04],j*13+(x>0?3:7));
 await stoneBlock(B,[1.38,.19,.84],[0,1.68,-.02],54);
 for(let j=0;j<3;j++)await stoneBlock(B,[.91,.20,.70],[0,1.87+j*.21,-.06],71+j*3);
 // A genuinely recessed iron cavity; its opening faces +Z toward the living seats.
 await block(B,'iron',[.80,.94,.035],[0,.77,-.365],1,.43,.004);
 for(const x of [-.40,.40])await block(B,'iron',[.035,.94,.71],[x,.77,0],1,.60,.004);
 for(const y of [.29,1.25])await block(B,'iron',[.83,.035,.73],[0,y,0],0,.62,.004);
 for(const x of [-.415,.415])await block(B,'iron',[.045,1.02,.035],[x,.77,.375],1,.95,.004);
 for(const y of [.27,1.28])await block(B,'iron',[.86,.045,.035],[0,y,.375],0,.95,.004);
 for(const x of [-.29,-.1,.1,.29])rod(B,'iron',[x,.35,-.27],[x,.35,.30],.012,8);
 for(let i=0;i<5;i++){const g=new THREE.CylinderGeometry(.065,.08,.63,9,3);g.rotateZ(Math.PI/2);put(B.dark,weather(g,i*9,.006),[0,.43+(i%2)*.10,-.20+i*.105],[0,(i%2?-.13:.17),0],[1,1,1],.24,'wood',0);}
 for(let i=0;i<18;i++){const x=-.28+(i%6)*.108,z=-.19+Math.floor(i/6)*.18;put(B.ember,weather(new THREE.IcosahedronGeometry(.035,0),i,.05),[x,.405+(i%3)*.018,z],[0,i,0],[1,.7,1],.75+(i%4)*.08);}
}
async function chimneyBuild(B){
 await block(B,'stone',[.59,7.2,.59],[0,3.6,0],1,.5,.002);
 const H=.225;for(let j=0;j<32;j++)for(const side of [-1,1])for(let k=0;k<2;k++){const w=(k===0?.31:.29),x=k===0?-.145:.165;const p=j%2?[side*.15,(j+.5)*H,x]:[x,(j+.5)*H,side*.15];await stoneBlock(B,[.294,H-.013,.294],p,j*13+k*7+(side>0?3:1));}
 // Roof flashing follows the east 40-degree pitch and surrounds the masonry shaft.
 await block(B,'iron',[1.08,.028,1.08],[0,6.1,0],0,.85,.004,[0,0,-40*Math.PI/180]);
 for(const x of [-.35,.35])for(const z of [-.35,.35])await block(B,'iron',[.045,.22,.045],[x,7.19,z],1,1,.004);
 await block(B,'iron',[.88,.08,.88],[0,7.28,0],0,1,.014);snowBlob(B,[.10,7.355,.08],[.35,.07,.35],5);
}
async function lampBuild(B){
 for(let i=0;i<3;i++){const a=i*Math.PI*2/3;await beam(B,'warm',[Math.cos(a)*.22,.015,Math.sin(a)*.22],[Math.cos(a)*.035,1.25,Math.sin(a)*.035],.038,.038);}
 rod(B,'iron',[0,.15,0],[0,1.50,0],.012,10);
 const shade=new THREE.CylinderGeometry(.20,.29,.37,32,1,true);put(B.linen,shade,[0,1.43,0],[0,0,0],[1,1,1],1,'cloth');
 for(const y of [1.245,1.615]){const g=new THREE.TorusGeometry(y<1.3?.29:.20,.006,5,32);g.rotateX(Math.PI/2);put(B.iron,g,[0,y,0]);}
 put(B.ember,new THREE.SphereGeometry(.034,12,8),[0,1.44,0]);
}
async function construct(B,cfg){
 if(['floor','deck'].includes(cfg.kind))return floorBuild(B,cfg);
 if(cfg.kind==='wall')return wallBuild(B,cfg);
 if(cfg.kind==='plinth')return plinthBuild(B);
 if(cfg.kind==='post')return postBuild(B);
 if(cfg.kind==='rafter')return rafterBuild(B);
 if(cfg.kind==='roof')return roofBuild(B,cfg);
 if(cfg.kind==='pane')return paneBuild(B);
 if(cfg.kind==='door')return doorBuild(B,cfg);
 if(cfg.kind==='tread')return treadBuild(B);
 if(cfg.kind==='stairRail')return stairRailBuild(B,cfg);
 if(cfg.kind==='rail')return railBuild(B);
 if(cfg.kind==='storage')return storageBuild(B);
 if(cfg.kind==='sofa')return sofaBuild(B);
 if(cfg.kind==='chair')return chairBuild(B);
 if(cfg.kind==='table')return tableBuild(B);
 if(cfg.kind==='rug')return rugBuild(B);
 if(cfg.kind==='hearth')return hearthBuild(B);
 if(cfg.kind==='chimney')return chimneyBuild(B);
 if(cfg.kind==='lamp')return lampBuild(B);
 throw new Error('Unknown asset module '+cfg.kind);
}
async function build(){const root=createRoot(CFG.id),B=bins(),M=await mats();await construct(B,CFG);for(const k of Object.keys(B))finish(root,k,B[k],M[k]);return root;}

Asset content is dedicated CC0-1.0. Kiln and Holodeck software keep their own licences.