{"format":"relay-web-workflow","version":1,"title":"One-Line Pavilion · Branching Timber","request":"Grow a pavilion on four fixed anchors with straight 300 × 300 mm timber beams and steel sockets. Give every support two forks that find distinct open canopy sockets. Inspect the model and generate the technical drawing set. Optionally visualize its material and setting with Gemini.","workflow":{"schema_version":1,"kind":"workflow","inputs":{},"tasks":[{"schema_version":3,"id":"growth-settings","operation":"code.javascript","objective":"Choose the seed used by the model and the drawings.","inputs":{"code":{"value":"function run(inputs, controls) { return { seed: controls.seed }; }"},"controls":{"value":{"seed":12}},"files":{"value":[]}}},{"schema_version":3,"id":"pavilion","operation":"code.javascript","objective":"Grow forks from all four corner supports in 300 × 300 mm timber. Reach at least three distinct canopy joints per support.","inputs":{"code":{"value":"// A concept model in metres. Connection dimensions are illustrative.\nconst timberSide = 0.300;\nconst beamLength = 1.450;\nconst nodeHalf = 0.170;\nconst socketDepth = 0.290;\nconst plate = 0.012;\nconst throatHalf = 0.153;\nconst boreRadius = 0.012;\nconst boltRadius = 0.010;\nconst drillStations = [0.125, 0.225];\nconst wood = '#b77b47';\nconst steel = '#3a4857';\nconst fixing = '#98a9bc';\nconst outwardFaces = [[3,2,1,0],[4,5,6,7],[0,1,5,4],[1,2,6,5],[2,3,7,6],[3,0,4,7]];\n\nfunction body(name, color, module) {\n  return {type:'MESH',name,color,module,vertices:[],faces:[]};\n}\n\nfunction block(mesh, x0, x1, y0, y1, z0, z1) {\n  const first = mesh.vertices.length;\n  for (const z of [z0,z1]) {\n    mesh.vertices.push([x0,y0,z],[x1,y0,z],[x1,y1,z],[x0,y1,z]);\n  }\n  for (const face of outwardFaces) mesh.faces.push(face.map(i => i+first));\n}\n\nconst segments = 8;\nconst angles = Array.from({length:segments},(_,i) => i*Math.PI*2/segments);\n\n// A rectangular bar or plate with round through-bores along local Y.\n// The side faces are cut into small patches around each bore.\nfunction drilledSolid(mesh, x0, x1, y0, y1, z0, z1, holes) {\n  const first = mesh.vertices.length;\n  const corner = [[x0,z0],[x1,z0],[x1,z1],[x0,z1]];\n  for (const y of [y0,y1]) for (const [x,z] of corner) mesh.vertices.push([x,y,z]);\n  for (let i=0;i<4;i++) {\n    const a=first+i,b=first+(i+1)%4;\n    mesh.faces.push([a,a+4,b+4,b]);\n  }\n  const halfPatch=0.022;\n  for (const y of [y0,y1]) {\n    function flat(points) {\n      const start=mesh.vertices.length;\n      mesh.vertices.push(...points.map(([x,z])=>[x,y,z]));\n      const face=points.map((_,i)=>start+i);\n      mesh.faces.push(y===y0?face:face.reverse());\n    }\n    function rect(a,b,c,d) {\n      if (b>a && d>c) flat([[a,c],[b,c],[b,d],[a,d]]);\n    }\n    rect(x0,x1,z0,-halfPatch);\n    rect(x0,x1,halfPatch,z1);\n    let last=x0;\n    for (const x of holes) {\n      rect(last,x-halfPatch,-halfPatch,halfPatch);\n      last=x+halfPatch;\n      const perimeter=angles.map(angle=>{\n        const u=Math.cos(angle),v=Math.sin(angle),extent=halfPatch/Math.max(Math.abs(u),Math.abs(v));\n        return [[x+u*extent,v*extent],[x+u*boreRadius,v*boreRadius]];\n      });\n      for (const [i,[a,b]] of perimeter.entries()) {\n        const [c,d]=perimeter[(i+1)%segments];\n        flat([a,c,d,b]);\n      }\n    }\n    rect(last,x1,-halfPatch,halfPatch);\n  }\n  for (const x of holes) tubeWall(mesh,x,y0,y1,boreRadius,segments,false);\n}\n\nfunction tubeWall(mesh,x,y0,y1,radius,sides,capped) {\n  const start=mesh.vertices.length;\n  for (const y of [y0,y1]) for (let i=0;i<sides;i++) {\n    const a=2*Math.PI*i/sides;\n    mesh.vertices.push([x+Math.cos(a)*radius,y,Math.sin(a)*radius]);\n  }\n  for (let i=0;i<sides;i++) {\n    const a=start+i,b=start+(i+1)%sides;\n    const face=[a,b,b+sides,a+sides];\n    mesh.faces.push(capped?face.reverse():face);\n  }\n  if (capped) {\n    mesh.faces.push(Array.from({length:sides},(_,i)=>start+i));\n    mesh.faces.push(Array.from({length:sides},(_,i)=>start+2*sides-1-i));\n  }\n}\n\nfunction transform(mesh, orientation, arm, slide=0) {\n  mesh.vertices = mesh.vertices.map(([x,y,z])=>{\n    const p = arm===0 ? [x+slide,y,z] : [-y,x+slide,z];\n    return (orientation==='knee' ? [p[0],p[2],-p[1]] : p).map(n=>Number(n.toFixed(6)));\n  });\n  return mesh;\n}\n\nfunction connector() {\n  const mesh=body('Welded box elbow',steel,'Steel elbow');\n  const h=nodeHalf,i=h-plate;\n  block(mesh,-h,h,-h,h,-h,-i);\n  block(mesh,-h,h,-h,h,i,h);\n  for (const sign of [-1,1]) {\n    const [a,b]=sign<0?[-h,-i]:[i,h];\n    block(mesh,a,b,-h,h,-i,i);\n    block(mesh,-i,i,a,b,-i,i);\n  }\n  return mesh;\n}\n\nfunction socket(name) {\n  const mesh=body(name,steel,'Steel elbow');\n  const a=nodeHalf,b=nodeHalf+socketDepth,h=throatHalf+plate;\n  const holes=drillStations.map(d=>a+d);\n  drilledSolid(mesh,a,b,-h,-throatHalf,-throatHalf,throatHalf,holes);\n  drilledSolid(mesh,a,b,throatHalf,h,-throatHalf,throatHalf,holes);\n  block(mesh,a,b,-h,h,-h,-throatHalf);\n  block(mesh,a,b,-h,h,throatHalf,h);\n  return mesh;\n}\n\nfunction beam(name) {\n  const mesh=body(name,wood,'300 × 300 glulam');\n  const start=nodeHalf,end=start+beamLength,h=timberSide/2;\n  const holes=[...drillStations.map(d=>start+d),...drillStations.toReversed().map(d=>end-d)];\n  drilledSolid(mesh,start,end,-h,h,-h,h,holes);\n  return mesh;\n}\n\nfunction bolts(name, exploded) {\n  const mesh=body(name,fixing,'Through-bolts');\n  const h=throatHalf+plate, gap=exploded?.405:0, nutGap=exploded?.072:0;\n  for (const d of drillStations) {\n    const x=nodeHalf+d;\n    tubeWall(mesh,x,-h-.027-gap,h+.025-gap,boltRadius,12,true);\n    tubeWall(mesh,x,-h-.008-gap,-h-gap,.023,16,true);\n    tubeWall(mesh,x,-h-.020-gap,-h-.008-gap,.019,6,true);\n    tubeWall(mesh,x,h+nutGap,h+.008+nutGap,.023,16,true);\n    tubeWall(mesh,x,h+.008+nutGap,h+.020+nutGap,.019,6,true);\n  }\n  return mesh;\n}\n\nfunction seat(name, sideBearing) {\n  const mesh=body(name,'#788b9c','Bearing shim');\n  const a=nodeHalf,b=nodeHalf+socketDepth;\n  // In a knee, the horizontal socket bears on a cheek after rotation.\n  if(sideBearing) drilledSolid(mesh,a,b,.15,throatHalf,-.145,.145,drillStations.map(d=>a+d));\n  else block(mesh,a,b,-.145,.145,-throatHalf,-.15);\n  return mesh;\n}\n\n\n// Combined with the reviewed timber-joint module by the build.\nconst up = [0,0,1];\nconst add = (a,b) => a.map((n,i) => n+b[i]);\nconst subtract = (a,b) => a.map((n,i) => n-b[i]);\nconst scale = (a,k) => a.map(n => n*k);\nconst dot = (a,b) => a.reduce((sum,n,i) => sum+n*b[i],0);\nconst cross = ([x,y,z],[a,b,c]) => [y*c-z*b,z*a-x*c,x*b-y*a];\nconst unit = v => scale(v,1/Math.hypot(...v));\nconst round = n => Number(n.toFixed(6));\nconst numbered = (prefix,n) => prefix+String(n).padStart(2,'0');\n\nfunction place(mesh, origin, x, y, z) {\n  mesh.vertices=mesh.vertices.map(p=>add(origin,add(scale(x,p[0]),\n    add(scale(y,p[1]),scale(z,p[2])))).map(round));\n  return mesh;\n}\n\n// Retain both bays, the corner coordinates, and the return of the source line.\n// A seed selects legal depths at the three inside turns of each bay.\nfunction layout(seed) {\n  const route=[];\n  const roof=3.16, base=0.18, lowReturn=0.24;\n  let random=seed;\n  function next() {\n    random=(Math.imul(random,1664525)+1013904223)>>>0;\n    return random/4294967296;\n  }\n  const depths=[0.34,0.56,0.78,1.00];\n  const push=(x,y,z)=>route.push([round(x),round(y),z]);\n  function bay(sign, from) {\n    for (let i=0;i<6;i+=2) {\n      const x=from*(3-i*1.2), nextX=from*(3-(i+1)*1.2);\n      const inner=sign*depths[Math.floor(next()*depths.length)];\n      push(x,sign*3,roof);\n      push(x,inner,roof);\n      push(nextX,inner,roof);\n      push(nextX,sign*3,roof);\n    }\n  }\n  push(-3,-3,base);\n  bay(-1,-1);\n  push(3,-3,lowReturn);\n  push(3,3,lowReturn);\n  bay(1,1);\n  push(-3,3,base);\n  return assembleRoute(route);\n}\n\nfunction assembleRoute(route) {\n  const lowReturn=0.24;\n  const fittings=route.map((center,i)=>{\n    if (i===0 || i===route.length-1) {\n      return {center,arms:[{x:up,y:[0,1,0],z:[-1,0,0],offset:0}]};\n    }\n    // Put the horizontal arm first for the column knee so its local cheek\n    // always maps to a horizontal bearing surface after rotation.\n    const arms=[i-1,i+1].map(index=>({index,x:unit(subtract(route[index],center))}));\n    if (arms[0].x[2]!==0) arms.reverse();\n    const normal=cross(arms[0].x,arms[1].x);\n    return {center,arms:arms.map(arm=>({...arm,y:cross(normal,arm.x),z:normal,offset:nodeHalf}))};\n  });\n\n  const spans=route.slice(1).map((end,i)=>{\n    const start=route[i];\n    const a=fittings[i].arms.find(arm=>arm.index===i+1 || arm.index===undefined);\n    const b=fittings[i+1].arms.find(arm=>arm.index===i || arm.index===undefined);\n    const length=round(Math.hypot(...subtract(end,start))-a.offset-b.offset);\n    if(length<2*socketDepth+.075) throw Error('The sockets need more room at '+numbered('B',i+1)+'.');\n    return {id:numbered('B',i+1),start:add(start,scale(a.x,a.offset)),end:add(end,scale(b.x,b.offset)),\n      length,x:a.x,boltsA:a.y,boltsB:b.y,\n      usage:a.x[2]!==0 && Math.min(start[2],end[2])<1?'Column':\n        start[2]===lowReturn && end[2]===lowReturn?'Low side return':'Canopy'};\n  });\n  return {route,fittings,spans};\n}\n\nfunction timber(span) {\n  const mesh=body(span.id+' · Glulam '+Math.round(span.length*1000)+' mm',wood,'300 × 300 glulam');\n  const {x,boltsA,boltsB,start,length}=span;\n  const h=timberSide/2;\n  const startHoles=drillStations;\n  const endHoles=drillStations.toReversed().map(d=>length-d);\n  if (Math.abs(dot(boltsA,boltsB))>.5) {\n    drilledSolid(mesh,0,length,-h,h,-h,h,[...startHoles,...endHoles]);\n    return place(mesh,start,x,boltsA,cross(x,boltsA));\n  }\n  // Keep the drill stations at each end while indexing the bore face 90°.\n  const middle=length/2;\n  drilledSolid(mesh,0,middle,-h,h,-h,h,startHoles);\n  mesh.faces.splice(1,1);\n  place(mesh,start,x,boltsA,cross(x,boltsA));\n  const second=body('second half',wood,mesh.module);\n  drilledSolid(second,middle,length,-h,h,-h,h,endHoles);\n  second.faces.splice(3,1);\n  place(second,start,x,boltsB,cross(x,boltsB));\n  const first=mesh.vertices.length;\n  mesh.vertices.push(...second.vertices);\n  mesh.faces.push(...second.faces.map(face=>face.map(i=>i+first)));\n  return mesh;\n}\n\nfunction fittingsAt(fitting,i) {\n  const {center,arms,shared,omitCore}=fitting;\n  const id=numbered('J',i+1);\n  const objects=[];\n  if (arms.length===2 && !omitCore) {\n    const [a,b]=arms;\n    const core=connector();\n    core.name=id+(shared?' · Shared steel node':' · Welded box elbow');\n    objects.push(place(core,center,a.x,b.x,a.z));\n  }\n  for (const [index,arm] of arms.entries()) {\n    const port=id+(index===0?'A':'B');\n    const origin=add(center,scale(arm.x,arm.offset-nodeHalf));\n    objects.push(place(socket(port+' · Socket'),origin,arm.x,arm.y,arm.z));\n    objects.push(place(bolts(port+' · Through-bolts',false),origin,arm.x,arm.y,arm.z));\n    if (arm.x[2]===0) {\n      const shim=seat(port+' · Bearing shim',Math.abs(arm.y[2])===1);\n      // The shim prototype bears on local -Z or +Y. Rotate it about\n      // the beam when that face points up in the installed orientation.\n      const reverse=arm.z[2]<0 || arm.y[2]>0;\n      objects.push(place(shim,origin,arm.x,scale(arm.y,reverse?-1:1),scale(arm.z,reverse?-1:1)));\n    }\n  }\n  return objects;\n}\n\nfunction supports(layout) {\n  const last=layout.route.length-1;\n  const feet=[layout.route[0],layout.route.find(p=>p[2]===.24),layout.route.findLast(p=>p[2]===.24),layout.route[last]];\n  return feet.map(([x,y,z],i)=>{\n    const shoe=body(numbered('S',i+1)+' · Support pedestal',steel,'Support pedestal');\n    block(shoe,x-.25,x+.25,y-.25,y+.25,0,.024);\n    block(shoe,x-.115,x+.115,y-.115,y+.115,.024,z-(i===0||i===3?.024:nodeHalf));\n    if (i===0 || i===3) block(shoe,x-.165,x+.165,y-.165,y+.165,z-.024,z);\n    return shoe;\n  });\n}\n\n\nconst GUIDE = [[-3,-3,0],[-3,-3,2.4],[-3,-2.972,2.609],[-3,-2.89,2.777],[-3,-2.752,2.904],[-3,-2.558,2.989],[-3,-2.072,3.131],[-3,-1.85,3.183],[-3,-1.627,3.211],[-3,-1.402,3.215],[-3,-1.175,3.194],[-3,-0.479,3.092],[-2.986,-0.383,3.078],[-2.945,-0.315,3.068],[-2.875,-0.274,3.062],[-2.778,-0.26,3.06],[-2.76,-0.26,3.06],[-2.663,-0.274,3.062],[-2.594,-0.315,3.068],[-2.552,-0.383,3.078],[-2.538,-0.479,3.092],[-2.538,-1.175,3.194],[-2.538,-1.402,3.215],[-2.538,-1.627,3.211],[-2.538,-1.85,3.183],[-2.538,-2.072,3.131],[-2.538,-2.787,2.922],[-2.525,-2.88,2.895],[-2.483,-2.947,2.876],[-2.414,-2.987,2.864],[-2.317,-3,2.86],[-2.298,-3,2.86],[-2.202,-2.987,2.864],[-2.132,-2.947,2.876],[-2.091,-2.88,2.895],[-2.077,-2.787,2.922],[-2.077,-2.072,3.131],[-2.077,-1.85,3.183],[-2.077,-1.627,3.211],[-2.077,-1.402,3.215],[-2.077,-1.175,3.194],[-2.077,-0.479,3.092],[-2.063,-0.383,3.078],[-2.022,-0.315,3.068],[-1.952,-0.274,3.062],[-1.855,-0.26,3.06],[-1.837,-0.26,3.06],[-1.74,-0.274,3.062],[-1.671,-0.315,3.068],[-1.629,-0.383,3.078],[-1.615,-0.479,3.092],[-1.615,-1.175,3.194],[-1.615,-1.402,3.215],[-1.615,-1.627,3.211],[-1.615,-1.85,3.183],[-1.615,-2.072,3.131],[-1.615,-2.787,2.922],[-1.602,-2.88,2.895],[-1.56,-2.947,2.876],[-1.491,-2.987,2.864],[-1.394,-3,2.86],[-1.375,-3,2.86],[-1.278,-2.987,2.864],[-1.209,-2.947,2.876],[-1.168,-2.88,2.895],[-1.154,-2.787,2.922],[-1.154,-2.072,3.131],[-1.154,-1.85,3.183],[-1.154,-1.627,3.211],[-1.154,-1.402,3.215],[-1.154,-1.175,3.194],[-1.154,-0.479,3.092],[-1.14,-0.383,3.078],[-1.098,-0.315,3.068],[-1.029,-0.274,3.062],[-0.932,-0.26,3.06],[-0.914,-0.26,3.06],[-0.817,-0.274,3.062],[-0.748,-0.315,3.068],[-0.706,-0.383,3.078],[-0.692,-0.479,3.092],[-0.692,-1.175,3.194],[-0.692,-1.402,3.215],[-0.692,-1.627,3.211],[-0.692,-1.85,3.183],[-0.692,-2.072,3.131],[-0.692,-2.787,2.922],[-0.678,-2.88,2.895],[-0.637,-2.947,2.876],[-0.568,-2.987,2.864],[-0.471,-3,2.86],[-0.452,-3,2.86],[-0.355,-2.987,2.864],[-0.286,-2.947,2.876],[-0.245,-2.88,2.895],[-0.231,-2.787,2.922],[-0.231,-2.072,3.131],[-0.231,-1.85,3.183],[-0.231,-1.627,3.211],[-0.231,-1.402,3.215],[-0.231,-1.175,3.194],[-0.231,-0.479,3.092],[-0.217,-0.383,3.078],[-0.175,-0.315,3.068],[-0.106,-0.274,3.062],[-0.009,-0.26,3.06],[0.009,-0.26,3.06],[0.106,-0.274,3.062],[0.175,-0.315,3.068],[0.217,-0.383,3.078],[0.231,-0.479,3.092],[0.231,-1.175,3.194],[0.231,-1.402,3.215],[0.231,-1.627,3.211],[0.231,-1.85,3.183],[0.231,-2.072,3.131],[0.231,-2.787,2.922],[0.245,-2.88,2.895],[0.286,-2.947,2.876],[0.355,-2.987,2.864],[0.452,-3,2.86],[0.471,-3,2.86],[0.568,-2.987,2.864],[0.637,-2.947,2.876],[0.678,-2.88,2.895],[0.692,-2.787,2.922],[0.692,-2.072,3.131],[0.692,-1.85,3.183],[0.692,-1.627,3.211],[0.692,-1.402,3.215],[0.692,-1.175,3.194],[0.692,-0.479,3.092],[0.706,-0.383,3.078],[0.748,-0.315,3.068],[0.817,-0.274,3.062],[0.914,-0.26,3.06],[0.932,-0.26,3.06],[1.029,-0.274,3.062],[1.098,-0.315,3.068],[1.14,-0.383,3.078],[1.154,-0.479,3.092],[1.154,-1.175,3.194],[1.154,-1.402,3.215],[1.154,-1.627,3.211],[1.154,-1.85,3.183],[1.154,-2.072,3.131],[1.154,-2.787,2.922],[1.168,-2.88,2.895],[1.209,-2.947,2.876],[1.278,-2.987,2.864],[1.375,-3,2.86],[1.394,-3,2.86],[1.491,-2.987,2.864],[1.56,-2.947,2.876],[1.602,-2.88,2.895],[1.615,-2.787,2.922],[1.615,-2.072,3.131],[1.615,-1.85,3.183],[1.615,-1.627,3.211],[1.615,-1.402,3.215],[1.615,-1.175,3.194],[1.615,-0.479,3.092],[1.629,-0.383,3.078],[1.671,-0.315,3.068],[1.74,-0.274,3.062],[1.837,-0.26,3.06],[1.855,-0.26,3.06],[1.952,-0.274,3.062],[2.022,-0.315,3.068],[2.063,-0.383,3.078],[2.077,-0.479,3.092],[2.077,-1.175,3.194],[2.077,-1.402,3.215],[2.077,-1.627,3.211],[2.077,-1.85,3.183],[2.077,-2.072,3.131],[2.077,-2.787,2.922],[2.091,-2.88,2.895],[2.132,-2.947,2.876],[2.202,-2.987,2.864],[2.298,-3,2.86],[2.317,-3,2.86],[2.414,-2.987,2.864],[2.483,-2.947,2.876],[2.525,-2.88,2.895],[2.538,-2.787,2.922],[2.538,-2.072,3.131],[2.538,-1.85,3.183],[2.538,-1.627,3.211],[2.538,-1.402,3.215],[2.538,-1.175,3.194],[2.538,-0.479,3.092],[2.552,-0.383,3.078],[2.594,-0.315,3.068],[2.663,-0.274,3.062],[2.76,-0.26,3.06],[2.778,-0.26,3.06],[2.875,-0.274,3.062],[2.945,-0.315,3.068],[2.986,-0.383,3.078],[3,-0.479,3.092],[3,-1.175,3.194],[3,-1.402,3.215],[3,-1.627,3.211],[3,-1.85,3.183],[3,-2.072,3.131],[3,-2.558,2.989],[3,-2.752,2.904],[3,-2.89,2.777],[3,-2.972,2.609],[3,-3,2.4],[3,-3,0.58],[3,-2.971,0.379],[3,-2.885,0.235],[3,-2.741,0.149],[3,-2.54,0.12],[3,2.54,0.12],[3,2.741,0.149],[3,2.885,0.235],[3,2.971,0.379],[3,3,0.58],[3,3,2.4],[3,2.972,2.609],[3,2.89,2.777],[3,2.752,2.904],[3,2.558,2.989],[3,2.072,3.131],[3,1.85,3.183],[3,1.627,3.211],[3,1.402,3.215],[3,1.175,3.194],[3,0.479,3.092],[2.986,0.383,3.078],[2.945,0.315,3.068],[2.875,0.274,3.062],[2.778,0.26,3.06],[2.76,0.26,3.06],[2.663,0.274,3.062],[2.594,0.315,3.068],[2.552,0.383,3.078],[2.538,0.479,3.092],[2.538,1.175,3.194],[2.538,1.402,3.215],[2.538,1.627,3.211],[2.538,1.85,3.183],[2.538,2.072,3.131],[2.538,2.787,2.922],[2.525,2.88,2.895],[2.483,2.947,2.876],[2.414,2.987,2.864],[2.317,3,2.86],[2.298,3,2.86],[2.202,2.987,2.864],[2.132,2.947,2.876],[2.091,2.88,2.895],[2.077,2.787,2.922],[2.077,2.072,3.131],[2.077,1.85,3.183],[2.077,1.627,3.211],[2.077,1.402,3.215],[2.077,1.175,3.194],[2.077,0.479,3.092],[2.063,0.383,3.078],[2.022,0.315,3.068],[1.952,0.274,3.062],[1.855,0.26,3.06],[1.837,0.26,3.06],[1.74,0.274,3.062],[1.671,0.315,3.068],[1.629,0.383,3.078],[1.615,0.479,3.092],[1.615,1.175,3.194],[1.615,1.402,3.215],[1.615,1.627,3.211],[1.615,1.85,3.183],[1.615,2.072,3.131],[1.615,2.787,2.922],[1.602,2.88,2.895],[1.56,2.947,2.876],[1.491,2.987,2.864],[1.394,3,2.86],[1.375,3,2.86],[1.278,2.987,2.864],[1.209,2.947,2.876],[1.168,2.88,2.895],[1.154,2.787,2.922],[1.154,2.072,3.131],[1.154,1.85,3.183],[1.154,1.627,3.211],[1.154,1.402,3.215],[1.154,1.175,3.194],[1.154,0.479,3.092],[1.14,0.383,3.078],[1.098,0.315,3.068],[1.029,0.274,3.062],[0.932,0.26,3.06],[0.914,0.26,3.06],[0.817,0.274,3.062],[0.748,0.315,3.068],[0.706,0.383,3.078],[0.692,0.479,3.092],[0.692,1.175,3.194],[0.692,1.402,3.215],[0.692,1.627,3.211],[0.692,1.85,3.183],[0.692,2.072,3.131],[0.692,2.787,2.922],[0.678,2.88,2.895],[0.637,2.947,2.876],[0.568,2.987,2.864],[0.471,3,2.86],[0.452,3,2.86],[0.355,2.987,2.864],[0.286,2.947,2.876],[0.245,2.88,2.895],[0.231,2.787,2.922],[0.231,2.072,3.131],[0.231,1.85,3.183],[0.231,1.627,3.211],[0.231,1.402,3.215],[0.231,1.175,3.194],[0.231,0.479,3.092],[0.217,0.383,3.078],[0.175,0.315,3.068],[0.106,0.274,3.062],[0.009,0.26,3.06],[-0.009,0.26,3.06],[-0.106,0.274,3.062],[-0.175,0.315,3.068],[-0.217,0.383,3.078],[-0.231,0.479,3.092],[-0.231,1.175,3.194],[-0.231,1.402,3.215],[-0.231,1.627,3.211],[-0.231,1.85,3.183],[-0.231,2.072,3.131],[-0.231,2.787,2.922],[-0.245,2.88,2.895],[-0.286,2.947,2.876],[-0.355,2.987,2.864],[-0.452,3,2.86],[-0.471,3,2.86],[-0.568,2.987,2.864],[-0.637,2.947,2.876],[-0.678,2.88,2.895],[-0.692,2.787,2.922],[-0.692,2.072,3.131],[-0.692,1.85,3.183],[-0.692,1.627,3.211],[-0.692,1.402,3.215],[-0.692,1.175,3.194],[-0.692,0.479,3.092],[-0.706,0.383,3.078],[-0.748,0.315,3.068],[-0.817,0.274,3.062],[-0.914,0.26,3.06],[-0.932,0.26,3.06],[-1.029,0.274,3.062],[-1.098,0.315,3.068],[-1.14,0.383,3.078],[-1.154,0.479,3.092],[-1.154,1.175,3.194],[-1.154,1.402,3.215],[-1.154,1.627,3.211],[-1.154,1.85,3.183],[-1.154,2.072,3.131],[-1.154,2.787,2.922],[-1.168,2.88,2.895],[-1.209,2.947,2.876],[-1.278,2.987,2.864],[-1.375,3,2.86],[-1.394,3,2.86],[-1.491,2.987,2.864],[-1.56,2.947,2.876],[-1.602,2.88,2.895],[-1.615,2.787,2.922],[-1.615,2.072,3.131],[-1.615,1.85,3.183],[-1.615,1.627,3.211],[-1.615,1.402,3.215],[-1.615,1.175,3.194],[-1.615,0.479,3.092],[-1.629,0.383,3.078],[-1.671,0.315,3.068],[-1.74,0.274,3.062],[-1.837,0.26,3.06],[-1.855,0.26,3.06],[-1.952,0.274,3.062],[-2.022,0.315,3.068],[-2.063,0.383,3.078],[-2.077,0.479,3.092],[-2.077,1.175,3.194],[-2.077,1.402,3.215],[-2.077,1.627,3.211],[-2.077,1.85,3.183],[-2.077,2.072,3.131],[-2.077,2.787,2.922],[-2.091,2.88,2.895],[-2.132,2.947,2.876],[-2.202,2.987,2.864],[-2.298,3,2.86],[-2.317,3,2.86],[-2.414,2.987,2.864],[-2.483,2.947,2.876],[-2.525,2.88,2.895],[-2.538,2.787,2.922],[-2.538,2.072,3.131],[-2.538,1.85,3.183],[-2.538,1.627,3.211],[-2.538,1.402,3.215],[-2.538,1.175,3.194],[-2.538,0.479,3.092],[-2.552,0.383,3.078],[-2.594,0.315,3.068],[-2.663,0.274,3.062],[-2.76,0.26,3.06],[-2.778,0.26,3.06],[-2.875,0.274,3.062],[-2.945,0.315,3.068],[-2.986,0.383,3.078],[-3,0.479,3.092],[-3,1.175,3.194],[-3,1.402,3.215],[-3,1.627,3.211],[-3,1.85,3.183],[-3,2.072,3.131],[-3,2.558,2.989],[-3,2.752,2.904],[-3,2.89,2.777],[-3,2.972,2.609],[-3,3,2.4],[-3,3,0]];\nfunction guideMesh() {\n  const mesh=body('Published serpentine guide','#2244c8','Guide');\n  for(let i=1;i<GUIDE.length;i++){\n    const a=GUIDE[i-1], b=GUIDE[i];\n    const span=subtract(b,a), length=Math.hypot(...span);\n    if(length<1e-6) continue;\n    const x=unit(span),y=unit(cross(x,Math.abs(x[2])>.9?[1,0,0]:up)),z=cross(x,y);\n    const segment=body('segment',mesh.color,mesh.module);\n    block(segment,0,length,-.012,.012,-.012,.012);\n    place(segment,a,x,y,z);\n    const base=mesh.vertices.length;\n    mesh.vertices.push(...segment.vertices);\n    mesh.faces.push(...segment.faces.map(face=>face.map(v=>v+base)));\n  }\n  return mesh;\n}\n\n// The build binds GUIDE to the published 3D centreline.\nconst step=1;\nconst low=2.76;\nconst high=3.76;\nconst directions=[[1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]];\nconst key=p=>p.join(',');\nconst cell=([x,y,z])=>(x*4+y)*2+z;\nconst bit=p=>1n<<BigInt(cell(p));\nconst same=(a,b)=>a.every((v,i)=>v===b[i]);\nconst axisOf=d=>d.findIndex(n=>n!==0);\nconst world=([x,y,z],sign)=>[sign*(3-x*step),sign*(3-y*step),z?high:low].map(round);\nconst heading=(d,sign)=>[-sign*d[0],-sign*d[1],d[2]].map(round);\nconst toward=(a,b)=>unit(subtract(b,a)).map(round);\nconst start=[0,0,0], goal=[6,0,0];\n\nfunction field(sign){\n  const segments=[];\n  for(let i=1;i<GUIDE.length;i++){\n    const a=GUIDE[i-1],b=GUIDE[i];\n    if(a[2]<2.7 || b[2]<2.7 || sign*a[1]<0 || sign*b[1]<0) continue;\n    const d=subtract(b,a),l2=dot(d,d);\n    if(l2>1e-10) segments.push({a,d,l2,length:Math.sqrt(l2)});\n  }\n  return segments;\n}\n\nfunction steer(a,b,direction,segments){\n  const [ax,ay,az]=a, [bx,by,bz]=b, [hx,hy,hz]=direction;\n  let score=0;\n  for(const t of [.2,.5,.8]){\n    const px=ax+t*(bx-ax), py=ay+t*(by-ay), pz=az+t*(bz-az);\n    let closest=Infinity,alignment=0;\n    for(const {a:[x,y,z],d:[dx,dy,dz],l2,length} of segments){\n      const ox=px-x, oy=py-y, oz=pz-z;\n      const u=Math.max(0,Math.min(1,(ox*dx+oy*dy+oz*dz)/l2));\n      const ex=ox-u*dx, ey=oy-u*dy, ez=oz-u*dz;\n      const align=Math.abs(hx*dx+hy*dy+hz*dz)/(length||Math.sqrt(l2));\n      const cost=Math.sqrt(ex*ex+ey*ey+ez*ez)-.18*align;\n      if(cost<closest){closest=cost;alignment=align;}\n    }\n    score+=1.22-.67*closest+.36*alignment;\n  }\n  return score/3;\n}\n\nfunction bayOptions(sign,seed,other,scores=new Map(),ends={start,goal},volume){\n  const guide=field(sign), cache=new Map();\n  const {start:origin,goal:target}=ends;\n  const toWorld=volume?.world||world;\n  const encode=volume?.cell||cell;\n  const mask=p=>1n<<BigInt(encode(p));\n  const limits=volume?.limits||[6,3,1];\n  let random=(seed^Math.imul(sign,0x7f4a7c15))>>>0;\n  function next(){\n    random=(Math.imul(random,1664525)+1013904223)>>>0;\n    return random/4294967296;\n  }\n  function candidates(p){\n    const id=encode(p);\n    if(cache.has(id)) return cache.get(id);\n    const moves=[];\n    for(const d of directions){\n      const axis=axisOf(d), direction=heading(d,sign);\n      let occupied=0n;\n      for(let length=1;length<=3;length++){\n        const end=add(p,scale(d,length));\n        if(end.some((n,i)=>n<0 || n>limits[i])) break;\n        if(same(end,target) && axis===2) break;\n        const a=toWorld(p,sign),b=toWorld(end,sign);\n        const contact=other?.get(key(b));\n        if(contact==='span' || (contact && contact.some(v=>dot(v,direction)<-.99))) break;\n        occupied|=mask(end);\n        const scoreKey=id*128+encode(end);\n        if(!scores.has(scoreKey)) scores.set(scoreKey,steer(a,b,direction,guide)*length);\n        const score=scores.get(scoreKey);\n        moves.push({end,axis,direction,occupied,score,contact,length});\n        // A shared location is a joint, so the current piece must end here.\n        if(contact || same(end,target)) break;\n      }\n    }\n    cache.set(id,moves);\n    return moves;\n  }\n\n  let active=[{tip:origin,axis:2,used:mask(origin),path:[origin],score:0,lifts:0,meets:0,middle:0,crest:false}];\n  const finished=[];\n  for(let i=0;i<25 && active.length;i++){\n    const fresh=[];\n    for(const state of active){\n      const atJoint=other?.get(key(toWorld(state.tip,sign)));\n      for(const move of candidates(state.tip)){\n        if(move.axis===state.axis || (state.used&move.occupied)!==0n) continue;\n        if(atJoint && atJoint.some(d=>dot(d,move.direction)>.99)) continue;\n        const {end,axis,occupied}=move;\n        const lifts=state.lifts+Number(axis===2);\n        if(lifts>(volume?10:6)) continue;\n        const middle=state.middle+Number(end[1]===3);\n        const meets=state.meets+Number(Boolean(move.contact));\n        const ends=same(end,target);\n        const crest=state.crest||end[2]===2;\n        if(ends && (lifts<2 || (volume&&!crest) || (other?meets<2:middle<4))) continue;\n        const gain=move.score-.56+.3*Number(axis===2)+.25*Number(axis===1)+\n          (volume?.bonus?.(state,move)||0)+\n          .48*Number(end[1]===3)+2.9*Number(Boolean(move.contact))+.58*(next()-.5);\n        const result={tip:end,axis,used:state.used|occupied,path:[...state.path,end],\n          score:state.score+gain,lifts,meets,middle,crest};\n        if(ends) finished.push(result);\n        else fresh.push(result);\n      }\n    }\n    fresh.sort((a,b)=>b.score-a.score);\n    const groups=new Map();\n    active=fresh.filter(s=>{\n      const id=encode(s.tip)*3+s.axis, count=groups.get(id)||0;\n      if(count>=5) return false;\n      groups.set(id,count+1);\n      return true;\n    }).slice(0,104);\n  }\n  const paths=new Map();\n  for(const s of finished.sort((a,b)=>b.score-a.score)){\n    const id=s.path.join('|');\n    if(!paths.has(id)) paths.set(id,{...s,path:s.path.map(p=>toWorld(p,sign))});\n    if(paths.size===(other?1:volume?7:10)) break;\n  }\n  return [...paths.values()];\n}\n\nfunction occupiedCenterline(path){\n  const index=new Map();\n  for(let i=1;i<path.length;i++){\n    const a=path[i-1],b=path[i],length=Math.round(Math.hypot(...subtract(b,a)));\n    for(let j=1;j<length;j++) index.set(key(add(a,scale(subtract(b,a),j/length)).map(round)),'span');\n  }\n  path.forEach((p,i)=>{\n    if(p[1]===0) index.set(key(p),[path[i-1],path[i+1]].filter(Boolean).map(n=>toward(p,n)));\n  });\n  return index;\n}\n\nfunction sharedLayout(seed){\n  let selected=null;\n  const scores=new Map();\n  for(const first of bayOptions(-1,seed)){\n    const [second]=bayOptions(1,seed,occupiedCenterline(first.path),scores);\n    if(!second) continue;\n    const score=first.score+second.score;\n    if(!selected || score>selected.score) selected={first,second,score};\n  }\n  if(!selected) throw Error('No connected canopy found for this seed.');\n  const route=[[-3,-3,.18],...selected.first.path,[3,-3,.24],[3,3,.24],\n    ...selected.second.path,[-3,3,.18]];\n  return assembleSharedRoute(route);\n}\n\nfunction assembleSharedRoute(route){\n  const model=assembleRoute(route);\n  const seen=new Map(), shared=[];\n  for(const [i,p] of route.entries()){\n    const id=key(p);\n    if(seen.has(id)){\n      const first=seen.get(id);\n      const arms=[...model.fittings[first].arms,...model.fittings[i].arms];\n      if(new Set(arms.map(arm=>key(arm.x))).size!==4) throw Error('Shared node needs four distinct faces.');\n      model.fittings[first].shared=true;\n      model.fittings[i].omitCore=true;\n      shared.push({first,second:i,center:p});\n    }else seen.set(id,i);\n  }\n  return {...model,shared};\n}\n\nfunction sharedScene(model){\n  return [...model.spans.map(timber),...model.fittings.flatMap(fittingsAt),...supports(model)];\n}\n\nconst cornerAnchors=[[-3,-3,.18],[3,-3,.24],[3,3,.24],[-3,3,.18]];\nconst turningLevels=[1.28,1.51,1.73];\n\nfunction supportGuide(foot){\n  const segments=[];\n  for(let i=1;i<GUIDE.length;i++){\n    const a=GUIDE[i-1], b=GUIDE[i];\n    if(a[2]>low || b[2]>low || Math.hypot(a[0]-foot[0],a[1]-foot[1])>.65 ||\n      Math.hypot(b[0]-foot[0],b[1]-foot[1])>.65) continue;\n    const d=subtract(b,a), l2=dot(d,d);\n    if(l2>1e-9) segments.push({a,d,l2});\n  }\n  return segments;\n}\n\nfunction supportOptions(foot,seed){\n  const [x,y]=foot, sx=-Math.sign(x), sy=-Math.sign(y);\n  const guide=supportGuide(foot);\n  let rng=seed>>>0;\n  const noise=()=>{\n    rng=(Math.imul(rng,1664525)+1013904223)>>>0;\n    return rng/4294967296-.5;\n  };\n  const options=[];\n  for(const z of turningLevels){\n    const shoulder=[x,y,z];\n    const firstMoves=[[sx,0,0],[0,sy,0]];\n    for(const direction of firstMoves){\n      const elbow=add(shoulder,direction);\n      // An optional second turn wraps the support in plan as well as in section.\n      const turn=direction[0]?[0,sy,0]:[sx,0,0];\n      for(const moves of [[elbow],[elbow,add(elbow,turn)]]){\n        const last=moves.at(-1);\n        const landing=[last[0],last[1],low];\n        const path=[foot,shoulder,...moves,landing];\n        const follow=path.slice(1).reduce((total,p,i)=>{\n          const a=path[i];\n          return total+steer(a,p,toward(a,p),guide);\n        },0)/(path.length-1);\n        const score=follow-.22*(moves.length-1)+1.5*noise();\n        options.push({path,score});\n      }\n    }\n  }\n  return options.sort((a,b)=>b.score-a.score);\n}\n\nfunction localPoint(p,sign){\n  return [3-sign*p[0],3-sign*p[1],0];\n}\n\nfunction spatialLayout(seed){\n  const variants=cornerAnchors.map((foot,i)=>supportOptions(foot,seed+2179*i));\n  let selected=null;\n  for(let attempt=0;attempt<3 && !selected;attempt++){\n    const legs=variants.map((options,i)=>options[(attempt*(i+1))%options.length].path);\n    const landings=legs.map(leg=>leg.at(-1));\n    const endpoints=sign=>{\n      const a=sign<0?0:2;\n      return {start:localPoint(landings[a],sign),goal:localPoint(landings[a+1],sign)};\n    };\n    const scores=new Map();\n    for(const first of bayOptions(-1,seed,undefined,new Map(),endpoints(-1))){\n      const [second]=bayOptions(1,seed,occupiedCenterline(first.path),scores,endpoints(1));\n      if(!second) continue;\n      const score=first.score+second.score;\n      if(!selected || score>selected.score) selected={legs,first,second,score};\n    }\n  }\n  if(!selected) throw Error('No shared canopy could reach all four grown supports for this seed.');\n  const {legs,first,second}=selected;\n  const route=[...legs[0].slice(0,-1),...first.path,...legs[1].toReversed().slice(1),\n    ...legs[2].slice(0,-1),...second.path,...legs[3].toReversed().slice(1)];\n  const model=assembleSharedRoute(route);\n  const supportKeys=new Set(legs.flatMap(leg=>leg.slice(1).map((p,i)=>[key(leg[i]),key(p)].sort().join('|'))));\n  const supportSpans=[];\n  for(const [i,span] of model.spans.entries()){\n    const edge=[key(route[i]),key(route[i+1])].sort().join('|');\n    if(supportKeys.has(edge)){\n      span.usage='Stepped support';\n      supportSpans.push(i+1);\n    }\n  }\n  return {...model,legs,supportSpans};\n}\n\n// Keep the original guide, but allow each front into the other's half.\nconst volume={\n  limits:[6,4,2],\n  cell:([x,y,z])=>(x*5+y)*3+z,\n  world:([x,y,z],sign)=>[sign*(3-x),sign*(3-y),round(low+z)],\n  bonus:(state,move)=>{\n    const [,y,z]=move.end;\n    const heightReward=z===2 ? .95+.12*Number(y>=2) : z===1?.34:0;\n    return heightReward + .32*Number(y===4) + .32*Number(move.axis===2) -\n      .12*Number(move.length>2);\n  },\n};\n\nfunction coveredPoints(path){\n  const index=new Map();\n  for(let i=1;i<path.length;i++){\n    const a=path[i-1],b=path[i],delta=subtract(b,a);\n    const count=Math.round(Math.hypot(...delta));\n    for(let j=1;j<count;j++) index.set(key(add(a,scale(delta,j/count)).map(round)),'span');\n  }\n  path.forEach((p,i)=>{\n    index.set(key(p),i===0||i===path.length-1?'span':\n      [toward(p,path[i-1]),toward(p,path[i+1])]);\n  });\n  return index;\n}\n\nfunction volumetricLayout(seed){\n  const variants=cornerAnchors.map((foot,i)=>supportOptions(foot,seed+2179*i));\n  const northScores=new Map(), southScores=new Map();\n  let selected=null;\n  for(let attempt=0;attempt<4 && !selected;attempt++){\n    const legs=variants.map((options,i)=>{\n      const path=options[(attempt*(i+1))%options.length].path.map(p=>[...p]);\n      // A longer last riser can land at the middle tier when the guide and\n      // continuation permit it. The two front endpoints are still fixed.\n      if((seed+i*7+attempt)%5<2) path.at(-1)[2]=round(low+1);\n      return path;\n    });\n    const landings=legs.map(leg=>leg.at(-1));\n    const toLocal=(p,sign)=>[3-sign*p[0],3-sign*p[1],Math.round(p[2]-low)];\n    const ends=sign=>{\n      const i=sign<0?0:2;\n      return {start:toLocal(landings[i],sign),goal:toLocal(landings[i+1],sign)};\n    };\n    const south=bayOptions(-1,seed,undefined,southScores,ends(-1),volume);\n    for(const first of south){\n      const blocked=coveredPoints(first.path);\n      if(landings.slice(2).some(p=>blocked.has(key(p)))) continue;\n      const [second]=bayOptions(1,seed,blocked,northScores,ends(1),volume);\n      if(!second || second.meets<3) continue;\n      const contacts=second.path.filter(p=>Array.isArray(blocked.get(key(p))));\n      const spread=new Set(contacts.map(p=>p[1])).size;\n      const heights=new Set(contacts.map(p=>p[2])).size;\n      if(spread<2 || heights<2) continue;\n      const score=first.score+second.score+spread+heights;\n      if(!selected || score>selected.score) selected={legs,first,second,score};\n    }\n  }\n  if(!selected) throw Error('No volumetric canopy could link the four supports for this seed.');\n  const {legs,first,second}=selected;\n  const route=[...legs[0].slice(0,-1),...first.path,...legs[1].toReversed().slice(1),\n    ...legs[2].slice(0,-1),...second.path,...legs[3].toReversed().slice(1)];\n  const model=assembleSharedRoute(route);\n  const supportEdges=new Set(legs.flatMap(leg=>leg.slice(1).map((p,i)=>[key(leg[i]),key(p)].sort().join('|'))));\n  const supportSpans=[];\n  for(const [i,span] of model.spans.entries()){\n    if(!supportEdges.has([key(route[i]),key(route[i+1])].sort().join('|'))) continue;\n    span.usage='Stepped support';\n    supportSpans.push(i+1);\n  }\n  return {...model,legs,supportSpans};\n}\n\n// Each fork grows from an existing support joint and stops at a free canopy port.\nconst branchAxes=[[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1]];\nconst edgeId=(a,b)=>[key(a),key(b)].sort().join('|');\nconst branchLength=(a,b)=>Math.hypot(...subtract(b,a));\nconst branchAxis=(a,b)=>axisOf(toward(a,b));\n\nfunction reserveEdge(a,b){\n  return {a,b,box:a.map((v,i)=>[Math.min(v,b[i])-.196,Math.max(v,b[i])+.196])};\n}\n\nfunction intersects(a,b){\n  // Orthogonal sockets can touch a common box on different faces.\n  if([a.a,a.b].some(p=>[b.a,b.b].some(q=>same(p,q)))) return false;\n  return a.box.every(([lo,hi],i)=>Math.min(hi,b.box[i][1])-Math.max(lo,b.box[i][0])>1e-6);\n}\n\nfunction directionAvailable(ports,a,b){\n  const d=toward(a,b);\n  return !(ports.get(key(a))||[]).some(n=>same(n,d));\n}\n\nfunction growForks(model,seed){\n  const {route,legs}=model;\n  const supportEnds=new Set(legs.map(leg=>key(leg.at(-1))));\n  const trunkNodes=new Set(legs.flatMap(leg=>leg.slice(0,-1).map(key)));\n  const targets=new Map();\n  route.forEach(p=>{\n    if(p[2]>=low && !trunkNodes.has(key(p)) && !supportEnds.has(key(p))) targets.set(key(p),p);\n  });\n  const ports=new Map();\n  function occupy(a,b){\n    const id=key(a), d=toward(a,b);\n    if(!ports.has(id)) ports.set(id,[]);\n    ports.get(id).push(d);\n  }\n  const baseEdges=route.slice(1).map((b,i)=>{\n    const a=route[i];\n    occupy(a,b);\n    occupy(b,a);\n    return reserveEdge(a,b);\n  });\n  const allOptions=legs.map((leg,owner)=>{\n    const [fx,fy]=leg[0];\n    const options=new Map();\n    const guides=field(Math.sign(fy));\n    const guidance=new Map();\n    function scoreEdge(a,b){\n      const id=edgeId(a,b);\n      if(!guidance.has(id)) guidance.set(id,steer(a,b,toward(a,b),guides));\n      return guidance.get(id);\n    }\n    let states=leg.slice(1,-1).map(point=>({path:[point],edges:[],score:0,axis:-1}));\n    for(let depth=0;depth<4 && states.length;depth++){\n      const next=[];\n      for(const state of states){\n        const a=state.path.at(-1);\n        for(const direction of branchAxes){\n          const axis=axisOf(direction);\n          if(state.axis===axis || !directionAvailable(ports,a,add(a,direction))) continue;\n          const distances=axis===2?[low,high,round(low+2)].filter(z=>z-a[2]>=.995).map(z=>round(z-a[2])):[1,2];\n          for(const distance of distances){\n            const b=add(a,scale(direction,distance)).map(round), id=key(b), contact=targets.get(id);\n            if(Math.abs(b[0])>3 || Math.abs(b[1])>3 ||\n              Math.sign(fx)*b[0]<-1 || Math.sign(fy)*b[1]<-1 ||\n              Math.abs(b[0]-fx)+Math.abs(b[1]-fy)>5 || state.path.some(p=>same(p,b))) continue;\n            if((ports.has(id)&&!contact) || (contact&&!directionAvailable(ports,b,a))) continue;\n            const edge=reserveEdge(a,b);\n            if([...baseEdges,...state.edges].some(other=>intersects(edge,other))) continue;\n            const cost=.43*distance+.18*(depth+1);\n            const score=state.score+scoreEdge(a,b)*.44-cost;\n            const result={owner,path:[...state.path,b],edges:[...state.edges,edge],axis,score};\n            if(contact){\n              const separated=branchLength(b,leg.at(-1));\n              if(separated<.99) continue;\n              const rank=score+.50*Math.min(separated,3)+.14*(b[2]-low)+\n                .12*Math.sin(seed*7+owner*37+b[0]*15+b[1]*11+b[2]*8);\n              const signature=[key(result.path[0]),id,key(toward(result.path[0],result.path[1]))].join('|');\n              if(!options.has(signature)||options.get(signature).score<rank) options.set(signature,{...result,score:rank});\n            }else next.push(result);\n          }\n        }\n      }\n      states=next.sort((a,b)=>b.score-a.score).slice(0,48);\n    }\n    return [...options.values()].sort((a,b)=>b.score-a.score).slice(0,20);\n  });\n\n  function compatible(a,b){\n    const root=a.path[0], otherRoot=b.path[0];\n    if(same(a.path.at(-1),b.path.at(-1))) return false;\n    if(same(root,otherRoot)&&same(toward(root,a.path[1]),toward(root,b.path[1]))) return false;\n    if(a.path.slice(1,-1).some(p=>b.path.some(q=>same(p,q))) ||\n      b.path.slice(1,-1).some(p=>same(p,root))) return false;\n    return !a.edges.some(edge=>b.edges.some(other=>intersects(edge,other)));\n  }\n  const chosen=[];\n  function cover(owner){\n    if(owner===legs.length) return true;\n    for(const option of allOptions[owner]){\n      if(!chosen.every(other=>compatible(option,other))) continue;\n      chosen.push(option);\n      if(cover(owner+1)) return true;\n      chosen.pop();\n    }\n    return false;\n  }\n  if(!cover(0)) throw Error('This seed did not find two separate canopy connections per support.');\n  for(const options of allOptions){\n    const extra=options.find(option=>chosen.every(other=>compatible(option,other)));\n    if(extra) chosen.push(extra);\n  }\n  return chosen;\n}\n\nfunction addBranches(model,seed){\n  const branches=growForks(model,seed);\n  const fittings=[...model.fittings];\n  const junctions=new Set();\n  const spans=[];\n  for(const branch of branches){\n    const {path,owner}=branch;\n    const nodes=path.map((p,i)=>{\n      const ends=i===0||i===path.length-1;\n      const arms=[path[i-1],path[i+1]].filter(Boolean).map(next=>{\n        const x=toward(p,next);\n        const y=x[2]===0?up:[0,1,0];\n        return {x,y,z:cross(x,y),offset:nodeHalf,target:key(next)};\n      });\n      // The original core remains at each fork and canopy contact.\n      if(ends) junctions.add(key(p));\n      const index=fittings.length;\n      const fitting={center:p,arms,omitCore:ends};\n      fittings.push(fitting);\n      return {fitting,index};\n    });\n    for(let i=1;i<path.length;i++){\n      const a=nodes[i-1],b=nodes[i];\n      const aArm=a.fitting.arms.find(arm=>arm.target===key(path[i]));\n      const bArm=b.fitting.arms.find(arm=>arm.target===key(path[i-1]));\n      const x=aArm.x;\n      spans.push({id:numbered('R',spans.length+1),usage:'Branch from support '+(owner+1),\n        start:add(path[i-1],scale(x,nodeHalf)),end:add(path[i],scale(x,-nodeHalf)),\n        length:round(branchLength(path[i-1],path[i])-2*nodeHalf),x,boltsA:aArm.y,boltsB:bArm.y,\n        ports:[{index:a.index,arm:aArm},{index:b.index,arm:bArm}]});\n    }\n  }\n  return {...model,branches:branches.map(({path,owner})=>({path,owner})),branchSpans:spans,\n    branchFittings:fittings.slice(model.fittings.length),branchJunctions:[...junctions]};\n}\n\nfunction branchingLayout(seed){\n  return addBranches(volumetricLayout(seed),seed);\n}\n\nfunction run(inputs,controls={}){\n  const {seed=12,showGuide=false} = {...controls, ...inputs[0]};\n  if(!Number.isInteger(seed)||seed<1||seed>9999) throw Error('Use a whole-number seed from 1 to 9999.');\n  const model=branchingLayout(seed);\n  const objects=[...sharedScene(model),...model.branchSpans.map(timber),\n    ...model.branchFittings.flatMap((fitting,i)=>fittingsAt(fitting,i+model.fittings.length)),\n    ...(showGuide?[guideMesh()]:[])];\n  for(const object of objects){\n    const vertices=[], indexed=new Map();\n    const remap=object.vertices.map(vertex=>{\n      const id=key(vertex);\n      if(!indexed.has(id)){indexed.set(id,vertices.length);vertices.push(vertex);}\n      return indexed.get(id);\n    });\n    object.vertices=vertices;\n    object.faces=object.faces.map(face=>face.map(index=>remap[index]));\n  }\n  return {version:2,units:'metres',objects};\n}\n"},"files":{"value":[{"task":"growth-settings","output":"result"}]}}},{"schema_version":3,"id":"viewer","operation":"scene.view","objective":"Inspect the pavilion, the four bases, the low side return and the elbow connections in 3D.","inputs":{"scene":{"task":"pavilion","output":"result"},"azimuth":{"value":39},"elevation":{"value":23},"resolution":{"value":1280}}},{"schema_version":3,"id":"drawing-layout","operation":"code.javascript","objective":"Calculate dimensions and lay out 10 vector sheets.","inputs":{"code":{"value":"// A concept model in metres. Connection dimensions are illustrative.\nconst timberSide = 0.300;\nconst beamLength = 1.450;\nconst nodeHalf = 0.170;\nconst socketDepth = 0.290;\nconst plate = 0.012;\nconst throatHalf = 0.153;\nconst boreRadius = 0.012;\nconst boltRadius = 0.010;\nconst drillStations = [0.125, 0.225];\nconst wood = '#b77b47';\nconst steel = '#3a4857';\nconst fixing = '#98a9bc';\nconst outwardFaces = [[3,2,1,0],[4,5,6,7],[0,1,5,4],[1,2,6,5],[2,3,7,6],[3,0,4,7]];\n\nfunction body(name, color, module) {\n  return {type:'MESH',name,color,module,vertices:[],faces:[]};\n}\n\nfunction block(mesh, x0, x1, y0, y1, z0, z1) {\n  const first = mesh.vertices.length;\n  for (const z of [z0,z1]) {\n    mesh.vertices.push([x0,y0,z],[x1,y0,z],[x1,y1,z],[x0,y1,z]);\n  }\n  for (const face of outwardFaces) mesh.faces.push(face.map(i => i+first));\n}\n\nconst segments = 8;\nconst angles = Array.from({length:segments},(_,i) => i*Math.PI*2/segments);\n\n// A rectangular bar or plate with round through-bores along local Y.\n// The side faces are cut into small patches around each bore.\nfunction drilledSolid(mesh, x0, x1, y0, y1, z0, z1, holes) {\n  const first = mesh.vertices.length;\n  const corner = [[x0,z0],[x1,z0],[x1,z1],[x0,z1]];\n  for (const y of [y0,y1]) for (const [x,z] of corner) mesh.vertices.push([x,y,z]);\n  for (let i=0;i<4;i++) {\n    const a=first+i,b=first+(i+1)%4;\n    mesh.faces.push([a,a+4,b+4,b]);\n  }\n  const halfPatch=0.022;\n  for (const y of [y0,y1]) {\n    function flat(points) {\n      const start=mesh.vertices.length;\n      mesh.vertices.push(...points.map(([x,z])=>[x,y,z]));\n      const face=points.map((_,i)=>start+i);\n      mesh.faces.push(y===y0?face:face.reverse());\n    }\n    function rect(a,b,c,d) {\n      if (b>a && d>c) flat([[a,c],[b,c],[b,d],[a,d]]);\n    }\n    rect(x0,x1,z0,-halfPatch);\n    rect(x0,x1,halfPatch,z1);\n    let last=x0;\n    for (const x of holes) {\n      rect(last,x-halfPatch,-halfPatch,halfPatch);\n      last=x+halfPatch;\n      const perimeter=angles.map(angle=>{\n        const u=Math.cos(angle),v=Math.sin(angle),extent=halfPatch/Math.max(Math.abs(u),Math.abs(v));\n        return [[x+u*extent,v*extent],[x+u*boreRadius,v*boreRadius]];\n      });\n      for (const [i,[a,b]] of perimeter.entries()) {\n        const [c,d]=perimeter[(i+1)%segments];\n        flat([a,c,d,b]);\n      }\n    }\n    rect(last,x1,-halfPatch,halfPatch);\n  }\n  for (const x of holes) tubeWall(mesh,x,y0,y1,boreRadius,segments,false);\n}\n\nfunction tubeWall(mesh,x,y0,y1,radius,sides,capped) {\n  const start=mesh.vertices.length;\n  for (const y of [y0,y1]) for (let i=0;i<sides;i++) {\n    const a=2*Math.PI*i/sides;\n    mesh.vertices.push([x+Math.cos(a)*radius,y,Math.sin(a)*radius]);\n  }\n  for (let i=0;i<sides;i++) {\n    const a=start+i,b=start+(i+1)%sides;\n    const face=[a,b,b+sides,a+sides];\n    mesh.faces.push(capped?face.reverse():face);\n  }\n  if (capped) {\n    mesh.faces.push(Array.from({length:sides},(_,i)=>start+i));\n    mesh.faces.push(Array.from({length:sides},(_,i)=>start+2*sides-1-i));\n  }\n}\n\nfunction transform(mesh, orientation, arm, slide=0) {\n  mesh.vertices = mesh.vertices.map(([x,y,z])=>{\n    const p = arm===0 ? [x+slide,y,z] : [-y,x+slide,z];\n    return (orientation==='knee' ? [p[0],p[2],-p[1]] : p).map(n=>Number(n.toFixed(6)));\n  });\n  return mesh;\n}\n\nfunction connector() {\n  const mesh=body('Welded box elbow',steel,'Steel elbow');\n  const h=nodeHalf,i=h-plate;\n  block(mesh,-h,h,-h,h,-h,-i);\n  block(mesh,-h,h,-h,h,i,h);\n  for (const sign of [-1,1]) {\n    const [a,b]=sign<0?[-h,-i]:[i,h];\n    block(mesh,a,b,-h,h,-i,i);\n    block(mesh,-i,i,a,b,-i,i);\n  }\n  return mesh;\n}\n\nfunction socket(name) {\n  const mesh=body(name,steel,'Steel elbow');\n  const a=nodeHalf,b=nodeHalf+socketDepth,h=throatHalf+plate;\n  const holes=drillStations.map(d=>a+d);\n  drilledSolid(mesh,a,b,-h,-throatHalf,-throatHalf,throatHalf,holes);\n  drilledSolid(mesh,a,b,throatHalf,h,-throatHalf,throatHalf,holes);\n  block(mesh,a,b,-h,h,-h,-throatHalf);\n  block(mesh,a,b,-h,h,throatHalf,h);\n  return mesh;\n}\n\nfunction beam(name) {\n  const mesh=body(name,wood,'300 × 300 glulam');\n  const start=nodeHalf,end=start+beamLength,h=timberSide/2;\n  const holes=[...drillStations.map(d=>start+d),...drillStations.toReversed().map(d=>end-d)];\n  drilledSolid(mesh,start,end,-h,h,-h,h,holes);\n  return mesh;\n}\n\nfunction bolts(name, exploded) {\n  const mesh=body(name,fixing,'Through-bolts');\n  const h=throatHalf+plate, gap=exploded?.405:0, nutGap=exploded?.072:0;\n  for (const d of drillStations) {\n    const x=nodeHalf+d;\n    tubeWall(mesh,x,-h-.027-gap,h+.025-gap,boltRadius,12,true);\n    tubeWall(mesh,x,-h-.008-gap,-h-gap,.023,16,true);\n    tubeWall(mesh,x,-h-.020-gap,-h-.008-gap,.019,6,true);\n    tubeWall(mesh,x,h+nutGap,h+.008+nutGap,.023,16,true);\n    tubeWall(mesh,x,h+.008+nutGap,h+.020+nutGap,.019,6,true);\n  }\n  return mesh;\n}\n\nfunction seat(name, sideBearing) {\n  const mesh=body(name,'#788b9c','Bearing shim');\n  const a=nodeHalf,b=nodeHalf+socketDepth;\n  // In a knee, the horizontal socket bears on a cheek after rotation.\n  if(sideBearing) drilledSolid(mesh,a,b,.15,throatHalf,-.145,.145,drillStations.map(d=>a+d));\n  else block(mesh,a,b,-.145,.145,-throatHalf,-.15);\n  return mesh;\n}\n\n\n// Combined with the reviewed timber-joint module by the build.\nconst up = [0,0,1];\nconst add = (a,b) => a.map((n,i) => n+b[i]);\nconst subtract = (a,b) => a.map((n,i) => n-b[i]);\nconst scale = (a,k) => a.map(n => n*k);\nconst dot = (a,b) => a.reduce((sum,n,i) => sum+n*b[i],0);\nconst cross = ([x,y,z],[a,b,c]) => [y*c-z*b,z*a-x*c,x*b-y*a];\nconst unit = v => scale(v,1/Math.hypot(...v));\nconst round = n => Number(n.toFixed(6));\nconst numbered = (prefix,n) => prefix+String(n).padStart(2,'0');\n\nfunction place(mesh, origin, x, y, z) {\n  mesh.vertices=mesh.vertices.map(p=>add(origin,add(scale(x,p[0]),\n    add(scale(y,p[1]),scale(z,p[2])))).map(round));\n  return mesh;\n}\n\n// Retain both bays, the corner coordinates, and the return of the source line.\n// A seed selects legal depths at the three inside turns of each bay.\nfunction layout(seed) {\n  const route=[];\n  const roof=3.16, base=0.18, lowReturn=0.24;\n  let random=seed;\n  function next() {\n    random=(Math.imul(random,1664525)+1013904223)>>>0;\n    return random/4294967296;\n  }\n  const depths=[0.34,0.56,0.78,1.00];\n  const push=(x,y,z)=>route.push([round(x),round(y),z]);\n  function bay(sign, from) {\n    for (let i=0;i<6;i+=2) {\n      const x=from*(3-i*1.2), nextX=from*(3-(i+1)*1.2);\n      const inner=sign*depths[Math.floor(next()*depths.length)];\n      push(x,sign*3,roof);\n      push(x,inner,roof);\n      push(nextX,inner,roof);\n      push(nextX,sign*3,roof);\n    }\n  }\n  push(-3,-3,base);\n  bay(-1,-1);\n  push(3,-3,lowReturn);\n  push(3,3,lowReturn);\n  bay(1,1);\n  push(-3,3,base);\n  return assembleRoute(route);\n}\n\nfunction assembleRoute(route) {\n  const lowReturn=0.24;\n  const fittings=route.map((center,i)=>{\n    if (i===0 || i===route.length-1) {\n      return {center,arms:[{x:up,y:[0,1,0],z:[-1,0,0],offset:0}]};\n    }\n    // Put the horizontal arm first for the column knee so its local cheek\n    // always maps to a horizontal bearing surface after rotation.\n    const arms=[i-1,i+1].map(index=>({index,x:unit(subtract(route[index],center))}));\n    if (arms[0].x[2]!==0) arms.reverse();\n    const normal=cross(arms[0].x,arms[1].x);\n    return {center,arms:arms.map(arm=>({...arm,y:cross(normal,arm.x),z:normal,offset:nodeHalf}))};\n  });\n\n  const spans=route.slice(1).map((end,i)=>{\n    const start=route[i];\n    const a=fittings[i].arms.find(arm=>arm.index===i+1 || arm.index===undefined);\n    const b=fittings[i+1].arms.find(arm=>arm.index===i || arm.index===undefined);\n    const length=round(Math.hypot(...subtract(end,start))-a.offset-b.offset);\n    if(length<2*socketDepth+.075) throw Error('The sockets need more room at '+numbered('B',i+1)+'.');\n    return {id:numbered('B',i+1),start:add(start,scale(a.x,a.offset)),end:add(end,scale(b.x,b.offset)),\n      length,x:a.x,boltsA:a.y,boltsB:b.y,\n      usage:a.x[2]!==0 && Math.min(start[2],end[2])<1?'Column':\n        start[2]===lowReturn && end[2]===lowReturn?'Low side return':'Canopy'};\n  });\n  return {route,fittings,spans};\n}\n\nfunction timber(span) {\n  const mesh=body(span.id+' · Glulam '+Math.round(span.length*1000)+' mm',wood,'300 × 300 glulam');\n  const {x,boltsA,boltsB,start,length}=span;\n  const h=timberSide/2;\n  const startHoles=drillStations;\n  const endHoles=drillStations.toReversed().map(d=>length-d);\n  if (Math.abs(dot(boltsA,boltsB))>.5) {\n    drilledSolid(mesh,0,length,-h,h,-h,h,[...startHoles,...endHoles]);\n    return place(mesh,start,x,boltsA,cross(x,boltsA));\n  }\n  // Keep the drill stations at each end while indexing the bore face 90°.\n  const middle=length/2;\n  drilledSolid(mesh,0,middle,-h,h,-h,h,startHoles);\n  mesh.faces.splice(1,1);\n  place(mesh,start,x,boltsA,cross(x,boltsA));\n  const second=body('second half',wood,mesh.module);\n  drilledSolid(second,middle,length,-h,h,-h,h,endHoles);\n  second.faces.splice(3,1);\n  place(second,start,x,boltsB,cross(x,boltsB));\n  const first=mesh.vertices.length;\n  mesh.vertices.push(...second.vertices);\n  mesh.faces.push(...second.faces.map(face=>face.map(i=>i+first)));\n  return mesh;\n}\n\nfunction fittingsAt(fitting,i) {\n  const {center,arms,shared,omitCore}=fitting;\n  const id=numbered('J',i+1);\n  const objects=[];\n  if (arms.length===2 && !omitCore) {\n    const [a,b]=arms;\n    const core=connector();\n    core.name=id+(shared?' · Shared steel node':' · Welded box elbow');\n    objects.push(place(core,center,a.x,b.x,a.z));\n  }\n  for (const [index,arm] of arms.entries()) {\n    const port=id+(index===0?'A':'B');\n    const origin=add(center,scale(arm.x,arm.offset-nodeHalf));\n    objects.push(place(socket(port+' · Socket'),origin,arm.x,arm.y,arm.z));\n    objects.push(place(bolts(port+' · Through-bolts',false),origin,arm.x,arm.y,arm.z));\n    if (arm.x[2]===0) {\n      const shim=seat(port+' · Bearing shim',Math.abs(arm.y[2])===1);\n      // The shim prototype bears on local -Z or +Y. Rotate it about\n      // the beam when that face points up in the installed orientation.\n      const reverse=arm.z[2]<0 || arm.y[2]>0;\n      objects.push(place(shim,origin,arm.x,scale(arm.y,reverse?-1:1),scale(arm.z,reverse?-1:1)));\n    }\n  }\n  return objects;\n}\n\nfunction supports(layout) {\n  const last=layout.route.length-1;\n  const feet=[layout.route[0],layout.route.find(p=>p[2]===.24),layout.route.findLast(p=>p[2]===.24),layout.route[last]];\n  return feet.map(([x,y,z],i)=>{\n    const shoe=body(numbered('S',i+1)+' · Support pedestal',steel,'Support pedestal');\n    block(shoe,x-.25,x+.25,y-.25,y+.25,0,.024);\n    block(shoe,x-.115,x+.115,y-.115,y+.115,.024,z-(i===0||i===3?.024:nodeHalf));\n    if (i===0 || i===3) block(shoe,x-.165,x+.165,y-.165,y+.165,z-.024,z);\n    return shoe;\n  });\n}\n\n\nconst GUIDE = [[-3,-3,0],[-3,-3,2.4],[-3,-2.972,2.609],[-3,-2.89,2.777],[-3,-2.752,2.904],[-3,-2.558,2.989],[-3,-2.072,3.131],[-3,-1.85,3.183],[-3,-1.627,3.211],[-3,-1.402,3.215],[-3,-1.175,3.194],[-3,-0.479,3.092],[-2.986,-0.383,3.078],[-2.945,-0.315,3.068],[-2.875,-0.274,3.062],[-2.778,-0.26,3.06],[-2.76,-0.26,3.06],[-2.663,-0.274,3.062],[-2.594,-0.315,3.068],[-2.552,-0.383,3.078],[-2.538,-0.479,3.092],[-2.538,-1.175,3.194],[-2.538,-1.402,3.215],[-2.538,-1.627,3.211],[-2.538,-1.85,3.183],[-2.538,-2.072,3.131],[-2.538,-2.787,2.922],[-2.525,-2.88,2.895],[-2.483,-2.947,2.876],[-2.414,-2.987,2.864],[-2.317,-3,2.86],[-2.298,-3,2.86],[-2.202,-2.987,2.864],[-2.132,-2.947,2.876],[-2.091,-2.88,2.895],[-2.077,-2.787,2.922],[-2.077,-2.072,3.131],[-2.077,-1.85,3.183],[-2.077,-1.627,3.211],[-2.077,-1.402,3.215],[-2.077,-1.175,3.194],[-2.077,-0.479,3.092],[-2.063,-0.383,3.078],[-2.022,-0.315,3.068],[-1.952,-0.274,3.062],[-1.855,-0.26,3.06],[-1.837,-0.26,3.06],[-1.74,-0.274,3.062],[-1.671,-0.315,3.068],[-1.629,-0.383,3.078],[-1.615,-0.479,3.092],[-1.615,-1.175,3.194],[-1.615,-1.402,3.215],[-1.615,-1.627,3.211],[-1.615,-1.85,3.183],[-1.615,-2.072,3.131],[-1.615,-2.787,2.922],[-1.602,-2.88,2.895],[-1.56,-2.947,2.876],[-1.491,-2.987,2.864],[-1.394,-3,2.86],[-1.375,-3,2.86],[-1.278,-2.987,2.864],[-1.209,-2.947,2.876],[-1.168,-2.88,2.895],[-1.154,-2.787,2.922],[-1.154,-2.072,3.131],[-1.154,-1.85,3.183],[-1.154,-1.627,3.211],[-1.154,-1.402,3.215],[-1.154,-1.175,3.194],[-1.154,-0.479,3.092],[-1.14,-0.383,3.078],[-1.098,-0.315,3.068],[-1.029,-0.274,3.062],[-0.932,-0.26,3.06],[-0.914,-0.26,3.06],[-0.817,-0.274,3.062],[-0.748,-0.315,3.068],[-0.706,-0.383,3.078],[-0.692,-0.479,3.092],[-0.692,-1.175,3.194],[-0.692,-1.402,3.215],[-0.692,-1.627,3.211],[-0.692,-1.85,3.183],[-0.692,-2.072,3.131],[-0.692,-2.787,2.922],[-0.678,-2.88,2.895],[-0.637,-2.947,2.876],[-0.568,-2.987,2.864],[-0.471,-3,2.86],[-0.452,-3,2.86],[-0.355,-2.987,2.864],[-0.286,-2.947,2.876],[-0.245,-2.88,2.895],[-0.231,-2.787,2.922],[-0.231,-2.072,3.131],[-0.231,-1.85,3.183],[-0.231,-1.627,3.211],[-0.231,-1.402,3.215],[-0.231,-1.175,3.194],[-0.231,-0.479,3.092],[-0.217,-0.383,3.078],[-0.175,-0.315,3.068],[-0.106,-0.274,3.062],[-0.009,-0.26,3.06],[0.009,-0.26,3.06],[0.106,-0.274,3.062],[0.175,-0.315,3.068],[0.217,-0.383,3.078],[0.231,-0.479,3.092],[0.231,-1.175,3.194],[0.231,-1.402,3.215],[0.231,-1.627,3.211],[0.231,-1.85,3.183],[0.231,-2.072,3.131],[0.231,-2.787,2.922],[0.245,-2.88,2.895],[0.286,-2.947,2.876],[0.355,-2.987,2.864],[0.452,-3,2.86],[0.471,-3,2.86],[0.568,-2.987,2.864],[0.637,-2.947,2.876],[0.678,-2.88,2.895],[0.692,-2.787,2.922],[0.692,-2.072,3.131],[0.692,-1.85,3.183],[0.692,-1.627,3.211],[0.692,-1.402,3.215],[0.692,-1.175,3.194],[0.692,-0.479,3.092],[0.706,-0.383,3.078],[0.748,-0.315,3.068],[0.817,-0.274,3.062],[0.914,-0.26,3.06],[0.932,-0.26,3.06],[1.029,-0.274,3.062],[1.098,-0.315,3.068],[1.14,-0.383,3.078],[1.154,-0.479,3.092],[1.154,-1.175,3.194],[1.154,-1.402,3.215],[1.154,-1.627,3.211],[1.154,-1.85,3.183],[1.154,-2.072,3.131],[1.154,-2.787,2.922],[1.168,-2.88,2.895],[1.209,-2.947,2.876],[1.278,-2.987,2.864],[1.375,-3,2.86],[1.394,-3,2.86],[1.491,-2.987,2.864],[1.56,-2.947,2.876],[1.602,-2.88,2.895],[1.615,-2.787,2.922],[1.615,-2.072,3.131],[1.615,-1.85,3.183],[1.615,-1.627,3.211],[1.615,-1.402,3.215],[1.615,-1.175,3.194],[1.615,-0.479,3.092],[1.629,-0.383,3.078],[1.671,-0.315,3.068],[1.74,-0.274,3.062],[1.837,-0.26,3.06],[1.855,-0.26,3.06],[1.952,-0.274,3.062],[2.022,-0.315,3.068],[2.063,-0.383,3.078],[2.077,-0.479,3.092],[2.077,-1.175,3.194],[2.077,-1.402,3.215],[2.077,-1.627,3.211],[2.077,-1.85,3.183],[2.077,-2.072,3.131],[2.077,-2.787,2.922],[2.091,-2.88,2.895],[2.132,-2.947,2.876],[2.202,-2.987,2.864],[2.298,-3,2.86],[2.317,-3,2.86],[2.414,-2.987,2.864],[2.483,-2.947,2.876],[2.525,-2.88,2.895],[2.538,-2.787,2.922],[2.538,-2.072,3.131],[2.538,-1.85,3.183],[2.538,-1.627,3.211],[2.538,-1.402,3.215],[2.538,-1.175,3.194],[2.538,-0.479,3.092],[2.552,-0.383,3.078],[2.594,-0.315,3.068],[2.663,-0.274,3.062],[2.76,-0.26,3.06],[2.778,-0.26,3.06],[2.875,-0.274,3.062],[2.945,-0.315,3.068],[2.986,-0.383,3.078],[3,-0.479,3.092],[3,-1.175,3.194],[3,-1.402,3.215],[3,-1.627,3.211],[3,-1.85,3.183],[3,-2.072,3.131],[3,-2.558,2.989],[3,-2.752,2.904],[3,-2.89,2.777],[3,-2.972,2.609],[3,-3,2.4],[3,-3,0.58],[3,-2.971,0.379],[3,-2.885,0.235],[3,-2.741,0.149],[3,-2.54,0.12],[3,2.54,0.12],[3,2.741,0.149],[3,2.885,0.235],[3,2.971,0.379],[3,3,0.58],[3,3,2.4],[3,2.972,2.609],[3,2.89,2.777],[3,2.752,2.904],[3,2.558,2.989],[3,2.072,3.131],[3,1.85,3.183],[3,1.627,3.211],[3,1.402,3.215],[3,1.175,3.194],[3,0.479,3.092],[2.986,0.383,3.078],[2.945,0.315,3.068],[2.875,0.274,3.062],[2.778,0.26,3.06],[2.76,0.26,3.06],[2.663,0.274,3.062],[2.594,0.315,3.068],[2.552,0.383,3.078],[2.538,0.479,3.092],[2.538,1.175,3.194],[2.538,1.402,3.215],[2.538,1.627,3.211],[2.538,1.85,3.183],[2.538,2.072,3.131],[2.538,2.787,2.922],[2.525,2.88,2.895],[2.483,2.947,2.876],[2.414,2.987,2.864],[2.317,3,2.86],[2.298,3,2.86],[2.202,2.987,2.864],[2.132,2.947,2.876],[2.091,2.88,2.895],[2.077,2.787,2.922],[2.077,2.072,3.131],[2.077,1.85,3.183],[2.077,1.627,3.211],[2.077,1.402,3.215],[2.077,1.175,3.194],[2.077,0.479,3.092],[2.063,0.383,3.078],[2.022,0.315,3.068],[1.952,0.274,3.062],[1.855,0.26,3.06],[1.837,0.26,3.06],[1.74,0.274,3.062],[1.671,0.315,3.068],[1.629,0.383,3.078],[1.615,0.479,3.092],[1.615,1.175,3.194],[1.615,1.402,3.215],[1.615,1.627,3.211],[1.615,1.85,3.183],[1.615,2.072,3.131],[1.615,2.787,2.922],[1.602,2.88,2.895],[1.56,2.947,2.876],[1.491,2.987,2.864],[1.394,3,2.86],[1.375,3,2.86],[1.278,2.987,2.864],[1.209,2.947,2.876],[1.168,2.88,2.895],[1.154,2.787,2.922],[1.154,2.072,3.131],[1.154,1.85,3.183],[1.154,1.627,3.211],[1.154,1.402,3.215],[1.154,1.175,3.194],[1.154,0.479,3.092],[1.14,0.383,3.078],[1.098,0.315,3.068],[1.029,0.274,3.062],[0.932,0.26,3.06],[0.914,0.26,3.06],[0.817,0.274,3.062],[0.748,0.315,3.068],[0.706,0.383,3.078],[0.692,0.479,3.092],[0.692,1.175,3.194],[0.692,1.402,3.215],[0.692,1.627,3.211],[0.692,1.85,3.183],[0.692,2.072,3.131],[0.692,2.787,2.922],[0.678,2.88,2.895],[0.637,2.947,2.876],[0.568,2.987,2.864],[0.471,3,2.86],[0.452,3,2.86],[0.355,2.987,2.864],[0.286,2.947,2.876],[0.245,2.88,2.895],[0.231,2.787,2.922],[0.231,2.072,3.131],[0.231,1.85,3.183],[0.231,1.627,3.211],[0.231,1.402,3.215],[0.231,1.175,3.194],[0.231,0.479,3.092],[0.217,0.383,3.078],[0.175,0.315,3.068],[0.106,0.274,3.062],[0.009,0.26,3.06],[-0.009,0.26,3.06],[-0.106,0.274,3.062],[-0.175,0.315,3.068],[-0.217,0.383,3.078],[-0.231,0.479,3.092],[-0.231,1.175,3.194],[-0.231,1.402,3.215],[-0.231,1.627,3.211],[-0.231,1.85,3.183],[-0.231,2.072,3.131],[-0.231,2.787,2.922],[-0.245,2.88,2.895],[-0.286,2.947,2.876],[-0.355,2.987,2.864],[-0.452,3,2.86],[-0.471,3,2.86],[-0.568,2.987,2.864],[-0.637,2.947,2.876],[-0.678,2.88,2.895],[-0.692,2.787,2.922],[-0.692,2.072,3.131],[-0.692,1.85,3.183],[-0.692,1.627,3.211],[-0.692,1.402,3.215],[-0.692,1.175,3.194],[-0.692,0.479,3.092],[-0.706,0.383,3.078],[-0.748,0.315,3.068],[-0.817,0.274,3.062],[-0.914,0.26,3.06],[-0.932,0.26,3.06],[-1.029,0.274,3.062],[-1.098,0.315,3.068],[-1.14,0.383,3.078],[-1.154,0.479,3.092],[-1.154,1.175,3.194],[-1.154,1.402,3.215],[-1.154,1.627,3.211],[-1.154,1.85,3.183],[-1.154,2.072,3.131],[-1.154,2.787,2.922],[-1.168,2.88,2.895],[-1.209,2.947,2.876],[-1.278,2.987,2.864],[-1.375,3,2.86],[-1.394,3,2.86],[-1.491,2.987,2.864],[-1.56,2.947,2.876],[-1.602,2.88,2.895],[-1.615,2.787,2.922],[-1.615,2.072,3.131],[-1.615,1.85,3.183],[-1.615,1.627,3.211],[-1.615,1.402,3.215],[-1.615,1.175,3.194],[-1.615,0.479,3.092],[-1.629,0.383,3.078],[-1.671,0.315,3.068],[-1.74,0.274,3.062],[-1.837,0.26,3.06],[-1.855,0.26,3.06],[-1.952,0.274,3.062],[-2.022,0.315,3.068],[-2.063,0.383,3.078],[-2.077,0.479,3.092],[-2.077,1.175,3.194],[-2.077,1.402,3.215],[-2.077,1.627,3.211],[-2.077,1.85,3.183],[-2.077,2.072,3.131],[-2.077,2.787,2.922],[-2.091,2.88,2.895],[-2.132,2.947,2.876],[-2.202,2.987,2.864],[-2.298,3,2.86],[-2.317,3,2.86],[-2.414,2.987,2.864],[-2.483,2.947,2.876],[-2.525,2.88,2.895],[-2.538,2.787,2.922],[-2.538,2.072,3.131],[-2.538,1.85,3.183],[-2.538,1.627,3.211],[-2.538,1.402,3.215],[-2.538,1.175,3.194],[-2.538,0.479,3.092],[-2.552,0.383,3.078],[-2.594,0.315,3.068],[-2.663,0.274,3.062],[-2.76,0.26,3.06],[-2.778,0.26,3.06],[-2.875,0.274,3.062],[-2.945,0.315,3.068],[-2.986,0.383,3.078],[-3,0.479,3.092],[-3,1.175,3.194],[-3,1.402,3.215],[-3,1.627,3.211],[-3,1.85,3.183],[-3,2.072,3.131],[-3,2.558,2.989],[-3,2.752,2.904],[-3,2.89,2.777],[-3,2.972,2.609],[-3,3,2.4],[-3,3,0]];\nfunction guideMesh() {\n  const mesh=body('Published serpentine guide','#2244c8','Guide');\n  for(let i=1;i<GUIDE.length;i++){\n    const a=GUIDE[i-1], b=GUIDE[i];\n    const span=subtract(b,a), length=Math.hypot(...span);\n    if(length<1e-6) continue;\n    const x=unit(span),y=unit(cross(x,Math.abs(x[2])>.9?[1,0,0]:up)),z=cross(x,y);\n    const segment=body('segment',mesh.color,mesh.module);\n    block(segment,0,length,-.012,.012,-.012,.012);\n    place(segment,a,x,y,z);\n    const base=mesh.vertices.length;\n    mesh.vertices.push(...segment.vertices);\n    mesh.faces.push(...segment.faces.map(face=>face.map(v=>v+base)));\n  }\n  return mesh;\n}\n\n// The build binds GUIDE to the published 3D centreline.\nconst step=1;\nconst low=2.76;\nconst high=3.76;\nconst directions=[[1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]];\nconst key=p=>p.join(',');\nconst cell=([x,y,z])=>(x*4+y)*2+z;\nconst bit=p=>1n<<BigInt(cell(p));\nconst same=(a,b)=>a.every((v,i)=>v===b[i]);\nconst axisOf=d=>d.findIndex(n=>n!==0);\nconst world=([x,y,z],sign)=>[sign*(3-x*step),sign*(3-y*step),z?high:low].map(round);\nconst heading=(d,sign)=>[-sign*d[0],-sign*d[1],d[2]].map(round);\nconst toward=(a,b)=>unit(subtract(b,a)).map(round);\nconst start=[0,0,0], goal=[6,0,0];\n\nfunction field(sign){\n  const segments=[];\n  for(let i=1;i<GUIDE.length;i++){\n    const a=GUIDE[i-1],b=GUIDE[i];\n    if(a[2]<2.7 || b[2]<2.7 || sign*a[1]<0 || sign*b[1]<0) continue;\n    const d=subtract(b,a),l2=dot(d,d);\n    if(l2>1e-10) segments.push({a,d,l2,length:Math.sqrt(l2)});\n  }\n  return segments;\n}\n\nfunction steer(a,b,direction,segments){\n  const [ax,ay,az]=a, [bx,by,bz]=b, [hx,hy,hz]=direction;\n  let score=0;\n  for(const t of [.2,.5,.8]){\n    const px=ax+t*(bx-ax), py=ay+t*(by-ay), pz=az+t*(bz-az);\n    let closest=Infinity,alignment=0;\n    for(const {a:[x,y,z],d:[dx,dy,dz],l2,length} of segments){\n      const ox=px-x, oy=py-y, oz=pz-z;\n      const u=Math.max(0,Math.min(1,(ox*dx+oy*dy+oz*dz)/l2));\n      const ex=ox-u*dx, ey=oy-u*dy, ez=oz-u*dz;\n      const align=Math.abs(hx*dx+hy*dy+hz*dz)/(length||Math.sqrt(l2));\n      const cost=Math.sqrt(ex*ex+ey*ey+ez*ez)-.18*align;\n      if(cost<closest){closest=cost;alignment=align;}\n    }\n    score+=1.22-.67*closest+.36*alignment;\n  }\n  return score/3;\n}\n\nfunction bayOptions(sign,seed,other,scores=new Map(),ends={start,goal},volume){\n  const guide=field(sign), cache=new Map();\n  const {start:origin,goal:target}=ends;\n  const toWorld=volume?.world||world;\n  const encode=volume?.cell||cell;\n  const mask=p=>1n<<BigInt(encode(p));\n  const limits=volume?.limits||[6,3,1];\n  let random=(seed^Math.imul(sign,0x7f4a7c15))>>>0;\n  function next(){\n    random=(Math.imul(random,1664525)+1013904223)>>>0;\n    return random/4294967296;\n  }\n  function candidates(p){\n    const id=encode(p);\n    if(cache.has(id)) return cache.get(id);\n    const moves=[];\n    for(const d of directions){\n      const axis=axisOf(d), direction=heading(d,sign);\n      let occupied=0n;\n      for(let length=1;length<=3;length++){\n        const end=add(p,scale(d,length));\n        if(end.some((n,i)=>n<0 || n>limits[i])) break;\n        if(same(end,target) && axis===2) break;\n        const a=toWorld(p,sign),b=toWorld(end,sign);\n        const contact=other?.get(key(b));\n        if(contact==='span' || (contact && contact.some(v=>dot(v,direction)<-.99))) break;\n        occupied|=mask(end);\n        const scoreKey=id*128+encode(end);\n        if(!scores.has(scoreKey)) scores.set(scoreKey,steer(a,b,direction,guide)*length);\n        const score=scores.get(scoreKey);\n        moves.push({end,axis,direction,occupied,score,contact,length});\n        // A shared location is a joint, so the current piece must end here.\n        if(contact || same(end,target)) break;\n      }\n    }\n    cache.set(id,moves);\n    return moves;\n  }\n\n  let active=[{tip:origin,axis:2,used:mask(origin),path:[origin],score:0,lifts:0,meets:0,middle:0,crest:false}];\n  const finished=[];\n  for(let i=0;i<25 && active.length;i++){\n    const fresh=[];\n    for(const state of active){\n      const atJoint=other?.get(key(toWorld(state.tip,sign)));\n      for(const move of candidates(state.tip)){\n        if(move.axis===state.axis || (state.used&move.occupied)!==0n) continue;\n        if(atJoint && atJoint.some(d=>dot(d,move.direction)>.99)) continue;\n        const {end,axis,occupied}=move;\n        const lifts=state.lifts+Number(axis===2);\n        if(lifts>(volume?10:6)) continue;\n        const middle=state.middle+Number(end[1]===3);\n        const meets=state.meets+Number(Boolean(move.contact));\n        const ends=same(end,target);\n        const crest=state.crest||end[2]===2;\n        if(ends && (lifts<2 || (volume&&!crest) || (other?meets<2:middle<4))) continue;\n        const gain=move.score-.56+.3*Number(axis===2)+.25*Number(axis===1)+\n          (volume?.bonus?.(state,move)||0)+\n          .48*Number(end[1]===3)+2.9*Number(Boolean(move.contact))+.58*(next()-.5);\n        const result={tip:end,axis,used:state.used|occupied,path:[...state.path,end],\n          score:state.score+gain,lifts,meets,middle,crest};\n        if(ends) finished.push(result);\n        else fresh.push(result);\n      }\n    }\n    fresh.sort((a,b)=>b.score-a.score);\n    const groups=new Map();\n    active=fresh.filter(s=>{\n      const id=encode(s.tip)*3+s.axis, count=groups.get(id)||0;\n      if(count>=5) return false;\n      groups.set(id,count+1);\n      return true;\n    }).slice(0,104);\n  }\n  const paths=new Map();\n  for(const s of finished.sort((a,b)=>b.score-a.score)){\n    const id=s.path.join('|');\n    if(!paths.has(id)) paths.set(id,{...s,path:s.path.map(p=>toWorld(p,sign))});\n    if(paths.size===(other?1:volume?7:10)) break;\n  }\n  return [...paths.values()];\n}\n\nfunction occupiedCenterline(path){\n  const index=new Map();\n  for(let i=1;i<path.length;i++){\n    const a=path[i-1],b=path[i],length=Math.round(Math.hypot(...subtract(b,a)));\n    for(let j=1;j<length;j++) index.set(key(add(a,scale(subtract(b,a),j/length)).map(round)),'span');\n  }\n  path.forEach((p,i)=>{\n    if(p[1]===0) index.set(key(p),[path[i-1],path[i+1]].filter(Boolean).map(n=>toward(p,n)));\n  });\n  return index;\n}\n\nfunction sharedLayout(seed){\n  let selected=null;\n  const scores=new Map();\n  for(const first of bayOptions(-1,seed)){\n    const [second]=bayOptions(1,seed,occupiedCenterline(first.path),scores);\n    if(!second) continue;\n    const score=first.score+second.score;\n    if(!selected || score>selected.score) selected={first,second,score};\n  }\n  if(!selected) throw Error('No connected canopy found for this seed.');\n  const route=[[-3,-3,.18],...selected.first.path,[3,-3,.24],[3,3,.24],\n    ...selected.second.path,[-3,3,.18]];\n  return assembleSharedRoute(route);\n}\n\nfunction assembleSharedRoute(route){\n  const model=assembleRoute(route);\n  const seen=new Map(), shared=[];\n  for(const [i,p] of route.entries()){\n    const id=key(p);\n    if(seen.has(id)){\n      const first=seen.get(id);\n      const arms=[...model.fittings[first].arms,...model.fittings[i].arms];\n      if(new Set(arms.map(arm=>key(arm.x))).size!==4) throw Error('Shared node needs four distinct faces.');\n      model.fittings[first].shared=true;\n      model.fittings[i].omitCore=true;\n      shared.push({first,second:i,center:p});\n    }else seen.set(id,i);\n  }\n  return {...model,shared};\n}\n\nfunction sharedScene(model){\n  return [...model.spans.map(timber),...model.fittings.flatMap(fittingsAt),...supports(model)];\n}\n\nconst cornerAnchors=[[-3,-3,.18],[3,-3,.24],[3,3,.24],[-3,3,.18]];\nconst turningLevels=[1.28,1.51,1.73];\n\nfunction supportGuide(foot){\n  const segments=[];\n  for(let i=1;i<GUIDE.length;i++){\n    const a=GUIDE[i-1], b=GUIDE[i];\n    if(a[2]>low || b[2]>low || Math.hypot(a[0]-foot[0],a[1]-foot[1])>.65 ||\n      Math.hypot(b[0]-foot[0],b[1]-foot[1])>.65) continue;\n    const d=subtract(b,a), l2=dot(d,d);\n    if(l2>1e-9) segments.push({a,d,l2});\n  }\n  return segments;\n}\n\nfunction supportOptions(foot,seed){\n  const [x,y]=foot, sx=-Math.sign(x), sy=-Math.sign(y);\n  const guide=supportGuide(foot);\n  let rng=seed>>>0;\n  const noise=()=>{\n    rng=(Math.imul(rng,1664525)+1013904223)>>>0;\n    return rng/4294967296-.5;\n  };\n  const options=[];\n  for(const z of turningLevels){\n    const shoulder=[x,y,z];\n    const firstMoves=[[sx,0,0],[0,sy,0]];\n    for(const direction of firstMoves){\n      const elbow=add(shoulder,direction);\n      // An optional second turn wraps the support in plan as well as in section.\n      const turn=direction[0]?[0,sy,0]:[sx,0,0];\n      for(const moves of [[elbow],[elbow,add(elbow,turn)]]){\n        const last=moves.at(-1);\n        const landing=[last[0],last[1],low];\n        const path=[foot,shoulder,...moves,landing];\n        const follow=path.slice(1).reduce((total,p,i)=>{\n          const a=path[i];\n          return total+steer(a,p,toward(a,p),guide);\n        },0)/(path.length-1);\n        const score=follow-.22*(moves.length-1)+1.5*noise();\n        options.push({path,score});\n      }\n    }\n  }\n  return options.sort((a,b)=>b.score-a.score);\n}\n\nfunction localPoint(p,sign){\n  return [3-sign*p[0],3-sign*p[1],0];\n}\n\nfunction spatialLayout(seed){\n  const variants=cornerAnchors.map((foot,i)=>supportOptions(foot,seed+2179*i));\n  let selected=null;\n  for(let attempt=0;attempt<3 && !selected;attempt++){\n    const legs=variants.map((options,i)=>options[(attempt*(i+1))%options.length].path);\n    const landings=legs.map(leg=>leg.at(-1));\n    const endpoints=sign=>{\n      const a=sign<0?0:2;\n      return {start:localPoint(landings[a],sign),goal:localPoint(landings[a+1],sign)};\n    };\n    const scores=new Map();\n    for(const first of bayOptions(-1,seed,undefined,new Map(),endpoints(-1))){\n      const [second]=bayOptions(1,seed,occupiedCenterline(first.path),scores,endpoints(1));\n      if(!second) continue;\n      const score=first.score+second.score;\n      if(!selected || score>selected.score) selected={legs,first,second,score};\n    }\n  }\n  if(!selected) throw Error('No shared canopy could reach all four grown supports for this seed.');\n  const {legs,first,second}=selected;\n  const route=[...legs[0].slice(0,-1),...first.path,...legs[1].toReversed().slice(1),\n    ...legs[2].slice(0,-1),...second.path,...legs[3].toReversed().slice(1)];\n  const model=assembleSharedRoute(route);\n  const supportKeys=new Set(legs.flatMap(leg=>leg.slice(1).map((p,i)=>[key(leg[i]),key(p)].sort().join('|'))));\n  const supportSpans=[];\n  for(const [i,span] of model.spans.entries()){\n    const edge=[key(route[i]),key(route[i+1])].sort().join('|');\n    if(supportKeys.has(edge)){\n      span.usage='Stepped support';\n      supportSpans.push(i+1);\n    }\n  }\n  return {...model,legs,supportSpans};\n}\n\n// Keep the original guide, but allow each front into the other's half.\nconst volume={\n  limits:[6,4,2],\n  cell:([x,y,z])=>(x*5+y)*3+z,\n  world:([x,y,z],sign)=>[sign*(3-x),sign*(3-y),round(low+z)],\n  bonus:(state,move)=>{\n    const [,y,z]=move.end;\n    const heightReward=z===2 ? .95+.12*Number(y>=2) : z===1?.34:0;\n    return heightReward + .32*Number(y===4) + .32*Number(move.axis===2) -\n      .12*Number(move.length>2);\n  },\n};\n\nfunction coveredPoints(path){\n  const index=new Map();\n  for(let i=1;i<path.length;i++){\n    const a=path[i-1],b=path[i],delta=subtract(b,a);\n    const count=Math.round(Math.hypot(...delta));\n    for(let j=1;j<count;j++) index.set(key(add(a,scale(delta,j/count)).map(round)),'span');\n  }\n  path.forEach((p,i)=>{\n    index.set(key(p),i===0||i===path.length-1?'span':\n      [toward(p,path[i-1]),toward(p,path[i+1])]);\n  });\n  return index;\n}\n\nfunction volumetricLayout(seed){\n  const variants=cornerAnchors.map((foot,i)=>supportOptions(foot,seed+2179*i));\n  const northScores=new Map(), southScores=new Map();\n  let selected=null;\n  for(let attempt=0;attempt<4 && !selected;attempt++){\n    const legs=variants.map((options,i)=>{\n      const path=options[(attempt*(i+1))%options.length].path.map(p=>[...p]);\n      // A longer last riser can land at the middle tier when the guide and\n      // continuation permit it. The two front endpoints are still fixed.\n      if((seed+i*7+attempt)%5<2) path.at(-1)[2]=round(low+1);\n      return path;\n    });\n    const landings=legs.map(leg=>leg.at(-1));\n    const toLocal=(p,sign)=>[3-sign*p[0],3-sign*p[1],Math.round(p[2]-low)];\n    const ends=sign=>{\n      const i=sign<0?0:2;\n      return {start:toLocal(landings[i],sign),goal:toLocal(landings[i+1],sign)};\n    };\n    const south=bayOptions(-1,seed,undefined,southScores,ends(-1),volume);\n    for(const first of south){\n      const blocked=coveredPoints(first.path);\n      if(landings.slice(2).some(p=>blocked.has(key(p)))) continue;\n      const [second]=bayOptions(1,seed,blocked,northScores,ends(1),volume);\n      if(!second || second.meets<3) continue;\n      const contacts=second.path.filter(p=>Array.isArray(blocked.get(key(p))));\n      const spread=new Set(contacts.map(p=>p[1])).size;\n      const heights=new Set(contacts.map(p=>p[2])).size;\n      if(spread<2 || heights<2) continue;\n      const score=first.score+second.score+spread+heights;\n      if(!selected || score>selected.score) selected={legs,first,second,score};\n    }\n  }\n  if(!selected) throw Error('No volumetric canopy could link the four supports for this seed.');\n  const {legs,first,second}=selected;\n  const route=[...legs[0].slice(0,-1),...first.path,...legs[1].toReversed().slice(1),\n    ...legs[2].slice(0,-1),...second.path,...legs[3].toReversed().slice(1)];\n  const model=assembleSharedRoute(route);\n  const supportEdges=new Set(legs.flatMap(leg=>leg.slice(1).map((p,i)=>[key(leg[i]),key(p)].sort().join('|'))));\n  const supportSpans=[];\n  for(const [i,span] of model.spans.entries()){\n    if(!supportEdges.has([key(route[i]),key(route[i+1])].sort().join('|'))) continue;\n    span.usage='Stepped support';\n    supportSpans.push(i+1);\n  }\n  return {...model,legs,supportSpans};\n}\n\n// Each fork grows from an existing support joint and stops at a free canopy port.\nconst branchAxes=[[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1]];\nconst edgeId=(a,b)=>[key(a),key(b)].sort().join('|');\nconst branchLength=(a,b)=>Math.hypot(...subtract(b,a));\nconst branchAxis=(a,b)=>axisOf(toward(a,b));\n\nfunction reserveEdge(a,b){\n  return {a,b,box:a.map((v,i)=>[Math.min(v,b[i])-.196,Math.max(v,b[i])+.196])};\n}\n\nfunction intersects(a,b){\n  // Orthogonal sockets can touch a common box on different faces.\n  if([a.a,a.b].some(p=>[b.a,b.b].some(q=>same(p,q)))) return false;\n  return a.box.every(([lo,hi],i)=>Math.min(hi,b.box[i][1])-Math.max(lo,b.box[i][0])>1e-6);\n}\n\nfunction directionAvailable(ports,a,b){\n  const d=toward(a,b);\n  return !(ports.get(key(a))||[]).some(n=>same(n,d));\n}\n\nfunction growForks(model,seed){\n  const {route,legs}=model;\n  const supportEnds=new Set(legs.map(leg=>key(leg.at(-1))));\n  const trunkNodes=new Set(legs.flatMap(leg=>leg.slice(0,-1).map(key)));\n  const targets=new Map();\n  route.forEach(p=>{\n    if(p[2]>=low && !trunkNodes.has(key(p)) && !supportEnds.has(key(p))) targets.set(key(p),p);\n  });\n  const ports=new Map();\n  function occupy(a,b){\n    const id=key(a), d=toward(a,b);\n    if(!ports.has(id)) ports.set(id,[]);\n    ports.get(id).push(d);\n  }\n  const baseEdges=route.slice(1).map((b,i)=>{\n    const a=route[i];\n    occupy(a,b);\n    occupy(b,a);\n    return reserveEdge(a,b);\n  });\n  const allOptions=legs.map((leg,owner)=>{\n    const [fx,fy]=leg[0];\n    const options=new Map();\n    const guides=field(Math.sign(fy));\n    const guidance=new Map();\n    function scoreEdge(a,b){\n      const id=edgeId(a,b);\n      if(!guidance.has(id)) guidance.set(id,steer(a,b,toward(a,b),guides));\n      return guidance.get(id);\n    }\n    let states=leg.slice(1,-1).map(point=>({path:[point],edges:[],score:0,axis:-1}));\n    for(let depth=0;depth<4 && states.length;depth++){\n      const next=[];\n      for(const state of states){\n        const a=state.path.at(-1);\n        for(const direction of branchAxes){\n          const axis=axisOf(direction);\n          if(state.axis===axis || !directionAvailable(ports,a,add(a,direction))) continue;\n          const distances=axis===2?[low,high,round(low+2)].filter(z=>z-a[2]>=.995).map(z=>round(z-a[2])):[1,2];\n          for(const distance of distances){\n            const b=add(a,scale(direction,distance)).map(round), id=key(b), contact=targets.get(id);\n            if(Math.abs(b[0])>3 || Math.abs(b[1])>3 ||\n              Math.sign(fx)*b[0]<-1 || Math.sign(fy)*b[1]<-1 ||\n              Math.abs(b[0]-fx)+Math.abs(b[1]-fy)>5 || state.path.some(p=>same(p,b))) continue;\n            if((ports.has(id)&&!contact) || (contact&&!directionAvailable(ports,b,a))) continue;\n            const edge=reserveEdge(a,b);\n            if([...baseEdges,...state.edges].some(other=>intersects(edge,other))) continue;\n            const cost=.43*distance+.18*(depth+1);\n            const score=state.score+scoreEdge(a,b)*.44-cost;\n            const result={owner,path:[...state.path,b],edges:[...state.edges,edge],axis,score};\n            if(contact){\n              const separated=branchLength(b,leg.at(-1));\n              if(separated<.99) continue;\n              const rank=score+.50*Math.min(separated,3)+.14*(b[2]-low)+\n                .12*Math.sin(seed*7+owner*37+b[0]*15+b[1]*11+b[2]*8);\n              const signature=[key(result.path[0]),id,key(toward(result.path[0],result.path[1]))].join('|');\n              if(!options.has(signature)||options.get(signature).score<rank) options.set(signature,{...result,score:rank});\n            }else next.push(result);\n          }\n        }\n      }\n      states=next.sort((a,b)=>b.score-a.score).slice(0,48);\n    }\n    return [...options.values()].sort((a,b)=>b.score-a.score).slice(0,20);\n  });\n\n  function compatible(a,b){\n    const root=a.path[0], otherRoot=b.path[0];\n    if(same(a.path.at(-1),b.path.at(-1))) return false;\n    if(same(root,otherRoot)&&same(toward(root,a.path[1]),toward(root,b.path[1]))) return false;\n    if(a.path.slice(1,-1).some(p=>b.path.some(q=>same(p,q))) ||\n      b.path.slice(1,-1).some(p=>same(p,root))) return false;\n    return !a.edges.some(edge=>b.edges.some(other=>intersects(edge,other)));\n  }\n  const chosen=[];\n  function cover(owner){\n    if(owner===legs.length) return true;\n    for(const option of allOptions[owner]){\n      if(!chosen.every(other=>compatible(option,other))) continue;\n      chosen.push(option);\n      if(cover(owner+1)) return true;\n      chosen.pop();\n    }\n    return false;\n  }\n  if(!cover(0)) throw Error('This seed did not find two separate canopy connections per support.');\n  for(const options of allOptions){\n    const extra=options.find(option=>chosen.every(other=>compatible(option,other)));\n    if(extra) chosen.push(extra);\n  }\n  return chosen;\n}\n\nfunction addBranches(model,seed){\n  const branches=growForks(model,seed);\n  const fittings=[...model.fittings];\n  const junctions=new Set();\n  const spans=[];\n  for(const branch of branches){\n    const {path,owner}=branch;\n    const nodes=path.map((p,i)=>{\n      const ends=i===0||i===path.length-1;\n      const arms=[path[i-1],path[i+1]].filter(Boolean).map(next=>{\n        const x=toward(p,next);\n        const y=x[2]===0?up:[0,1,0];\n        return {x,y,z:cross(x,y),offset:nodeHalf,target:key(next)};\n      });\n      // The original core remains at each fork and canopy contact.\n      if(ends) junctions.add(key(p));\n      const index=fittings.length;\n      const fitting={center:p,arms,omitCore:ends};\n      fittings.push(fitting);\n      return {fitting,index};\n    });\n    for(let i=1;i<path.length;i++){\n      const a=nodes[i-1],b=nodes[i];\n      const aArm=a.fitting.arms.find(arm=>arm.target===key(path[i]));\n      const bArm=b.fitting.arms.find(arm=>arm.target===key(path[i-1]));\n      const x=aArm.x;\n      spans.push({id:numbered('R',spans.length+1),usage:'Branch from support '+(owner+1),\n        start:add(path[i-1],scale(x,nodeHalf)),end:add(path[i],scale(x,-nodeHalf)),\n        length:round(branchLength(path[i-1],path[i])-2*nodeHalf),x,boltsA:aArm.y,boltsB:bArm.y,\n        ports:[{index:a.index,arm:aArm},{index:b.index,arm:bArm}]});\n    }\n  }\n  return {...model,branches:branches.map(({path,owner})=>({path,owner})),branchSpans:spans,\n    branchFittings:fittings.slice(model.fittings.length),branchJunctions:[...junctions]};\n}\n\nfunction branchingLayout(seed){\n  return addBranches(volumetricLayout(seed),seed);\n}\n\nfunction run(inputs,controls={}){\n  const {seed=12,showGuide=false}=controls;\n  if(!Number.isInteger(seed)||seed<1||seed>9999) throw Error('Use a whole-number seed from 1 to 9999.');\n  const model=branchingLayout(seed);\n  const objects=[...sharedScene(model),...model.branchSpans.map(timber),\n    ...model.branchFittings.flatMap((fitting,i)=>fittingsAt(fitting,i+model.fittings.length)),\n    ...(showGuide?[guideMesh()]:[])];\n  for(const object of objects){\n    const vertices=[], indexed=new Map();\n    const remap=object.vertices.map(vertex=>{\n      const id=key(vertex);\n      if(!indexed.has(id)){indexed.set(id,vertices.length);vertices.push(vertex);}\n      return indexed.get(id);\n    });\n    object.vertices=vertices;\n    object.faces=object.faces.map(face=>face.map(index=>remap[index]));\n  }\n  return {version:2,units:'metres',objects};\n}\n\n\nfunction drafting(){\n  const palette={ink:'#1c2637',soft:'#536172',light:'#d5dce5',blue:'#203ee5',teal:'#117970',wood:'#a66942',paper:'#f2f5fa'};\n  const sheets=[];\n  let e;\n  const paper=[431.8,279.4];\n  const issueDate='11/13/2026';\n  \n  const inch=n=>(n/25.4).toFixed(2)+'\"';\n  function feetInches(millimetres){\n    const eighths=Math.round(Math.abs(millimetres)/25.4*8);\n    const feet=Math.floor(eighths/96), whole=Math.floor(eighths/8)%12, remainder=eighths%8;\n    const divisor=remainder%2?1:remainder%4?2:4;\n    return (millimetres<0?'−':'')+feet+\"'-\"+whole+(remainder?' '+remainder/divisor+'/'+8/divisor:'')+'\"';\n  }\n  const path=(points,color=palette.ink,width=.23,fill)=>e.push({type:'path',points,color,width,closed:Boolean(fill),...(fill?{fill}:{})});\n  const line=(a,b,color,width)=>path([a,b],color,width);\n  const box=(x,y,w,h,color=palette.light,fill)=>path([[x,y],[x+w,y],[x+w,y+h],[x,y+h],[x,y]],color,.22,fill);\n  const text=(x,y,value,size=3,color=palette.ink)=>e.push({type:'text',at:[x,y],text:String(value),size,color});\n  function sheet(id,title,note,scale='AS SHOWN'){\n    e=[];\n    sheets.push({id,title,size:paper,elements:e});\n    box(8,7,415.8,265.4,palette.ink);\n    text(16,11,'FORM FAMILIES / ONE-LINE PAVILION',3.1,palette.blue);\n    text(16,20,title,7.2);\n    text(16,32,note,3.05,palette.soft);\n    text(272,12,'PRELIMINARY — NOT FOR CONSTRUCTION',4.3,'#a32928');\n    line([8,43],[423.8,43],palette.ink,.25);\n    line([8,249],[423.8,249],palette.ink,.25);\n    for(const x of [132,211,284,353,386]) line([x,249],[x,272.4],palette.ink,.22);\n    text(14,252,'PROJECT / SHEET TITLE',2.15,palette.soft);\n    text(14,257,'ONE-LINE PAVILION / BRANCHING TIMBER',3.0);\n    text(14,263,title,3.1);\n    text(138,252,'ISSUE / REV / DATE',2.15,palette.soft);\n    text(138,257,'DESIGN REVIEW   /   00',3.0);\n    text(138,263,issueDate,3.0);\n    text(217,252,'DRAWN / CHECKED',2.15,palette.soft);\n    text(217,257,'RELAY / PENDING REVIEW',2.8);\n    text(217,263,'SCALE: '+scale,2.65);\n    text(290,252,'PROFESSIONAL SEAL',2.15,palette.soft);\n    text(290,265,'RESERVED',2.6,palette.soft);\n    text(357,252,'FORMAT',2.15,palette.soft);\n    text(357,257,'ANSI B',2.75);\n    text(357,263,'11 × 17 IN',2.55);\n    text(390,251,'SHEET',2.15,palette.soft);\n    text(390,256,id,5.1);\n    text(390,265,String(sheets.length),2.45);\n    return e;\n  }\n  return {sheets,sheet,path,line,box,text,palette,inch,feetInches,issueDate};\n}\n\nfunction generate(inputs,controls){\n  const model=branchingLayout(inputs[0].seed);\n  const d=drafting();\n  const {sheets,path,line,box,text,palette,inch,feetInches,issueDate}=d;\n  let e;\n  const sheet=(...args)=>{e=d.sheet(...args);};\n  const generalScale='1/8\" = 1\\'-0\"';\n  const planFactor=1000/96;\n  function scaleBar(x,y,scaleMm,feet){\n    const tick=304.8*scaleMm*feet/2;\n    box(x,y,tick,1.6,palette.ink,palette.ink);\n    box(x+tick,y,tick,1.6,palette.ink,'#ffffff');\n    for(const [i,label] of ['0',String(feet/2),feet+\"'\"].entries()) text(x+i*tick-1,y+2.1,label,2.4);\n  }\n  function bubble(x,y,letter){\n    const r=3.2;\n    path(Array.from({length:21},(_,i)=>[x+r*Math.cos(i*Math.PI/10),y+r*Math.sin(i*Math.PI/10)]),palette.ink,.2,'#ffffff');\n    text(x-1,y-1.8,letter,2.6);\n  }\n  function dim(a,b,offset,label){\n    const horizontal=Math.abs(a[1]-b[1])<.01;\n    const A=horizontal?[a[0],a[1]+offset]:[a[0]+offset,a[1]];\n    const B=horizontal?[b[0],b[1]+offset]:[b[0]+offset,b[1]];\n    line(a,A,palette.soft,.14);line(b,B,palette.soft,.14);line(A,B,palette.soft,.18);\n    for(const [x,y] of [A,B])line([x-1.35,y+1.35],[x+1.35,y-1.35],palette.soft,.22);\n    if(horizontal) text((A[0]+B[0])/2-4.5,A[1]-5,label,2.75);\n    else text(A[0]+2,(A[1]+B[1])/2-1.8,label,2.75);\n  }\n  const route=model.route;\n  const forks=model.branches;\n  const main=route.slice(1).map((b,i)=>({a:route[i],b,role:model.spans[i].usage,order:i+1}));\n  const forkEdges=forks.flatMap(({owner,path})=>path.slice(1).map((b,i)=>({a:path[i],b,role:'Fork',owner})));\n  const edges=[...main,...forkEdges];\n  const spans=[...model.spans,...model.branchSpans];\n  const feet=model.legs.map(leg=>leg[0]);\n  const highlight={ 'Stepped support':palette.blue,Fork:palette.teal,'Low side return':'#8993a0',Canopy:palette.wood };\n  function plot(edges,projection,weight=1.5){\n    for(const {a,b,role} of edges) line(projection(a),projection(b),highlight[role],weight);\n  }\n  const xyz=(x,y,s)=>p=>[x+s*(.77*p[0]-.64*p[1]),y+s*(.35*p[0]+.42*p[1]-.88*p[2])];\n  const mm=n=>Math.round(n*1000);\n  const id=(p,n)=>p+String(n).padStart(2,'0');\n  const tag=(p,name)=>{\n    box(p[0]-2,p[1]-2,4,4,palette.blue,'#ffffff');\n    text(p[0]+3,p[1]-2,name,2.75,palette.blue);\n  };\n\n  sheet('G-002','Drawing index and design notes','Design review set   /   Form Families workbook', 'NTS');\n  const index=[['G-002','Drawing index and design notes'],\n    ['A-101','Setting out and canopy plans'],['A-201','Elevations and datum levels'],\n    ['A-601','Assembly scheme'],['S-501','Beam, socket and elbow'],['S-502','Fork and landing joints'],\n    ['S-503','Base and socket sections'],['S-504','Connector axonometrics'],\n    ...Array.from({length:Math.ceil(spans.length/40)},(_,i)=>['S-'+(601+i),'Beam schedule '+(i+1)])];\n  text(21,58,'DRAWING INDEX',4.1,palette.blue);\n  index.forEach(([id,title],i)=>text(21,69+i*8,id+'   '+title,3.2));\n  text(21,170,'ISSUE RECORD',4.1,palette.blue);\n  line([21,184],[193,184],palette.light);\n  text(21,178,'REV',2.65);text(44,178,'DATE',2.65);text(82,178,'DESCRIPTION',2.65);\n  text(21,189,'00',3);text(44,189,issueDate,3);text(82,189,'Issued for design review',3);\n  text(21,212,'PROJECT LOCATION / AUTHORITY HAVING JURISDICTION',3.25,palette.blue);\n  text(21,223,'Site and permitting jurisdiction to be assigned.',3.1);\n  text(222,58,'DESIGN AND DRAWING NOTES',4.1,palette.blue);\n  const notes=[\n    '01   The pavilion is a workbook design study.',\n    '02   Overall dimensions use feet and inches, rounded to 1/8 in.',\n    '03   Component dimensions use decimal inches. Metric model values',\n    '        govern the geometric study, including the 300 × 300 mm beams.',\n    '04   Check the graphic scales. Print at 100% on 11 × 17 in paper.',\n    '05   The arrow indicates project north. Survey orientation is pending.',\n    '06   Verify structural design, site loads, foundations and erection.',\n    '07   Joint dimensions describe the model. Confirm welds, fastener',\n    '        specification, tolerances and drainage during engineering.',\n  ];\n  notes.forEach((note,i)=>text(222,70+i*8.1,note,2.85));\n  text(222,155,'SYMBOLS AND GROWTH',4.1,palette.blue);\n  const legend=[['Stepped support','Main support run'],['Fork','Branch to a free canopy socket'],['Canopy','Canopy beam'],['Low side return','Low side return']];\n  legend.forEach(([role,label],i)=>{\n    line([222,169+i*8],[234,169+i*8],highlight[role],1);\n    text(240,167+i*8,label,2.9);\n  });\n  text(222,205,'The guide steers a search over grid points and straight spans.',2.95);\n  text(222,213,'Accepted spans become timber beams and socket connections.',2.95);\n  text(222,221,'Branches reserve member clearance and dock at free faces.',2.95);\n  text(222,229,'See A-601 for the assembly diagram and S-502 for contact locations.',2.95);\n\n  sheet('A-101','Setting out and canopy plans','Anchor setout and three canopy levels   /   '+generalScale,generalScale);\n  const plans=[[112,96],[305,96],[112,197],[305,197]];\n  plans.forEach(([x,y],i)=>{\n    const scale=planFactor;\n    const plan=([px,py])=>[x+px*scale,y-py*scale];\n    const z=[null,low,high,round(low+2)][i];\n    const heading=i===0?'1   ANCHOR SETOUT':(i+1)+'   CANOPY AT +'+feetInches(mm(z));\n    text(x-68,y-3*scale-11,heading,3.1,palette.blue);\n    const [x0,y0]=plan([-3,3]);\n    box(x0,y0,6*scale,6*scale,palette.light);\n    for(const f of feet){const [a,b]=plan(f);box(a-2,b-2,4,4,palette.soft,'#ffffff');}\n    if(i===0){\n      plot(main.filter(edge=>edge.role==='Low side return'),plan);\n      feet.forEach((f,j)=>tag(plan(f),id('S',j+1)));\n      dim(plan([-3,-3]),plan([3,-3]),5,feetInches(6000));\n      dim(plan([-3,3]),plan([-3,-3]),-16,feetInches(6000));\n      for(const [gx,label] of [[-3,'1'],[3,'2']]) bubble(plan([gx,3])[0],y0-4,label);\n      for(const [gy,label] of [[-3,'A'],[3,'B']]) bubble(x0-26,plan([-3,gy])[1],label);\n      line([185,90],[185,70],palette.ink,.35);\n      path([[182,76],[185,70],[188,76]],palette.ink,.3,palette.ink);\n      text(183.8,64,'N',3.0);\n      text(171,94,'PROJECT NORTH',2.4);\n    }else{\n      plot(edges.filter(({a,b})=>a[2]===z&&b[2]===z),plan,2.9);\n      for(const f of model.shared.filter(f=>f.center[2]===z)){\n        const [a,b]=plan(f.center);\n        box(a-1.3,b-1.3,2.6,2.6,palette.ink,palette.ink);\n      }\n      forks.forEach(({path,owner})=>{const p=path.at(-1);if(p[2]===z)tag(plan(p),id('S',owner+1));});\n    }\n    const [barX,barY]=plan([3,-3]);\n    scaleBar(barX+8,barY-6,scale/1000,8);\n    text(barX+8,barY+2,generalScale,2.4,palette.soft);\n  });\n\n  const elevationScale='1/4\" = 1\\'-0\"';\n  sheet('A-201','Elevations and datum levels','Orthographic member profiles   /   '+elevationScale,elevationScale);\n  const solids=elevationSolids();\n  for(const [row,axis] of [0,1].entries()){\n    const x=104+row*197,y=178,s=planFactor*2;\n    const project=p=>[x+p[axis]*s,y-p[2]*s];\n    text(x-61,65,row===0?'SOUTH ELEVATION':'EAST ELEVATION',3.5,palette.blue);\n    for(const z of [0,low,high,round(low+2)]){\n      line([x-73,y-z*s],[x+72,y-z*s],palette.light,.16);\n      text(x+74,y-z*s-2,z?'+'+feetInches(mm(z)):\"0'-0\\\"\",2.7,palette.soft);\n    }\n    meshAxo(solids,project,row===0?[0,-1,0]:[1,0,0],.17);\n    dim([x-3*s,y],[x+3*s,y],12,feetInches(6000));\n    scaleBar(x+20,212,s/1000,8);\n  }\n  text(22,229,'Timber glulam in ochre; steel sleeves, node boxes and pedestals in blue gray.',3.1,palette.soft);\n  text(22,238,'Visible member faces and connector envelopes. See S-501 and S-504 for fastener and socket details.',3.0,palette.soft);\n\n  function meshAxo(meshes,project,direction,width=.16){\n    const polygons=[];\n    for(const mesh of meshes) for(const face of mesh.faces){\n      const points=face.map(i=>mesh.vertices[i]);\n      const n=cross(subtract(points[1],points[0]),subtract(points[2],points[0]));\n      if(dot(n,direction)<=1e-9) continue;\n      const lighting=dot(unit(n),unit([-.3,-.5,1]));\n      const shade=.9+.24*lighting;\n      const fill='#'+mesh.color.slice(1).match(/../g).map(v=>Math.min(255,Math.round(parseInt(v,16)*shade)).toString(16).padStart(2,'0')).join('');\n      const depth=points.reduce((sum,p)=>sum+dot(p,direction),0)/points.length;\n      polygons.push({depth,points:points.map(project),fill,stroke:mesh.stroke||fill});\n    }\n    polygons.sort((a,b)=>a.depth-b.depth).forEach(f=>path(f.points,f.stroke,width,f.fill));\n  }\n  const axDirection=[.64*.88,.77*.88,.77*.42+.64*.35];\n  function beamBox(span,tint,length=span.length,stroke=palette.ink){\n    const mesh=body(span.id,tint);\n    mesh.stroke=stroke;\n    const h=timberSide/2;\n    block(mesh,0,length,-h,h,-h,h);\n    return place(mesh,span.start,span.x,span.boltsA,cross(span.x,span.boltsA));\n  }\n  function jointBox(center,tint,half=nodeHalf){\n    const mesh=body('Joint',tint);\n    mesh.stroke=palette.ink;\n    block(mesh,-half,half,-half,half,-half,half);\n    return place(mesh,center,[1,0,0],[0,1,0],[0,0,1]);\n  }\n  function elevationSolids(){\n    const beamMeshes=spans.map(span=>beamBox(span,'#e7b789',span.length,'#77583d'));\n    const connectorMeshes=[...model.fittings,...model.branchFittings].flatMap(({center,arms,omitCore})=>{\n      const core=arms.length===2&&!omitCore?[jointBox(center,'#677c93')]:[];\n      return [...core,...arms.map(arm=>{\n        const m=body('Steel sleeve','#95a8bd');\n        m.stroke='#293b4b';\n        const h=throatHalf+plate;\n        block(m,arm.offset,arm.offset+socketDepth,-h,h,-h,h);\n        return place(m,center,arm.x,arm.y,arm.z);\n      })];\n    });\n    const pedestals=supports(model).map(m=>({...m,color:'#95a8bd',stroke:'#293b4b'}));\n    return [...beamMeshes,...connectorMeshes,...pedestals];\n  }\n  function leader(a,b,label,align='right'){\n    line(a,b,palette.ink,.2);\n    box(a[0]-.45,a[1]-.45,.9,.9,palette.ink,palette.ink);\n    text(align==='right'?b[0]+2:b[0]-label.length*1.7-4,b[1]-2.1,label,2.85);\n  }\n  sheet('A-601','Axonometric assembly scheme','Centerline growth sequence and model-derived member volumes','NTS');\n  const panels=[{x:110,y:137,role:'foot',label:'1   SET OUT THE FOUR ANCHORS'},\n    {x:310,y:137,role:'support',label:'2   GROW THE MAIN SUPPORT RUNS'},\n    {x:110,y:221,role:'main',label:'3   CLOSE THE TWO CANOPY ROUTES'},\n    {x:310,y:221,role:'all',label:'4   DOCK THE SUPPORT FORKS'}];\n  for(const {x,y,role,label} of panels){\n    const p=xyz(x,y-11,9);\n    text(x-82,y-72,label,3.1,palette.blue);\n    if(role==='support')plot(main.filter(m=>m.role==='Stepped support'),p,1.2);\n    if(role==='main')plot(main,p,1.15);\n    if(role==='all'){\n      const memberSolids=spans.map(span=>beamBox(span,span.usage.startsWith('Branch')?'#328e83':'#c28b5e'));\n      const unique=new Map(edges.flatMap(({a,b})=>[[key(a),a],[key(b),b]]));\n      meshAxo([...memberSolids,...[...unique.values()].map(v=>jointBox(v,'#6f7e90'))],p,axDirection,.13);\n      leader(p(forks[0].path.at(-1)),[368,169],'S-502');\n      leader(p(forks.find(fork=>fork.owner===1).path[0]),[368,192],'S-504');\n    }\n    feet.forEach((f,i)=>{\n      const [a,b]=p(f);\n      if(role!=='all')tag([a,b],id('S',i+1));\n      else text(a+3,b+1,id('S',i+1),2.75,palette.blue);\n    });\n  }\n  text(18,240,'Diagram shows growth order. See S-502 for junctions. Engineer temporary support and erection sequencing.',3,palette.soft);\n\n  const detailScale='1 1/2\" = 1\\'-0\"';\n  const shrinkView=(start,origin,ratio)=>{\n    const convert=p=>p.map((n,i)=>origin[i]+(n-origin[i])*ratio);\n    for(const element of e.slice(start)){\n      if(element.type==='path') element.points=element.points.map(convert);\n      else element.at=convert(element.at);\n    }\n  };\n  sheet('S-501','Beam, socket and elbow','Nominal model geometry   /   dimensions in decimal inches');\n  text(20,56,'T1   GLULAM SECTION   /   '+detailScale,3.4,palette.blue);\n  let viewStart=e.length;\n  box(32,84,60,60,palette.wood,'#f4e2d4');\n  dim([32,84],[92,84],-8,inch(300));\n  dim([32,84],[32,144],-10,inch(300));\n  shrinkView(viewStart,[39,91],.625);\n  text(24,135,'Square-cut ends. See S-601 and S-602 for lengths.',2.95,palette.soft);\n  text(147,56,'C1   SOCKET AND DRILL PATTERN   /   '+detailScale,3.4,palette.blue);\n  viewStart=e.length;\n  const x=168,y=88;\n  box(x-34,y,34,66,palette.ink,'#c9d2df');\n  box(x,y,58,66,palette.ink,'#e1e6ee');\n  box(x,y+3,87,60,palette.wood,'#f4e2d4');\n  line([x-35,y+33],[x+101,y+33],palette.soft,.18);\n  for(const pos of [25,45]){\n    const px=x+pos,py=y+33,r=2.4;\n    path(Array.from({length:17},(_,i)=>[px+r*Math.cos(i*Math.PI/8),py+r*Math.sin(i*Math.PI/8)]),palette.blue,.28);\n    line([px,py-5],[px,py+5],palette.soft,.15);\n  }\n  dim([x,y],[x+58,y],-11,inch(290));\n  dim([x,y+66],[x+25,y+66],10,inch(125));\n  dim([x+25,y+66],[x+45,y+66],10,inch(100));\n  shrinkView(viewStart,[167,88],.625);\n  for(const [i,[value,label]] of [[330,'Sleeve outside'],[306,'Clear opening'],[12,'Plate thickness'],[24,'Hole diameter'],[20,'Bolt shank diameter'],[3,'Bearing shim']].entries()){\n    text(262,79+i*8,inch(value)+'   '+label,3.15);\n  }\n  text(20,153,'C2   WELDED ELBOW   /   1\" = 1\\'-0\"',3.4,palette.blue);\n  viewStart=e.length;\n  box(35,177,34,34,palette.ink,'#c9d2df');\n  box(69,177.5,29,33,palette.ink,'#e1e6ee');\n  box(35.5,211,33,29,palette.ink,'#e1e6ee');\n  dim([35,177],[69,177],-9,inch(340));\n  shrinkView(viewStart,[35,177],5/6);\n  text(120,173,'Two '+inch(290)+' sockets meet a '+inch(340)+' box on perpendicular faces.',3.25);\n  text(120,186,'Beam end faces seat at the steel box. Bolt axes run through both cheeks.',3.25);\n  text(120,199,'See S-502 for additional socket faces at forks and canopy joints.',3.25);\n  text(120,219,'Plate, fastener and weld capacities require structural verification.',3.25,palette.soft);\n\n  sheet('S-502','Fork and landing joints','Socket directions from the model   /   axonometric','NTS');\n  const all=model.branches;\n  const examples=[{p:all.find(branch=>branch.owner===1).path[0],x:106,label:'C3   SUPPORT FORK'},\n    {p:all[0].path.at(-1),x:209,label:'C4   FORK LANDING'},\n    {p:model.shared[0].center,x:312,label:'C5   SHARED CANOPY NODE'}];\n  examples.forEach(({p,x,label})=>{\n    const projected=v=>xyz(x,139,24)(subtract(v,p));\n    const around=edges.filter(({a,b})=>key(a)===key(p)||key(b)===key(p));\n    const detail=around.map(({a,b,role})=>{\n      const other=key(a)===key(p)?b:a;\n      return {a:p,b:add(p,scale(toward(p,other),1.12)),role};\n    });\n    text(x-43,73,label,3.3,palette.blue);\n    plot(detail,projected,3);\n    box(x-4,135,8,8,palette.ink,'#435365');\n    text(x-43,166,'XYZ  '+p.map(v=>feetInches(mm(v))).join(' / '),2.85,palette.soft);\n    text(x-43,176,around.length+' occupied socket faces',3.2);\n  });\n  line([20,192],[400,192],palette.light);\n  text(20,198,'CONTACTS BY SUPPORT   /   XYZ FROM MODEL ORIGIN',3.5,palette.blue);\n  model.legs.forEach((leg,i)=>{\n    const locations=[leg.at(-1),...forks.filter(b=>b.owner===i).map(b=>b.path.at(-1))];\n    text(20,209+i*8,id('S',i+1)+'   '+locations.map(p=>'('+p.map(v=>feetInches(mm(v))).join(', ')+')').join('   /   '),2.85);\n  });\n\n  sheet('S-503','Base and socket sections','Nominal model geometry   /   dimensions in decimal inches',detailScale);\n  text(23,58,'C6   TERMINAL PEDESTAL',3.4,palette.blue);\n  text(215,58,'C3 / C4 / C5   TYPICAL SOCKET SECTION',3.4,palette.blue);\n  viewStart=e.length;\n  box(35,213.2,100,4.8,palette.ink,'#aebaca');\n  box(62,186.8,46,26.4,palette.ink,'#cad3df');\n  box(52,182,66,4.8,palette.ink,'#aebaca');\n  box(55,105,60,77,palette.wood,'#f4e2d4');\n  box(52,124,2.4,58,palette.ink,'#aebaca');\n  box(115.6,124,2.4,58,palette.ink,'#aebaca');\n  dim([35,218],[135,218],15,inch(500));\n  dim([62,204],[108,204],-5,inch(230));\n  dim([35,218],[35,182],-9,inch(180));\n  dim([135,218],[135,213.2],15,inch(24));\n  shrinkView(viewStart,[42,74],.625);\n  text(25,190,'The return-side bases carry the two low elbows.',3.2);\n  text(25,203,'Anchor bolts and foundation sizes require engineering.',3.0,palette.soft);\n  const outerX=235,outerY=105;\n  viewStart=e.length;\n  box(outerX,outerY,66,66,palette.ink,'#b9c6d5');\n  box(outerX+2.4,outerY+2.4,61.2,61.2,palette.ink,'#ffffff');\n  box(outerX+3,outerY+3,60,60,palette.wood,'#f4e2d4');\n  box(outerX+4,outerY+63,58,.6,palette.ink,'#b9c6d5');\n  dim([outerX,outerY],[outerX+66,outerY],-12,inch(330));\n  dim([outerX,outerY],[outerX,outerY+66],-12,inch(330));\n  line([outerX-2,outerY+33],[outerX+76,outerY+33],palette.blue,.35);\n  shrinkView(viewStart,[outerX,outerY],.625);\n  text(318,103,inch(12)+'   Sleeve walls',3.1);\n  text(318,114,inch(306)+'   Clear opening',3.1);\n  text(318,125,inch(300)+'   Timber section',3.1);\n  text(318,136,inch(3)+'   Bearing shim',3.1);\n  text(318,161,'Bolt axis in blue',3.1,palette.blue);\n  text(214,174,'See S-502 for socket orientations at the junctions.',3.2);\n  text(214,186,'See S-501 for hole positions from the beam end.',3.2);\n  text(214,211,'Shared nodes use free faces of the same box.',3.1);\n  text(214,223,'Allow for access to bolts, drainage and timber movement.',3.05,palette.soft);\n\n  sheet('S-504','Connector axonometrics','C3 support fork   /   installed and exploded detail from the model','NTS');\n  const pivot=examples[0].p;\n  const primary=model.fittings.map((f,i)=>({f,i})).filter(({f})=>same(f.center,pivot));\n  const secondary=model.branchFittings.map((f,i)=>({f,i:i+model.fittings.length})).filter(({f})=>same(f.center,pivot));\n  const joint=[...primary,...secondary];\n  const jointParts=joint.flatMap(({f,i})=>fittingsAt(f,i));\n  const nearby=spans.filter(span=>[span.start,span.end].some(v=>Math.abs(Math.hypot(...subtract(v,pivot))-nodeHalf)<1e-5));\n  function stub(span,distance=0){\n    const atStart=Math.hypot(...subtract(span.start,pivot))<nodeHalf+1e-5;\n    const length=Math.min(.77,span.length);\n    const end=atStart?span.start:span.end;\n    const x=atStart?span.x:scale(span.x,-1);\n    const y=atStart?span.boltsA:span.boltsB;\n\n    const shifted={...span,start:add(end,scale(x,distance||socketDepth)),length,x,boltsA:y,boltsB:y};\n    const mesh=distance?timber(shifted):beamBox(shifted,wood,length-socketDepth,palette.wood);\n    mesh.stroke=palette.wood;\n    return mesh;\n  }\n  function annotate(p,x,y,name){\n    line(p,[x-4,y+1.6],palette.soft,.18);\n    box(p[0]-.5,p[1]-.5,1,1,palette.ink,palette.ink);\n    text(x,y,name,3.1);\n  }\n  text(24,58,'1   INSTALLED FORK',3.4,palette.blue);\n  text(216,58,'2   EXPLODED FORK',3.4,palette.blue);\n  line([205,54],[205,218],palette.light,.18);\n  const jp1=v=>xyz(103,133,65)(subtract(v,pivot));\n  const jp2=v=>xyz(306,128,54)(subtract(v,pivot));\n  const pullback=.54;\n  meshAxo([...jointParts,...nearby.map(v=>stub(v))],jp1,axDirection,.19);\n  meshAxo([...jointParts.filter(part=>!part.name.includes('Through-bolts')),\n    ...nearby.map(v=>stub(v,pullback))],jp2,axDirection,.19);\n  for(const {f} of joint) for(const arm of f.arms){\n    const x=arm.x;\n    const a=jp2(add(pivot,scale(x,nodeHalf+socketDepth))),b=jp2(add(pivot,scale(x,nodeHalf+pullback)));\n    line(a,b,palette.blue,.28);\n  }\n  const forkArm=secondary[0].f.arms[0];\n  const shownArm=joint.flatMap(({f})=>f.arms).find(arm=>arm.x[2]===0&&dot(arm.x,axDirection)>0)||forkArm;\n  annotate(jp1(add(pivot,scale(shownArm.x,nodeHalf+.6))),119,84,'300 × 300 mm glulam');\n  annotate(jp1(add(add(pivot,scale(shownArm.x,nodeHalf+drillStations[0])),scale(shownArm.y,Math.sign(dot(shownArm.y,axDirection))*.179))),124,181,'Through-bolt');\n  annotate(jp2(pivot),219,177,'Welded steel box');\n  annotate(jp2(add(pivot,scale(forkArm.x,nodeHalf+socketDepth))),349,91,'Socket mouth');\n  text(24,205,'Timber stubs shortened for clarity.  /  Dimensions: S-501 and S-503.',3.05,palette.soft);\n  text(216,209,'Timber pulled back along socket axes; through-bolts removed.',3.05,palette.soft);\n  line([24,219],[409,219],palette.light,.18);\n  text(24,230,'C3 fork location and the complete support contact list are shown on S-502.',3.1,palette.soft);\n\n  const schedulePages=Math.ceil(spans.length/40);\n  for(let page=0;page<schedulePages;page++){\n    sheet('S-'+(601+page),'Beam schedule '+(page+1)+' / '+schedulePages,\n      'Cut lengths in decimal inches   /   11.81 × 11.81 in glulam (300 × 300 mm model)','NTS');\n    const rows=spans.slice(page*40,(page+1)*40);\n    const columns=[20,41,111,154,251,354];\n    ['ID','Member role','Length','End A  XYZ','End B  XYZ','Bolt face indexing'].forEach((s,i)=>text(columns[i],57,s,3.15,palette.blue));\n    rows.forEach((row,i)=>{\n      const y=63+i*4.58;\n      if(i%2===0)box(17,y-.2,397,4.4,palette.paper,palette.paper);\n      const indexing=Math.abs(dot(row.boltsA,row.boltsB))>.5?'Same faces':'Quarter turn';\n      [row.id,row.usage,inch(mm(row.length)),row.start.map(v=>feetInches(mm(v))).join(' / '),row.end.map(v=>feetInches(mm(v))).join(' / '),indexing]\n        .forEach((s,j)=>text(columns[j],y,s,2.48));\n    });\n  }\n  return {version:1,kind:'drawing-layout',title:'One-Line Pavilion · Branching timber drawings',sheets};\n}\n"},"entry":{"value":"generate"},"files":{"value":[{"task":"growth-settings","output":"result"}]}}},{"schema_version":3,"id":"drawing-package","operation":"drawings.pdf","objective":"Export the dimensioned drawing package as an ANSI B vector PDF.","inputs":{"geometry":{"task":"pavilion","output":"result"},"layout":{"task":"drawing-layout","output":"result"},"display_mode":{"value":"color"}}},{"schema_version":3,"id":"visualization","operation":"gemini.image","objective":"Make a concept image of the timber pavilion in a warm sculpture courtyard.","inputs":{"model":{"value":"gemini-3.1-flash-image"},"aspect_ratio":{"value":"16:9"},"files":{"value":[{"task":"viewer","output":"preview"}]},"prompt":{"value":"Use the attached exact branching timber pavilion as the geometry reference. Preserve its silhouette, the four anchors and support forks, the stepped open canopy and the square 300 × 300 mm glulam and dark steel sockets. Place it in a quiet sculpture courtyard with pale plaster walls, gravel, low grasses and warm late afternoon light. Make the timber grain and dark steel readable. No lettering."}}}]}}
