{
  "format": "relay-web-workflow",
  "version": 1,
  "title": "One-Line Pavilion · Cellular Automaton 3D · Ground to Ground · Endpoint Growth",
  "request": "Grow one unbranched line by asynchronous cell births at the active endpoint. Start on the same four ground contacts as the other One-Line Pavilion studies. Let the 3D rules generate the line in the site volume, then sweep a 100 mm tube.",
  "workflow": {
    "schema_version": 1,
    "kind": "workflow",
    "inputs": {},
    "tasks": [
      {
        "schema_version": 3,
        "id": "pavilion",
        "operation": "code.javascript",
        "objective": "Use a directional endpoint automaton. Grow one cell per step, without revisiting any cell. A seeded field biases the growth and a reachability check preserves a route to the next contact.",
        "inputs": {
          "code": {
            "value": "function run(inputs, controls) {\n  const trace = inputs?.capture ? [] : null;\n  const path = [];\n  const feet = [[-3,-3,0],[3,-3,0],[3,3,0],[-3,3,0]];\n  const clamp = (n, low, high) => Math.max(low, Math.min(high, n));\n  const mix = (a, b, t) => a + (b - a) * t;\n  const add = (a, b) => a.map((v, i) => v + b[i]);\n  const scale = (a, n) => a.map(v => v * n);\n  const sub = (a, b) => add(a, scale(b, -1));\n  const dot = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0);\n  const length = v => Math.hypot(...v);\n  const unit = a => scale(a, 1 / Math.max(1e-8,length(a)));\n  const cross = ([a,b,c], [x,y,z]) => [b*z-c*y, c*x-a*z, a*y-b*x];\n  const lerp = (a,b,t) => add(a,scale(sub(b,a),t));\n  const smooth = t => { const p=clamp(t,0,1); return p*p*(3-2*p); };\n  const clip = ([x,y,z])=>[clamp(x,-3,3),clamp(y,-3,3),clamp(z,.14,4.8)];\n\n  // Keep the site and the four contacts fixed. Each rule supplies its own route.\n  function append(route) {\n    path.push(...route.slice(path.length ? 1 : 0));\n  }\n\n  function round(route, amount, steps) {\n    const points=[route[0]];\n    for(let i=1;i<route.length-1;i++){\n      const p=route[i], a=lerp(p,route[i-1],amount), b=lerp(p,route[i+1],amount);\n      for(let j=0;j<steps;j++){\n        const t=j/(steps-1);\n        points.push(lerp(lerp(a,p,t),lerp(p,b,t),t));\n      }\n    }\n    points.push(route.at(-1));\n    return points;\n  }\n\n  function randomFrom(seed) {\n    let state = seed >>> 0;\n    return () => {\n      state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n      return state / 4294967296;\n    };\n  }\n\n  function checkedSeed(seed) {\n    if (!Number.isInteger(seed) || seed < 1 || seed > 9999) throw new Error('Choose a seed from 1 to 9999.');\n    return randomFrom(seed);\n  }\n\n  const { seed } = controls;\n  const random = checkedSeed(seed);\n  const width=17, levels=13, step=.375;\n  const id=(x,y,z)=>(x*width+y)*levels+z;\n  const xyz=i=>[Math.floor(i/(width*levels)),Math.floor(i/levels)%width,i%levels];\n  const world=i=>xyz(i).map((n,k)=>n*step-(k<2?3:0));\n  const anchors=feet.map(([x,y])=>id(x<0?0:width-1,y<0?0:width-1,0));\n  const count=width*width*levels;\n  const adjacent=Array.from({length:count},()=>[]);\n  const unitSteps=[[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]];\n  for(let i=0;i<count;i++){\n    const p=xyz(i);\n    if(p[2]===0) continue;\n    for(const d of unitSteps){\n      const [x,y,z]=add(p,d);\n      if(x>=0 && x<width && y>=0 && y<width && z>0 && z<levels) adjacent[i].push(id(x,y,z));\n    }\n  }\n  // Reserve separate arrival and departure cells at each ground contact.\n  // All births within the volume use the six face neighbours.\n  const reserved=new Map();\n  anchors.forEach((foot,n)=>{\n    const [x,y]=xyz(foot);\n    const ports=[[x+(x===0?1:-1),y,n-1],[x,y+(y===0?1:-1),n]];\n    for(const [u,v,span] of ports){\n      const near=id(u,v,1);\n      adjacent[foot].push(near);\n      adjacent[near].push(foot);\n      for(let z=1;z<levels;z++) reserved.set(id(u,v,z),span);\n    }\n  });\n  // Leave one unused layer reachable from the reserved ports on each\n  // subsequent span. This keeps earlier births from sealing off a later foot.\n  const available=(i,span)=>i%levels<=levels-3+span &&\n    (!reserved.has(i)||reserved.get(i)===span);\n  const phase=Array.from({length:6},()=>2*Math.PI*random());\n  const field=Array.from({length:count},(_,i)=>{\n    const [x,y,z]=xyz(i);\n    return Math.sin(.47*x+.24*y+.54*z+phase[0])\n      + .72*Math.sin(-.28*x+.61*y-.31*z+phase[1])\n      + .46*Math.cos(.37*x-.34*y+.44*z+phase[2]);\n  });\n  const born=new Uint8Array(count);\n  const distance=new Int16Array(count), queue=new Uint16Array(count);\n  function reachable(goal,span){\n    distance.fill(-1);\n    let size=1;\n    queue[0]=goal;\n    distance[goal]=0;\n    for(let k=0;k<size;k++){\n      const i=queue[k];\n      for(const j of adjacent[i]){\n        if(distance[j]>=0 || born[j] || j%levels===0 || !available(j,span)) continue;\n        distance[j]=distance[i]+1;\n        queue[size++]=j;\n      }\n    }\n  }\n  for(let span=0;span<3;span++){\n    const start=anchors[span], goal=anchors[span+1], budget=86;\n    const route=[world(start)], steps=[];\n    let tip=start, previous=-1;\n    born[start]=1;\n    for(let tick=0;tip!==goal;tick++){\n      reachable(goal,span);\n      const rest=budget-tick;\n      const p=xyz(tip), last=previous<0?[0,0,1]:sub(p,xyz(previous));\n      const [x,y,z]=p;\n      const drift=[\n        Math.sin(.44*y+.22*z+phase[3]),\n        Math.sin(.41*z-.28*x+phase[4]),\n        Math.cos(.38*x+.33*y+phase[5])\n      ];\n      const options=[];\n      for(const next of adjacent[tip]){\n        if(born[next] || distance[next]<0 || distance[next]>=rest) continue;\n        const q=xyz(next), heading=sub(q,p);\n        const crowd=adjacent[next].reduce((sum,j)=>sum+Number(j!==tip && born[j]),0);\n        const returnBias=.035+1.3*smooth((tick/budget-.58)/.42);\n        const score=1.12*dot(heading,drift)+.60*dot(heading,last)+.97*field[next]\n          + .16*q[2] + .32*random()-1.05*crowd-returnBias*distance[next]\n          - (next===goal && tick<budget*.70 ? 5 : 0);\n        options.push({next,score});\n      }\n      options.sort((a,b)=>b.score-a.score);\n      if(!options.length) throw new Error(`No remaining connected route at span ${span+1}, tick ${tick}.`);\n      previous=tip;\n      tip=options[0].next;\n      born[tip]=1;\n      const q=world(tip);\n      if(trace) steps.push({from:route.at(-1),chosen:q,candidates:options.map(({next})=>world(next)),index:tick+1});\n      route.push(q);\n    }\n    if(trace) trace.push({route,steps});\n    append(round(route,.36,2));\n  }\n\n  // Give overlapping passes room for the tube, leaving the four contacts fixed.\n  // The simulation decides the route. This step only moves near neighbours.\n  const gap = 0.185;\n  const fixed = path.map(p => p[2] === 0);\n  const stations = [0];\n  for (let i=1; i<path.length; i++) {\n    stations.push(stations.at(-1)+Math.hypot(...sub(path[i],path[i-1])));\n  }\n\n  function separate(i,j) {\n    const a=path[i], b=path[i+1], c=path[j], d=path[j+1];\n    for(let k=0;k<3;k++){\n      if(Math.min(a[k],b[k])-Math.max(c[k],d[k])>gap || Math.min(c[k],d[k])-Math.max(a[k],b[k])>gap) return false;\n    }\n    const u=sub(b,a), v=sub(d,c), r=sub(a,c);\n    const uu=dot(u,u), vv=dot(v,v), uv=dot(u,v);\n    const ru=dot(r,u), rv=dot(r,v), den=uu*vv-uv*uv;\n    let s=den>1e-10 ? clamp((uv*rv-vv*ru)/den,0,1) : 0;\n    let t=(uv*s+rv)/vv;\n    if(t<0 || t>1){\n      t=clamp(t,0,1);\n      s=clamp((uv*t-ru)/uu,0,1);\n    }\n    const delta=sub(add(r,scale(u,s)),scale(v,t));\n    const distance=Math.hypot(...delta);\n    if(distance>=gap) return false;\n    const direction=distance>1e-7 ? scale(delta,1/distance) : [1,0,0];\n    const weights=[1-s,s,t-1,-t].map((weight,k)=>fixed[[i,i+1,j,j+1][k]] ? 0 : weight);\n    const total=dot(weights,weights);\n    if(total<1e-8) return false;\n    const shift=scale(direction,0.72*(gap-distance)/total);\n    [i,i+1,j,j+1].forEach((k,n)=>{\n      if(fixed[k]) return;\n      const [x,y,z]=add(path[k],scale(shift,weights[n]));\n      path[k]=clip([x,y,z]);\n    });\n    return true;\n  }\n\n  for(let iteration=0;iteration<32;iteration++){\n    let moved=false;\n    for(let i=0;i<path.length-1;i++){\n      for(let j=i+2;j<path.length-1;j++){\n        if(stations[j]-stations[i+1]>.30 && separate(i,j)) moved=true;\n      }\n    }\n    if(!moved) break;\n  }\n\n  const curve = path;\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:'#3954b5',vertices,faces}]\n  };\n}\n"
          },
          "controls": {
            "value": {
              "seed": 11
            }
          }
        }
      },
      {
        "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": -55
          },
          "elevation": {
            "value": 25
          },
          "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 one architectural concept image based on the supplied 3D viewer reference. Match its camera and preserve the exact continuous blue tube, including every curve where it forms the enclosure, folds overhead, and reaches the ground. Keep the spaces between the folds visible. Show matte powder-coated steel on paving in a public garden in daylight. Keep the geometry fixed and change materials, light and surroundings. No text or extra architecture."
          }
        }
      }
    ]
  }
}
