{
  "format": "relay-web-workflow",
  "version": 1,
  "title": "One-Line Pavilion · Vector Field 3D · Ground to Ground",
  "request": "Integrate a line through overlapping oblique vortices. Curl, radial attraction and axial drift make it coil, stretch and return to the site. Sweep a single 100 mm tube through the four shared corner contacts on a 6 × 6 m grid.",
  "workflow": {
    "schema_version": 1,
    "kind": "workflow",
    "inputs": {},
    "tasks": [
      {
        "schema_version": 3,
        "id": "pavilion",
        "operation": "code.javascript",
        "objective": "Integrate a line through overlapping oblique vortices. Curl, radial attraction and axial drift make it coil, stretch and return to the site.",
        "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  const { curl } = controls;\n  if (curl!==7 && curl!==5) throw new Error('Choose curl setting 5 or 7.');\n  const vortices=[\n    {center:[-.96,-1.5,2.25],axis:unit([1,.38,.76]),hand:1},\n    {center:[1.36,-.48,2.61],axis:unit([.35,1,.49]),hand:-1},\n    {center:[.55,1.13,2.97],axis:unit([-.74,.44,1]),hand:1},\n    {center:[-1.63,.59,2.05],axis:unit([.21,1,-.67]),hand:-1}\n  ];\n  for(let span=0;span<3;span++){\n    const start=feet[span], end=feet[span+1];\n    const count=144, dt=6.8/count;\n    function vortex(p,t,{center,axis,hand}){\n      const shift=scale(axis,.76*Math.sin(4.6*t+1.3*span));\n      const offset=sub(p,add(center,shift));\n      const axial=dot(offset,axis);\n      const radial=sub(offset,scale(axis,axial));\n      const r=length(radial);\n      const orbit=scale(cross(axis,radial), hand*curl*.78/(1+.22*r*r));\n      const draw=scale(unit(radial),1.65*(1.08+.26*Math.sin(13*t)-r));\n      const glide=scale(axis,.70*Math.cos(7*t+span)-1.15*axial);\n      return add(add(orbit,draw),glide);\n    }\n    function field(p,t){\n      const share=smooth((t-.25)/.48);\n      const first=vortex(p,t,vortices[span]);\n      const second=vortex(p,t,vortices[span+1]);\n      const inside=[-1.1*Math.sign(p[0])*Math.max(0,Math.abs(p[0])-2.35),-1.1*Math.sign(p[1])*Math.max(0,Math.abs(p[1])-2.35),.8*Math.max(0,.65-p[2])-.9*Math.max(0,p[2]-4.45)];\n      return add(lerp(first,second,share),inside);\n    }\n    const route=[start], vectors=[];\n    let p=start;\n    for(let i=0;i<count;i++){\n      const t=i/count;\n      const a=field(p,t);\n      const b=field(add(p,scale(a,dt/2)),t+.5/count);\n      const c=field(add(p,scale(b,dt/2)),t+.5/count);\n      const d=field(add(p,scale(c,dt)),t+1/count);\n      p=clip(add(p,scale(add(add(a,d),scale(add(b,c),2)),dt/6)));\n      const blend=smooth(((i+1)/count-.71)/.29);\n      const q=i===count-1 ? end : lerp(p,end,blend);\n      route.push(q);\n      if(trace && i%8===0){\n        for(const delta of [[0,0,0],[0,.65,0],[0,-.65,0],[0,0,.7]]){\n          const q=add(p,delta);\n          vectors.push([q,add(q,scale(field(q,t),.135))]);\n        }\n      }\n    }\n    if(trace) trace.push({route,vectors});\n    append(route.filter((p,i)=>i===0||i===route.length-1||length(sub(p,route[i-1]))>.026));\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": {
              "curl": 7
            }
          }
        }
      },
      {
        "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."
          }
        }
      }
    ]
  }
}
