SHOE DESIGNER BLOB BASED

Ausführen ZIP (lokal ausführbar)
Embed
<iframe src="https://code.turboflip.de/run/7d96e1b24a955b4b/" sandbox="allow-scripts allow-forms allow-modals allow-downloads" loading="lazy" style="border:0;width:100%;height:480px"></iframe>
index.html
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SHOE:BLOB</title>
<style>
  :root{--bd:#e7e7e7;--tx:#111;--mu:#8c8c8c}
  *{box-sizing:border-box}
  html,body{margin:0;height:100%;background:#fff;color:var(--tx);font:12px/1.35 -apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;overflow:hidden}
  #c{position:fixed;inset:0;width:100%;height:100%;display:block;touch-action:none}
  #panel{position:fixed;left:12px;top:12px;bottom:12px;width:304px;background:#fff;border:1px solid var(--bd);border-radius:10px;overflow-y:auto;padding:14px 14px 18px}
  #panel h1{font-size:13px;letter-spacing:.14em;margin:0 0 2px;font-weight:700}
  #panel .sub{color:var(--mu);font-size:11px;margin-bottom:12px}
  .sec{margin-top:14px;padding-top:12px;border-top:1px solid var(--bd)}
  .sec h4{margin:0 0 8px;font-size:10px;letter-spacing:.12em;color:var(--mu);font-weight:600}
  .row{display:grid;grid-template-columns:96px 1fr 46px;align-items:center;gap:8px;margin:5px 0}
  .row output{text-align:right;color:var(--mu);font-variant-numeric:tabular-nums}
  input[type=range]{width:100%;accent-color:#111;height:16px;margin:0}
  input[type=checkbox]{accent-color:#111;margin:0}
  input[type=color]{width:100%;height:26px;border:1px solid #d9d9d9;border-radius:6px;padding:2px;background:#fff}
  button{font:inherit;background:#fff;border:1px solid #d9d9d9;border-radius:6px;padding:6px 9px;cursor:pointer;color:#111}
  button:hover{border-color:#111}
  button.on,button.pri{background:#111;color:#fff;border-color:#111}
  button.pri{width:100%;padding:9px;margin-top:8px}
  .btns{display:flex;flex-wrap:wrap;gap:6px;margin:6px 0}
  .pz{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;margin:4px 0 8px}
  .pz button{padding:7px 4px;text-align:center}
  .chk{display:flex;gap:8px;align-items:center;margin:7px 0;cursor:pointer}
  .seg{display:grid;grid-template-columns:1fr 1fr;border:1px solid #111;border-radius:7px;overflow:hidden}
  .seg button{border:0;border-radius:0;padding:8px;letter-spacing:.1em;font-weight:600}
  .hint{color:var(--mu);font-size:11px;margin:6px 0}
  .warn{color:#111;background:#f4f4f4;border-radius:6px;padding:6px 8px;font-size:11px;margin:6px 0}
  .leg{display:flex;gap:10px;flex-wrap:wrap;font-size:11px;color:var(--mu);margin:6px 0}
  .leg i{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:4px;vertical-align:-1px}
  #status{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);background:#fff;border:1px solid var(--bd);border-radius:20px;padding:5px 14px;color:var(--mu);font-size:11px;pointer-events:none;opacity:0;transition:opacity .15s}
  #status.show{opacity:1}
  #stat{color:var(--mu);font-size:11px;line-height:1.6;font-variant-numeric:tabular-nums}
  [hidden]{display:none!important}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="panel"></div>
<div id="status"></div>

<!-- ============ KERN: Felder, Voronoi-Poren, Surface Nets (ohne Three.js) ============ -->
<script id="core">
window.Core=(function(){
  const BAND=40;
  const smin=(a,b,k)=>{const h=Math.max(k-Math.abs(a-b),0)/k;return (a<b?a:b)-h*h*k*0.25;};
  // Ellipsoide packen: stride 10 = cx,cy,cz, 1/rx,1/ry,1/rz, cos,sin, cullR², -
  function pack(list,K){
    const A=new Float32Array(Math.max(1,list.length)*10);
    list.forEach((b,k)=>{
      const o=k*10,m=Math.max(b.rx,b.ry,b.rz),t=m+K+BAND;
      A[o]=b.x;A[o+1]=b.y;A[o+2]=b.z;A[o+3]=1/b.rx;A[o+4]=1/b.ry;A[o+5]=1/b.rz;
      A[o+6]=Math.cos(b.rot);A[o+7]=Math.sin(b.rot);A[o+8]=t*t;
    });
    return A;
  }
  // gesamtes Ellipsoid-Set mit weicher Vereinigung; weit entfernte Blobs werden übersprungen
  function ellSDF(A,n,K,x,y,z){
    let d=0,any=false;
    for(let i=0,o=0;i<n;i++,o+=10){
      const dx=x-A[o],dy=y-A[o+1],dz=z-A[o+2];
      if(dx*dx+dy*dy+dz*dz>A[o+8])continue;
      const c=A[o+6],s=A[o+7];
      const qx=dx*c+dy*s,qy=-dx*s+dy*c;
      const ix=A[o+3],iy=A[o+4],iz=A[o+5];
      const ax=qx*ix,ay=qy*iy,az=dz*iz;
      const k0=Math.sqrt(ax*ax+ay*ay+az*az);
      const bx=ax*ix,by=ay*iy,bz=az*iz;
      const k1=Math.sqrt(bx*bx+by*by+bz*bz);
      const di=k1<1e-9?-1/Math.max(ix,iy,iz):k0*(k0-1)/k1;
      if(!any){d=di;any=true;}
      else{const df=d-di;const h=Math.max(K-(df<0?-df:df),0)/K;d=(d<di?d:di)-h*h*K*0.25;}
    }
    return any?d:BAND*2;
  }
  let _db=0,_dc=0;
  // Material < 0. Blobs, minus Subtraktiv-Blobs, minus Fußraum, abgeschnitten am Boden.
  function f0(S,x,y,z){
    let db=ellSDF(S.B,S.nb,S.K,x,y,z);
    if(S.nx>0){const q=-ellSDF(S.X,S.nx,6,x,y,z);if(q>db)db=q;}
    const dc=(S.cavOn&&S.nc>0)?ellSDF(S.C,S.nc,6,x,y,z):BAND*2;
    _db=db;_dc=dc;
    let f=db;if(-dc>f)f=-dc;if(-y>f)f=-y;
    return f;
  }
  // >0 : hier darf ein Hohlraum liegen (Abstand zu Außenhaut / Fußraum / Boden jeweils größer als die Hautdicke)
  // Haut ist nie dünner als ~1.4 Voxel, sonst bricht das grobe Gitter durch
  function zone(S,x,y,z){
    f0(S,x,y,z);
    const m=1.4*(S.hZone||0);
    const a=S.sOut<=0.01?-_db:-_db-Math.max(S.sOut,m);   // Außenhaut 0 = offen: Poren dürfen nach außen durchbrechen
    return Math.min(a,_dc-Math.max(S.sFoot,m),y-Math.max(S.sFloor,m));
  }
  // gerundeter Voronoi-Abstand: weich-minimum über alle Bisektor-Abstände (d_j-d_i)/2 → abgerundete Zellen
  function wf(S,x,y,z){
    const P=S.sites,n=S.ns;
    let i1=0,b1=1e18;
    for(let i=0,o=0;i<n;i++,o+=3){
      const dx=x-P[o],dy=y-P[o+1],dz=z-P[o+2];
      const d2=dx*dx+dy*dy+dz*dz;
      if(d2<b1){b1=d2;i1=i;}
    }
    const d1=Math.sqrt(b1);
    if(n<2)return 60-d1;
    const kr=S.kr;let w=1e9,first=true;
    for(let j=0,o=0;j<n;j++,o+=3){
      if(j===i1)continue;
      const dx=x-P[o],dy=y-P[o+1],dz=z-P[o+2],q=dx*dx+dy*dy+dz*dz;
      if(!first){const lim=d1+2*(w+kr);if(q>lim*lim)continue;}   // zu weit weg, beeinflusst das Weich-Minimum nicht
      const v=(Math.sqrt(q)-d1)*0.5;
      if(first){w=v;first=false;}else if(v<w+kr)w=smin(w,v,kr);
    }
    return w;
  }
  function grid(S,h){
    const pad=S.K*0.6+3*h+4;
    const ox=S.bmin[0]-pad,oy=-h,oz=S.bmin[2]-pad;
    const nx=Math.ceil((S.bmax[0]+pad-ox)/h)+1,ny=Math.ceil((S.bmax[1]+pad-oy)/h)+1,nz=Math.ceil((S.bmax[2]+pad-oz)/h)+1;
    const N=nx*ny*nz;
    if(N>1.6e6)return grid(S,h*Math.cbrt(N/1.6e6)*1.03);
    const F=new Float32Array(N);
    const cells=S.cellsOn&&S.ns>0,kr=S.kr;
    const open=S.sOut<=0.01,sO=open?0:Math.max(S.sOut,1.4*h),sF=Math.max(S.sFoot,1.4*h),sB=Math.max(S.sFloor,1.3*h),k2=kr*0.4;
    let i=0;
    for(let z=0;z<nz;z++){const Z=oz+z*h;for(let y=0;y<ny;y++){const Y=oy+y*h;for(let x=0;x<nx;x++){
      const X=ox+x*h;let f=f0(S,X,Y,Z);
      if(cells){
        const a=-_db-sO,b=_dc-sF,c=Y-sB;
        if(b>0&&c>0&&(open?f<S.cellT+h:a>0)){
          let w=wf(S,X,Y,Z)-S.cellT;
          if(!open)w=smin(w,a,k2);
          w=smin(w,b,k2);w=smin(w,c,k2);
          if(w>f)f=w;
        }
      }
      F[i++]=f;}}}
    return {F,nx,ny,nz,ox,oy,oz,h};
  }
  const EA=[0,2,4,6,0,1,4,5,0,1,2,3],EB=[1,3,5,7,2,3,6,7,4,5,6,7];
  function surfaceNets(g){
    const {F,nx,ny,nz,ox,oy,oz,h}=g;
    const cx=nx-1,cy=ny-1;
    const cm=new Int32Array(cx*cy*(nz-1)).fill(-1);
    const verts=[];let vc=0;const T=new Float32Array(8);const szn=nx*ny;
    for(let z=0;z<nz-1;z++)for(let y=0;y<ny-1;y++)for(let x=0;x<nx-1;x++){
      const i0=x+nx*(y+ny*z);
      T[0]=F[i0];T[1]=F[i0+1];T[2]=F[i0+nx];T[3]=F[i0+nx+1];T[4]=F[i0+szn];T[5]=F[i0+szn+1];T[6]=F[i0+szn+nx];T[7]=F[i0+szn+nx+1];
      let m=0;for(let k=0;k<8;k++)if(T[k]<0)m|=1<<k;
      if(m===0||m===255)continue;
      let ax=0,ay=0,az=0,cnt=0;
      for(let e=0;e<12;e++){
        const a=EA[e],b=EB[e];const va=T[a],vb=T[b];
        if((va<0)!==(vb<0)){const t=va/(va-vb);
          ax+=(a&1)+t*((b&1)-(a&1));ay+=((a>>1)&1)+t*(((b>>1)&1)-((a>>1)&1));az+=((a>>2)&1)+t*(((b>>2)&1)-((a>>2)&1));cnt++;}
      }
      verts.push(ox+(x+ax/cnt)*h,oy+(y+ay/cnt)*h,oz+(z+az/cnt)*h);
      cm[x+cx*(y+cy*z)]=vc++;
    }
    const idx=[];
    const C=(x,y,z)=>cm[x+cx*(y+cy*z)];
    const quad=(a,b,c,d,flip)=>{if(flip){idx.push(a,c,b,a,d,c);}else{idx.push(a,b,c,a,c,d);}};
    for(let z=1;z<nz-1;z++)for(let y=1;y<ny-1;y++)for(let x=0;x<nx-1;x++){
      const i=x+nx*(y+ny*z);const a=F[i],b=F[i+1];
      if((a<0)!==(b<0))quad(C(x,y-1,z-1),C(x,y,z-1),C(x,y,z),C(x,y-1,z),!(a<0));
    }
    for(let z=1;z<nz-1;z++)for(let y=0;y<ny-1;y++)for(let x=1;x<nx-1;x++){
      const i=x+nx*(y+ny*z);const a=F[i],b=F[i+nx];
      if((a<0)!==(b<0))quad(C(x-1,y,z-1),C(x-1,y,z),C(x,y,z),C(x,y,z-1),!(a<0));
    }
    for(let z=0;z<nz-1;z++)for(let y=1;y<ny-1;y++)for(let x=1;x<nx-1;x++){
      const i=x+nx*(y+ny*z);const a=F[i],b=F[i+szn];
      if((a<0)!==(b<0))quad(C(x-1,y-1,z),C(x,y-1,z),C(x,y,z),C(x-1,y,z),!(a<0));
    }
    return {pos:new Float32Array(verts),idx:new Uint32Array(idx),h:g.h};
  }
  // Taubin-Glättung (schrumpft kaum): entfernt Voxel-Wellen, Boden bleibt bei y >= 0
  function smooth(m,iters){
    if(!iters)return m;
    const pos=m.pos,idx=m.idx,N=pos.length/3;
    const st=new Int32Array(N+1);
    for(let i=0;i<idx.length;i++)st[idx[i]+1]+=2;
    for(let i=0;i<N;i++)st[i+1]+=st[i];
    const fill=st.slice(0,N),nb=new Int32Array(st[N]);
    for(let t=0;t<idx.length;t+=3){
      const a=idx[t],b=idx[t+1],c=idx[t+2];
      nb[fill[a]++]=b;nb[fill[a]++]=c;nb[fill[b]++]=a;nb[fill[b]++]=c;nb[fill[c]++]=a;nb[fill[c]++]=b;
    }
    let src=pos,dst=new Float32Array(pos.length);
    for(let it=0;it<iters*2;it++){
      const lam=(it&1)?-0.53:0.5;
      for(let i=0;i<N;i++){
        const a=st[i],b=st[i+1];if(a===b){dst[i*3]=src[i*3];dst[i*3+1]=src[i*3+1];dst[i*3+2]=src[i*3+2];continue;}
        let mx=0,my=0,mz=0;
        for(let j=a;j<b;j++){const q=nb[j]*3;mx+=src[q];my+=src[q+1];mz+=src[q+2];}
        const n=b-a,o=i*3;
        dst[o]=src[o]+lam*(mx/n-src[o]);dst[o+1]=src[o+1]+lam*(my/n-src[o+1]);dst[o+2]=src[o+2]+lam*(mz/n-src[o+2]);
      }
      const t=src;src=dst;dst=t;
    }
    for(let i=1;i<src.length;i+=3)if(src[i]<0)src[i]=0;
    m.pos=src;return m;
  }
  function volume(pos,idx){
    let v=0;
    for(let t=0;t<idx.length;t+=3){
      const a=idx[t]*3,b=idx[t+1]*3,c=idx[t+2]*3;
      const ax=pos[a],ay=pos[a+1],az=pos[a+2],bx=pos[b],by=pos[b+1],bz=pos[b+2],cx=pos[c],cy=pos[c+1],cz=pos[c+2];
      v+=ax*(by*cz-bz*cy)-ay*(bx*cz-bz*cx)+az*(bx*cy-by*cx);
    }
    return Math.abs(v/6);
  }
  return {pack,ellSDF,f0,zone,wf,grid,surfaceNets,smooth,volume};
})();
</script>

<script type="importmap">
{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"}}
</script>

<script type="module">
import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import {TransformControls} from 'three/addons/controls/TransformControls.js';
import {RoomEnvironment} from 'three/addons/environments/RoomEnvironment.js';

/* ================= Zustand ================= */
const PRESETS={
  lowtop :{label:'Low-Top', v:{tilt:0, soleH:28,soleF:20,spring:9, collar:60, throat:0.52,toeH:1.0, widthF:1.0, flange:4, wall:16,ease:2,heelW:0}},
  hightop:{label:'High-Top',v:{tilt:0, soleH:30,soleF:22,spring:8, collar:140,throat:0.50,toeH:1.05,widthF:1.02,flange:5, wall:16,ease:2,heelW:0}},
  runner :{label:'Runner',  v:{tilt:0, soleH:40,soleF:25,spring:17,collar:54, throat:0.56,toeH:0.95,widthF:0.97,flange:5, wall:14,ease:2,heelW:0}},
  chunky :{label:'Chunky',  v:{tilt:0, soleH:46,soleF:36,spring:8, collar:62, throat:0.50,toeH:1.3, widthF:1.1, flange:9, wall:20,ease:2,heelW:0}},
  pump   :{label:'Pump',    v:{tilt:52,soleH:6, soleF:4, spring:0, collar:22, throat:0.74,toeH:0.85,widthF:0.94,flange:0, wall:10,ease:2,heelW:0}},
  boot   :{label:'Boot',    v:{tilt:0, soleH:30,soleF:22,spring:6, collar:170,throat:0.46,toeH:1.0, widthF:1.02,flange:4, wall:15,ease:2,heelW:0}}
};
const P={
  preset:'lowtop',size:42,
  tilt:0,soleH:28,soleF:20,spring:9,collar:60,throat:0.52,toeH:1,widthF:1,flange:4,wall:16,ease:2,heelW:0,
  cavOn:true,baseOn:true,blobK:14,press:0.6,
  cellsOn:true,steg:5,rnd:3,sOut:0,sFoot:3.5,sFloor:4,N:70,
  voxel:4.5,sculptVox:3.8,
  showBlobs:true,showCells:true,wire:false,cut:false,cutZ:0,xray:0,
  color:'#e4e0d8',smooth:0.45
};
let items=[],sel=null,mode='clay',baseOuter=[],curCav=[];
const sc={tool:'build',R:18,str:0.8,floor:true,down:false,ray:false,hit:null,undo:[],start:null,nbr:null,tmp:null,aff:null,w:null};

/* ================= Szene ================= */
const canvas=document.getElementById('c');
const renderer=new THREE.WebGLRenderer({canvas,antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));
renderer.setClearColor(0xffffff,1);
renderer.shadowMap.enabled=true;renderer.shadowMap.type=THREE.PCFShadowMap;renderer.shadowMap.autoUpdate=false;
renderer.toneMapping=THREE.ACESFilmicToneMapping;renderer.toneMappingExposure=1.0;
renderer.localClippingEnabled=true;
const scene=new THREE.Scene();
const pm=new THREE.PMREMGenerator(renderer);
scene.environment=pm.fromScene(new RoomEnvironment(),0.04).texture;
const camera=new THREE.PerspectiveCamera(35,1,1,6000);
const controls=new OrbitControls(camera,canvas);
controls.enableDamping=true;controls.dampingFactor=0.12;
const sun=new THREE.DirectionalLight(0xffffff,1.7);
sun.position.set(180,420,260);sun.target.position.set(-60,0,0);
sun.castShadow=true;sun.shadow.mapSize.set(1024,1024);
Object.assign(sun.shadow.camera,{left:-280,right:280,top:280,bottom:-280,near:10,far:1400});
sun.shadow.bias=-0.0008;sun.shadow.normalBias=1.2;
scene.add(sun,sun.target,new THREE.HemisphereLight(0xffffff,0xeeeeee,0.45));
const floor=new THREE.Mesh(new THREE.PlaneGeometry(3000,3000),new THREE.ShadowMaterial({opacity:0.16}));
floor.rotation.x=-Math.PI/2;floor.position.y=-0.3;floor.receiveShadow=true;scene.add(floor);
const gridH=new THREE.GridHelper(800,40,0xe2e2e2,0xefefef);gridH.position.y=-0.2;scene.add(gridH);

const mat=new THREE.MeshStandardMaterial({color:P.color,roughness:0.55,metalness:0,envMapIntensity:0.9});
const clipPlane=new THREE.Plane(new THREE.Vector3(0,0,-1),0);
const shoe=new THREE.Mesh(new THREE.BufferGeometry(),mat);
shoe.castShadow=true;shoe.receiveShadow=true;scene.add(shoe);
const shadowDirty=()=>{renderer.shadowMap.needsUpdate=true;};

function applyMaterial(){
  mat.color.set(P.color);mat.roughness=1-0.9*P.smooth;mat.wireframe=P.wire;
  mat.clippingPlanes=P.cut?[clipPlane]:[];mat.clipShadows=true;clipPlane.constant=P.cutZ;
  const tr=P.xray>0;
  mat.transparent=tr;mat.opacity=tr?1-P.xray:1;mat.depthWrite=!tr;
  mat.side=(P.cut||tr)?THREE.DoubleSide:THREE.FrontSide;mat.needsUpdate=true;
  shoe.castShadow=!tr;shadowDirty();
}

/* ================= Handles ================= */
const handleGroup=new THREE.Group();scene.add(handleGroup);
const hGeo=new THREE.SphereGeometry(1,14,10);
const COL={blob:0x3b6cff,sub:0xe0463c,cell:0xff7a1a,off:0xbdbdbd,sel:0x111111};
const colorOf=it=>it===sel?COL.sel:(it.type==='cell'?(it.valid===false?COL.off:COL.cell):(it.sub?COL.sub:COL.blob));
function makeHandle(it){
  const m=new THREE.Mesh(hGeo,new THREE.MeshBasicMaterial({color:colorOf(it),depthTest:false,transparent:true,opacity:0.95}));
  m.renderOrder=20;m.position.copy(it.p);m.userData.item=it;handleGroup.add(m);return m;
}
const ghost=new THREE.Mesh(new THREE.SphereGeometry(1,28,18),new THREE.MeshBasicMaterial({color:0x3b6cff,wireframe:true,transparent:true,opacity:0.25,depthTest:false}));
ghost.renderOrder=19;ghost.visible=false;scene.add(ghost);
const tc=new TransformControls(camera,canvas);tc.setSize(0.75);
scene.add(tc.getHelper?tc.getHelper():tc);
let clickBlock=false;
tc.addEventListener('dragging-changed',e=>{controls.enabled=!e.value;if(e.value)clickBlock=true;if(!e.value&&sel)requestRebuild(false);});
tc.addEventListener('objectChange',()=>{
  if(!sel)return;sel.p.copy(sel.handle.position);updateGhost();requestRebuild(true);
});

function addItem(it){it.handle=makeHandle(it);items.push(it);applyHandleVis();return it;}
function newBlob(o){const r=Math.cbrt(o.rx*o.ry*o.rz);return {type:'blob',p:new THREE.Vector3(o.x,o.y,o.z||0),r,sx:o.rx/r,sy:o.ry/r,sz:o.rz/r,rot:o.rot||0,sub:false};}
function removeItems(pred){
  items=items.filter(it=>{if(pred(it)){handleGroup.remove(it.handle);if(sel===it)select(null);return false;}return true;});
}
function colorAll(){items.forEach(i=>i.handle.material.color.setHex(colorOf(i)));}
function select(it){
  sel=it;colorAll();
  if(it&&mode==='clay'&&it.handle.visible)tc.attach(it.handle);else tc.detach();
  updateGhost();refreshProps();
}
function updateGhost(){
  if(!sel||mode!=='clay'||!sel.handle.visible){ghost.visible=false;return;}
  ghost.visible=true;ghost.position.copy(sel.p);
  if(sel.type==='blob'){
    ghost.material.color.setHex(sel.sub?COL.sub:COL.blob);
    ghost.scale.set(sel.r*sel.sx,sel.r*sel.sy,sel.r*sel.sz);ghost.rotation.set(0,0,sel.rot);
  }else{
    let m=1e9;items.forEach(o=>{if(o.type==='cell'&&o!==sel)m=Math.min(m,o.p.distanceTo(sel.p));});
    const r=m<1e8?Math.max(3,m*0.5-P.steg*0.5):20;   // grobe Porengröße
    ghost.material.color.setHex(COL.cell);ghost.scale.setScalar(r);ghost.rotation.set(0,0,0);
  }
}
function applyHandleVis(){
  items.forEach(it=>{it.handle.visible=mode==='clay'&&(it.type==='cell'?P.showCells:P.showBlobs);});
  handleGroup.visible=mode==='clay';
  if(sel&&!sel.handle.visible)select(null);
}

/* ================= Grundform-Generator ================= */
const sm01=t=>{t=Math.min(1,Math.max(0,t));return t*t*(3-2*t);};
const lerp=(a,b,t)=>a+(b-a)*t;
const footL=()=>P.size*6.6667-15;
function bendA(x,L){return -(P.tilt*Math.PI/180)*sm01((x-0.02*L)/(-0.4*L));}
function bendPt(x,y,L){const a=bendA(x,L),c=Math.cos(a),s=Math.sin(a);return {x:x*c-y*s,y:x*s+y*c,a};}
// Fußprofile (normiert auf Fußlänge L, u = 0 Ferse … 1 Zehenspitze)
const HW=[[0,0.055],[0.05,0.095],[0.12,0.112],[0.22,0.108],[0.34,0.110],[0.46,0.126],[0.58,0.160],[0.68,0.186],[0.76,0.193],[0.85,0.176],[0.93,0.138],[1,0.07]];
const HT=[[0,0.10],[0.06,0.17],[0.14,0.235],[0.24,0.265],[0.36,0.265],[0.48,0.228],[0.60,0.162],[0.72,0.125],[0.84,0.105],[0.93,0.095],[1,0.07]];
function crTab(tab,u){
  const n=tab.length;if(u<=tab[0][0])return tab[0][1];if(u>=tab[n-1][0])return tab[n-1][1];
  let i=0;while(u>tab[i+1][0])i++;
  const p0=tab[Math.max(i-1,0)],p1=tab[i],p2=tab[i+1],p3=tab[Math.min(i+2,n-1)];
  const t=(u-p1[0])/(p2[0]-p1[0]),dd=p2[0]-p1[0];
  const m1=(p2[1]-p0[1])/(p2[0]-p0[0])*dd,m2=(p3[1]-p1[1])/(p3[0]-p1[0])*dd;
  const t2=t*t,t3=t2*t;
  return (2*t3-3*t2+1)*p1[1]+(t3-2*t2+t)*m1+(-2*t3+3*t2)*p2[1]+(t3-t2)*m2;
}
function footModel(){
  const L=footL(),e=P.ease,w=P.wall,fl=P.flange;
  const xs=u=>(u-0.72)*L;
  const ins=u=>lerp(P.soleH,P.soleF,sm01((u-0.08)/0.64));
  const hwF=u=>crTab(HW,u)*L*P.widthF;
  const htF=u=>crTab(HT,u)*L*lerp(1,P.toeH,sm01((u-0.55)/0.3));
  const sprRad=P.spring*Math.PI/180;
  const T=(xl,yl)=>{
    let x=xl,y=yl,a=0;
    if(sprRad>0&&x>0.02*L){const k=Math.tan(sprRad)/(2*0.26*L),d=x-0.02*L;y+=k*d*d;a+=Math.atan(2*k*d);}
    const b=bendPt(x,y,L);return {x:b.x,y:b.y,a:a+b.a};
  };
  const cav=[],outer=[];
  const place=(list,xl,yl,rx,ry,rz,rot)=>{const t=T(xl,yl);list.push({x:t.x,y:t.y,z:0,rx,ry,rz,rot:t.a+(rot||0)});};
  const slope=u=>Math.atan((ins(u+0.02)-ins(u-0.02))/(0.04*L));
  // Fuß-Querschnitte → Fußraum (cav) und Hülle (outer = Fußraum + Wand)
  const FU=[0.05,0.13,0.22,0.32,0.43,0.54,0.64,0.73,0.82,0.90,0.955];
  FU.forEach((u,i)=>{
    const um=FU[Math.max(i-1,0)],up=FU[Math.min(i+1,FU.length-1)];
    let rx=(up-um)*0.5*L*1.45;if(i===0)rx=0.058*L;if(i===FU.length-1)rx=0.052*L;
    const hw=hwF(u),ht=htF(u),sag=1.04;
    const rz=hw*sag+e,ry=ht*0.5*sag+e*0.5,cy=ins(u)+ht*0.5,an=slope(u);
    place(cav,xs(u),cy,rx+e,ry,rz,an);
    place(outer,xs(u),cy,rx+e+w,ry+w,rz+w,an);
  });
  // Sohle als flache Platte entlang des Umrisses (Rand = Flansch)
  const SU=[0.03,0.10,0.19,0.29,0.39,0.49,0.59,0.69,0.79,0.88,0.955];
  SU.forEach((u,i)=>{
    const um=SU[Math.max(i-1,0)],up=SU[Math.min(i+1,SU.length-1)];
    let rx=(up-um)*0.5*L*1.5;if(i===0)rx=0.07*L;if(i===SU.length-1)rx=0.06*L;
    const ry=(ins(u)+5)*0.5+1,cy=ry*0.8;
    place(outer,xs(u),cy,rx+(e+w)*0.6,ry,hwF(u)+e+w+fl,slope(u));
  });
  // Knöchel-Röhre (Einstieg hinten) + Kragen
  const ua=0.18,xa=xs(ua),insA=ins(ua),y0=insA+22,collarY=insA+P.collar;
  const rxT=0.128*L+e,rzT=0.113*L*P.widthF+e;
  // Röhre bleibt senkrecht (das Bein steht, der Fuß kippt) – nur der Fußpunkt folgt der Kippung
  const tb=T(xa,y0);
  const stack=(list,y,rx,ry,rz)=>list.push({x:tb.x,y:tb.y+(y-y0),z:0,rx,ry,rz,rot:0});
  for(let y=y0;y<=collarY+36;y+=10)stack(cav,y,rxT,11,rzT);
  const ryO=11+w,yTop=Math.max(y0,collarY-ryO);
  for(let y=y0;y<yTop;y+=12)stack(outer,y,rxT+w,ryO,rzT+w);
  stack(outer,yTop-2,rxT+w,ryO+2,rzT+w);
  // Schnürung / Einstieg über den Spann
  for(let u=0.30;u<=P.throat+0.001;u+=0.045){
    const ht=htF(u),hw=hwF(u),k=sm01((P.throat-u)/0.12);
    const ry=ht*0.1+e+w+9,rx=0.04*L*lerp(0.9,1.2,k)+e*0.5,rz=Math.max(hw*0.58*lerp(0.6,1,k),8);
    place(cav,xs(u),ins(u)+ht*0.9,rx,ry,rz,slope(u));
  }
  // Absatz / Pfahl, wenn die Ferse angehoben ist
  const hs=T(xs(0.09),0),yb=hs.y;
  if(yb>10){
    const tt=sm01((P.tilt-8)/42),hwH=hwF(0.12)+e+w;
    const Rb=P.heelW>0?P.heelW/2:lerp(hwH*0.9,5.2,tt);
    const Rt=Math.min(Rb*lerp(1,1.7,tt)+1,hwH);
    const xp=hs.x+lerp(0,0.045*L,tt);
    const n=Math.min(7,Math.max(2,Math.ceil(yb/(Rb*1.2)))),ya=Rb*0.8,yz=yb+4;
    for(let k=0;k<n;k++){
      const t=k/(n-1),r=lerp(Rb,Rt,t*t);
      outer.push({x:xp,y:lerp(ya,yz,t),z:0,rx:r,ry:Math.max(r,(yz-ya)/(n-1)*0.8),rz:r,rot:0});
    }
  }
  return {cav,outer};
}
function rebuildBase(){const fm=footModel();curCav=fm.cav;baseOuter=fm.outer;}
function applyPreset(k){
  removeItems(i=>i.fromBase);
  Object.assign(P,PRESETS[k].v);P.preset=k;P.baseOn=true;
  rebuildBase();syncUI();requestRebuild(false);
}

/* ================= Felddaten ================= */
function prepare(){
  const adds=[],subs=[];
  if(P.baseOn)baseOuter.forEach(b=>adds.push(b));
  items.forEach(it=>{
    if(it.type!=='blob')return;
    const o={x:it.p.x,y:it.p.y,z:it.p.z,rx:it.r*it.sx,ry:it.r*it.sy,rz:it.r*it.sz,rot:it.rot};
    (it.sub?subs:adds).push(o);
  });
  if(!adds.length)return null;
  const bmin=[1e9,1e9,1e9],bmax=[-1e9,-1e9,-1e9];
  const addsP=adds.map(b=>{
    let {rx,ry,rz}=b;const pen=ry-b.y;
    if(pen>0&&P.press>0){const q=Math.min(1,pen/ry)*P.press;rx*=1+0.45*q;rz*=1+0.45*q;ry*=1-0.2*q;}
    const m=Math.max(rx,ry,rz);
    bmin[0]=Math.min(bmin[0],b.x-m);bmin[2]=Math.min(bmin[2],b.z-m);
    bmax[0]=Math.max(bmax[0],b.x+m);bmax[1]=Math.max(bmax[1],b.y+m);bmax[2]=Math.max(bmax[2],b.z+m);
    return {x:b.x,y:b.y,z:b.z,rx,ry,rz,rot:b.rot};
  });
  const cells=items.filter(i=>i.type==='cell');
  const sites=new Float32Array(Math.max(1,cells.length)*3);
  cells.forEach((c,k)=>{sites[k*3]=c.p.x;sites[k*3+1]=c.p.y;sites[k*3+2]=c.p.z;});
  return {
    B:Core.pack(addsP,P.blobK),nb:addsP.length,K:P.blobK,
    X:Core.pack(subs,6),nx:subs.length,
    C:Core.pack(curCav,6),nc:curCav.length,cavOn:P.cavOn,
    sites,ns:cells.length,cellsOn:P.cellsOn,cellT:P.steg*0.5,kr:P.rnd*2.5+0.01,
    sOut:P.sOut,sFoot:P.sFoot,sFloor:P.sFloor,hZone:P.voxel,bmin,bmax
  };
}

/* ================= Mesh-Aufbau ================= */
const statusEl=document.getElementById('status');
function status(t){statusEl.textContent=t||'';statusEl.classList.toggle('show',!!t);}
function applyMesh(m){
  const g=new THREE.BufferGeometry();
  g.setAttribute('position',new THREE.BufferAttribute(m.pos,3));
  g.setIndex(new THREE.BufferAttribute(m.idx,1));
  g.computeVertexNormals();g.computeBoundingSphere();g.computeBoundingBox();
  shoe.geometry.dispose();shoe.geometry=g;shadowDirty();
}
const statEl=()=>document.getElementById('stat');
function updateStats(m,ms,note){
  const vol=Core.volume(m.pos,m.idx)/1000,wt=vol*1.2;
  const bb=shoe.geometry.boundingBox;const len=bb?(bb.max.x-bb.min.x):0,hh=bb?(bb.max.y):0;
  statEl().innerHTML=`${(m.pos.length/3).toLocaleString('de')} Vertices · ${(m.idx.length/3).toLocaleString('de')} Dreiecke<br>Volumen ${vol.toFixed(1)} cm³ · ≈ ${wt.toFixed(0)} g <span style="opacity:.7">(1,2 g/cm³)</span><br>Länge ${len.toFixed(0)} mm · Höhe ${hh.toFixed(0)} mm<br>${note||''} ${ms?Math.round(ms)+' ms':''}`;
}
let pend=null,idleT=0;
function requestRebuild(draft){pend=draft;clearTimeout(idleT);if(draft)idleT=setTimeout(()=>{pend=false;},350);}
function colorSites(S){
  let changed=false;
  items.forEach(it=>{
    if(it.type!=='cell')return;
    const ok=Core.zone(S,it.p.x,it.p.y,it.p.z)>0;
    if(it.valid!==ok){it.valid=ok;changed=true;}
  });
  if(changed)colorAll();
}
function rebuildClay(draft){
  const t0=performance.now();
  const S=prepare();
  if(!S){applyMesh({pos:new Float32Array(0),idx:new Uint32Array(0)});return;}
  const h=draft?Math.max(P.voxel*1.4,7):P.voxel;
  const g=Core.grid(S,h);const m=Core.smooth(Core.surfaceNets(g),draft?1:2);
  applyMesh(m);colorSites(S);
  updateStats(m,performance.now()-t0,draft?'Vorschau · ':`Voxel ${g.h.toFixed(1)} mm · `);
}

/* ================= Sculpt ================= */
function buildAdj(idx,N){
  const start=new Int32Array(N+1);
  for(let i=0;i<idx.length;i++)start[idx[i]+1]+=2;
  for(let i=0;i<N;i++)start[i+1]+=start[i];
  const fill=start.slice(0,N),nbr=new Int32Array(start[N]);
  for(let t=0;t<idx.length;t+=3){
    const a=idx[t],b=idx[t+1],c=idx[t+2];
    nbr[fill[a]++]=b;nbr[fill[a]++]=c;nbr[fill[b]++]=a;nbr[fill[b]++]=c;nbr[fill[c]++]=a;nbr[fill[c]++]=b;
  }
  return {start,nbr};
}
async function enterSculpt(){
  const S=prepare();if(!S)return;
  status('Verdichte Mesh …');await new Promise(r=>setTimeout(r,40));
  try{
    const t0=performance.now();
    const g=Core.grid(S,P.sculptVox);const m=Core.smooth(Core.surfaceNets(g),1);
    applyMesh(m);
    const N=m.pos.length/3,a=buildAdj(m.idx,N);
    sc.start=a.start;sc.nbr=a.nbr;sc.tmp=new Float32Array(N*3);sc.aff=new Int32Array(N);sc.w=new Float32Array(N);sc.undo=[];
    mode='sculpt';tc.detach();ghost.visible=false;
    updateStats(m,performance.now()-t0,`Sculpt · Voxel ${g.h.toFixed(1)} mm · `);
    applyHandleVis();updateModeUI();status('');
  }catch(err){status('Fehler: '+err.message);console.error(err);}
}
function leaveSculpt(){
  if(sc.undo.length&&!confirm('Zurück zu CLAY? Sculpt-Änderungen gehen verloren.'))return;
  mode='clay';cursor.visible=false;applyHandleVis();
  if(sel&&sel.handle.visible)tc.attach(sel.handle);updateGhost();
  updateModeUI();requestRebuild(false);
}
function pushUndo(){
  sc.undo.push(shoe.geometry.attributes.position.array.slice());
  if(sc.undo.length>14)sc.undo.shift();
}
function undo(){
  if(mode!=='sculpt'||!sc.undo.length)return;
  const g=shoe.geometry,p=g.attributes.position;p.array.set(sc.undo.pop());p.needsUpdate=true;
  g.computeVertexNormals();g.computeBoundingSphere();g.computeBoundingBox();shadowDirty();
}
const raycaster=new THREE.Raycaster();const ndc=new THREE.Vector2();
const cursor=new THREE.Group();
cursor.add(new THREE.Mesh(new THREE.RingGeometry(0.965,1,72),new THREE.MeshBasicMaterial({color:0x111111,transparent:true,opacity:0.85,depthTest:false,side:THREE.DoubleSide})));
cursor.add(new THREE.Mesh(new THREE.CircleGeometry(1,48),new THREE.MeshBasicMaterial({color:0x111111,transparent:true,opacity:0.06,depthTest:false,side:THREE.DoubleSide})));
cursor.children.forEach(c=>c.renderOrder=30);cursor.visible=false;scene.add(cursor);
let ptr={x:0,y:0,in:false},ptrAlt=false,ptrShift=false;
function castBrush(){
  const r=canvas.getBoundingClientRect();
  ndc.set(((ptr.x-r.left)/r.width)*2-1,-((ptr.y-r.top)/r.height)*2+1);
  raycaster.setFromCamera(ndc,camera);
  const h=raycaster.intersectObject(shoe,false)[0];
  if(!h){sc.hit=null;cursor.visible=false;return null;}
  sc.hit=h;
  const n=h.face.normal.clone();
  cursor.position.copy(h.point).addScaledVector(n,0.4);cursor.lookAt(h.point.clone().add(n));
  cursor.scale.setScalar(sc.R);cursor.visible=true;return h;
}
function dab(kdt){
  const h=sc.hit;if(!h)return;
  const g=shoe.geometry,pa=g.attributes.position,pos=pa.array,nor=g.attributes.normal.array,N=pos.length/3;
  const R=sc.R,R2=R*R,px=h.point.x,py=h.point.y,pz=h.point.z;
  const aff=sc.aff,W=sc.w;let cnt=0;
  for(let i=0,o=0;i<N;i++,o+=3){
    const dx=pos[o]-px;if(dx>R||dx<-R)continue;
    const dy=pos[o+1]-py;if(dy>R||dy<-R)continue;
    const dz=pos[o+2]-pz,d2=dx*dx+dy*dy+dz*dz;
    if(d2<R2){const t=1-d2/R2;aff[cnt]=i;W[cnt]=t*t;cnt++;}
  }
  if(!cnt)return;
  const tool=ptrShift?'smooth':sc.tool,str=sc.str;
  if(tool==='build'||tool==='carve'){
    let nx=0,ny=0,nz=0;
    for(let k=0;k<cnt;k++){const i=aff[k]*3,w=W[k];nx+=nor[i]*w;ny+=nor[i+1]*w;nz+=nor[i+2]*w;}
    const l=Math.hypot(nx,ny,nz)||1;nx/=l;ny/=l;nz/=l;
    let s=str*R*0.08*kdt;if(tool==='carve')s=-s;if(ptrAlt)s=-s;
    for(let k=0;k<cnt;k++){const i=aff[k]*3,a=s*W[k];pos[i]+=nx*a;pos[i+1]+=ny*a;pos[i+2]+=nz*a;}
  }else if(tool==='smooth'){
    const tmp=sc.tmp,st=sc.start,nb=sc.nbr;
    for(let k=0;k<cnt;k++){
      const i=aff[k],a=st[i],b=st[i+1];let mx=0,my=0,mz=0;
      for(let j=a;j<b;j++){const q=nb[j]*3;mx+=pos[q];my+=pos[q+1];mz+=pos[q+2];}
      const n=b-a||1;tmp[k*3]=mx/n;tmp[k*3+1]=my/n;tmp[k*3+2]=mz/n;
    }
    for(let k=0;k<cnt;k++){
      const i=aff[k]*3,f=Math.min(1,W[k]*str*0.5*kdt);
      pos[i]+=(tmp[k*3]-pos[i])*f;pos[i+1]+=(tmp[k*3+1]-pos[i+1])*f;pos[i+2]+=(tmp[k*3+2]-pos[i+2])*f;
    }
  }else if(tool==='flat'){
    let ax=0,ay=0,az=0,nx=0,ny=0,nz=0,ws=0;
    for(let k=0;k<cnt;k++){const i=aff[k]*3,w=W[k];ax+=pos[i]*w;ay+=pos[i+1]*w;az+=pos[i+2]*w;nx+=nor[i]*w;ny+=nor[i+1]*w;nz+=nor[i+2]*w;ws+=w;}
    ax/=ws;ay/=ws;az/=ws;const l=Math.hypot(nx,ny,nz)||1;nx/=l;ny/=l;nz/=l;
    for(let k=0;k<cnt;k++){
      const i=aff[k]*3,d=(pos[i]-ax)*nx+(pos[i+1]-ay)*ny+(pos[i+2]-az)*nz,f=Math.min(1,W[k]*str*0.35*kdt);
      pos[i]-=nx*d*f;pos[i+1]-=ny*d*f;pos[i+2]-=nz*d*f;
    }
  }
  if(sc.floor)for(let k=0;k<cnt;k++){const i=aff[k]*3+1;if(pos[i]<0)pos[i]=0;}
  pa.needsUpdate=true;g.computeVertexNormals();shadowDirty();
}

/* ================= Eingabe ================= */
window.addEventListener('pointermove',e=>{ptr.x=e.clientX;ptr.y=e.clientY;ptrAlt=e.altKey;ptrShift=e.shiftKey;ptr.in=e.target===canvas||sc.down;sc.ray=true;});
let downPos=null;
window.addEventListener('pointerdown',e=>{
  if(e.target!==canvas)return;
  ptr.x=e.clientX;ptr.y=e.clientY;ptrAlt=e.altKey;ptrShift=e.shiftKey;
  if(mode==='sculpt'&&e.button===0){
    const h=castBrush();
    if(h){e.stopPropagation();pushUndo();sc.down=true;controls.enabled=false;try{canvas.setPointerCapture(e.pointerId);}catch(_){}}
  }else downPos={x:e.clientX,y:e.clientY};
},true);
window.addEventListener('pointerup',e=>{
  if(sc.down){sc.down=false;controls.enabled=true;const g=shoe.geometry;g.computeBoundingSphere();g.computeBoundingBox();
    const m={pos:g.attributes.position.array,idx:g.index.array};updateStats(m,0,'Sculpt · ');return;}
  if(mode!=='clay'||!downPos)return;
  const moved=Math.hypot(e.clientX-downPos.x,e.clientY-downPos.y);downPos=null;
  if(clickBlock){clickBlock=false;return;}
  if(moved>5||e.target!==canvas)return;
  const r=canvas.getBoundingClientRect();
  ndc.set(((e.clientX-r.left)/r.width)*2-1,-((e.clientY-r.top)/r.height)*2+1);
  raycaster.setFromCamera(ndc,camera);
  const hit=raycaster.intersectObjects(handleGroup.children.filter(c=>c.visible),false)[0];
  select(hit?hit.object.userData.item:null);
});
window.addEventListener('keydown',e=>{
  if(/INPUT|TEXTAREA/.test(document.activeElement.tagName)&&document.activeElement.type!=='range')return;
  ptrAlt=e.altKey;ptrShift=e.shiftKey;
  if((e.metaKey||e.ctrlKey)&&e.key==='z'){e.preventDefault();undo();return;}
  if(mode==='sculpt'){
    const t={'1':'build','2':'carve','3':'smooth','4':'flat'}[e.key];if(t)setTool(t);
    if(e.key==='['){setBrush(Math.max(3,sc.R-2));}if(e.key===']'){setBrush(Math.min(70,sc.R+2));}
  }else if((e.key==='Delete'||e.key==='Backspace')&&sel){e.preventDefault();deleteSel();}
});
window.addEventListener('keyup',e=>{ptrAlt=e.altKey;ptrShift=e.shiftKey;});

/* ================= UI ================= */
const panel=document.getElementById('panel');
const el=(tag,cls,html)=>{const d=document.createElement(tag);if(cls)d.className=cls;if(html!=null)d.innerHTML=html;return d;};
const sec=(title)=>el('div','sec',title?`<h4>${title}</h4>`:'');
function slider(parent,label,min,max,step,val,fmt,cb){
  const r=el('label','row',`<span>${label}</span><input type="range" min="${min}" max="${max}" step="${step}" value="${val}"><output></output>`);
  const inp=r.querySelector('input'),out=r.querySelector('output');
  const upd=()=>{out.textContent=fmt(+inp.value);};
  inp.addEventListener('input',()=>{upd();cb(+inp.value,false);});
  inp.addEventListener('change',()=>{cb(+inp.value,true);});
  upd();parent.appendChild(r);
  return {set(v){inp.value=v;upd();},row:r};
}
function checkbox(parent,label,val,cb){
  const l=el('label','chk',`<input type="checkbox"${val?' checked':''}><span>${label}</span>`);
  const inp=l.querySelector('input');inp.addEventListener('change',e=>cb(e.target.checked));parent.appendChild(l);
  return {set(v){inp.checked=v;},row:l};
}
function button(parent,label,cb,cls){const b=el('button',cls||'',label);b.addEventListener('click',cb);parent.appendChild(b);return b;}
const reg={},geoKeys=new Set(['size','tilt','soleH','soleF','spring','collar','throat','toeH','widthF','flange','wall','ease','heelW']);
function prm(parent,key,label,min,max,step,fmt,after){
  reg[key]=slider(parent,label,min,max,step,P[key],fmt||(v=>v),(v,fin)=>{
    P[key]=v;
    if(geoKeys.has(key)){rebuildBase();showHint();}
    if(after)after();
    if(mode==='clay')requestRebuild(!fin);
  });
  return reg[key];
}
const pre=(v)=>v;
const mm=v=>v+' mm',deg=v=>v+'°';
const regChk={};
function syncUI(){
  for(const k in reg)reg[k].set(P[k]);
  for(const k in regChk)regChk[k].set(P[k]);
  for(const k in presetBtns)presetBtns[k].classList.toggle('on',k===P.preset);
  showHint();
}

panel.appendChild(el('h1','','SHOE:BLOB'));
panel.appendChild(el('div','sub','Mono-Material Schuh-Designer'));
const seg=el('div','seg');const bClay=button(seg,'CLAY',()=>{if(mode==='sculpt')leaveSculpt();});const bSculpt=button(seg,'SCULPT',()=>{if(mode==='clay')enterSculpt();});
panel.appendChild(seg);

/* ---- CLAY ---- */
const clay=el('div');panel.appendChild(clay);
let s=sec('GRUNDFORM');clay.appendChild(s);
const pz=el('div','pz');s.appendChild(pz);const presetBtns={};
for(const k in PRESETS)presetBtns[k]=button(pz,PRESETS[k].label,()=>applyPreset(k));
prm(s,'size','Größe EU',35,47,0.5);
prm(s,'tilt','Kippung Ferse',0,70,1,deg);
prm(s,'soleH','Sohle Ferse',3,60,1,mm);
prm(s,'soleF','Sohle vorne',3,50,1,mm);
prm(s,'spring','Spitzenhub',0,30,1,deg);
prm(s,'collar','Schafthöhe',15,200,1,mm);
prm(s,'throat','Einstieg bis',0.30,0.80,0.01,v=>Math.round(v*100)+'%');
prm(s,'toeH','Zehenbox',0.7,1.6,0.05,v=>v.toFixed(2));
prm(s,'widthF','Breite',0.85,1.25,0.01,v=>v.toFixed(2));
prm(s,'flange','Sohlenrand',0,14,0.5,mm);
prm(s,'wall','Wand',3,24,0.5,mm);
prm(s,'ease','Spiel',0,8,0.5,mm);
prm(s,'heelW','Absatzbreite',0,80,1,v=>v===0?'auto':v+' mm');
regChk.cavOn=checkbox(s,'Fußraum aushöhlen',P.cavOn,v=>{P.cavOn=v;requestRebuild(false);});
regChk.baseOn=checkbox(s,'Grundform aktiv',P.baseOn,v=>{
  if(v)removeItems(i=>i.fromBase);P.baseOn=v;showHint();requestRebuild(false);
});
const hintEl=el('div','warn','Grundform ist in Blobs umgewandelt – die Fuß-Regler wirken nur noch auf den Fußraum. Preset wählen setzt alles zurück.');
s.appendChild(hintEl);
function showHint(){hintEl.hidden=P.baseOn||!items.some(i=>i.fromBase);}
button(s,'Grundform in Blobs umwandeln',()=>{
  if(!P.baseOn)return;
  baseOuter.forEach(o=>{const b=newBlob(o);b.fromBase=true;addItem(b);});
  P.baseOn=false;regChk.baseOn.set(false);showHint();requestRebuild(false);
});

s=sec('BLOBS');clay.appendChild(s);
let bb=el('div','btns');s.appendChild(bb);
button(bb,'+ Blob',()=>addAtTarget('blob'));
button(bb,'Duplizieren',()=>{
  if(!sel||sel.type!=='blob')return;
  const c=Object.assign({},sel);c.p=sel.p.clone().add(new THREE.Vector3(0,0,14));delete c.handle;c.fromBase=false;
  select(addItem(c));requestRebuild(false);
});
button(bb,'Löschen',()=>deleteSel());
slider(s,'Weichheit',2,40,1,P.blobK,mm,(v,f)=>{P.blobK=v;requestRebuild(!f);});
slider(s,'Bodendruck',0,1,0.05,P.press,v=>v.toFixed(2),(v,f)=>{P.press=v;requestRebuild(!f);});
const props=el('div');props.hidden=true;s.appendChild(props);
const pr={};
const setBlob=(fn,fin)=>{if(!sel||sel.type!=='blob')return;fn(sel);updateGhost();requestRebuild(!fin);};
pr.r=slider(props,'Radius',3,120,0.5,18,mm,(v,f)=>setBlob(b=>b.r=v,f));
pr.sx=slider(props,'Streckung X',0.15,4,0.05,1,v=>v.toFixed(2),(v,f)=>setBlob(b=>b.sx=v,f));
pr.sy=slider(props,'Streckung Y',0.15,4,0.05,1,v=>v.toFixed(2),(v,f)=>setBlob(b=>b.sy=v,f));
pr.sz=slider(props,'Streckung Z',0.15,4,0.05,1,v=>v.toFixed(2),(v,f)=>setBlob(b=>b.sz=v,f));
pr.rot=slider(props,'Drehung Z',-90,90,1,0,deg,(v,f)=>setBlob(b=>b.rot=v*Math.PI/180,f));
pr.sub=checkbox(props,'Subtraktiv (schneidet Material weg)',false,v=>{if(sel&&sel.type==='blob'){sel.sub=v;colorAll();updateGhost();requestRebuild(false);}});
const props2=el('div','hint','Zellpunkt gewählt – das gestrichelte Gitter zeigt die ungefähre Porengröße.');props2.hidden=true;s.appendChild(props2);
const leg=el('div','leg','<span><i style="background:#3b6cff"></i>Blob</span><span><i style="background:#e0463c"></i>Subtraktiv</span><span><i style="background:#ff7a1a"></i>Zelle aktiv</span><span><i style="background:#bdbdbd"></i>Zelle wirkungslos</span>');
s.appendChild(leg);
function refreshProps(){
  props.hidden=!(sel&&sel.type==='blob');props2.hidden=!(sel&&sel.type==='cell');
  if(sel&&sel.type==='blob'){pr.r.set(sel.r);pr.sx.set(sel.sx);pr.sy.set(sel.sy);pr.sz.set(sel.sz);pr.rot.set(Math.round(sel.rot*180/Math.PI));pr.sub.set(!!sel.sub);}
}
function addAtTarget(type){
  const p=controls.target.clone().add(new THREE.Vector3((Math.random()-.5)*20,(Math.random()-.5)*10,(Math.random()-.5)*20));
  if(p.y<4)p.y=4;
  const it=type==='blob'?addItem(newBlob({x:p.x,y:Math.max(p.y,20),z:p.z,rx:20,ry:20,rz:20,rot:0})):addItem({type:'cell',p});
  select(it);requestRebuild(false);
}
function deleteSel(){if(!sel)return;const it=sel;removeItems(i=>i===it);showHint();requestRebuild(false);}

s=sec('VORONOI-HOHLRÄUME');clay.appendChild(s);
s.appendChild(el('div','hint','Poren sitzen in Seitenwänden, Spann und Sohle. „Haut außen“ 0 = offen (Poren brechen nach außen durch), größer = geschlossene Haut. Fußraum und Boden bleiben durch ihre Haut geschlossen. Hautdicken sind nie dünner als ca. 1,4 Voxel.'));
regChk.cellsOn=checkbox(s,'Hohlräume aktiv',P.cellsOn,v=>{P.cellsOn=v;requestRebuild(false);});
bb=el('div','btns');s.appendChild(bb);
button(bb,'+ Zelle',()=>addAtTarget('cell'));
button(bb,'Zufällig füllen',()=>fillCells());
button(bb,'Entspannen',()=>relaxCells());
button(bb,'Alle löschen',()=>{removeItems(i=>i.type==='cell');requestRebuild(false);});
slider(s,'Anzahl (Fill)',8,200,1,P.N,v=>v,v=>{P.N=v;});
prm(s,'steg','Stegdicke',2,14,0.5,mm,()=>updateGhost());
prm(s,'rnd','Rundung',0,8,0.25,mm);
prm(s,'sOut','Haut außen',0,20,0.5,mm);
prm(s,'sFoot','Haut Fußraum',0,20,0.5,mm);
prm(s,'sFloor','Haut Boden',0,20,0.5,mm);
function sampler(S){
  const bmn=S.bmin,bmx=S.bmax;
  return ()=>{for(let t=0;t<900;t++){
    const x=lerp(bmn[0],bmx[0],Math.random()),y=Math.random()*bmx[1],z=lerp(bmn[2],bmx[2],Math.random());
    if(Core.zone(S,x,y,z)>1.5)return [x,y,z];}return null;};
}
function fillCells(){
  const S=prepare();if(!S)return;
  removeItems(i=>i.type==='cell');
  const pts=[],cand=sampler(S);
  for(let n=0;n<P.N;n++){
    let best=null,bd=-1;
    for(let k=0;k<12;k++){const c=cand();if(!c)continue;let md=1e9;
      for(const p of pts){const d=Math.hypot(p[0]-c[0],p[1]-c[1],p[2]-c[2]);if(d<md)md=d;}
      if(md>bd){bd=md;best=c;}}
    if(best)pts.push(best);
  }
  pts.forEach(p=>addItem({type:'cell',p:new THREE.Vector3(p[0],p[1],p[2])}));
  requestRebuild(false);
}
function relaxCells(){
  const S=prepare();if(!S)return;
  const cells=items.filter(i=>i.type==='cell');if(cells.length<2)return;
  const cand=sampler(S),acc=cells.map(()=>[0,0,0,0]);
  for(let k=0;k<2500;k++){
    const p=cand();if(!p)break;let bi=0,bd=1e18;
    for(let j=0;j<cells.length;j++){const q=cells[j].p;const d=(q.x-p[0])**2+(q.y-p[1])**2+(q.z-p[2])**2;if(d<bd){bd=d;bi=j;}}
    const a=acc[bi];a[0]+=p[0];a[1]+=p[1];a[2]+=p[2];a[3]++;
  }
  cells.forEach((c,j)=>{const a=acc[j];if(a[3]>3){c.p.lerp(new THREE.Vector3(a[0]/a[3],a[1]/a[3],a[2]/a[3]),0.7);c.handle.position.copy(c.p);}});
  updateGhost();requestRebuild(false);
}

s=sec('ANSICHT / QUALITÄT');clay.appendChild(s);
prm(s,'voxel','Auflösung',3,10,0.25,v=>v+' mm');
regChk.showBlobs=checkbox(s,'Blob-Punkte anzeigen',P.showBlobs,v=>{P.showBlobs=v;applyHandleVis();});
regChk.showCells=checkbox(s,'Zellpunkte anzeigen',P.showCells,v=>{P.showCells=v;applyHandleVis();});
regChk.wire=checkbox(s,'Drahtgitter',P.wire,v=>{P.wire=v;applyMaterial();});
slider(s,'Röntgen',0,0.9,0.05,P.xray,v=>v===0?'aus':Math.round(v*100)+'%',v=>{P.xray=v;applyMaterial();});
regChk.cut=checkbox(s,'Schnitt (Hohlräume sehen)',P.cut,v=>{P.cut=v;applyMaterial();});
slider(s,'Schnitt-Position',-70,70,1,P.cutZ,mm,v=>{P.cutZ=v;applyMaterial();});
s=sec('');clay.appendChild(s);
prm(s,'sculptVox','Sculpt-Detail',2,6,0.1,v=>v+' mm');
button(s,'→ SCULPT (Mesh verdichten)',()=>enterSculpt(),'pri');

/* ---- SCULPT ---- */
const sculpt=el('div');sculpt.hidden=true;panel.appendChild(sculpt);
s=sec('PINSEL');sculpt.appendChild(s);
const tools=[['build','Aufbauen','1'],['carve','Abtragen','2'],['smooth','Glätten','3'],['flat','Abflachen','4']];
const tb=el('div','btns');s.appendChild(tb);const toolBtns={};
tools.forEach(t=>{toolBtns[t[0]]=button(tb,`${t[1]} <span style="opacity:.5">${t[2]}</span>`,()=>setTool(t[0]));});
function setTool(t){sc.tool=t;for(const k in toolBtns)toolBtns[k].classList.toggle('on',k===t);}
setTool('build');
const brushSl=slider(s,'Pinselbreite',3,70,1,sc.R,mm,v=>{sc.R=v;});
function setBrush(v){sc.R=v;brushSl.set(v);}
slider(s,'Stärke',0.05,1.5,0.05,sc.str,v=>v.toFixed(2),v=>{sc.str=v;});
checkbox(s,'Boden halten (y ≥ 0)',sc.floor,v=>{sc.floor=v;});
s.appendChild(el('div','hint','Shift = temporär glätten · Alt = Wirkung umkehren · [ ] Pinselgröße · Strg/Cmd+Z Undo. Drehen/Zoomen: Maus neben dem Schuh oder rechte Taste.'));
bb=el('div','btns');s.appendChild(bb);
button(bb,'Undo',undo);
button(bb,'← zurück zu CLAY',()=>leaveSculpt());
checkbox(s,'Drahtgitter',P.wire,v=>{P.wire=v;regChk.wire.set(v);applyMaterial();});
slider(s,'Röntgen',0,0.9,0.05,P.xray,v=>v===0?'aus':Math.round(v*100)+'%',v=>{P.xray=v;applyMaterial();});

/* ---- Material / Export ---- */
s=sec('MATERIAL (MONO)');panel.appendChild(s);
const cr=el('div','row','<span>Farbe</span><input type="color" value="'+P.color+'"><span></span>');
cr.querySelector('input').addEventListener('input',e=>{P.color=e.target.value;applyMaterial();});s.appendChild(cr);
slider(s,'Glätte',0,1,0.01,P.smooth,v=>v.toFixed(2),v=>{P.smooth=v;applyMaterial();});
s=sec('EXPORT / WERTE');panel.appendChild(s);
button(s,'STL exportieren (mm, Z oben)',()=>exportSTL());
const st=el('div','');st.id='stat';st.style.marginTop='10px';s.appendChild(st);

function updateModeUI(){
  bClay.classList.toggle('on',mode==='clay');bSculpt.classList.toggle('on',mode==='sculpt');
  clay.hidden=mode!=='clay';sculpt.hidden=mode!=='sculpt';
}

/* ---- STL ---- */
function exportSTL(){
  const g=shoe.geometry;if(!g.index||!g.index.count)return;
  const pos=g.attributes.position.array,idx=g.index.array,nt=idx.length/3;
  const buf=new ArrayBuffer(84+nt*50),dv=new DataView(buf);dv.setUint32(80,nt,true);let o=84;
  const v=(i)=>[pos[i*3],-pos[i*3+2],pos[i*3+1]];
  for(let t=0;t<nt;t++){
    const a=v(idx[t*3]),b=v(idx[t*3+1]),c=v(idx[t*3+2]);
    const ux=b[0]-a[0],uy=b[1]-a[1],uz=b[2]-a[2],vx=c[0]-a[0],vy=c[1]-a[1],vz=c[2]-a[2];
    let nx=uy*vz-uz*vy,ny=uz*vx-ux*vz,nz=ux*vy-uy*vx;const l=Math.hypot(nx,ny,nz)||1;
    [nx/l,ny/l,nz/l,...a,...b,...c].forEach(f=>{dv.setFloat32(o,f,true);o+=4;});o+=2;
  }
  const url=URL.createObjectURL(new Blob([buf],{type:'model/stl'}));
  const a=document.createElement('a');a.href=url;a.download=`shoe_EU${P.size}.stl`;a.click();setTimeout(()=>URL.revokeObjectURL(url),2000);
}

/* ================= Loop ================= */
function resize(){
  const w=innerWidth,h=innerHeight;renderer.setSize(w,h,false);camera.aspect=w/h;
  const off=innerWidth>700?158:0;   // Szene rechts neben dem Panel zentrieren
  if(off)camera.setViewOffset(w,h,-off,0,w,h);else camera.clearViewOffset();
  camera.updateProjectionMatrix();
}
addEventListener('resize',resize);resize();

let lastT=performance.now();
function loop(){
  requestAnimationFrame(loop);
  const now=performance.now(),dt=Math.min(0.05,(now-lastT)/1000);lastT=now;
  if(mode==='clay'&&pend!==null){const d=pend;pend=null;try{rebuildClay(d);}catch(err){status('Fehler: '+err.message);console.error(err);}}
  if(mode==='sculpt'){
    if(sc.down){castBrush();dab(Math.max(0.3,dt*60));}
    else if(sc.ray&&ptr.in){castBrush();}
    else if(!ptr.in)cursor.visible=false;
    sc.ray=false;
  }
  if(handleGroup.visible)for(const it of items){
    const hd=it.handle;if(!hd.visible)continue;
    hd.position.copy(it.p);
    hd.scale.setScalar(camera.position.distanceTo(hd.position)*(it===sel?0.016:(it.type==='cell'?0.0085:0.012)));
  }
  controls.update();renderer.render(scene,camera);
}

/* ================= Start ================= */
applyMaterial();
{const L=footL();controls.target.set(-0.22*L,40,0);camera.position.set(-0.22*L+210,150,330);controls.update();}
rebuildBase();syncUI();updateModeUI();rebuildClay(false);loop();
/*__HOOK__*/
</script>
</body>
</html>