@floless/app 0.43.0 → 0.44.0

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.
@@ -0,0 +1,211 @@
1
+ app: tekla-bom-by-phase
2
+ version: 0.3.0
3
+ display-name: Tekla BOM by Phase
4
+ description: |
5
+ Reusable bill-of-materials for any Tekla model, scoped to one construction
6
+ phase. Three REAL data-carrying nodes, like the desktop floless canvas, and the
7
+ data actually flows along the edges:
8
+ (1) Phase input — reads the {{ inputs.phase }} knob, scopes it against the
9
+ live model (counts the parts in that phase), and emits the phase it
10
+ validated;
11
+ (2) BOM core logic — consumes the phase from node 1, selects every part in
12
+ that phase via the Open API, and builds a BOM categorized by part type
13
+ (BEAM / COLUMN / PLATE / ANGLE / ...), grouped by profile + material with
14
+ per-category subtotals, returning the styled HTML inline;
15
+ (3) HTML Report Viewer — consumes the HTML from node 2 and carries it as its
16
+ own output, so it is the genuine terminal the floless.app viewer renders
17
+ on double-click.
18
+ All three nodes run read-only (mode: read — no model mutation, no filesystem
19
+ write), so the app runs on any model with no write permission. The phase
20
+ travels node1 → node2 and the report travels node2 → node3: the canvas story
21
+ and the data flow are the same thing.
22
+ exposes-as-agent: false
23
+ inputs:
24
+ phase:
25
+ type: integer
26
+ default: 1
27
+ description: Construction phase number to report on (Tekla PHASE) — editable per run.
28
+ requires:
29
+ - tekla@0.1.x
30
+ layout: linear
31
+ nodes:
32
+ # ── node 1: Phase input — scope the run to one phase against the live model ──
33
+ - id: phase-input
34
+ agent: tekla
35
+ command: exec
36
+ mode: read
37
+ config:
38
+ version: "2025.0"
39
+ args:
40
+ phase: "{{ inputs.phase }}"
41
+ code: |
42
+ // Phase input — reads args["phase"] (default 1), scopes it against the
43
+ // live model by counting the parts in that phase, and EMITS the phase
44
+ // (plus the count + model name) so the downstream BOM node consumes a
45
+ // value that travels along the edge. Read-only.
46
+ using System;
47
+ using Tekla.Structures.Model;
48
+
49
+ if (model == null) return new { ok = false, error = "no active Tekla model" };
50
+ var m = (Model)model;
51
+
52
+ int phase = 1;
53
+ if (args != null && args.TryGetValue("phase", out var pv) && pv != null)
54
+ {
55
+ if (pv is int pi) phase = pi;
56
+ else if (pv is long pl) phase = (int)pl;
57
+ else if (pv is double pd) phase = (int)pd;
58
+ else { int.TryParse(pv.ToString(), out var ps); if (ps != 0) phase = ps; }
59
+ }
60
+
61
+ string modelName;
62
+ try { modelName = m.GetInfo().ModelName; } catch { modelName = "Tekla model"; }
63
+
64
+ int partsInPhase = 0;
65
+ var en = m.GetModelObjectSelector().GetAllObjects();
66
+ while (en.MoveNext())
67
+ {
68
+ var part = en.Current as Part;
69
+ if (part == null) continue;
70
+ Phase ph = null;
71
+ try { part.GetPhase(out ph); } catch { ph = null; }
72
+ if (ph != null && ph.PhaseNumber == phase) partsInPhase++;
73
+ }
74
+
75
+ return new { ok = true, phase, partsInPhase, modelName };
76
+ # ── node 2: BOM core logic — consume node 1's phase, build the report ────────
77
+ - id: bom
78
+ agent: tekla
79
+ command: exec
80
+ mode: read
81
+ config:
82
+ version: "2025.0"
83
+ args:
84
+ phase: "{{ phase-input.result.phase }}"
85
+ code: |
86
+ // Tekla BOM by Phase — reusable across models. exec globals: `dynamic model`
87
+ // (Tekla.Structures.Model.Model) + `IDictionary<string,object> args`.
88
+ // Reads args["phase"] (forwarded from the phase-input node, default 1),
89
+ // selects parts in that phase, builds a BOM categorized by part NAME
90
+ // (BEAM/COLUMN/PLATE/...) → grouped by profile+material within each
91
+ // category, and RETURNS the styled HTML inline (no file write — so it runs
92
+ // on any model with no filesystem permission). Read-only on the model.
93
+ using System;
94
+ using System.Collections.Generic;
95
+ using System.Linq;
96
+ using System.Text;
97
+ using Tekla.Structures.Model;
98
+
99
+ if (model == null) return new { ok = false, error = "no active Tekla model" };
100
+ var m = (Model)model;
101
+
102
+ // ── phase input (default 1) — robust to int / long / string templating ────────
103
+ int phase = 1;
104
+ if (args != null && args.TryGetValue("phase", out var pv) && pv != null)
105
+ {
106
+ if (pv is int pi) phase = pi;
107
+ else if (pv is long pl) phase = (int)pl;
108
+ else if (pv is double pd) phase = (int)pd;
109
+ else { int.TryParse(pv.ToString(), out var ps); if (ps != 0) phase = ps; }
110
+ }
111
+
112
+ string modelName;
113
+ try { modelName = m.GetInfo().ModelName; } catch { modelName = "Tekla model"; }
114
+
115
+ // ── aggregate: category(NAME) → "profile|material" → {qty,weight,length} ──────
116
+ var cats = new Dictionary<string, Dictionary<string, double[]>>(StringComparer.OrdinalIgnoreCase);
117
+ var disp = new Dictionary<string, string[]>(); // line key → [profile, material]
118
+ int totalParts = 0, phaseParts = 0; double totalWeight = 0;
119
+
120
+ var en = m.GetModelObjectSelector().GetAllObjects();
121
+ while (en.MoveNext())
122
+ {
123
+ var part = en.Current as Part;
124
+ if (part == null) continue;
125
+ totalParts++;
126
+
127
+ // phase filter
128
+ Phase ph = null;
129
+ try { part.GetPhase(out ph); } catch { ph = null; }
130
+ if (ph == null || ph.PhaseNumber != phase) continue;
131
+ phaseParts++;
132
+
133
+ string name = ""; part.GetReportProperty("NAME", ref name);
134
+ string profile = ""; part.GetReportProperty("PROFILE", ref profile);
135
+ string material = ""; part.GetReportProperty("MATERIAL", ref material);
136
+ double w = 0; part.GetReportProperty("WEIGHT", ref w);
137
+ double l = 0; part.GetReportProperty("LENGTH", ref l);
138
+
139
+ var cat = string.IsNullOrWhiteSpace(name) ? "(unnamed)" : name.Trim().ToUpperInvariant();
140
+ var line = profile + "|" + material;
141
+ if (!cats.TryGetValue(cat, out var lines)) { lines = new Dictionary<string, double[]>(); cats[cat] = lines; }
142
+ if (!lines.TryGetValue(line, out var agg)) { agg = new double[3]; lines[line] = agg; disp[line] = new[] { profile, material }; }
143
+ agg[0] += 1; agg[1] += w; agg[2] += l;
144
+ totalWeight += w;
145
+ }
146
+
147
+ // category order: heaviest first
148
+ var catOrder = cats.Keys.OrderByDescending(c => cats[c].Values.Sum(a => a[1])).ToList();
149
+ Func<string, string> E = s => (s ?? "").Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;");
150
+
151
+ var sb = new StringBuilder();
152
+ sb.Append("<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\">");
153
+ sb.Append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
154
+ sb.Append("<title>" + E(modelName) + " - BOM Phase " + phase + "</title><style>");
155
+ sb.Append("*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,'Segoe UI',Roboto,sans-serif;background:#0f172a;color:#e2e8f0;padding:2rem;line-height:1.6}.container{max-width:1200px;margin:0 auto}");
156
+ sb.Append(".report-header{border-bottom:2px solid #d946ef;padding-bottom:1rem;margin-bottom:2rem}.report-header h1{font-size:1.75rem;font-weight:700;color:#f8fafc}.report-header .subtitle{color:#94a3b8;font-size:.875rem;margin-top:.25rem}");
157
+ sb.Append(".summary-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin-bottom:2rem}.card{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:1.25rem}.card .label{font-size:.75rem;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8;margin-bottom:.25rem}.card .value{font-size:1.5rem;font-weight:700;color:#f8fafc;font-family:Consolas,monospace}.card .value.accent{color:#d946ef}");
158
+ sb.Append(".cat{margin-bottom:1.75rem}.cat h2{font-size:1rem;font-weight:700;color:#f8fafc;margin-bottom:.5rem;display:flex;align-items:baseline;gap:.5rem}.cat h2 .pill{font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#0f172a;background:#d946ef;border-radius:999px;padding:.1rem .5rem}.cat h2 .sub{font-size:.8rem;font-weight:400;color:#94a3b8;font-family:Consolas,monospace;margin-left:auto}");
159
+ sb.Append(".table-wrapper{overflow-x:auto;border-radius:8px;border:1px solid #334155}table{width:100%;border-collapse:collapse;font-size:.875rem}thead th{background:#1e293b;color:#94a3b8;font-weight:600;text-transform:uppercase;font-size:.7rem;letter-spacing:.05em;padding:.6rem 1rem;text-align:left;border-bottom:2px solid #334155}tbody td{padding:.55rem 1rem;border-bottom:1px solid #1e293b;font-family:Consolas,monospace;font-size:.8125rem}tbody tr{background:#0f172a}tbody tr:nth-child(even){background:#162032}tfoot td{padding:.55rem 1rem;font-family:Consolas,monospace;font-weight:700;color:#e2e8f0;background:#1e293b;border-top:2px solid #334155}.num{text-align:right}");
160
+ sb.Append(".report-footer{margin-top:2rem;padding-top:1rem;border-top:1px solid #334155;color:#64748b;font-size:.75rem;text-align:center}.empty{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:2rem;text-align:center;color:#94a3b8}");
161
+ sb.Append("</style></head><body><div class=\"container\">");
162
+ sb.Append("<div class=\"report-header\"><h1>Bill of Materials &middot; Phase " + phase + "</h1><div class=\"subtitle\">" + E(modelName) + " &middot; generated " + DateTime.Now.ToString("yyyy-MM-dd HH:mm") + " &middot; live Tekla Open API</div></div>");
163
+ sb.Append("<div class=\"summary-cards\">");
164
+ sb.Append("<div class=\"card\"><div class=\"label\">Phase</div><div class=\"value accent\">" + phase + "</div></div>");
165
+ sb.Append("<div class=\"card\"><div class=\"label\">Parts in phase</div><div class=\"value\">" + phaseParts.ToString("N0") + "</div></div>");
166
+ sb.Append("<div class=\"card\"><div class=\"label\">Categories</div><div class=\"value\">" + catOrder.Count.ToString("N0") + "</div></div>");
167
+ sb.Append("<div class=\"card\"><div class=\"label\">Total weight</div><div class=\"value accent\">" + (totalWeight / 1000.0).ToString("N1") + " t</div></div>");
168
+ sb.Append("</div>");
169
+
170
+ if (phaseParts == 0)
171
+ {
172
+ sb.Append("<div class=\"empty\">No parts found in phase " + phase + ". Try a different phase number.</div>");
173
+ }
174
+ else foreach (var cat in catOrder)
175
+ {
176
+ var lines = cats[cat];
177
+ var ordered = lines.Keys.OrderByDescending(k => lines[k][1]).ToList();
178
+ double cq = lines.Values.Sum(a => a[0]), cw = lines.Values.Sum(a => a[1]), cl = lines.Values.Sum(a => a[2]);
179
+ sb.Append("<div class=\"cat\"><h2><span class=\"pill\">" + E(cat) + "</span><span class=\"sub\">" + ((int)cq).ToString("N0") + " pcs &middot; " + cw.ToString("N1") + " kg</span></h2>");
180
+ sb.Append("<div class=\"table-wrapper\"><table><thead><tr><th>Profile</th><th>Material</th><th class=\"num\">Qty</th><th class=\"num\">Weight (kg)</th><th class=\"num\">Length (mm)</th></tr></thead><tbody>");
181
+ foreach (var k in ordered)
182
+ sb.Append("<tr><td>" + E(disp[k][0]) + "</td><td>" + E(disp[k][1]) + "</td><td class=\"num\">" + ((int)lines[k][0]) + "</td><td class=\"num\">" + lines[k][1].ToString("N1") + "</td><td class=\"num\">" + lines[k][2].ToString("N0") + "</td></tr>");
183
+ sb.Append("</tbody><tfoot><tr><td colspan=\"2\">" + E(cat) + " subtotal</td><td class=\"num\">" + ((int)cq) + "</td><td class=\"num\">" + cw.ToString("N1") + "</td><td class=\"num\">" + cl.ToString("N0") + "</td></tr></tfoot></table></div></div>");
184
+ }
185
+
186
+ sb.Append("<div class=\"report-footer\">FloLess &middot; AWARE - reusable BOM-by-phase, generated from the live Tekla model</div>");
187
+ sb.Append("</div></body></html>");
188
+
189
+ return new { ok = true, phase, html = sb.ToString(), modelName, totalParts, phaseParts, categories = catOrder.Count, totalWeight_kg = Math.Round(totalWeight, 1) };
190
+ # ── node 3: HTML Report Viewer — consume node 2's HTML, be the terminal ──────
191
+ - id: viewer
192
+ agent: tekla
193
+ command: exec
194
+ mode: read
195
+ config:
196
+ version: "2025.0"
197
+ args:
198
+ html: "{{ bom.result.html }}"
199
+ phase: "{{ bom.result.phase }}"
200
+ code: |
201
+ // HTML Report Viewer — the genuine terminal of the chain. It CONSUMES the
202
+ // report HTML built by the bom node ({{ bom.result.html }}) and returns it
203
+ // as its own `html`, so the report travels node2 → node3 along the edge and
204
+ // floless.app renders THIS node's output in the in-app HTML Viewer on
205
+ // double-click. Read-only; touches no model state.
206
+ string html = (args != null && args.TryGetValue("html", out var h) && h != null) ? h.ToString() : "";
207
+ object phase = (args != null && args.TryGetValue("phase", out var p)) ? p : null;
208
+ return new { ok = true, phase, html };
209
+ connections:
210
+ - { from: phase-input, to: bom }
211
+ - { from: bom, to: viewer }
@@ -0,0 +1,39 @@
1
+ app: tekla-connection-xray
2
+ version: 0.1.0
3
+ display-name: Tekla Connection X-Ray
4
+ description: |
5
+ Reproduces the desktop "connection X-ray" outcome on AWARE via the
6
+ host-agnostic ui-inspector agent (#128): walk every tab of an open
7
+ connection dialog capturing controls + screenshots, sentinelize a preset
8
+ to map opaque fields to their named attributes, and emit a self-contained
9
+ interactive HTML overlay. Host-agnostic — Tekla is just the target window.
10
+ exposes-as-agent: false
11
+ requires:
12
+ - ui-inspector@0.1.x
13
+ layout: dag
14
+ nodes:
15
+ - id: walk
16
+ agent: ui-inspector
17
+ command: walk-tabs
18
+ config:
19
+ window: '{{ inputs.dialog-window }}'
20
+ output-dir: '{{ inputs.out-dir }}'
21
+ - id: mapping
22
+ agent: ui-inspector
23
+ command: map-fields
24
+ config:
25
+ window: '{{ inputs.dialog-window }}'
26
+ preset: '{{ inputs.preset-file }}'
27
+ load-via: { control: 'button#load-attributes' }
28
+ sentinel-strategy: numeric-stride
29
+ - id: overlay
30
+ agent: ui-inspector
31
+ command: build-overlay
32
+ config:
33
+ controls: '{{ walk.tabs }}'
34
+ output-path: '{{ inputs.out-dir }}/connection-xray.html'
35
+ title: 'Connection X-ray'
36
+ connections:
37
+ - { from: walk, to: mapping }
38
+ - { from: walk, to: overlay }
39
+ - { from: mapping, to: overlay }
@@ -0,0 +1,106 @@
1
+ app: tekla-pdf-takeoff
2
+ version: 0.1.0
3
+ display-name: PDF Takeoff (deterministic)
4
+ description: |
5
+ B1 reference (Visual Inputs): a SWAPPABLE image/PDF input read DETERMINISTICALLY
6
+ at run time — no LLM, no model. The parse node opens the file at the run-supplied
7
+ path, sniffs it, and (for PDFs) counts pages with a regex over the raw bytes; the
8
+ viewer renders the summary inline. Swap the file via "Set inputs ▸" and re-run —
9
+ nothing recompiles. Neither node dereferences `model`, so it runs on the host
10
+ bridge with nothing open in Tekla (a true deterministic-parse demo). A real
11
+ geometry takeoff (the PdfPig nearest-neighbour pairing) is the same shape with a
12
+ parser library in place of the page-count.
13
+ exposes-as-agent: false
14
+ inputs:
15
+ drawing:
16
+ type: image
17
+ widget: file
18
+ accept: [pdf, png, jpg]
19
+ read-strategy: parse
20
+ description: The framing PDF / image to take off (swappable per run, no recompile).
21
+ requires:
22
+ - tekla@0.1.x
23
+ layout: linear
24
+ nodes:
25
+ # ── node 1: deterministic parse of the run-supplied file PATH (no model, no LLM) ──
26
+ - id: parse
27
+ agent: tekla
28
+ command: exec
29
+ mode: read
30
+ config:
31
+ version: "2025.0"
32
+ args:
33
+ drawing: "{{ inputs.drawing }}"
34
+ code: |
35
+ // Deterministic read of the file PATH in args["drawing"]. System.IO is legal
36
+ // in exec (only System.Net is banned). Never touches `model`. NO LLM. No
37
+ // System.Text.RegularExpressions (not referenced by the exec sandbox) — count
38
+ // pages with a plain IndexOf scan instead.
39
+ using System;
40
+ using System.Text;
41
+
42
+ string path = null;
43
+ if (args != null && args.TryGetValue("drawing", out var dv) && dv != null)
44
+ path = dv.ToString();
45
+ if (string.IsNullOrWhiteSpace(path) || !System.IO.File.Exists(path))
46
+ return new { ok = false, error = "no readable file at: " + (path ?? "(null)") };
47
+
48
+ byte[] bytes = System.IO.File.ReadAllBytes(path);
49
+ bool isPdf = bytes.Length >= 5 && bytes[0] == 0x25 && bytes[1] == 0x50 && bytes[2] == 0x44 && bytes[3] == 0x46 && bytes[4] == 0x2D;
50
+ // ASCII view keeps the structural "/Type /Page" markers intact (high bytes → '?').
51
+ string text = Encoding.ASCII.GetString(bytes);
52
+ int pages = 0, idx = 0;
53
+ if (isPdf)
54
+ {
55
+ while (true)
56
+ {
57
+ int hit = text.IndexOf("/Type /Page", idx, StringComparison.Ordinal);
58
+ if (hit < 0) break;
59
+ pages++;
60
+ idx = hit + 11;
61
+ }
62
+ if (pages == 0) pages = 1; // single-page PDFs may not carry the marker form
63
+ }
64
+ string name = System.IO.Path.GetFileName(path);
65
+ return new { ok = true, file = name, kind = isPdf ? "pdf" : "image", bytes = bytes.Length, pages };
66
+ # ── node 2: viewer — build the summary HTML inline; the terminal the HTML Viewer renders ──
67
+ - id: viewer
68
+ agent: tekla
69
+ command: exec
70
+ mode: read
71
+ config:
72
+ version: "2025.0"
73
+ args:
74
+ file: "{{ parse.result.file }}"
75
+ kind: "{{ parse.result.kind }}"
76
+ pages: "{{ parse.result.pages }}"
77
+ bytes: "{{ parse.result.bytes }}"
78
+ code: |
79
+ // Forward the parse summary as a styled report. No System.Net → manual escape.
80
+ using System;
81
+ using System.Text;
82
+
83
+ Func<string, string> E = s => (s ?? "").Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;");
84
+ string file = args != null && args.TryGetValue("file", out var f) && f != null ? f.ToString() : "";
85
+ string kind = args != null && args.TryGetValue("kind", out var k) && k != null ? k.ToString() : "";
86
+ string pages = args != null && args.TryGetValue("pages", out var p) && p != null ? p.ToString() : "";
87
+ string bytes = args != null && args.TryGetValue("bytes", out var b) && b != null ? b.ToString() : "";
88
+
89
+ var sb = new StringBuilder();
90
+ sb.Append("<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>PDF Takeoff</title><style>");
91
+ sb.Append("body{font-family:-apple-system,'Segoe UI',Roboto,sans-serif;background:#f1f5f9;color:#0f172a;padding:2.5rem;line-height:1.6}");
92
+ sb.Append(".wrap{max-width:560px;margin:0 auto;background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:1.75rem 2rem;box-shadow:0 10px 30px rgba(2,6,23,.06)}");
93
+ sb.Append("h1{font-size:1.5rem;font-weight:800;border-bottom:2px solid #4a9eff;padding-bottom:.75rem;margin-bottom:1.25rem}");
94
+ sb.Append("table{width:100%;border-collapse:collapse;font-size:.92rem}");
95
+ sb.Append("td{padding:.55rem .75rem;border-bottom:1px solid #eef2f7}");
96
+ sb.Append("td:first-child{color:#64748b;width:40%}td:last-child{font-weight:600}");
97
+ sb.Append("</style></head><body><div class=\"wrap\"><h1>PDF Takeoff</h1><table>");
98
+ sb.Append("<tr><td>File</td><td>" + E(file) + "</td></tr>");
99
+ sb.Append("<tr><td>Kind</td><td>" + E(kind) + "</td></tr>");
100
+ sb.Append("<tr><td>Pages</td><td>" + E(pages) + "</td></tr>");
101
+ sb.Append("<tr><td>Bytes</td><td>" + E(bytes) + "</td></tr>");
102
+ sb.Append("</table></div></body></html>");
103
+ string html = sb.ToString();
104
+ return new { ok = true, file, kind, pages, html };
105
+ connections:
106
+ - { from: parse, to: viewer }
@@ -0,0 +1,129 @@
1
+ app: tekla-selection-now
2
+ version: 0.1.0
3
+ display-name: Tekla Selection → properties (on demand)
4
+ description: |
5
+ On-demand: reports the CURRENTLY-selected objects' key properties (type, mark,
6
+ name, profile, material, weight) from the active Tekla model as an HTML table.
7
+ Select objects in Tekla, then press ▶ Run workflow — the report renders in the
8
+ HTML Viewer. A single read-only exec node, so the Run button returns a report
9
+ immediately (no waiting on an event). The event-triggered sibling
10
+ `tekla-selection-report` fires automatically on each SelectionChange instead
11
+ (set it up as a ⏱ trigger routine). Needs a running Tekla (2025 or 2026) with a
12
+ model open; pick the version via the `version` input.
13
+ exposes-as-agent: false
14
+ inputs:
15
+ version:
16
+ type: string
17
+ default: "2025.0"
18
+ description: Tekla version to attach to (e.g. "2025.0" or "2026.0") — editable per run.
19
+ layout: linear
20
+ requires:
21
+ - tekla@0.1.x
22
+ nodes:
23
+ - id: report # single read-only node — reads the live selection on Run
24
+ agent: tekla
25
+ command: exec
26
+ mode: read # author intent; AWARE defaults exec→write (issue #165), cosmetic
27
+ config:
28
+ version: "{{ inputs.version }}"
29
+ code: |
30
+ // Report the currently-selected objects' key properties. Read-only.
31
+ Func<string,string> E = s => (s ?? "")
32
+ .Replace("&","&amp;").Replace("<","&lt;").Replace(">","&gt;").Replace("\"","&quot;");
33
+
34
+ var selector = new Tekla.Structures.Model.UI.ModelObjectSelector();
35
+ var en = selector.GetSelectedObjects();
36
+ var rows = new System.Collections.Generic.List<object>();
37
+ var sb = new System.Text.StringBuilder(); // tbody rows only; the table chrome is built below
38
+
39
+ int i = 0;
40
+ double totalWeight = 0;
41
+ while (en.MoveNext())
42
+ {
43
+ // FQN: bare `ModelObject` is ambiguous (exec imports both
44
+ // Tekla.Structures.Model and Tekla.Structures.Drawing).
45
+ var mo = en.Current as Tekla.Structures.Model.ModelObject;
46
+ if (mo == null) continue;
47
+ i++;
48
+ string type = mo.GetType().Name;
49
+ string mark = ""; mo.GetReportProperty("ASSEMBLY_POS", ref mark);
50
+ string name = ""; mo.GetReportProperty("NAME", ref name);
51
+ string profile = ""; mo.GetReportProperty("PROFILE", ref profile);
52
+ string material = ""; mo.GetReportProperty("MATERIAL", ref material);
53
+ double weight = 0; mo.GetReportProperty("WEIGHT", ref weight);
54
+ string guid = mo.Identifier.GUID.ToString();
55
+
56
+ rows.Add(new { index = i, type, mark, name, profile, material, weight, guid });
57
+ totalWeight += weight;
58
+ sb.Append("<tr><td class='num'>").Append(i)
59
+ .Append("</td><td>").Append(E(type))
60
+ .Append("</td><td>").Append(E(mark))
61
+ .Append("</td><td class='col-text'>").Append(E(name))
62
+ .Append("</td><td class='col-text'>").Append(E(profile))
63
+ .Append("</td><td class='col-text'>").Append(E(material))
64
+ .Append("</td><td class='num'>").Append(weight.ToString("0.0"))
65
+ .Append("</td><td class='col-guid' title='").Append(E(guid)).Append("'>").Append(E(guid)).Append("</td></tr>");
66
+ }
67
+
68
+ string modelName;
69
+ try { modelName = ((Tekla.Structures.Model.Model)model).GetInfo().ModelName; }
70
+ catch { modelName = "Tekla model"; }
71
+ string at = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
72
+
73
+ // Modern FloLess report skin (shadcn dark slate-blue baseline). Self-contained
74
+ // inline <style> so it renders in the in-app dark iframe, a new tab, and the
75
+ // hosted shared page alike. The "Made with FloLess" badge is injected by the
76
+ // server (report-badge.ts) — never add it here. Accent is brand blue #3b82f6
77
+ // (a selection report is general-purpose; fuchsia is bom-by-phase's phase hue).
78
+ string css =
79
+ "*{margin:0;padding:0;box-sizing:border-box}"
80
+ + "body{font-family:-apple-system,'Segoe UI',Roboto,sans-serif;background:#0f172a;color:#e2e8f0;padding:2rem;line-height:1.6}"
81
+ + ".container{max-width:1200px;margin:0 auto}"
82
+ + ".report-header{border-bottom:2px solid #3b82f6;padding-bottom:1rem;margin-bottom:2rem}"
83
+ + ".report-header h1{font-size:1.75rem;font-weight:700;color:#f8fafc}"
84
+ + ".report-header .subtitle{color:#94a3b8;font-size:.875rem;margin-top:.25rem}"
85
+ + ".summary-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin-bottom:2rem}"
86
+ + ".card{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:1.25rem}"
87
+ + ".card .label{font-size:.75rem;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8;margin-bottom:.25rem}"
88
+ + ".card .value{font-size:1.5rem;font-weight:700;color:#f8fafc;font-family:Consolas,monospace}"
89
+ + ".card .value.accent{color:#3b82f6}"
90
+ + ".table-wrapper{overflow-x:auto;border-radius:8px;border:1px solid #334155}"
91
+ + "table{width:100%;border-collapse:collapse;font-size:.875rem}"
92
+ + "thead th{background:#1e293b;color:#94a3b8;font-weight:600;text-transform:uppercase;font-size:.7rem;letter-spacing:.05em;padding:.6rem 1rem;text-align:left;border-bottom:2px solid #334155;white-space:nowrap}"
93
+ + "tbody td{padding:.55rem 1rem;border-bottom:1px solid #1e293b;font-family:Consolas,monospace;font-size:.8125rem}"
94
+ + "tbody tr{background:#0f172a}tbody tr:nth-child(even){background:#162032}"
95
+ + ".col-guid{max-width:14rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}"
96
+ + ".col-text{word-break:break-word}.num{text-align:right}"
97
+ + ".empty{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:2rem;text-align:center;color:#94a3b8}"
98
+ + ".empty .empty-icon{font-size:1.5rem;margin-bottom:.5rem;color:#475569}"
99
+ + ".report-footer{margin-top:2rem;padding-top:1rem;border-top:1px solid #334155;color:#64748b;font-size:.75rem;text-align:center}";
100
+
101
+ var doc = new System.Text.StringBuilder();
102
+ doc.Append("<!doctype html><html lang='en'><head><meta charset='utf-8'>")
103
+ .Append("<meta name='viewport' content='width=device-width, initial-scale=1'>")
104
+ .Append("<title>Selection report &mdash; ").Append(E(modelName)).Append("</title>")
105
+ .Append("<style>").Append(css).Append("</style></head><body><div class='container'>");
106
+ doc.Append("<div class='report-header'><h1>Selection report &mdash; ").Append(E(modelName)).Append("</h1>")
107
+ .Append("<div class='subtitle'>").Append(i).Append(" object(s) selected &middot; ").Append(E(at)).Append(" &middot; live Tekla Open API</div></div>");
108
+ if (i == 0)
109
+ {
110
+ doc.Append("<div class='empty'><div class='empty-icon'>&#9723;</div>")
111
+ .Append("<p>Nothing selected. Select objects in Tekla, then press &#9654; Run workflow.</p></div>");
112
+ }
113
+ else
114
+ {
115
+ doc.Append("<div class='summary-cards'>")
116
+ .Append("<div class='card'><div class='label'>Objects selected</div><div class='value'>").Append(i).Append("</div></div>")
117
+ .Append("<div class='card'><div class='label'>Total weight</div><div class='value accent'>").Append(totalWeight.ToString("N1")).Append(" kg</div></div>")
118
+ .Append("</div>");
119
+ doc.Append("<div class='table-wrapper'><table><thead><tr>")
120
+ .Append("<th class='num'>#</th><th>Type</th><th>Mark</th><th class='col-text'>Name</th>")
121
+ .Append("<th class='col-text'>Profile</th><th class='col-text'>Material</th><th class='num'>Weight (kg)</th><th>GUID</th>")
122
+ .Append("</tr></thead><tbody>").Append(sb.ToString()).Append("</tbody></table></div>");
123
+ }
124
+ doc.Append("<div class='report-footer'>Selection report &middot; ").Append(E(modelName)).Append(" &middot; generated ").Append(E(at)).Append("</div>");
125
+ doc.Append("</div></body></html>");
126
+ string html = doc.ToString();
127
+
128
+ return new { ok = true, count = i, selected = rows, modelName, at, totalWeight, html };
129
+ connections: []
@@ -0,0 +1,145 @@
1
+ app: tekla-selection-report
2
+ version: 0.1.0
3
+ display-name: Tekla Selection → properties report
4
+ description: |
5
+ Event-triggered: a tekla.watch source streams SelectionChange events from the
6
+ active Tekla model; on each selection change a downstream exec node reads the
7
+ currently-selected objects and reports their key properties (type, mark, name,
8
+ profile, material, weight) as an HTML table. Exercises the full Tekla Events
9
+ surface delivered by aware #219/#220 (watch now receives live events, and
10
+ `events` selects which to stream). Needs a running Tekla (2025 or 2026) with a
11
+ model open; pick the version per run via the `version` input.
12
+ exposes-as-agent: false
13
+ inputs:
14
+ version:
15
+ type: string
16
+ default: "2025.0"
17
+ description: Tekla version to attach to (e.g. "2025.0" or "2026.0") — editable per run.
18
+ layout: linear
19
+ requires:
20
+ - tekla@0.1.x
21
+ nodes:
22
+ - id: watch # SOURCE — streaming lifecycle:start; one fire per SelectionChange
23
+ agent: tekla
24
+ command: watch
25
+ config:
26
+ version: "{{ inputs.version }}"
27
+ events: SelectionChange # full-Events-surface selector (#219/#220)
28
+ filter: all
29
+ - id: report # SINK — read the live selection, report each object's properties
30
+ agent: tekla
31
+ command: exec
32
+ mode: read # author intent; AWARE defaults exec→write (issue #165), cosmetic
33
+ config:
34
+ version: "{{ inputs.version }}"
35
+ # The fired SelectionChange event carries no object payload (the event has no
36
+ # args), so the report reads the CURRENT selection from the live model. The
37
+ # event's own fields are bound under {{ inputs.* }} by the streaming runtime.
38
+ args:
39
+ trigger: "{{ inputs.event }}"
40
+ at: "{{ inputs.delivered_at }}"
41
+ code: |
42
+ // Report the currently-selected objects' key properties. Fires on each
43
+ // SelectionChange from the upstream watch node. Read-only.
44
+ Func<string,string> E = s => (s ?? "")
45
+ .Replace("&","&amp;").Replace("<","&lt;").Replace(">","&gt;").Replace("\"","&quot;");
46
+ string Arg(string k) => args != null && args.ContainsKey(k) && args[k] != null
47
+ ? args[k].ToString() : "";
48
+
49
+ var selector = new Tekla.Structures.Model.UI.ModelObjectSelector();
50
+ var en = selector.GetSelectedObjects();
51
+ var rows = new System.Collections.Generic.List<object>();
52
+ var sb = new System.Text.StringBuilder(); // tbody rows only; the table chrome is built below
53
+
54
+ int i = 0;
55
+ double totalWeight = 0;
56
+ while (en.MoveNext())
57
+ {
58
+ // FQN: bare `ModelObject` is ambiguous (exec imports both
59
+ // Tekla.Structures.Model and Tekla.Structures.Drawing).
60
+ var mo = en.Current as Tekla.Structures.Model.ModelObject;
61
+ if (mo == null) continue;
62
+ i++;
63
+ string type = mo.GetType().Name;
64
+ string mark = ""; mo.GetReportProperty("ASSEMBLY_POS", ref mark);
65
+ string name = ""; mo.GetReportProperty("NAME", ref name);
66
+ string profile = ""; mo.GetReportProperty("PROFILE", ref profile);
67
+ string material = ""; mo.GetReportProperty("MATERIAL", ref material);
68
+ double weight = 0; mo.GetReportProperty("WEIGHT", ref weight);
69
+ string guid = mo.Identifier.GUID.ToString();
70
+
71
+ rows.Add(new { index = i, type, mark, name, profile, material, weight, guid });
72
+ totalWeight += weight;
73
+ sb.Append("<tr><td class='num'>").Append(i)
74
+ .Append("</td><td>").Append(E(type))
75
+ .Append("</td><td>").Append(E(mark))
76
+ .Append("</td><td class='col-text'>").Append(E(name))
77
+ .Append("</td><td class='col-text'>").Append(E(profile))
78
+ .Append("</td><td class='col-text'>").Append(E(material))
79
+ .Append("</td><td class='num'>").Append(weight.ToString("0.0"))
80
+ .Append("</td><td class='col-guid' title='").Append(E(guid)).Append("'>").Append(E(guid)).Append("</td></tr>");
81
+ }
82
+
83
+ string modelName;
84
+ try { modelName = ((Tekla.Structures.Model.Model)model).GetInfo().ModelName; }
85
+ catch { modelName = "Tekla model"; }
86
+ string at = Arg("at"); if (string.IsNullOrEmpty(at)) at = System.DateTime.UtcNow.ToString("o");
87
+
88
+ // Modern FloLess report skin (shadcn dark slate-blue baseline). Self-contained
89
+ // inline <style> so it renders in the in-app dark iframe, a new tab, and the
90
+ // hosted shared page alike. The "Made with FloLess" badge is injected by the
91
+ // server (report-badge.ts) — never add it here. Accent is brand blue #3b82f6
92
+ // (a selection report is general-purpose; fuchsia is bom-by-phase's phase hue).
93
+ string css =
94
+ "*{margin:0;padding:0;box-sizing:border-box}"
95
+ + "body{font-family:-apple-system,'Segoe UI',Roboto,sans-serif;background:#0f172a;color:#e2e8f0;padding:2rem;line-height:1.6}"
96
+ + ".container{max-width:1200px;margin:0 auto}"
97
+ + ".report-header{border-bottom:2px solid #3b82f6;padding-bottom:1rem;margin-bottom:2rem}"
98
+ + ".report-header h1{font-size:1.75rem;font-weight:700;color:#f8fafc}"
99
+ + ".report-header .subtitle{color:#94a3b8;font-size:.875rem;margin-top:.25rem}"
100
+ + ".summary-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin-bottom:2rem}"
101
+ + ".card{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:1.25rem}"
102
+ + ".card .label{font-size:.75rem;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8;margin-bottom:.25rem}"
103
+ + ".card .value{font-size:1.5rem;font-weight:700;color:#f8fafc;font-family:Consolas,monospace}"
104
+ + ".card .value.accent{color:#3b82f6}"
105
+ + ".table-wrapper{overflow-x:auto;border-radius:8px;border:1px solid #334155}"
106
+ + "table{width:100%;border-collapse:collapse;font-size:.875rem}"
107
+ + "thead th{background:#1e293b;color:#94a3b8;font-weight:600;text-transform:uppercase;font-size:.7rem;letter-spacing:.05em;padding:.6rem 1rem;text-align:left;border-bottom:2px solid #334155;white-space:nowrap}"
108
+ + "tbody td{padding:.55rem 1rem;border-bottom:1px solid #1e293b;font-family:Consolas,monospace;font-size:.8125rem}"
109
+ + "tbody tr{background:#0f172a}tbody tr:nth-child(even){background:#162032}"
110
+ + ".col-guid{max-width:14rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}"
111
+ + ".col-text{word-break:break-word}.num{text-align:right}"
112
+ + ".empty{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:2rem;text-align:center;color:#94a3b8}"
113
+ + ".empty .empty-icon{font-size:1.5rem;margin-bottom:.5rem;color:#475569}"
114
+ + ".report-footer{margin-top:2rem;padding-top:1rem;border-top:1px solid #334155;color:#64748b;font-size:.75rem;text-align:center}";
115
+
116
+ var doc = new System.Text.StringBuilder();
117
+ doc.Append("<!doctype html><html lang='en'><head><meta charset='utf-8'>")
118
+ .Append("<meta name='viewport' content='width=device-width, initial-scale=1'>")
119
+ .Append("<title>Selection report &mdash; ").Append(E(modelName)).Append("</title>")
120
+ .Append("<style>").Append(css).Append("</style></head><body><div class='container'>");
121
+ doc.Append("<div class='report-header'><h1>Selection report &mdash; ").Append(E(modelName)).Append("</h1>")
122
+ .Append("<div class='subtitle'>").Append(i).Append(" object(s) selected &middot; ").Append(E(at)).Append(" &middot; on selection change</div></div>");
123
+ if (i == 0)
124
+ {
125
+ doc.Append("<div class='empty'><div class='empty-icon'>&#9723;</div>")
126
+ .Append("<p>Nothing selected.</p></div>");
127
+ }
128
+ else
129
+ {
130
+ doc.Append("<div class='summary-cards'>")
131
+ .Append("<div class='card'><div class='label'>Objects selected</div><div class='value'>").Append(i).Append("</div></div>")
132
+ .Append("<div class='card'><div class='label'>Total weight</div><div class='value accent'>").Append(totalWeight.ToString("N1")).Append(" kg</div></div>")
133
+ .Append("</div>");
134
+ doc.Append("<div class='table-wrapper'><table><thead><tr>")
135
+ .Append("<th class='num'>#</th><th>Type</th><th>Mark</th><th class='col-text'>Name</th>")
136
+ .Append("<th class='col-text'>Profile</th><th class='col-text'>Material</th><th class='num'>Weight (kg)</th><th>GUID</th>")
137
+ .Append("</tr></thead><tbody>").Append(sb.ToString()).Append("</tbody></table></div>");
138
+ }
139
+ doc.Append("<div class='report-footer'>Selection report &middot; ").Append(E(modelName)).Append(" &middot; generated ").Append(E(at)).Append("</div>");
140
+ doc.Append("</div></body></html>");
141
+ string html = doc.ToString();
142
+
143
+ return new { ok = true, count = i, selected = rows, modelName, at, totalWeight, html };
144
+ connections:
145
+ - { from: watch, to: report }