@floless/app 0.47.0 → 0.48.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/floless-server.cjs +598 -634
- package/dist/schemas/steel.takeoff.v1.schema.json +20 -2
- package/dist/web/steel-editor.html +41 -5
- package/package.json +1 -1
- package/dist/skills/floless-app-steel-from-drawings/SKILL.md +0 -120
- package/dist/templates/steel-from-drawings.flo +0 -73
- package/dist/templates/steel-to-ifc.flo +0 -294
- package/dist/templates/steel-to-tekla.flo +0 -293
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"plans": { "type": "array", "items": { "$ref": "#/$defs/plan" } },
|
|
40
40
|
"filter": { "$ref": "#/$defs/filter", "description": "Layer/thickness/colour filter pre-stage. `sheets[]` holds per-framing-plan geometry; the facets + `mode` + on-flag selection are GLOBAL across sheets. The saved selection scopes which lines the reader binds profile labels to per sheet; replaces the length heuristic, fallback when absent. CONFIDENTIAL: each sheet's `page.bg_b64` is machine-local, never committed." },
|
|
41
|
+
"connections": { "type": "array", "items": { "$ref": "#/$defs/connection" }, "description": "Vendor-neutral connection library: each row maps a connection type → the firm's design detail# → per-platform component ids. Member ends/columns reference a row by id via `ends[].conn` / `col.conn` (legacy `note`/`detail` = fallback when `conn` is empty)." },
|
|
41
42
|
"dims3d": { "type": "array", "items": { "$ref": "#/$defs/dim3" }, "description": "Draft-only 3D dimensions (editor annotations, model-global). World-scene mm. NOT baked into the lock / 3D scene / IFC / BOM." }
|
|
42
43
|
},
|
|
43
44
|
"$defs": {
|
|
@@ -164,6 +165,19 @@
|
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
},
|
|
168
|
+
"connection": {
|
|
169
|
+
"type": "object",
|
|
170
|
+
"required": ["id", "type"],
|
|
171
|
+
"additionalProperties": true,
|
|
172
|
+
"description": "One connection-library row. `detail` is the firm's VENDOR-NEUTRAL design detail#; `targets` is an open map platform→component id (tekla/sds2/revit/…), never hardcoded to one platform.",
|
|
173
|
+
"properties": {
|
|
174
|
+
"id": { "type": "string", "description": "Stable row id, e.g. cx-moment-1." },
|
|
175
|
+
"type": { "type": "string", "description": "moment | drag | shear | braced | pinned | fixed (free string allowed)." },
|
|
176
|
+
"detail": { "type": "string", "description": "Design detail number, e.g. 50S504 — the firm's document, platform-neutral." },
|
|
177
|
+
"targets": { "type": "object", "additionalProperties": { "type": "string" }, "description": "platform → component id, e.g. { \"tekla\": \"146\" }. Open map; add sds2/revit freely." },
|
|
178
|
+
"source": { "enum": ["company-rules", "user", "drawing"], "description": "Where the row came from (drives the connection confidence band)." }
|
|
179
|
+
}
|
|
180
|
+
},
|
|
167
181
|
"point2": {
|
|
168
182
|
"type": "array",
|
|
169
183
|
"items": { "type": "number" },
|
|
@@ -313,7 +327,9 @@
|
|
|
313
327
|
"tos": { "type": ["number", "null"], "description": "Top-of-steel elevation in inches at this end." },
|
|
314
328
|
"note": { "type": "string", "description": "Connection note (e.g. moment, shear, braced)." },
|
|
315
329
|
"tosDef": { "type": "boolean", "description": "True = follows the plan default; false = read from a drawing callout or set by hand." },
|
|
316
|
-
"detail": { "type": "string", "description": "Connection detail reference, e.g. \"5-S504\"." }
|
|
330
|
+
"detail": { "type": "string", "description": "Connection detail reference, e.g. \"5-S504\"." },
|
|
331
|
+
"conn": { "type": "string", "description": "Connection-library row id this end uses (references `connections[].id`). Empty = fall back to `note`/`detail`." },
|
|
332
|
+
"connVerified": { "type": "boolean", "description": "True = a human confirmed this end's connection (scores Verified)." }
|
|
317
333
|
}
|
|
318
334
|
},
|
|
319
335
|
"col": {
|
|
@@ -323,7 +339,9 @@
|
|
|
323
339
|
"bos": { "type": ["number", "null"], "description": "Bottom-of-steel elevation in inches." },
|
|
324
340
|
"tos": { "type": ["number", "null"], "description": "Top-of-steel elevation in inches." },
|
|
325
341
|
"note": { "type": "string" },
|
|
326
|
-
"detail": { "type": "string" }
|
|
342
|
+
"detail": { "type": "string" },
|
|
343
|
+
"conn": { "type": "string", "description": "Connection-library row id for this column's connection (references `connections[].id`)." },
|
|
344
|
+
"connVerified": { "type": "boolean", "description": "True = a human confirmed this column's connection (scores Verified)." }
|
|
327
345
|
}
|
|
328
346
|
}
|
|
329
347
|
}
|
|
@@ -891,7 +891,7 @@ function panel(){
|
|
|
891
891
|
else if(allBeam){const s0=beams.map(m=>m.ends[0]),s1=beams.map(m=>m.ends[1]);
|
|
892
892
|
wMTos('tosA',s0);wMText('ntA',s0,(o,v)=>o.note=v);wMText('dtA',s0,(o,v)=>o.detail=v);
|
|
893
893
|
wMTos('tosB',s1);wMText('ntB',s1,(o,v)=>o.note=v);wMText('dtB',s1,(o,v)=>o.detail=v);
|
|
894
|
-
{const me=document.getElementById('matchEnds');if(me)me.onclick=()=>edit(()=>{for(const m of selArr())if(m.role!=='column')m.ends[1]={tos:m.ends[0].tos,note:m.ends[0].note,tosDef:m.ends[0].tosDef,detail:m.ends[0].detail};});}}
|
|
894
|
+
{const me=document.getElementById('matchEnds');if(me)me.onclick=()=>edit(()=>{for(const m of selArr())if(m.role!=='column')m.ends[1]={tos:m.ends[0].tos,note:m.ends[0].note,tosDef:m.ends[0].tosDef,detail:m.ends[0].detail,conn:m.ends[0].conn,connVerified:m.ends[0].connVerified};});}}
|
|
895
895
|
return;}
|
|
896
896
|
const m=ensureMeta(arr[0]), wpf=WT[m.profile], L=(len(m.wp[0],m.wp[1])/FT).toFixed(1), col=(m.role==='column'), pos=posDefault(m);
|
|
897
897
|
const _lvl=({'S-202':'second','S-203':'roof'})[P.sheet];
|
|
@@ -951,7 +951,7 @@ function panel(){
|
|
|
951
951
|
wireTos('tosB',m.ends[1]);
|
|
952
952
|
document.getElementById('ntB').onchange=e=>edit(()=>{m.ends[1].note=e.target.value;});
|
|
953
953
|
wireDet('dtB',m.ends[1]);
|
|
954
|
-
document.getElementById('matchEnds').onclick=()=>edit(()=>{m.ends[1]={tos:m.ends[0].tos,note:m.ends[0].note,tosDef:m.ends[0].tosDef,detail:m.ends[0].detail};});
|
|
954
|
+
document.getElementById('matchEnds').onclick=()=>edit(()=>{m.ends[1]={tos:m.ends[0].tos,note:m.ends[0].note,tosDef:m.ends[0].tosDef,detail:m.ends[0].detail,conn:m.ends[0].conn,connVerified:m.ends[0].connVerified};});
|
|
955
955
|
}
|
|
956
956
|
document.getElementById('del').onclick=()=>edit(()=>{P.members=P.members.filter(x=>x.id!==m.id);selIds.clear();if(selDimIds.size){P.dims=P.dims.filter(d=>!selDimIds.has(d.id));selDimIds.clear();}}); // also drop any dims a mixed marquee selected
|
|
957
957
|
{const vb=document.getElementById('verifyBtn');if(vb)vb.onclick=()=>edit(()=>{m.verified=!m.verified;});}
|
|
@@ -1527,7 +1527,29 @@ function _mTons(m,ptPerFt){const plf=_wt(m.profile);if(plf==null)return 0;let ft
|
|
|
1527
1527
|
return ft*plf/2000;}
|
|
1528
1528
|
function _detSheet(t){const m=String(t||'').toUpperCase().match(/S-?\s?\d{2,3}/);return m?m[0].replace(/\s/g,''):null;}
|
|
1529
1529
|
const _cnt0=()=>({verified:0,high:0,med:0,low:0,rfi:0});
|
|
1530
|
+
function _scoreConnections(){
|
|
1531
|
+
const rows={};for(const c of (C.connections||[]))if(c&&c.id)rows[c.id]=c;
|
|
1532
|
+
const bandEnd=(end)=>{const row=end.conn?rows[end.conn]:null;
|
|
1533
|
+
const complete=!!(row&&row.type&&row.detail&&row.targets&&Object.keys(row.targets).length>0);
|
|
1534
|
+
const hasNote=!!(end.note&&end.note.trim()!=='');
|
|
1535
|
+
// Only a drawing callout counts as evidence — a company-rules/user row assignment is a mapping, not drawing-confirmed.
|
|
1536
|
+
const evidence=hasNote||(row&&row.source==='drawing');
|
|
1537
|
+
if(end.connVerified===true)return{band:'verified',reason:''};
|
|
1538
|
+
if(!end.conn&&!hasNote)return{band:'rfi',reason:'no connection assigned'};
|
|
1539
|
+
if(complete&&evidence)return{band:'high',reason:''};
|
|
1540
|
+
if(complete)return{band:'med',reason:'connection type assumed (no drawing note)'};
|
|
1541
|
+
if(hasNote&&end.detail&&end.detail.trim()!=='')return{band:'med',reason:'free-text note + detail (not a mapped library row)'};
|
|
1542
|
+
if(end.conn&&!row)return{band:'low',reason:'references an unknown connection row'};
|
|
1543
|
+
return{band:'low',reason:row?'library row has a type only (no detail/component id)':'connection type only (no detail)'};};
|
|
1544
|
+
const byEnd=[];
|
|
1545
|
+
(C.plans||[]).forEach((plan,pi)=>{for(const m of (plan.members||[])){
|
|
1546
|
+
if(m.role==='column'){const r=bandEnd(m.col||{});byEnd.push({id:m.id+'#col',pi,sheet:plan.sheet||'',memberId:m.id,which:'col',band:r.band,reason:r.reason});}
|
|
1547
|
+
else{const ends=m.ends||[];for(const w of [0,1]){const r=bandEnd(ends[w]||{});byEnd.push({id:m.id+'#'+w,pi,sheet:plan.sheet||'',memberId:m.id,which:w,band:r.band,reason:r.reason});}}}});
|
|
1548
|
+
const counts=_cnt0();let num=0,den=0;
|
|
1549
|
+
for(const e of byEnd){counts[e.band]++;if(e.band==='rfi')continue;num+=BANDW[e.band];den+=1;}
|
|
1550
|
+
return{cat:{score:den>0?Math.round(num/den*100):null,tons:0,counts},ends:byEnd};}
|
|
1530
1551
|
function scoreContractJS(){const plans=C.plans||[];const byMember=[],byDetail=[];let segs=0;
|
|
1552
|
+
const _conn=_scoreConnections();
|
|
1531
1553
|
const known=new Set();for(const p of plans)if(p.sheet)known.add(p.sheet.toUpperCase());for(const s of Object.keys(C.detail_bubbles||{}))known.add(s.toUpperCase());
|
|
1532
1554
|
plans.forEach((plan,pi)=>{segs+=(plan.segments||[]).length;const ms=plan.members||[];const dup=_confDupIds(ms);
|
|
1533
1555
|
for(const m of ms){const r=_scoreMember(m,dup);byMember.push({id:m.id,planIdx:pi,sheet:plan.sheet||'',role:m.role==='column'?'column':'beam',profile:m.profile||'',band:r.band,tons:r.band==='rfi'?0:_mTons(m,plan.pt_per_ft),factors:r.factors});}
|
|
@@ -1536,7 +1558,7 @@ function scoreContractJS(){const plans=C.plans||[];const byMember=[],byDetail=[]
|
|
|
1536
1558
|
const dRoll=arr=>{const c=_cnt0();let n=0,d=0;for(const x of arr){c[x.band]++;n+=BANDW[x.band];d++;}return {score:d>0?Math.round(n/d*100):null,tons:0,counts:c};};
|
|
1537
1559
|
const beams=roll(byMember.filter(m=>m.role==='beam'));const columns=roll(byMember.filter(m=>m.role==='column'));const details=dRoll(byDetail);
|
|
1538
1560
|
const sc=byMember.filter(m=>m.band!=='rfi');const n=sc.reduce((s,m)=>s+m.tons*BANDW[m.band],0),d=sc.reduce((s,m)=>s+m.tons,0);
|
|
1539
|
-
return {byMember,byDetail,byCategory:{beams,columns,details,connections:
|
|
1561
|
+
return {byMember,byDetail,byConnection:_conn.ends,byCategory:{beams,columns,details,connections:_conn.cat},coverage:{members:byMember.length,segments:segs,note:'Approximate in v1 — precise per-segment binding is Slice 2.'},overall:{score:d>0?Math.round(n/d*100):null,tons:d,rfiCount:byMember.filter(m=>m.band==='rfi').length}};}
|
|
1540
1562
|
function bandColorForPct(p){return p==null?'var(--mut)':p>=95?BANDC.verified:p>=80?BANDC.high:p>=50?BANDC.med:BANDC.low;}
|
|
1541
1563
|
// Effective target: the user's per-read override (in the contract) wins; else the app's
|
|
1542
1564
|
// target_confidence input default (read in boot()); else none. Editing the override never touches
|
|
@@ -1569,7 +1591,7 @@ function renderConf(){const s=scoreContractJS();const cat=s.byCategory;
|
|
|
1569
1591
|
'<div class=cc-score style="color:'+(stub?'var(--mut)':bandColorForPct(cs.score))+'">'+(stub||cs.score==null?'—':cs.score+'%')+'</div>'+
|
|
1570
1592
|
_bar(stub?null:cs.score)+
|
|
1571
1593
|
'<div class=cc-counts>'+(stub?'<span>reserved slot</span>':_countsHtml(cs.counts))+'</div></div>';
|
|
1572
|
-
document.getElementById('confCats').innerHTML=card('beams','Beams',cat.beams)+card('columns','Columns',cat.columns)+card('details','Details',cat.details)+card('connections','Connections',cat.connections
|
|
1594
|
+
document.getElementById('confCats').innerHTML=card('beams','Beams',cat.beams)+card('columns','Columns',cat.columns)+card('details','Details',cat.details)+card('connections','Connections',cat.connections);
|
|
1573
1595
|
const bands=['all','verified','high','med','low','rfi'],cats=['all','beams','columns','details','connections'];
|
|
1574
1596
|
document.getElementById('confFilter').innerHTML=
|
|
1575
1597
|
'<span class=glab>Band</span><span class=grp>'+bands.map(b=>'<button data-band="'+b+'" class="'+(confBand===b?'on':'')+'">'+(b==='all'?'All':BANDLAB[b])+'</button>').join('')+'</span>'+
|
|
@@ -1579,7 +1601,21 @@ function renderConf(){const s=scoreContractJS();const cat=s.byCategory;
|
|
|
1579
1601
|
wireConf();}
|
|
1580
1602
|
function confBodyHtml(s){
|
|
1581
1603
|
if(!s.byMember.length&&!s.byDetail.length)return '<div class=hint style="padding:14px 0">No members in the takeoff yet.</div>';
|
|
1582
|
-
if(confCat==='connections')
|
|
1604
|
+
if(confCat==='connections'){
|
|
1605
|
+
// worst-first so the RFIs / unmapped ends surface at the top (the "what to fix next" view)
|
|
1606
|
+
const _ord={rfi:0,low:1,med:2,high:3,verified:4};
|
|
1607
|
+
let conn=s.byConnection.slice().sort((a,b)=>_ord[a.band]-_ord[b.band]);
|
|
1608
|
+
if(confBand!=='all')conn=conn.filter(e=>e.band===confBand);
|
|
1609
|
+
if(!conn.length)return '<div class=hint style="padding:14px 0">Nothing matches this filter.</div>';
|
|
1610
|
+
const whichLabel=w=>w==='col'?'Column':w===0?'End 1':'End 2';
|
|
1611
|
+
let h='<table class=ftab><thead><tr><th>Band</th><th>Member</th><th>End</th><th>Sheet</th><th>Reason</th><th></th></tr></thead><tbody>';
|
|
1612
|
+
conn.forEach(e=>{h+='<tr><td><span class=pill style="'+BANDPILL[e.band]+'">'+BANDLAB[e.band]+'</span></td>'+
|
|
1613
|
+
'<td>'+esc(e.memberId)+'</td>'+
|
|
1614
|
+
'<td>'+whichLabel(e.which)+'</td>'+
|
|
1615
|
+
'<td>'+esc(e.sheet)+'</td>'+
|
|
1616
|
+
'<td><span class=hint>'+esc(e.reason||'—')+'</span></td>'+
|
|
1617
|
+
'<td><button class=ghost data-loc="'+esc(e.memberId)+'" data-pi="'+e.pi+'">Locate</button></td></tr>';});
|
|
1618
|
+
h+='</tbody></table>';return h;}
|
|
1583
1619
|
let mem=s.byMember.slice();
|
|
1584
1620
|
if(confCat==='beams')mem=mem.filter(m=>m.role==='beam');else if(confCat==='columns')mem=mem.filter(m=>m.role==='column');else if(confCat==='details')mem=[];
|
|
1585
1621
|
if(confBand!=='all')mem=mem.filter(m=>m.band===confBand);
|
package/package.json
CHANGED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: floless-app-steel-from-drawings
|
|
3
|
-
description: This skill should be used to turn a structural steel drawing into an interactive 3D model in floless.app — the "Steel from Drawings" reader. Triggers on a pending floless `rebake` request for the `steel-from-drawings` app (queued from its "Re-read & re-bake ▸" button), or asks like "read this framing plan into 3D", "turn this steel drawing into a model", "bake my drawing into steel-from-drawings". It teaches the host AI to READ a steel drawing (PDF/image) with its own vision, extract a schematic 3D scene (grids, members, sizes, takeoff) in the viewer-3d scene schema, bake it INLINE into the app's `view` node, recompile, and ask the user to approve — all at COMPOSE time (no model in the run path, no API key). Also bakes the same scene into the `steel-to-ifc` companion (universal IFC file) and the `steel-to-tekla` companion (native Tekla parts via the Open API, needs Tekla open). Triggers also on "export this to IFC", "bake to Tekla/SDS2/Revit", "bake into Tekla".
|
|
4
|
-
metadata:
|
|
5
|
-
version: 0.1.0
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Steel from Drawings — read a steel drawing into an interactive 3D model
|
|
9
|
-
|
|
10
|
-
## What this is
|
|
11
|
-
|
|
12
|
-
`steel-from-drawings` is a FloLess app that renders an interactive, **licence-free 3D model** of a
|
|
13
|
-
steel frame (the `viewer-3d` agent) from a scene that **you, the terminal AI, bake from a drawing**.
|
|
14
|
-
The drawing is read **at compose time** with your own vision — there is **no model in the run path
|
|
15
|
-
and no AI API key**: the brain is you, in the terminal (the FloLess thin-UI contract). This is the
|
|
16
|
-
**bake** strategy (`floless-app-rebake` is the generic version; this skill is the steel-specific one,
|
|
17
|
-
and **for `steel-from-drawings` it supersedes the generic re-bake** — you bake the scene INLINE into
|
|
18
|
-
the `.flo`'s `view` node, not into `config.yaml`).
|
|
19
|
-
|
|
20
|
-
**Bake-out companions.** Two sibling apps consume the SAME scene: **`steel-to-ifc`** writes a
|
|
21
|
-
universal **IFC** file (open in Tekla, SDS2, Revit, Navisworks; no Tekla needed), and
|
|
22
|
-
**`steel-to-tekla`** creates **native Tekla members** directly via the Open API (needs Tekla open).
|
|
23
|
-
When you bake a scene, bake the *identical* scene into whichever of these are installed (loop steps
|
|
24
|
-
6–7) so every output matches the 3D the user confirmed. The whole loop: **read the drawing → see it
|
|
25
|
-
in 3D (`steel-from-drawings`) → bake it out (an IFC file and/or native Tekla parts) → finish in your tool.**
|
|
26
|
-
|
|
27
|
-
> **Pasted a request?** If the user pastes a message beginning with a `[floless-request type=rebake
|
|
28
|
-
> id=…]` marker, that's a request copied from the FloLess Dashboard — resolve it via
|
|
29
|
-
> `GET /api/requests` (match the id), don't run the pasted line verbatim, and `DELETE` it when done.
|
|
30
|
-
|
|
31
|
-
## The loop
|
|
32
|
-
|
|
33
|
-
1. **Find the drawing.** From a `rebake` request: `GET http://localhost:<port>/api/requests` → the
|
|
34
|
-
entry with `type:"rebake"` and `appId:"steel-from-drawings"` carries `snapshots: ["<abs path>"]`.
|
|
35
|
-
(Or the user just attaches/points at a drawing in prose — same procedure, skip the lookup.)
|
|
36
|
-
2. **Read the drawing with your own vision.** Open the file (PDF or image) and READ it. If it's a
|
|
37
|
-
large multi-sheet PDF, focus on one framing plan / GA sheet (say which). This is the compose-time
|
|
38
|
-
extraction — your vision is the reader, not any API.
|
|
39
|
-
3. **Extract a schematic 3D scene** in the schema below (§ Scene schema), following § Reading a steel
|
|
40
|
-
drawing. Aim for a complete, plausible stick model — the user confirms/nudges it in 3D.
|
|
41
|
-
4. **Bake it INLINE into the app.** Replace the entire `scene:` mapping under the `view` node's
|
|
42
|
-
`config:` in the app's `.flo` with your extracted scene — in BOTH the installed copy
|
|
43
|
-
`~/.aware/apps/steel-from-drawings/steel-from-drawings.flo` AND the editable
|
|
44
|
-
`demos/steel-from-drawings/steel-from-drawings.flo` if present. Change ONLY the `scene:` block;
|
|
45
|
-
leave `inputs`, `requires`, the node id/agent/command, and everything else identical.
|
|
46
|
-
5. **Recompile.** `aware app compile ~/.aware/apps/steel-from-drawings` → rewrites the `.lock`. The
|
|
47
|
-
source-hash changes, so FloLess's Run gate disarms until the user approves. Copy the fresh `.lock`
|
|
48
|
-
to `demos/steel-from-drawings/` if you keep the editable copy in sync.
|
|
49
|
-
6. **Bake the SAME scene into `steel-to-ifc` (the IFC bake-out), if it's installed.** Check
|
|
50
|
-
`~/.aware/apps/steel-to-ifc/`. If present, replace the `scene:` mapping under that app's `bake-ifc`
|
|
51
|
-
node `config` (the `ifc` agent's `scene` input — NOT `config.args`; the app now calls the host-free
|
|
52
|
-
`ifc` AWARE agent, not an exec) with the IDENTICAL scene (the installed copy and `demos/steel-to-ifc/`
|
|
53
|
-
if present), then `aware app compile ~/.aware/apps/steel-to-ifc`. This keeps the exported IFC matching
|
|
54
|
-
the 3D the user confirmed. (`steel-to-ifc` writes a real IFC file — IfcColumn/IfcBeam on the grid —
|
|
55
|
-
openable in Tekla, SDS2, Revit or Navisworks; it never touches a Tekla model, so no Tekla needed.)
|
|
56
|
-
7. **Bake the SAME scene into `steel-to-tekla` (native Tekla parts), if it's installed.** Check
|
|
57
|
-
`~/.aware/apps/steel-to-tekla/`. If present, replace the `scene:` under that app's `bake-tekla` node
|
|
58
|
-
`config` (the `tekla` `bake-scene` verb's `scene` input — NOT `config.args`; the app now calls the
|
|
59
|
-
first-class `bake-scene` verb, not an exec) with the IDENTICAL scene (installed + `demos/steel-to-tekla/`), set `config.version`
|
|
60
|
-
to the user's RUNNING Tekla version (e.g. "2026.0" — the Open API connects to a version-specific
|
|
61
|
-
channel, so a wrong version silently finds no model), then `aware app compile
|
|
62
|
-
~/.aware/apps/steel-to-tekla`. It creates a real Tekla member per element via the Open API (mode:
|
|
63
|
-
write, snapshot — Undo reverts the whole bake); needs Tekla OPEN with a model. Profiles fall back
|
|
64
|
-
to a sized rectangular section when the environment lacks the drawing's catalogue profile.
|
|
65
|
-
8. **Hand the approval to the user.** Tell them what you read (e.g. "read 6 columns + 8 beams on grids
|
|
66
|
-
A–C / 1–2, heights assumed 4.5 m — review in Inspect → Code, then Compile/Approve, then ▶ Run to
|
|
67
|
-
see the 3D; run **steel-to-ifc** for an IFC file or **steel-to-tekla** for native Tekla parts").
|
|
68
|
-
The human eyeballing the read in 3D **is the safety gate**. Never auto-run.
|
|
69
|
-
9. **Clear the request.** `DELETE http://localhost:<port>/api/requests/<id>` once done (if from one).
|
|
70
|
-
|
|
71
|
-
## Scene schema (the bake target — exactly what `viewer-3d` renders)
|
|
72
|
-
|
|
73
|
-
```yaml
|
|
74
|
-
scene:
|
|
75
|
-
meta: { name: "<sheet title / drawing no.>", units: mm, up: z }
|
|
76
|
-
groups: # one per group you use; drives legend + colour
|
|
77
|
-
- { key: column, label: Columns, color: "#60a5fa" }
|
|
78
|
-
- { key: beam, label: Beams, color: "#94a3b8" }
|
|
79
|
-
# rafter #38bdf8, brace #818cf8 — add others as needed
|
|
80
|
-
elements: # one per member, kind: box (a from→to bar)
|
|
81
|
-
- { id: COL-A1, group: column, kind: box, from: [x,y,z], to: [x,y,z],
|
|
82
|
-
section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", mark: "C1", grid: "A-1" } }
|
|
83
|
-
grids: # grid labels placed just outside the framing
|
|
84
|
-
- { label: A, at: [x,y,0] }
|
|
85
|
-
panels: # generic side tables; build a Takeoff
|
|
86
|
-
- title: Takeoff
|
|
87
|
-
note: "<assumptions, e.g. heights assumed — plan view, H=4.5 m>"
|
|
88
|
-
columns: [Section, "No.", Length]
|
|
89
|
-
rows: [["HSS6x6x3/8", 6, "27.0 m"]]
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
`from`/`to` are `[x,y,z]` in mm. `meta` is free-form (shown in the click-to-inspect panel) — put
|
|
93
|
-
`profile`, `mark`, `grid` there. `section.w`/`d` are visual thickness only.
|
|
94
|
-
|
|
95
|
-
## Reading a steel drawing (the method)
|
|
96
|
-
|
|
97
|
-
1. **Grid.** Find the grid (A,B,C… one way, 1,2,3… the other, plus half-grids). Read the dimension
|
|
98
|
-
strings to place grids in mm; if a spacing is unreadable, assume a regular ~6000 mm bay and say so.
|
|
99
|
-
2. **Sheet type & heights.** A PLAN view (top-down) shows no heights — pick one eave/storey height H
|
|
100
|
-
(a level/height note if present, else ~4500 mm) and note it; columns run z:0→H, beams/rafters sit
|
|
101
|
-
at z:H. An ELEVATION/SECTION gives real heights — read them.
|
|
102
|
-
3. **Members.** One `elements` entry per member, size label exactly as drawn. Columns → `column`,
|
|
103
|
-
`from:[gx,gy,0] to:[gx,gy,H]`. Beams/rafters → `beam`/`rafter`, along the grid at z:H. Braces →
|
|
104
|
-
`brace`, the diagonal. Put the size in `meta.profile`, the mark in `meta.mark`, the grid ref in
|
|
105
|
-
`meta.grid`; approximate `section.w`/`d` from the profile (W12≈300×310, HSS6x6≈150×150, L≈90×90).
|
|
106
|
-
4. **Takeoff.** One row per distinct profile (count + total length in m). Assumptions in the note.
|
|
107
|
-
5. **meta.name** = the sheet title / drawing number. Never invent grids or members the drawing does
|
|
108
|
-
not show; if a sheet has no readable steel framing, bake an empty `elements` with a note saying so.
|
|
109
|
-
|
|
110
|
-
## Guardrails
|
|
111
|
-
|
|
112
|
-
- **Compose-time only.** The drawing is read HERE, by you, then baked. Never add a runtime node that
|
|
113
|
-
reads the drawing — `aware app validate` rejects it and it breaks determinism.
|
|
114
|
-
- **Scene, not logic.** Re-bake only the `view.scene` block. A different output (new columns, a
|
|
115
|
-
schedule) is a workflow change — use `floless-app-workflows`, not this.
|
|
116
|
-
- **Schematic + honest.** ~90% is the goal; the user tidies the last 10% in 3D. State your
|
|
117
|
-
assumptions (heights, assumed bays) in the takeoff note. Don't fabricate sizes the drawing lacks.
|
|
118
|
-
- **Thin-UI contract.** The browser only recorded intent + the drawing; the brain is you, in the
|
|
119
|
-
terminal. The UI never reads the drawing and never bakes.
|
|
120
|
-
- **Resolve the port** from the running floless.app the same way the other floless-app skills do.
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
app: steel-from-drawings
|
|
2
|
-
version: 0.1.0
|
|
3
|
-
display-name: Steel from Drawings
|
|
4
|
-
description: |
|
|
5
|
-
Turn a structural steel drawing into an interactive, licence-free 3D model — no Tekla, no
|
|
6
|
-
modelling, no AI API key. Your terminal AI (Claude Code / Codex) reads the drawing once and
|
|
7
|
-
bakes a schematic 3D scene into this app; the viewer-3d agent renders it as an interactive
|
|
8
|
-
document anyone can orbit, click to inspect, and take off (counts + length by section) — so an
|
|
9
|
-
estimator, PM, fabricator or client can understand and price the steel before opening a seat.
|
|
10
|
-
|
|
11
|
-
The reading is a COMPOSE-TIME act — the FloLess way: the app is the prompt in your terminal, so
|
|
12
|
-
the brain is your AI terminal, never this server, and there is never a model in the run path.
|
|
13
|
-
Attach a drawing and use "Re-read & re-bake ▸" (or just ask your terminal AI to read it). The AI
|
|
14
|
-
reads the grids, members and sizes and bakes them straight into the `view` node's `scene` below
|
|
15
|
-
(the viewer-3d scene schema), recompiles, and asks you to approve; the deterministic run then
|
|
16
|
-
only renders the approved scene. Ships with a small example scene so it renders out of the box —
|
|
17
|
-
the `floless-app-steel-from-drawings` skill teaches your terminal AI how to read your own drawing in.
|
|
18
|
-
exposes-as-agent: false
|
|
19
|
-
inputs:
|
|
20
|
-
# BAKED at compose time: the terminal AI reads this drawing and writes the `view.scene` block
|
|
21
|
-
# below (the run never reads the drawing). read-strategy: bake surfaces "Re-read & re-bake ▸" so
|
|
22
|
-
# you can swap the drawing and have your AI re-read it. No runtime input node.
|
|
23
|
-
drawing:
|
|
24
|
-
type: image
|
|
25
|
-
widget: file
|
|
26
|
-
accept: [pdf, png, jpg]
|
|
27
|
-
read-strategy: bake
|
|
28
|
-
description: A structural steel drawing (framing plan / GA / elevation) — baked into the scene below; swap it to re-read & re-bake.
|
|
29
|
-
requires:
|
|
30
|
-
- viewer-3d@0.1.x
|
|
31
|
-
layout: linear
|
|
32
|
-
nodes:
|
|
33
|
-
# The viewer-3d render node IS the terminal. Ships with a small EXAMPLE scene (a sample 3x2 bay
|
|
34
|
-
# frame, schematic) so the app renders out of the box. Re-bake — or the
|
|
35
|
-
# floless-app-steel-from-drawings skill — replaces this whole `scene` with a reading of YOUR drawing.
|
|
36
|
-
- id: view
|
|
37
|
-
agent: viewer-3d
|
|
38
|
-
command: render
|
|
39
|
-
config:
|
|
40
|
-
scene:
|
|
41
|
-
meta: { name: "Example — steel framing (sample, not your drawing)", units: mm, up: z }
|
|
42
|
-
groups:
|
|
43
|
-
- { key: column, label: "Columns (HSS6x6x3/8)", color: "#60a5fa" }
|
|
44
|
-
- { key: beam, label: "Beams (W10x33)", color: "#94a3b8" }
|
|
45
|
-
elements:
|
|
46
|
-
# 6 columns (z 0 -> 4500) on a 3 x 2 grid
|
|
47
|
-
- { id: COL-1A, group: column, kind: box, from: [0,0,0], to: [0,0,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "1-A" } }
|
|
48
|
-
- { id: COL-2A, group: column, kind: box, from: [6000,0,0], to: [6000,0,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "2-A" } }
|
|
49
|
-
- { id: COL-3A, group: column, kind: box, from: [12000,0,0], to: [12000,0,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "3-A" } }
|
|
50
|
-
- { id: COL-1B, group: column, kind: box, from: [0,6000,0], to: [0,6000,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "1-B" } }
|
|
51
|
-
- { id: COL-2B, group: column, kind: box, from: [6000,6000,0], to: [6000,6000,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "2-B" } }
|
|
52
|
-
- { id: COL-3B, group: column, kind: box, from: [12000,6000,0],to: [12000,6000,4500],section: { w: 150, d: 150 }, meta: { profile: "HSS6x6x3/8", grid: "3-B" } }
|
|
53
|
-
# roof beams at z 4500 (W10x33): along each row, then tie beams along each grid line
|
|
54
|
-
- { id: BM-12A, group: beam, kind: box, from: [0,0,4500], to: [6000,0,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "A" } }
|
|
55
|
-
- { id: BM-23A, group: beam, kind: box, from: [6000,0,4500], to: [12000,0,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "A" } }
|
|
56
|
-
- { id: BM-12B, group: beam, kind: box, from: [0,6000,4500], to: [6000,6000,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "B" } }
|
|
57
|
-
- { id: BM-23B, group: beam, kind: box, from: [6000,6000,4500],to: [12000,6000,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "B" } }
|
|
58
|
-
- { id: BM-1AB, group: beam, kind: box, from: [0,0,4500], to: [0,6000,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "1" } }
|
|
59
|
-
- { id: BM-2AB, group: beam, kind: box, from: [6000,0,4500], to: [6000,6000,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "2" } }
|
|
60
|
-
- { id: BM-3AB, group: beam, kind: box, from: [12000,0,4500], to: [12000,6000,4500], section: { w: 150, d: 250 }, meta: { profile: "W10x33", grid: "3" } }
|
|
61
|
-
grids:
|
|
62
|
-
- { label: "1", at: [0,-3000,0] }
|
|
63
|
-
- { label: "2", at: [6000,-3000,0] }
|
|
64
|
-
- { label: "3", at: [12000,-3000,0] }
|
|
65
|
-
- { label: "A", at: [-3000,0,0] }
|
|
66
|
-
- { label: "B", at: [-3000,6000,0] }
|
|
67
|
-
panels:
|
|
68
|
-
- title: Takeoff
|
|
69
|
-
note: "EXAMPLE — not your drawing. A sample steel frame on a 1-3 x A-B grid; sizes & heights are illustrative. Use 'Re-read & re-bake ▸' (or ask your terminal AI) to read your own drawing."
|
|
70
|
-
columns: [Section, "No.", Length]
|
|
71
|
-
rows:
|
|
72
|
-
- ["HSS6x6x3/8", 6, "27.0 m"]
|
|
73
|
-
- ["W10x33", 7, "42.0 m"]
|
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
app: steel-to-ifc
|
|
2
|
-
version: 0.1.0
|
|
3
|
-
display-name: Steel to IFC
|
|
4
|
-
description: |
|
|
5
|
-
Bake a read steel model OUT to a universal IFC file — open it in Tekla, SDS2, Revit or
|
|
6
|
-
Navisworks. Companion to steel-from-drawings: that app reads a drawing into a 3D scene and shows
|
|
7
|
-
it; this one writes the SAME scene to an IFC file (IfcColumn/IfcBeam as extruded sections on the
|
|
8
|
-
real grid) so you can take the schematic model straight into your detailing tool and finish there.
|
|
9
|
-
|
|
10
|
-
Same terminal-bake pattern — the FloLess way: your AI terminal bakes the scene inline
|
|
11
|
-
(read-strategy: bake); a deterministic, MODEL-FREE exec node writes the .ifc (no Tekla needed to
|
|
12
|
-
export — it never touches a Tekla model, just writes the file). Ships with the same EXAMPLE
|
|
13
|
-
frame so it runs out of the box; the floless-app-steel-from-drawings skill bakes your drawing's
|
|
14
|
-
scene into both apps so the IFC matches what you saw in 3D.
|
|
15
|
-
exposes-as-agent: false
|
|
16
|
-
inputs:
|
|
17
|
-
# BAKED at compose time: the terminal AI reads this drawing and writes the `scene` block below
|
|
18
|
-
# (the run never reads the drawing). read-strategy: bake surfaces "Re-read & re-bake ▸".
|
|
19
|
-
drawing:
|
|
20
|
-
type: image
|
|
21
|
-
widget: file
|
|
22
|
-
accept: [pdf, png, jpg]
|
|
23
|
-
read-strategy: bake
|
|
24
|
-
description: A structural steel drawing — baked into the scene below, then written out as IFC; swap it to re-read & re-bake.
|
|
25
|
-
output_path:
|
|
26
|
-
type: string
|
|
27
|
-
default: steel-from-drawings.ifc
|
|
28
|
-
description: Where to write the .ifc — FloLess fills this with your Downloads folder; editable per run.
|
|
29
|
-
requires:
|
|
30
|
-
- ifc@0.1.x
|
|
31
|
-
layout: linear
|
|
32
|
-
nodes:
|
|
33
|
-
# Model-free exec: reads the baked `scene` and writes a valid IFC4 file (IfcColumn/IfcBeam as
|
|
34
|
-
# extruded rectangle sections on the real grid), returning a summary report. NEVER derefs `model`
|
|
35
|
-
# (no Tekla needed); writes the .ifc via System.IO to the user's Downloads (or home). Re-bake —
|
|
36
|
-
# or the floless-app-steel-from-drawings skill — replaces `scene` with YOUR drawing's read.
|
|
37
|
-
- id: bake-ifc
|
|
38
|
-
agent: ifc
|
|
39
|
-
command: write
|
|
40
|
-
config:
|
|
41
|
-
scene:
|
|
42
|
-
meta:
|
|
43
|
-
name: "Example \u2014 steel framing (sample, not your drawing)"
|
|
44
|
-
units: mm
|
|
45
|
-
up: z
|
|
46
|
-
groups:
|
|
47
|
-
- key: column
|
|
48
|
-
label: Columns (HSS6x6x3/8)
|
|
49
|
-
color: '#60a5fa'
|
|
50
|
-
- key: beam
|
|
51
|
-
label: Beams (W10x33)
|
|
52
|
-
color: '#94a3b8'
|
|
53
|
-
elements:
|
|
54
|
-
- id: COL-1A
|
|
55
|
-
group: column
|
|
56
|
-
kind: box
|
|
57
|
-
from:
|
|
58
|
-
- 0
|
|
59
|
-
- 0
|
|
60
|
-
- 0
|
|
61
|
-
to:
|
|
62
|
-
- 0
|
|
63
|
-
- 0
|
|
64
|
-
- 4500
|
|
65
|
-
section:
|
|
66
|
-
w: 150
|
|
67
|
-
d: 150
|
|
68
|
-
meta:
|
|
69
|
-
profile: HSS6x6x3/8
|
|
70
|
-
grid: 1-A
|
|
71
|
-
- id: COL-1B
|
|
72
|
-
group: column
|
|
73
|
-
kind: box
|
|
74
|
-
from:
|
|
75
|
-
- 0
|
|
76
|
-
- 6000
|
|
77
|
-
- 0
|
|
78
|
-
to:
|
|
79
|
-
- 0
|
|
80
|
-
- 6000
|
|
81
|
-
- 4500
|
|
82
|
-
section:
|
|
83
|
-
w: 150
|
|
84
|
-
d: 150
|
|
85
|
-
meta:
|
|
86
|
-
profile: HSS6x6x3/8
|
|
87
|
-
grid: 1-B
|
|
88
|
-
- id: COL-2A
|
|
89
|
-
group: column
|
|
90
|
-
kind: box
|
|
91
|
-
from:
|
|
92
|
-
- 6000
|
|
93
|
-
- 0
|
|
94
|
-
- 0
|
|
95
|
-
to:
|
|
96
|
-
- 6000
|
|
97
|
-
- 0
|
|
98
|
-
- 4500
|
|
99
|
-
section:
|
|
100
|
-
w: 150
|
|
101
|
-
d: 150
|
|
102
|
-
meta:
|
|
103
|
-
profile: HSS6x6x3/8
|
|
104
|
-
grid: 2-A
|
|
105
|
-
- id: COL-2B
|
|
106
|
-
group: column
|
|
107
|
-
kind: box
|
|
108
|
-
from:
|
|
109
|
-
- 6000
|
|
110
|
-
- 6000
|
|
111
|
-
- 0
|
|
112
|
-
to:
|
|
113
|
-
- 6000
|
|
114
|
-
- 6000
|
|
115
|
-
- 4500
|
|
116
|
-
section:
|
|
117
|
-
w: 150
|
|
118
|
-
d: 150
|
|
119
|
-
meta:
|
|
120
|
-
profile: HSS6x6x3/8
|
|
121
|
-
grid: 2-B
|
|
122
|
-
- id: COL-3A
|
|
123
|
-
group: column
|
|
124
|
-
kind: box
|
|
125
|
-
from:
|
|
126
|
-
- 12000
|
|
127
|
-
- 0
|
|
128
|
-
- 0
|
|
129
|
-
to:
|
|
130
|
-
- 12000
|
|
131
|
-
- 0
|
|
132
|
-
- 4500
|
|
133
|
-
section:
|
|
134
|
-
w: 150
|
|
135
|
-
d: 150
|
|
136
|
-
meta:
|
|
137
|
-
profile: HSS6x6x3/8
|
|
138
|
-
grid: 3-A
|
|
139
|
-
- id: COL-3B
|
|
140
|
-
group: column
|
|
141
|
-
kind: box
|
|
142
|
-
from:
|
|
143
|
-
- 12000
|
|
144
|
-
- 6000
|
|
145
|
-
- 0
|
|
146
|
-
to:
|
|
147
|
-
- 12000
|
|
148
|
-
- 6000
|
|
149
|
-
- 4500
|
|
150
|
-
section:
|
|
151
|
-
w: 150
|
|
152
|
-
d: 150
|
|
153
|
-
meta:
|
|
154
|
-
profile: HSS6x6x3/8
|
|
155
|
-
grid: 3-B
|
|
156
|
-
- id: BM-12A
|
|
157
|
-
group: beam
|
|
158
|
-
kind: box
|
|
159
|
-
from:
|
|
160
|
-
- 0
|
|
161
|
-
- 0
|
|
162
|
-
- 4500
|
|
163
|
-
to:
|
|
164
|
-
- 6000
|
|
165
|
-
- 0
|
|
166
|
-
- 4500
|
|
167
|
-
section:
|
|
168
|
-
w: 150
|
|
169
|
-
d: 250
|
|
170
|
-
meta:
|
|
171
|
-
profile: W10x33
|
|
172
|
-
- id: BM-23A
|
|
173
|
-
group: beam
|
|
174
|
-
kind: box
|
|
175
|
-
from:
|
|
176
|
-
- 6000
|
|
177
|
-
- 0
|
|
178
|
-
- 4500
|
|
179
|
-
to:
|
|
180
|
-
- 12000
|
|
181
|
-
- 0
|
|
182
|
-
- 4500
|
|
183
|
-
section:
|
|
184
|
-
w: 150
|
|
185
|
-
d: 250
|
|
186
|
-
meta:
|
|
187
|
-
profile: W10x33
|
|
188
|
-
- id: BM-12B
|
|
189
|
-
group: beam
|
|
190
|
-
kind: box
|
|
191
|
-
from:
|
|
192
|
-
- 0
|
|
193
|
-
- 6000
|
|
194
|
-
- 4500
|
|
195
|
-
to:
|
|
196
|
-
- 6000
|
|
197
|
-
- 6000
|
|
198
|
-
- 4500
|
|
199
|
-
section:
|
|
200
|
-
w: 150
|
|
201
|
-
d: 250
|
|
202
|
-
meta:
|
|
203
|
-
profile: W10x33
|
|
204
|
-
- id: BM-23B
|
|
205
|
-
group: beam
|
|
206
|
-
kind: box
|
|
207
|
-
from:
|
|
208
|
-
- 6000
|
|
209
|
-
- 6000
|
|
210
|
-
- 4500
|
|
211
|
-
to:
|
|
212
|
-
- 12000
|
|
213
|
-
- 6000
|
|
214
|
-
- 4500
|
|
215
|
-
section:
|
|
216
|
-
w: 150
|
|
217
|
-
d: 250
|
|
218
|
-
meta:
|
|
219
|
-
profile: W10x33
|
|
220
|
-
- id: BM-1AB
|
|
221
|
-
group: beam
|
|
222
|
-
kind: box
|
|
223
|
-
from:
|
|
224
|
-
- 0
|
|
225
|
-
- 0
|
|
226
|
-
- 4500
|
|
227
|
-
to:
|
|
228
|
-
- 0
|
|
229
|
-
- 6000
|
|
230
|
-
- 4500
|
|
231
|
-
section:
|
|
232
|
-
w: 150
|
|
233
|
-
d: 250
|
|
234
|
-
meta:
|
|
235
|
-
profile: W10x33
|
|
236
|
-
- id: BM-2AB
|
|
237
|
-
group: beam
|
|
238
|
-
kind: box
|
|
239
|
-
from:
|
|
240
|
-
- 6000
|
|
241
|
-
- 0
|
|
242
|
-
- 4500
|
|
243
|
-
to:
|
|
244
|
-
- 6000
|
|
245
|
-
- 6000
|
|
246
|
-
- 4500
|
|
247
|
-
section:
|
|
248
|
-
w: 150
|
|
249
|
-
d: 250
|
|
250
|
-
meta:
|
|
251
|
-
profile: W10x33
|
|
252
|
-
- id: BM-3AB
|
|
253
|
-
group: beam
|
|
254
|
-
kind: box
|
|
255
|
-
from:
|
|
256
|
-
- 12000
|
|
257
|
-
- 0
|
|
258
|
-
- 4500
|
|
259
|
-
to:
|
|
260
|
-
- 12000
|
|
261
|
-
- 6000
|
|
262
|
-
- 4500
|
|
263
|
-
section:
|
|
264
|
-
w: 150
|
|
265
|
-
d: 250
|
|
266
|
-
meta:
|
|
267
|
-
profile: W10x33
|
|
268
|
-
grids:
|
|
269
|
-
- label: '1'
|
|
270
|
-
at:
|
|
271
|
-
- 0
|
|
272
|
-
- -3000
|
|
273
|
-
- 0
|
|
274
|
-
- label: '2'
|
|
275
|
-
at:
|
|
276
|
-
- 6000
|
|
277
|
-
- -3000
|
|
278
|
-
- 0
|
|
279
|
-
- label: '3'
|
|
280
|
-
at:
|
|
281
|
-
- 12000
|
|
282
|
-
- -3000
|
|
283
|
-
- 0
|
|
284
|
-
- label: A
|
|
285
|
-
at:
|
|
286
|
-
- -3000
|
|
287
|
-
- 0
|
|
288
|
-
- 0
|
|
289
|
-
- label: B
|
|
290
|
-
at:
|
|
291
|
-
- -3000
|
|
292
|
-
- 6000
|
|
293
|
-
- 0
|
|
294
|
-
output-path: "{{ inputs.output_path }}"
|