@floless/app 0.43.0 → 0.45.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 +500 -335
- package/dist/schemas/steel.takeoff.v1.schema.json +20 -1
- package/dist/templates/college-phase-exporter.flo +86 -0
- package/dist/templates/dummy-report.flo +104 -0
- package/dist/templates/hello-world.flo +91 -0
- package/dist/templates/screenshot-to-saved-settings.flo +56 -0
- package/dist/templates/steel-from-drawings.flo +73 -0
- package/dist/templates/steel-takeoff.flo +151 -0
- package/dist/templates/steel-to-ifc.flo +294 -0
- package/dist/templates/steel-to-tekla.flo +293 -0
- package/dist/templates/supabase-drawing-register.flo +63 -0
- package/dist/templates/tekla-bom-by-phase.flo +211 -0
- package/dist/templates/tekla-connection-xray.flo +39 -0
- package/dist/templates/tekla-pdf-takeoff.flo +106 -0
- package/dist/templates/tekla-selection-now.flo +129 -0
- package/dist/templates/tekla-selection-report.flo +145 -0
- package/dist/templates/tekla-watch-logger.flo +57 -0
- package/dist/templates/tekla-watch-smoke.flo +20 -0
- package/dist/templates/trimble-list-projects.flo +41 -0
- package/dist/templates/viewer-3d-demo.flo +65 -0
- package/dist/web/app.css +17 -0
- package/dist/web/aware.js +79 -0
- package/dist/web/index.html +17 -0
- package/dist/web/steel-3d-core.js +19 -0
- package/dist/web/steel-3d-view.js +172 -5
- package/dist/web/steel-editor.html +30 -7
- package/package.json +1 -1
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"additionalProperties": false
|
|
38
38
|
},
|
|
39
39
|
"plans": { "type": "array", "items": { "$ref": "#/$defs/plan" } },
|
|
40
|
-
"filter": { "$ref": "#/$defs/filter", "description": "Layer/thickness/colour filter pre-stage dataset from one compose-time PyMuPDF pass. The saved selection (per-facet `on` flags + `mode`) scopes which lines the reader binds profile labels to; replaces the length heuristic, fallback when absent. CONFIDENTIAL: `page.bg_b64` is machine-local, never committed." }
|
|
40
|
+
"filter": { "$ref": "#/$defs/filter", "description": "Layer/thickness/colour filter pre-stage dataset from one compose-time PyMuPDF pass. The saved selection (per-facet `on` flags + `mode`) scopes which lines the reader binds profile labels to; replaces the length heuristic, fallback when absent. CONFIDENTIAL: `page.bg_b64` is machine-local, never committed." },
|
|
41
|
+
"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." }
|
|
41
42
|
},
|
|
42
43
|
"$defs": {
|
|
43
44
|
"filter": {
|
|
@@ -126,6 +127,13 @@
|
|
|
126
127
|
"maxItems": 2,
|
|
127
128
|
"description": "An [x,y] pair in display space (or [fx,fy] normalized for bubbles)."
|
|
128
129
|
},
|
|
130
|
+
"point3": {
|
|
131
|
+
"type": "array",
|
|
132
|
+
"items": { "type": "number" },
|
|
133
|
+
"minItems": 3,
|
|
134
|
+
"maxItems": 3,
|
|
135
|
+
"description": "An [x,y,z] point in world-scene mm (Z-up), matching contract-to-scene output."
|
|
136
|
+
},
|
|
129
137
|
"plan": {
|
|
130
138
|
"type": "object",
|
|
131
139
|
"required": ["sheet", "members"],
|
|
@@ -176,6 +184,17 @@
|
|
|
176
184
|
"rot": { "type": "number", "description": "Local-frame bearing (radians) baked at creation; 0 = global axes. x/y measure along this rotated frame so resetting the plan frame never moves placed dims." }
|
|
177
185
|
}
|
|
178
186
|
},
|
|
187
|
+
"dim3": {
|
|
188
|
+
"type": "object",
|
|
189
|
+
"required": ["id", "a", "b", "axis"],
|
|
190
|
+
"additionalProperties": true,
|
|
191
|
+
"properties": {
|
|
192
|
+
"id": { "type": "string" },
|
|
193
|
+
"a": { "$ref": "#/$defs/point3", "description": "First snapped anchor, world-scene mm." },
|
|
194
|
+
"b": { "$ref": "#/$defs/point3", "description": "Second snapped anchor, world-scene mm." },
|
|
195
|
+
"axis": { "enum": ["free", "x", "y", "z"], "description": "free = 3D distance ‖b-a‖; x|y|z = the component along that world axis. The dim line is drawn axis-aligned for x|y|z." }
|
|
196
|
+
}
|
|
197
|
+
},
|
|
179
198
|
"frame": {
|
|
180
199
|
"type": ["object", "null"],
|
|
181
200
|
"description": "Optional per-plan local coordinate frame for skewed framing. null/absent = global axes. Affects only editor ortho-snapping + X/Y dimensions; NOT baked into the lock / 3D / IFC / BOM.",
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
app: college-phase-exporter
|
|
2
|
+
version: 0.1.0
|
|
3
|
+
display-name: College — Phase Exporter
|
|
4
|
+
description: |
|
|
5
|
+
Reproduces the desktop "College - Phase Exporter" outcome on AWARE.
|
|
6
|
+
Given a phase number: Tekla BOM report -> SharePoint Excel -> Trimble Connect
|
|
7
|
+
upload -> Teams + email -> HTML summary. Composed against installed curated
|
|
8
|
+
commands only. Write-mode nodes carry the v0.11 safety contract.
|
|
9
|
+
exposes-as-agent: false
|
|
10
|
+
requires:
|
|
11
|
+
- tekla@0.1.x
|
|
12
|
+
- microsoft-365@0.1.x
|
|
13
|
+
- trimble-connect@0.1.x
|
|
14
|
+
- html-report@0.1.x
|
|
15
|
+
requires-permissions:
|
|
16
|
+
filesystem:
|
|
17
|
+
- write: '{{ inputs.report-dir }}'
|
|
18
|
+
network:
|
|
19
|
+
- https://graph.microsoft.com
|
|
20
|
+
- https://app.connect.trimble.com
|
|
21
|
+
layout: dag
|
|
22
|
+
nodes:
|
|
23
|
+
- id: report
|
|
24
|
+
agent: tekla
|
|
25
|
+
command: report-create
|
|
26
|
+
inputs:
|
|
27
|
+
template: '350 Advanced Bill'
|
|
28
|
+
scope: phase
|
|
29
|
+
phase: '{{ inputs.phase }}'
|
|
30
|
+
output-path: '{{ inputs.report-dir }}/BOM-Phase-{{ inputs.phase }}.xlsx'
|
|
31
|
+
- id: excel
|
|
32
|
+
agent: microsoft-365
|
|
33
|
+
command: excel.range.write
|
|
34
|
+
inputs:
|
|
35
|
+
workbook: '{{ inputs.sharepoint-workbook-url }}'
|
|
36
|
+
worksheet: 'Phase {{ inputs.phase }}'
|
|
37
|
+
range: 'A1:B2'
|
|
38
|
+
values:
|
|
39
|
+
- ['Phase', 'Rows']
|
|
40
|
+
- ['{{ inputs.phase }}', '{{ report.row-count }}']
|
|
41
|
+
safety: { transaction-group: phase-export, snapshot: false }
|
|
42
|
+
- id: upload
|
|
43
|
+
agent: trimble-connect
|
|
44
|
+
command: upload
|
|
45
|
+
inputs:
|
|
46
|
+
project-id: '{{ inputs.tc-project-id }}'
|
|
47
|
+
folder-id: '{{ inputs.tc-reports-folder-id }}'
|
|
48
|
+
file-path: '{{ report.path }}'
|
|
49
|
+
- id: summary
|
|
50
|
+
agent: html-report
|
|
51
|
+
command: render
|
|
52
|
+
inputs:
|
|
53
|
+
template: 'phase-export-summary'
|
|
54
|
+
data:
|
|
55
|
+
phase: '{{ inputs.phase }}'
|
|
56
|
+
report: '{{ report.path }}'
|
|
57
|
+
uploaded: '{{ upload.url }}'
|
|
58
|
+
output-path: '{{ inputs.report-dir }}/Phase-{{ inputs.phase }}-summary.html'
|
|
59
|
+
- id: teams
|
|
60
|
+
agent: microsoft-365
|
|
61
|
+
command: teams.channel.post-message
|
|
62
|
+
inputs:
|
|
63
|
+
team-id: '{{ inputs.teams-team-id }}'
|
|
64
|
+
channel-id: '{{ inputs.teams-channel-id }}'
|
|
65
|
+
message: 'BOM Phase {{ inputs.phase }} exported {{ run.date }}. Report uploaded to Trimble Connect.'
|
|
66
|
+
safety: { transaction-group: notify, snapshot: false }
|
|
67
|
+
- id: email
|
|
68
|
+
agent: microsoft-365
|
|
69
|
+
command: outlook.mail.send-with-attachment
|
|
70
|
+
inputs:
|
|
71
|
+
to: ['{{ inputs.notify-email }}']
|
|
72
|
+
subject: 'Done — Phase {{ inputs.phase }} BOM'
|
|
73
|
+
content-type: html
|
|
74
|
+
body: |
|
|
75
|
+
Phase {{ inputs.phase }} bill of materials is attached and uploaded to Trimble Connect.
|
|
76
|
+
attachments:
|
|
77
|
+
- path: '{{ report.path }}'
|
|
78
|
+
filename: 'BOM-Phase-{{ inputs.phase }}.xlsx'
|
|
79
|
+
safety: { transaction-group: notify, snapshot: false }
|
|
80
|
+
connections:
|
|
81
|
+
- { from: report, to: excel }
|
|
82
|
+
- { from: report, to: upload }
|
|
83
|
+
- { from: report, to: summary }
|
|
84
|
+
- { from: upload, to: summary }
|
|
85
|
+
- { from: summary, to: teams }
|
|
86
|
+
- { from: summary, to: email }
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
app: dummy-report
|
|
2
|
+
version: 0.1.0
|
|
3
|
+
display-name: Dummy Report
|
|
4
|
+
description: |
|
|
5
|
+
A self-contained test workflow that produces a styled HTML report — no Tekla
|
|
6
|
+
model required. Neither node touches `model`, so it runs on the host bridge with
|
|
7
|
+
nothing open. Use it to preview the in-app HTML Viewer and the "Made with
|
|
8
|
+
FloLess" report badge end to end without a real model. Two real tekla/exec nodes:
|
|
9
|
+
a title input → a report node that builds the HTML inline and is the terminal the
|
|
10
|
+
viewer renders.
|
|
11
|
+
exposes-as-agent: false
|
|
12
|
+
inputs:
|
|
13
|
+
title:
|
|
14
|
+
type: string
|
|
15
|
+
default: "FloLess Test Report"
|
|
16
|
+
description: Heading shown at the top of the generated report (editable per run).
|
|
17
|
+
requires:
|
|
18
|
+
- tekla@0.1.x
|
|
19
|
+
layout: linear
|
|
20
|
+
nodes:
|
|
21
|
+
# ── node 1: Title input — carries the report title downstream (no model deref) ──
|
|
22
|
+
- id: title-input
|
|
23
|
+
agent: tekla
|
|
24
|
+
command: exec
|
|
25
|
+
mode: read
|
|
26
|
+
config:
|
|
27
|
+
version: "2026.0"
|
|
28
|
+
args:
|
|
29
|
+
title: "{{ inputs.title }}"
|
|
30
|
+
code: |
|
|
31
|
+
// Title input — pure pass-through. Never touches `model`, so it runs even
|
|
32
|
+
// with no Tekla model open. Emits the title for the downstream report node.
|
|
33
|
+
string title = "FloLess Test Report";
|
|
34
|
+
if (args != null && args.TryGetValue("title", out var tv) && tv != null)
|
|
35
|
+
{
|
|
36
|
+
var s = tv.ToString();
|
|
37
|
+
if (!string.IsNullOrWhiteSpace(s)) title = s;
|
|
38
|
+
}
|
|
39
|
+
return new { ok = true, title };
|
|
40
|
+
# ── node 2: Report — build the styled HTML inline; be the terminal the viewer renders ──
|
|
41
|
+
- id: report
|
|
42
|
+
agent: tekla
|
|
43
|
+
command: exec
|
|
44
|
+
mode: read
|
|
45
|
+
config:
|
|
46
|
+
version: "2026.0"
|
|
47
|
+
args:
|
|
48
|
+
title: "{{ title-input.result.title }}"
|
|
49
|
+
code: |
|
|
50
|
+
// Dummy report — builds a styled HTML report inline and returns it as this
|
|
51
|
+
// terminal node's `html`, so the floless.app HTML Viewer renders it on
|
|
52
|
+
// double-click. Touches NO `model` (runs with no Tekla open) and writes no
|
|
53
|
+
// file. Pure string build, manual HTML-escape (no System.Net available).
|
|
54
|
+
using System;
|
|
55
|
+
using System.Text;
|
|
56
|
+
|
|
57
|
+
string title = "FloLess Test Report";
|
|
58
|
+
if (args != null && args.TryGetValue("title", out var tv) && tv != null)
|
|
59
|
+
{
|
|
60
|
+
var s = tv.ToString();
|
|
61
|
+
if (!string.IsNullOrWhiteSpace(s)) title = s;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Func<string, string> E = s => (s ?? "").Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """);
|
|
65
|
+
string now = DateTime.Now.ToString("yyyy-MM-dd HH:mm");
|
|
66
|
+
|
|
67
|
+
var sb = new StringBuilder();
|
|
68
|
+
sb.Append("<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\">");
|
|
69
|
+
sb.Append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
|
|
70
|
+
sb.Append("<title>" + E(title) + "</title><style>");
|
|
71
|
+
sb.Append("*{margin:0;padding:0;box-sizing:border-box}");
|
|
72
|
+
sb.Append("body{font-family:-apple-system,'Segoe UI',Roboto,sans-serif;background:#f1f5f9;color:#0f172a;padding:2.5rem;line-height:1.6}");
|
|
73
|
+
sb.Append(".wrap{max-width:760px;margin:0 auto;background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:2rem 2.25rem;box-shadow:0 10px 30px rgba(2,6,23,.06)}");
|
|
74
|
+
sb.Append(".hd{border-bottom:2px solid #4a9eff;padding-bottom:1rem;margin-bottom:1.5rem}");
|
|
75
|
+
sb.Append(".hd h1{font-size:1.6rem;font-weight:800;color:#0f172a}");
|
|
76
|
+
sb.Append(".hd p{color:#64748b;font-size:.85rem;margin-top:.3rem}");
|
|
77
|
+
sb.Append(".cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.85rem;margin-bottom:1.75rem}");
|
|
78
|
+
sb.Append(".card{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:.9rem 1rem}");
|
|
79
|
+
sb.Append(".card .l{font-size:.68rem;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8}");
|
|
80
|
+
sb.Append(".card .v{font-size:1.15rem;font-weight:700;color:#0f172a;margin-top:.15rem}");
|
|
81
|
+
sb.Append("table{width:100%;border-collapse:collapse;font-size:.9rem;margin-top:.25rem}");
|
|
82
|
+
sb.Append("th,td{text-align:left;padding:.6rem .75rem;border-bottom:1px solid #eef2f7}");
|
|
83
|
+
sb.Append("th{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8}");
|
|
84
|
+
sb.Append("td:first-child{font-weight:600;color:#334155;width:38%}");
|
|
85
|
+
sb.Append(".ft{margin-top:1.75rem;padding-top:1rem;border-top:1px solid #eef2f7;color:#94a3b8;font-size:.78rem;text-align:center}");
|
|
86
|
+
sb.Append("</style></head><body><div class=\"wrap\">");
|
|
87
|
+
sb.Append("<div class=\"hd\"><h1>" + E(title) + "</h1><p>A sample report generated by FloLess to preview the report viewer.</p></div>");
|
|
88
|
+
sb.Append("<div class=\"cards\">");
|
|
89
|
+
sb.Append("<div class=\"card\"><div class=\"l\">Status</div><div class=\"v\">OK</div></div>");
|
|
90
|
+
sb.Append("<div class=\"card\"><div class=\"l\">Generated</div><div class=\"v\">" + E(now) + "</div></div>");
|
|
91
|
+
sb.Append("<div class=\"card\"><div class=\"l\">Model needed</div><div class=\"v\">No</div></div>");
|
|
92
|
+
sb.Append("</div>");
|
|
93
|
+
sb.Append("<table><thead><tr><th>Field</th><th>Value</th></tr></thead><tbody>");
|
|
94
|
+
sb.Append("<tr><td>Workflow</td><td>dummy-report</td></tr>");
|
|
95
|
+
sb.Append("<tr><td>Status</td><td>OK</td></tr>");
|
|
96
|
+
sb.Append("<tr><td>Source</td><td>Generated by FloLess (no model required)</td></tr>");
|
|
97
|
+
sb.Append("<tr><td>Generated</td><td>" + E(now) + "</td></tr>");
|
|
98
|
+
sb.Append("</tbody></table>");
|
|
99
|
+
sb.Append("<div class=\"ft\">This is a dummy report for testing the FloLess HTML viewer.</div>");
|
|
100
|
+
sb.Append("</div></body></html>");
|
|
101
|
+
|
|
102
|
+
return new { ok = true, title, html = sb.ToString() };
|
|
103
|
+
connections:
|
|
104
|
+
- { from: title-input, to: report }
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
app: hello-world
|
|
2
|
+
version: 0.2.0
|
|
3
|
+
display-name: AWARE Hello World
|
|
4
|
+
description: |
|
|
5
|
+
Smoke test: writes a one-line greeting to Tekla's prompt (status) bar — once in
|
|
6
|
+
Tekla 2025 and once in Tekla 2026 — to prove the AWARE runtime can drive a real
|
|
7
|
+
desktop host end to end. Two real tekla/exec nodes running read-only Roslyn C#
|
|
8
|
+
(Operation.DisplayPrompt; no model mutation, no filesystem write), so it runs on
|
|
9
|
+
any open model with no write permission.
|
|
10
|
+
exposes-as-agent: false
|
|
11
|
+
inputs:
|
|
12
|
+
message:
|
|
13
|
+
type: string
|
|
14
|
+
default: "Hello from floless.app — running end to end on AWARE."
|
|
15
|
+
description: The greeting shown on Tekla's prompt/status bar (editable per run).
|
|
16
|
+
requires:
|
|
17
|
+
- tekla@0.1.x
|
|
18
|
+
layout: dag
|
|
19
|
+
nodes:
|
|
20
|
+
- id: message-input
|
|
21
|
+
agent: tekla
|
|
22
|
+
command: exec
|
|
23
|
+
mode: read
|
|
24
|
+
config:
|
|
25
|
+
version: "2025.0"
|
|
26
|
+
args:
|
|
27
|
+
message: "{{ inputs.message }}"
|
|
28
|
+
code: |
|
|
29
|
+
// Message input — carries the greeting downstream to both status nodes.
|
|
30
|
+
// Pure pass-through: returns the input message; never touches the model,
|
|
31
|
+
// so it runs even with no Tekla open (no model deref). Downstream nodes
|
|
32
|
+
// read it via the message-input.result.message template.
|
|
33
|
+
string message = "Hello from floless.app — running end to end on AWARE.";
|
|
34
|
+
if (args != null && args.TryGetValue("message", out var mv) && mv != null)
|
|
35
|
+
{
|
|
36
|
+
var s = mv.ToString();
|
|
37
|
+
if (!string.IsNullOrWhiteSpace(s)) message = s;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new { ok = true, message };
|
|
41
|
+
- id: tekla-2025-status
|
|
42
|
+
agent: tekla
|
|
43
|
+
command: exec
|
|
44
|
+
mode: read
|
|
45
|
+
config:
|
|
46
|
+
version: "2025.0"
|
|
47
|
+
args:
|
|
48
|
+
message: "{{ message-input.result.message }}"
|
|
49
|
+
code: |
|
|
50
|
+
// Hello World — write a one-line greeting to Tekla's prompt (status) bar.
|
|
51
|
+
// Read-only: a transient prompt; never touches the model or filesystem.
|
|
52
|
+
if (model == null)
|
|
53
|
+
return new { ok = false, error = "No live Tekla 2025 model — open one in Tekla Structures 2025." };
|
|
54
|
+
|
|
55
|
+
string message = "Hello from floless.app — running on AWARE (Tekla 2025).";
|
|
56
|
+
if (args != null && args.TryGetValue("message", out var mv) && mv != null)
|
|
57
|
+
{
|
|
58
|
+
var s = mv.ToString();
|
|
59
|
+
if (!string.IsNullOrWhiteSpace(s)) message = s;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Tekla.Structures.Model.Operations.Operation.DisplayPrompt(message);
|
|
63
|
+
|
|
64
|
+
return new { ok = true, host = "tekla", version = "2025.0", message };
|
|
65
|
+
- id: tekla-2026-status
|
|
66
|
+
agent: tekla
|
|
67
|
+
command: exec
|
|
68
|
+
mode: read
|
|
69
|
+
config:
|
|
70
|
+
version: "2026.0"
|
|
71
|
+
args:
|
|
72
|
+
message: "{{ message-input.result.message }}"
|
|
73
|
+
code: |
|
|
74
|
+
// Hello World — write a one-line greeting to Tekla's prompt (status) bar.
|
|
75
|
+
// Read-only: a transient prompt; never touches the model or filesystem.
|
|
76
|
+
if (model == null)
|
|
77
|
+
return new { ok = false, error = "No live Tekla 2026 model — open one in Tekla Structures 2026." };
|
|
78
|
+
|
|
79
|
+
string message = "Hello from floless.app — running on AWARE (Tekla 2026).";
|
|
80
|
+
if (args != null && args.TryGetValue("message", out var mv) && mv != null)
|
|
81
|
+
{
|
|
82
|
+
var s = mv.ToString();
|
|
83
|
+
if (!string.IsNullOrWhiteSpace(s)) message = s;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Tekla.Structures.Model.Operations.Operation.DisplayPrompt(message);
|
|
87
|
+
|
|
88
|
+
return new { ok = true, host = "tekla", version = "2026.0", message };
|
|
89
|
+
connections:
|
|
90
|
+
- { from: message-input, to: tekla-2025-status }
|
|
91
|
+
- { from: message-input, to: tekla-2026-status }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
app: screenshot-to-saved-settings
|
|
2
|
+
version: 0.1.0
|
|
3
|
+
display-name: Screenshot to Saved Settings
|
|
4
|
+
description: |
|
|
5
|
+
Reproduces the desktop "screenshot-to-saved-settings" outcome on AWARE,
|
|
6
|
+
the decalog-#9 way (app-spec "Reading a drawing / PDF / image"):
|
|
7
|
+
|
|
8
|
+
The terminal AI reads a steel connection-schedule screenshot at COMPOSE time
|
|
9
|
+
and bakes the parsed rows into config.yaml as literals. The run is then fully
|
|
10
|
+
deterministic — for-each row writes a Tekla saved-attribute (.j) file via the
|
|
11
|
+
curated save-attributes verb. No runtime think-node reads the drawing.
|
|
12
|
+
|
|
13
|
+
(Optionally gate the writes behind an approve: block so a human eyeballs the
|
|
14
|
+
extraction first — app-spec L325. Omitted here: approve runtime lands in
|
|
15
|
+
aware v0.19.x; the compose-time-extraction + deterministic-write core is the
|
|
16
|
+
point.)
|
|
17
|
+
exposes-as-agent: false
|
|
18
|
+
inputs:
|
|
19
|
+
# The source drawing is BAKED at compose time (its rows live in config.schedule-rows),
|
|
20
|
+
# so this is not a runtime input — declaring it with read-strategy: bake surfaces the
|
|
21
|
+
# "Re-read & re-bake ▸" affordance (B3) so the user can swap the drawing and have the
|
|
22
|
+
# host AI re-extract + re-bake. The deterministic run never reads it.
|
|
23
|
+
drawing:
|
|
24
|
+
type: image
|
|
25
|
+
widget: file
|
|
26
|
+
read-strategy: bake
|
|
27
|
+
description: The connection-schedule screenshot baked into config — swap it to re-read & re-bake.
|
|
28
|
+
requires:
|
|
29
|
+
- tekla@0.1.x
|
|
30
|
+
- html-report@0.1.x
|
|
31
|
+
layout: linear
|
|
32
|
+
nodes:
|
|
33
|
+
# Deterministic write: one .j attribute file per compose-time-extracted row.
|
|
34
|
+
- id: write-attributes
|
|
35
|
+
for-each: '{{ config.schedule-rows }}'
|
|
36
|
+
do:
|
|
37
|
+
- id: save
|
|
38
|
+
agent: tekla
|
|
39
|
+
command: save-attributes
|
|
40
|
+
config:
|
|
41
|
+
joint-id: '{{ item.joint-id }}'
|
|
42
|
+
filename: '{{ item.preset }}'
|
|
43
|
+
safety:
|
|
44
|
+
transaction-group: schedule-import
|
|
45
|
+
snapshot: true
|
|
46
|
+
|
|
47
|
+
- id: summary
|
|
48
|
+
agent: html-report
|
|
49
|
+
command: render
|
|
50
|
+
config:
|
|
51
|
+
template: 'schedule-import-summary'
|
|
52
|
+
data:
|
|
53
|
+
presets: '{{ config.schedule-rows }}'
|
|
54
|
+
output-path: '{{ inputs.report-dir }}/schedule-import.html'
|
|
55
|
+
connections:
|
|
56
|
+
- { from: write-attributes, to: summary }
|
|
@@ -0,0 +1,73 @@
|
|
|
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"]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
app: steel-takeoff
|
|
2
|
+
version: 0.2.0
|
|
3
|
+
display-name: Steel Takeoff
|
|
4
|
+
publisher: floless
|
|
5
|
+
module: steel-detailer
|
|
6
|
+
description: |
|
|
7
|
+
Turn a structural steel drawing into a verifiable takeoff — overlay + properties + a confidence
|
|
8
|
+
report you can judge — then an interactive 3D model (bake to IFC/Tekla) and a formatted BOM, all
|
|
9
|
+
in one workflow. The reading is a COMPOSE-TIME act (the FloLess way): your terminal AI reads the
|
|
10
|
+
drawing and bakes the takeoff; the deterministic run only renders the approved result. Attach a
|
|
11
|
+
drawing and use "Re-read & re-bake ▸" (or ask your terminal AI). Double-click the takeoff node to
|
|
12
|
+
open the editor; Approve bakes the lock; freeze a stage once it's right so Run never recomputes it.
|
|
13
|
+
exposes-as-agent: false
|
|
14
|
+
# Workflow changelog (FloLess-published). Newest first; the app shows entries newer than the
|
|
15
|
+
# installed version when offering an update. Plain-English — it surfaces to the user.
|
|
16
|
+
changelog:
|
|
17
|
+
- version: 0.2.0
|
|
18
|
+
summary: Filter your drawing down to the steel first
|
|
19
|
+
changes:
|
|
20
|
+
- "Added: a Filter step at the start that keeps only the real steel lines of your drawing — by layer, line weight, or colour, or by clicking an example — so the takeoff reads a clean drawing."
|
|
21
|
+
- version: 0.1.0
|
|
22
|
+
summary: Initial steel takeoff
|
|
23
|
+
inputs:
|
|
24
|
+
drawing:
|
|
25
|
+
type: image
|
|
26
|
+
widget: file
|
|
27
|
+
accept: [pdf, png, jpg]
|
|
28
|
+
read-strategy: bake
|
|
29
|
+
description: A structural steel drawing — read by your terminal AI into the takeoff; swap to re-read & re-bake.
|
|
30
|
+
target_confidence:
|
|
31
|
+
type: number
|
|
32
|
+
default: 70
|
|
33
|
+
description: Minimum aggregate confidence the AI read aims for before handing you the editor (%).
|
|
34
|
+
requires:
|
|
35
|
+
- html-report@0.2.x
|
|
36
|
+
- viewer-3d@0.1.x
|
|
37
|
+
layout: linear
|
|
38
|
+
nodes:
|
|
39
|
+
# ── node 1: filter — isolate the steel linework (FIRST stage) ─────────────────
|
|
40
|
+
# The pre-stage. Your terminal AI reads the drawing once (PyMuPDF, deterministic —
|
|
41
|
+
# no LLM) and bakes every element's layer / thickness / colour into the contract's
|
|
42
|
+
# `filter` block. Double-click this node to open the filter view: keep only the real
|
|
43
|
+
# steel lines — by facet (layer / thickness / colour) or the eyedropper (click a line
|
|
44
|
+
# or label → Add its layer/thickness/colour) — then Save. That saved selection is what
|
|
45
|
+
# the takeoff reader binds profile labels to (it replaces the length heuristic; falls
|
|
46
|
+
# back to it when no filter is saved). Ships with a tiny example summary so it renders
|
|
47
|
+
# out of the box; "Re-read & re-bake ▸" replaces it. The run renders a kept-layers
|
|
48
|
+
# summary (a baked read-only overlay is a later refinement).
|
|
49
|
+
- id: filter
|
|
50
|
+
agent: html-report
|
|
51
|
+
command: render
|
|
52
|
+
config:
|
|
53
|
+
surface: steel-filter
|
|
54
|
+
data:
|
|
55
|
+
- { Layer: "S-ANNO-BEAM-STEL", Elements: 374, Kept: "yes (steel)" }
|
|
56
|
+
- { Layer: "S-ANNO-GRID", Elements: 319, Kept: "no" }
|
|
57
|
+
- { Layer: "S-ANNO-DIMS", Elements: 575, Kept: "no" }
|
|
58
|
+
|
|
59
|
+
# ── node 2: read — takeoff summary ───────────────────────────────────────────
|
|
60
|
+
# config.contract is the FloLess editor discriminator (renderers.js contractTypeOf).
|
|
61
|
+
# config.takeoff is the SINGLE SOURCE the editor + Approve-bake + render all key on:
|
|
62
|
+
# server/contract-bake.ts writes the approved contract under config.takeoff (finding
|
|
63
|
+
# this node by config.contract === type), and GET /api/contract falls back to it so
|
|
64
|
+
# the editor opens with the baked contract. It is BAKED at compose time by the terminal
|
|
65
|
+
# AI from the drawing above; the run re-renders it as an HTML summary. Ships with a tiny
|
|
66
|
+
# EXAMPLE so it renders out of the box — "Re-read & re-bake ▸" replaces config.takeoff.
|
|
67
|
+
- id: read
|
|
68
|
+
agent: html-report
|
|
69
|
+
command: render
|
|
70
|
+
config:
|
|
71
|
+
contract: steel.takeoff/v1
|
|
72
|
+
takeoff:
|
|
73
|
+
type: steel.takeoff/v1
|
|
74
|
+
source: { name: "Example — sample framing (not a real drawing)" }
|
|
75
|
+
weights:
|
|
76
|
+
W10X33: 49.1
|
|
77
|
+
HSS6X6X3/8: 27.48
|
|
78
|
+
plans:
|
|
79
|
+
- sheet: EX-1
|
|
80
|
+
title: Example Framing Plan
|
|
81
|
+
members:
|
|
82
|
+
- { id: m1, profile: W10X33, role: beam, wp: [[0,0],[6000,0]] }
|
|
83
|
+
- { id: m2, profile: HSS6X6X3/8, role: column, wp: [[0,0],[0,4500]] }
|
|
84
|
+
template: steel-takeoff-summary
|
|
85
|
+
# NOTE: html-report does NOT template a sibling config key — `data: '{{ config.takeoff }}'`
|
|
86
|
+
# collapses to one opaque item (verified: subtitle "1 item", no member data). So we mirror
|
|
87
|
+
# config.takeoff inline here for the render. config.takeoff above remains the canonical
|
|
88
|
+
# source (editor + Approve-bake + GET /api/contract); a later task can wire the render
|
|
89
|
+
# to it once html-report (or the BOM formatter) consumes the structured contract.
|
|
90
|
+
data:
|
|
91
|
+
type: steel.takeoff/v1
|
|
92
|
+
source: { name: "Example — sample framing (not a real drawing)" }
|
|
93
|
+
weights:
|
|
94
|
+
W10X33: 49.1
|
|
95
|
+
HSS6X6X3/8: 27.48
|
|
96
|
+
plans:
|
|
97
|
+
- sheet: EX-1
|
|
98
|
+
title: Example Framing Plan
|
|
99
|
+
members:
|
|
100
|
+
- { id: m1, profile: W10X33, role: beam, wp: [[0,0],[6000,0]] }
|
|
101
|
+
- { id: m2, profile: HSS6X6X3/8, role: column, wp: [[0,0],[0,4500]] }
|
|
102
|
+
|
|
103
|
+
# ── node 3: view — interactive 3D model ──────────────────────────────────────
|
|
104
|
+
# scene is BAKED at compose time (matches config.takeoff above).
|
|
105
|
+
# Ships with a minimal EXAMPLE scene matching the takeoff above so it renders
|
|
106
|
+
# out of the box. The terminal AI replaces the whole scene when re-baking.
|
|
107
|
+
- id: view
|
|
108
|
+
agent: viewer-3d
|
|
109
|
+
command: render
|
|
110
|
+
config:
|
|
111
|
+
scene:
|
|
112
|
+
meta: { name: "Example — sample framing (not a real drawing)", units: mm, up: z }
|
|
113
|
+
groups:
|
|
114
|
+
- { key: column, label: "Columns (HSS6x6x3/8)", color: "#60a5fa" }
|
|
115
|
+
- { key: beam, label: "Beams (W10x33)", color: "#94a3b8" }
|
|
116
|
+
elements:
|
|
117
|
+
- { id: m1, group: beam, kind: box, from: [0,0,0], to: [6000,0,0], section: { w: 150, d: 250 }, meta: { profile: "W10X33", sheet: "EX-1" } }
|
|
118
|
+
- { id: m2, group: column, kind: box, from: [0,0,0], to: [0,0,4500], section: { w: 150, d: 150 }, meta: { profile: "HSS6X6X3/8", sheet: "EX-1" } }
|
|
119
|
+
panels:
|
|
120
|
+
- title: Takeoff
|
|
121
|
+
note: "EXAMPLE — not your drawing. Swap the drawing input and ask your terminal AI to re-read & re-bake."
|
|
122
|
+
columns: [Section, "No.", "Length / Height"]
|
|
123
|
+
rows:
|
|
124
|
+
- ["W10X33", 1, "6.0 m"]
|
|
125
|
+
- ["HSS6X6X3/8", 1, "4.5 m"]
|
|
126
|
+
|
|
127
|
+
# ── node 4: bom — bill of materials ──────────────────────────────────────────
|
|
128
|
+
# Stub: renders the same takeoff data as a formatted BOM table.
|
|
129
|
+
# Replace with a real BOM formatter skill (later task).
|
|
130
|
+
- id: bom
|
|
131
|
+
agent: html-report
|
|
132
|
+
command: render
|
|
133
|
+
config:
|
|
134
|
+
template: steel-takeoff-bom
|
|
135
|
+
data:
|
|
136
|
+
type: steel.takeoff/v1
|
|
137
|
+
source: { name: "Example — sample framing (not a real drawing)" }
|
|
138
|
+
weights:
|
|
139
|
+
W10X33: 49.1
|
|
140
|
+
HSS6X6X3/8: 27.48
|
|
141
|
+
plans:
|
|
142
|
+
- sheet: EX-1
|
|
143
|
+
title: Example Framing Plan
|
|
144
|
+
members:
|
|
145
|
+
- { id: m1, profile: W10X33, role: beam, wp: [[0,0],[6000,0]] }
|
|
146
|
+
- { id: m2, profile: HSS6X6X3/8, role: column, wp: [[0,0],[0,4500]] }
|
|
147
|
+
|
|
148
|
+
connections:
|
|
149
|
+
- { from: filter, to: read }
|
|
150
|
+
- { from: read, to: view }
|
|
151
|
+
- { from: view, to: bom }
|