{
  "format": "relay-web-workflow",
  "version": 1,
  "title": "One-Line Pavilion · Serpentine Growth · 25 rounds",
  "request": "lets do 25 growth rounds",
  "workflow": {
    "schema_version": 1,
    "kind": "workflow",
    "inputs": {},
    "tasks": [
      {
        "schema_version": 3,
        "id": "pavilion",
        "operation": "code.javascript",
        "objective": "Grow one continuous 3D line by adding points to stretched edges and separating nearby runs.",
        "inputs": {
          "code": {
            "value": "function run(inputs, controls) {\n  const { seed, growth } = controls;\n  if (!Number.isInteger(seed) || seed < 1 || seed > 9999 ||\n      !Number.isInteger(growth) || growth < 3 || growth > 25) {\n    throw new Error('Use a seed from 1 to 9999 and 3 to 25 rounds of growth.');\n  }\n\n  const feet = [[-3,-3,0],[3,-3,0],[3,3,0],[-3,3,0]];\n  const groundReturn = (foot, direction) => Array.from({length:13},(_,i)=>{\n    const angle=(i-6)*Math.PI/12;\n    const offset=.24*Math.sin(angle);\n    return [foot[0]+offset*direction[0],foot[1]+offset*direction[1],.24*(1-Math.cos(angle))];\n  });\n  const supports = [\n    [[-2.86,-2.89,.82],[-2.69,-2.72,1.84],[-2.47,-2.53,2.58]],\n    [[2.48,-2.63,2.64],[2.72,-2.98,1.83],[2.94,-3.23,.64],\n      ...groundReturn(feet[1],[0,1]),[2.99,-2.74,.68],[2.95,-2.61,1.59],[2.84,-2.36,2.58]],\n    [[2.75,2.38,2.77],[3.03,2.70,1.68],[3.22,2.96,.63],\n      ...groundReturn(feet[2],[-1,0]),[2.74,2.99,.64],[2.57,2.94,1.7],[2.36,2.81,2.62]],\n    [[-2.46,2.55,2.67],[-2.74,2.76,1.82],[-2.90,2.91,.79]],\n  ];\n  const bows = [[0,1.42,3.35],[-2.65,0,4.35],[0,-1.55,3.84]];\n  const trace = inputs?.capture ? [] : null;\n  const clamp = (x,a,b) => Math.max(a,Math.min(b,x));\n  const add = (a,b) => a.map((x,i)=>x+b[i]);\n  const sub = (a,b) => a.map((x,i)=>x-b[i]);\n  const scale = (a,s) => a.map(x=>x*s);\n  const mix = (a,b,t) => a.map((x,i)=>x+(b[i]-x)*t);\n  const dot = (a,b) => a.reduce((sum,x,i)=>sum+x*b[i],0);\n  const cross = (a,b) => [a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]];\n  const length = a => Math.hypot(...a);\n  const unit = a => scale(a,1/Math.max(1e-8,length(a)));\n\n  // Give each seed the same small initial line and its own growth tendency.\n  let state = seed;\n  const random = () => ((state = (Math.imul(state,1664525) + 1013904223) >>> 0) / 4294967296);\n  const bands = bows.map((bow,index) => {\n    const a=supports[index].at(-1), b=supports[index+1][0];\n    return Array.from({length:10},(_,i)=>{\n      const t=i/9;\n      const p=mix(mix(a,bow,t),mix(bow,b,t),t);\n      return { p, rate: .7+.6*random(), twist: random()-.5 };\n    });\n  });\n  function skeleton() {\n    return [feet[0], ...supports[0], ...bands[0].slice(1,-1).map(n=>n.p),\n      ...supports[1], ...bands[1].slice(1,-1).map(n=>n.p),\n      ...supports[2], ...bands[2].slice(1,-1).map(n=>n.p), ...supports[3], feet[3]];\n  }\n  const remember = () => { if(trace) trace.push(skeleton()); };\n\n  function grow() {\n    for (const band of bands) {\n      // Add points to the stretches under the most tension. Nothing assigns a return or a lane.\n      const priorities=band.slice(1).map((b,i)=>{\n        const a=band[i];\n        return {i, weight: length(sub(b.p,a.p))*(a.rate+b.rate)/2};\n      }).sort((a,b)=>b.weight-a.weight || a.i-b.i);\n      const split=new Set(priorities.slice(0,4).map(edge=>edge.i));\n      for(let i=band.length-2;i>=0;i--){\n        if(!split.has(i)) continue;\n        const a=band[i], b=band[i+1];\n        const centre=mix(a.p,b.p,.5);\n        const tangent=unit(sub(b.p,a.p));\n        const side=unit(cross(tangent,[0,0,1]));\n        const twist=.55*(a.twist+b.twist)+.42*(random()-.5);\n        const p=add(centre,add(scale(side,twist*.15),[0,0,.10*(random()-.5)]));\n        band.splice(i+1,0,{p,rate:clamp((a.rate+b.rate)/2+.20*(random()-.5),.7,1.3),twist});\n      }\n    }\n  }\n  function relax() {\n    const nodes=bands.flatMap((band,lane)=>band.map((node,i)=>({node,lane,i,edge:i===0||i===band.length-1})));\n    const forces=nodes.map(({node,lane,i,edge})=>{\n      if(edge) return [0,0,0];\n      const band=bands[lane], p=node.p, previous=band[i-1].p, next=band[i+1].p;\n      const links=[previous,next].map(neighbour=>{\n        const d=sub(neighbour,p);\n        return scale(unit(d),.18*(length(d)-.64));\n      });\n      const bend=scale(sub(mix(previous,next,.5),p),.16);\n      const ceiling=.012*(2.77+.30*lane+.16*node.twist-p[2]);\n      return add(add(add(links[0],links[1]),bend),[0,0,ceiling]);\n    });\n    const interactionRadius=.99;\n    const byX=nodes.map(({node},index)=>({x:node.p[0],index})).sort((a,b)=>a.x-b.x);\n    const neighbours=nodes.map(()=>[]);\n    // Index local pairs by x, then apply the forces in the original node order.\n    for(let i=0;i<byX.length;i++){\n      const first=byX[i];\n      for(let j=i+1;j<byX.length&&byX[j].x-first.x<interactionRadius;j++){\n        const second=byX[j];\n        neighbours[Math.min(first.index,second.index)].push(Math.max(first.index,second.index));\n      }\n    }\n    for(let a=0;a<nodes.length;a++){\n      const left=nodes[a];\n      const [x,y,z]=left.node.p;\n      for(const b of neighbours[a].sort((i,j)=>i-j)){\n        const right=nodes[b];\n        if(left.edge&&right.edge || left.lane===right.lane&&right.i-left.i<3) continue;\n        const q=right.node.p;\n        const dy=y-q[1],dz=z-q[2];\n        if(Math.abs(dy)>=interactionRadius||Math.abs(dz)>=interactionRadius) continue;\n        const difference=[x-q[0],dy,dz];\n        const distance=length(difference);\n        if(distance>=interactionRadius || distance<1e-8) continue;\n        const push=scale(difference,.25*(interactionRadius-distance)/distance);\n        if(!left.edge) forces[a]=add(forces[a],push);\n        if(!right.edge) forces[b]=sub(forces[b],push);\n      }\n    }\n    nodes.forEach(({node,edge},i)=>{\n      if(edge) return;\n      const [x,y,z]=add(node.p,forces[i]);\n      node.p=[clamp(x,-2.78,2.78),clamp(y,-2.78,2.78),clamp(z,2.36,4.26)];\n    });\n  }\n\n  remember();\n  for(let round=0;round<growth;round++){\n    grow();\n    for(let pass=0;pass<18;pass++){\n      relax();\n      if(pass%3===2) remember();\n    }\n  }\n  for(let pass=0;pass<18;pass++){\n    relax();\n    if(pass%3===2) remember();\n  }\n\n  // Interpolate the settled line. Preserve the four exact ground contacts.\n  const points=skeleton();\n  const curve=[points[0]];\n  for(let i=0;i<points.length-1;i++){\n    const b=points[i],c=points[i+1];\n    const a=points[i-1] ?? sub(scale(b,2),c);\n    const d=points[i+2] ?? sub(scale(c,2),b);\n    const [dt0,dt1,dt2]=[sub(b,a),sub(c,b),sub(d,c)].map(v=>Math.sqrt(length(v)));\n    const tangentIn=add(sub(scale(sub(b,a),dt1/dt0),scale(sub(c,a),dt1/(dt0+dt1))),sub(c,b));\n    const tangentOut=add(sub(sub(c,b),scale(sub(d,b),dt1/(dt1+dt2))),scale(sub(d,c),dt1/dt2));\n    const count=Math.max(2,Math.ceil(length(sub(c,b))/.14));\n    for(let j=1;j<=count;j++){\n      const t=j/count;\n      const u=t*t, w=u*t;\n      const p=b.map((v,k)=>(2*w-3*u+1)*v+(w-2*u+t)*tangentIn[k]+\n        (3*u-2*w)*c[k]+(w-u)*tangentOut[k]);\n      curve.push(j===count?c:[p[0],p[1],Math.max(0,p[2])]);\n    }\n  }\n  if(trace) return { curve, trace };\n\n  // Sweep one circular section in a parallel-transport frame.\n  const radius = 0.050;\n  const sides = 10;\n  const vertices = [];\n  const faces = [Array.from({length:sides},(_,j)=>sides-1-j)];\n  let normal=[0,1,0];\n  for(let i=0;i<curve.length;i++){\n    const p=curve[i];\n    const tangent=unit(sub(curve[Math.min(i+1,curve.length-1)],curve[Math.max(i-1,0)]));\n    normal=unit(sub(normal,scale(tangent,dot(normal,tangent))));\n    const binormal=cross(tangent,normal);\n    for(let j=0;j<sides;j++){\n      const angle=2*Math.PI*j/sides;\n      const q=add(p,add(scale(normal,radius*Math.cos(angle)),scale(binormal,radius*Math.sin(angle))));\n      vertices.push(q.map(v=>Number(v.toFixed(5))));\n      if(i){\n        const a=(i-1)*sides+j, b=(i-1)*sides+(j+1)%sides;\n        faces.push([a,b,b+sides,a+sides]);\n      }\n    }\n  }\n  const last=vertices.length-sides;\n  faces.push(Array.from({length:sides},(_,j)=>last+j));\n  return {\n    version:2, units:'metres',\n    objects:[{type:'MESH',name:'One continuous tube',color:'#253de8',vertices,faces}]\n  };\n}\n"
          },
          "controls": {
            "value": {
              "seed": 83,
              "growth": 25
            }
          }
        }
      },
      {
        "schema_version": 3,
        "id": "viewer",
        "operation": "scene.view",
        "objective": "Inspect the pavilion in 3D and render a model reference.",
        "inputs": {
          "scene": {
            "task": "pavilion",
            "output": "result"
          },
          "azimuth": {
            "value": -70
          },
          "elevation": {
            "value": 33
          },
          "resolution": {
            "value": 640
          }
        }
      },
      {
        "schema_version": 3,
        "id": "visualization",
        "operation": "gemini.image",
        "objective": "Visualize the exact pavilion in its setting with Gemini.",
        "inputs": {
          "model": {
            "value": "gemini-3.1-flash-image"
          },
          "files": {
            "value": [
              {
                "task": "viewer",
                "output": "preview"
              }
            ]
          },
          "aspect_ratio": {
            "value": "16:9"
          },
          "prompt": {
            "value": "Create a warm pedestrian architectural photograph based on the attached model view. Keep the complete continuous tube, organic nested folds, open ground area and all four ground contacts. Preserve the reference camera, silhouette and scale. Show the same line in bronze-finished steel in a quiet garden courtyard in warm afternoon light."
          }
        }
      }
    ]
  }
}
