@floless/app 0.34.2 → 0.35.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 +89 -32
- package/dist/schemas/steel.takeoff.v1.schema.json +1 -0
- package/dist/skills/floless-app-steel-takeoff/SKILL.md +6 -3
- package/dist/skills/floless-app-workflows/references/dev-server-and-run-trace.md +4 -2
- package/dist/web/app.css +24 -23
- package/dist/web/app.js +7 -3
- package/dist/web/aware.js +47 -5
- package/dist/web/index.html +22 -9
- package/dist/web/package.json +5 -0
- package/dist/web/panels.js +25 -11
- package/dist/web/steel-3d-core.js +137 -0
- package/dist/web/steel-3d-view.js +422 -0
- package/dist/web/steel-editor.html +140 -6
- package/dist/web/vendor/OrbitControls.js +1963 -0
- package/dist/web/vendor/README.md +15 -0
- package/dist/web/vendor/three.core.js +59732 -0
- package/dist/web/vendor/three.module.js +19552 -0
- package/package.json +1 -1
package/dist/web/index.html
CHANGED
|
@@ -33,13 +33,10 @@
|
|
|
33
33
|
</span>
|
|
34
34
|
<span class="name">FloLess</span>
|
|
35
35
|
</div>
|
|
36
|
-
<!-- Workspace view switch (Canvas = the workflow topology; Dashboard = the
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<button type="button" class="view-btn active" data-view="canvas" aria-pressed="true">Canvas</button>
|
|
41
|
-
<button type="button" class="view-btn" data-view="dashboard" aria-pressed="false">Dashboard<span class="view-dot" id="dash-dot" hidden aria-label="Dashboard updated">●</span></button>
|
|
42
|
-
</div>
|
|
36
|
+
<!-- Workspace view switch (Canvas = the workflow topology; Dashboard = the user's
|
|
37
|
+
custom panels from ~/.floless/ui/extensions.json) moved into the ≡ menu in #149.
|
|
38
|
+
The "dashboard updated" signal now rides the ≡ hamburger badge (.has-dash-update)
|
|
39
|
+
and the menu item's dot (#dash-dot, relocated into the menu). -->
|
|
43
40
|
<!-- Shown only while custom panels exist; opens Undo · History · Reset. -->
|
|
44
41
|
<button type="button" id="ext-badge" class="ext-badge" hidden aria-haspopup="menu" aria-expanded="false" aria-controls="ext-menu" data-tip="Your dashboard is customized — undo, history, reset to default">Customized</button>
|
|
45
42
|
</div>
|
|
@@ -57,8 +54,8 @@
|
|
|
57
54
|
</div>
|
|
58
55
|
</div>
|
|
59
56
|
<select id="prompt-select" hidden></select>
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
<!-- Agents (⊞) and Routines (⏱) buttons moved into the ≡ menu in #149
|
|
58
|
+
(Ctrl+G / Ctrl+R); the toolbar keeps only the run-critical controls. -->
|
|
62
59
|
<span class="ctl-sep" aria-hidden="true"></span>
|
|
63
60
|
<span class="run-state" id="run-state" role="status" aria-live="polite"></span>
|
|
64
61
|
<button id="compile-btn" data-tip="Compile + approve → freeze the .lock">⎙ Compile</button>
|
|
@@ -265,7 +262,22 @@
|
|
|
265
262
|
<span class="menu-label">Find on canvas</span>
|
|
266
263
|
<span class="menu-kbd">Ctrl+F</span>
|
|
267
264
|
</button>
|
|
265
|
+
<!-- Workspace view switch (was the header Canvas|Dashboard toggle, #149). One
|
|
266
|
+
contextual item: the label names the OTHER view; the dot lights when the
|
|
267
|
+
dashboard changed while on Canvas. Wired in panels.js (owns the view state). -->
|
|
268
|
+
<button class="menu-item" data-action="view-switch" role="menuitem">
|
|
269
|
+
<span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/></svg></span>
|
|
270
|
+
<span class="menu-label" id="menu-view-label">Switch to Dashboard</span>
|
|
271
|
+
<span class="view-dot" id="dash-dot" hidden aria-label="Dashboard updated">●</span>
|
|
272
|
+
</button>
|
|
268
273
|
<div class="menu-divider"></div>
|
|
274
|
+
<!-- Agents library (was the header ⊞ Agents button, #149). Heads the agent group
|
|
275
|
+
(Agents / Graft into agent / Bake into agent). Ctrl+G. -->
|
|
276
|
+
<button class="menu-item" data-action="agents" role="menuitem">
|
|
277
|
+
<span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg></span>
|
|
278
|
+
<span class="menu-label">Agents</span>
|
|
279
|
+
<span class="menu-kbd">Ctrl+G</span>
|
|
280
|
+
</button>
|
|
269
281
|
<button class="menu-item" data-action="graft" role="menuitem">
|
|
270
282
|
<span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 22v-5"/><path d="M15 8V2"/><path d="M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z"/><path d="M9 8V2"/></svg></span>
|
|
271
283
|
<span class="menu-label">Graft into agent</span>
|
|
@@ -283,6 +295,7 @@
|
|
|
283
295
|
<button class="menu-item" data-action="routines" role="menuitem">
|
|
284
296
|
<span class="menu-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><line x1="10" x2="14" y1="2" y2="2"/><line x1="12" x2="15" y1="14" y2="11"/><circle cx="12" cy="14" r="8"/></svg></span>
|
|
285
297
|
<span class="menu-label">Routines</span>
|
|
298
|
+
<span class="menu-kbd">Ctrl+R</span>
|
|
286
299
|
</button>
|
|
287
300
|
<div class="menu-divider"></div>
|
|
288
301
|
<button class="menu-item" data-action="report-issue" role="menuitem">
|
package/dist/web/panels.js
CHANGED
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
|
|
29
29
|
const $canvasMain = document.getElementById('canvas-main');
|
|
30
30
|
const $dashboard = document.getElementById('dashboard');
|
|
31
|
-
const $
|
|
32
|
-
const $
|
|
31
|
+
const $dashDot = document.getElementById('dash-dot'); // the "updated" dot, now in the ≡ menu item
|
|
32
|
+
const $menuBtn = document.getElementById('menu-btn'); // hosts the dashboard-updated badge (#149)
|
|
33
|
+
const $menuViewLabel = document.getElementById('menu-view-label'); // the ≡ "Switch to Dashboard/Canvas" label
|
|
33
34
|
const $extBadge = document.getElementById('ext-badge');
|
|
34
35
|
const $extMenu = document.getElementById('ext-menu');
|
|
35
36
|
const $extUndo = document.getElementById('ext-undo');
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
const $centerName = document.getElementById('center-panel-name');
|
|
39
40
|
const $centerRole = document.getElementById('center-panel-role');
|
|
40
41
|
const $resetModal = document.getElementById('ext-reset-modal');
|
|
41
|
-
if (!$dashboard
|
|
42
|
+
if (!$dashboard) return; // markup absent — nothing to wire
|
|
42
43
|
|
|
43
44
|
const LS_VIEW = 'floless:view';
|
|
44
45
|
let view = 'canvas';
|
|
@@ -65,18 +66,23 @@
|
|
|
65
66
|
const descriptorInvalid = () => !!(ext.validation && ext.validation.valid === false);
|
|
66
67
|
|
|
67
68
|
// ── view toggle (Canvas | Dashboard) ─────────────────────────────────────────
|
|
69
|
+
// The "dashboard updated" signal: a dot on the ≡ menu's view-switch item AND a badge on
|
|
70
|
+
// the ≡ hamburger, so it's visible without opening the menu (#149 — the old toggle that
|
|
71
|
+
// hosted the dot is gone).
|
|
72
|
+
function setDashUpdated(on) {
|
|
73
|
+
if ($dashDot) $dashDot.hidden = !on;
|
|
74
|
+
if ($menuBtn) $menuBtn.classList.toggle('has-dash-update', on);
|
|
75
|
+
}
|
|
68
76
|
function applyView() {
|
|
69
77
|
const dash = view === 'dashboard';
|
|
70
78
|
if ($canvasMain) $canvasMain.classList.toggle('view-dashboard', dash);
|
|
71
79
|
$dashboard.hidden = !dash;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
b.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
76
|
-
});
|
|
80
|
+
// The Canvas|Dashboard toggle now lives in the ≡ menu as one contextual item (#149):
|
|
81
|
+
// its label names the OTHER view — the destination of the switch.
|
|
82
|
+
if ($menuViewLabel) $menuViewLabel.textContent = dash ? 'Switch to Canvas' : 'Switch to Dashboard';
|
|
77
83
|
if ($centerName) $centerName.textContent = dash ? 'Dashboard' : 'Canvas';
|
|
78
84
|
if ($centerRole) $centerRole.textContent = dash ? 'your panels · composed by your terminal AI' : 'transparency layer · read-mostly';
|
|
79
|
-
if (dash
|
|
85
|
+
if (dash) setDashUpdated(false); // seen — clear the "updated" signal
|
|
80
86
|
}
|
|
81
87
|
function setView(v) {
|
|
82
88
|
view = v === 'dashboard' ? 'dashboard' : 'canvas';
|
|
@@ -84,7 +90,15 @@
|
|
|
84
90
|
applyView();
|
|
85
91
|
if (view === 'dashboard') renderDashboard();
|
|
86
92
|
}
|
|
87
|
-
|
|
93
|
+
function toggleView() { setView(view === 'dashboard' ? 'canvas' : 'dashboard'); }
|
|
94
|
+
// The ≡ menu's "Switch to Dashboard/Canvas" item drives the view (the header toggle was
|
|
95
|
+
// removed in #149). handleMenuAction lives in app.js and is already wrapped in aware.js
|
|
96
|
+
// for graft/bake; wrap once more here for view-switch and fall through for everything else.
|
|
97
|
+
const _prevHandleMenuAction = handleMenuAction;
|
|
98
|
+
handleMenuAction = function (action) {
|
|
99
|
+
if (action === 'view-switch') { toggleView(); return; }
|
|
100
|
+
_prevHandleMenuAction(action);
|
|
101
|
+
};
|
|
88
102
|
|
|
89
103
|
// ── block renderers (all output escaped; placeholders over errors) ──────────
|
|
90
104
|
const payloadFor = (source) => (typeof source === 'string' ? ext.data[source] : undefined);
|
|
@@ -685,7 +699,7 @@
|
|
|
685
699
|
renderDashboard();
|
|
686
700
|
// Content changed while the user is on Canvas → light the Dashboard dot
|
|
687
701
|
// (never auto-switch — don't yank their context).
|
|
688
|
-
if (opts.fromChange && view === 'canvas' && allPanels().length
|
|
702
|
+
if (opts.fromChange && view === 'canvas' && allPanels().length) setDashUpdated(true);
|
|
689
703
|
refreshPending();
|
|
690
704
|
}
|
|
691
705
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* steel-3d-core.js — PURE, framework-free core for the steel 3D editor (no Three.js, no DOM, no
|
|
3
|
+
* window). Unit-tested in node (server/steel-3d-core.test.ts) AND loaded by web/steel-3d-view.js.
|
|
4
|
+
* This is the extraction-ready unit: the generic snap engine + the typed-candidate model are what
|
|
5
|
+
* later lift into AWARE viewer-3d; only `snapCandidates` (beam top-flange centerline, column
|
|
6
|
+
* plan-center) is steel-specific.
|
|
7
|
+
*
|
|
8
|
+
* Coordinate map MUST match server/contract-to-scene.ts (the bake) so editing-space == render-space:
|
|
9
|
+
* scene is mm, Z-up. mmX = dispX·k, mmY = -dispY·k (k = FT_TO_MM / pt_per_ft, Y flipped),
|
|
10
|
+
* mmZ = elevation_inches · IN_TO_MM. planPointToWp is the inverse (editing-only; the bake never
|
|
11
|
+
* needs it).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const FT_TO_MM = 304.8;
|
|
15
|
+
export const IN_TO_MM = 25.4;
|
|
16
|
+
|
|
17
|
+
const kOf = (ptPerFt) => FT_TO_MM / (ptPerFt > 0 ? ptPerFt : 1);
|
|
18
|
+
|
|
19
|
+
const nz = (n) => n + 0; // normalize -0 → 0 (negating 0 yields -0, which surprises equality checks)
|
|
20
|
+
|
|
21
|
+
/** display-space wp point → scene mm [x,y] (Y flipped, matching contractToScene.dispToMm). */
|
|
22
|
+
export function dispToMm(pt, ptPerFt) { const k = kOf(ptPerFt); return [nz(pt[0] * k), nz(-pt[1] * k)]; }
|
|
23
|
+
|
|
24
|
+
/** scene mm point [x,y,…] → display-space wp [px,py] (inverse of dispToMm). */
|
|
25
|
+
export function planPointToWp(p, ptPerFt) { const k = kOf(ptPerFt); return [nz(p[0] / k), nz(-p[1] / k)]; }
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* member → its 3D centerline `{ kind, line:[[x,y,z],[x,y,z]] }` in mm, matching contractToScene's
|
|
29
|
+
* from/to. Beam: work-line at T.O.S. Column: vertical at the plan point, bos→tos.
|
|
30
|
+
*/
|
|
31
|
+
export function memberGeometry(m, ptPerFt, defaultTosMm = 0) {
|
|
32
|
+
const a = dispToMm(m.wp[0], ptPerFt), b = dispToMm(m.wp[1], ptPerFt);
|
|
33
|
+
if (m.role === 'column') {
|
|
34
|
+
const bos = m.col && m.col.bos != null ? m.col.bos * IN_TO_MM : 0;
|
|
35
|
+
const tos = m.col && m.col.tos != null ? m.col.tos * IN_TO_MM : defaultTosMm;
|
|
36
|
+
return { kind: 'column', line: [[a[0], a[1], bos], [a[0], a[1], tos]] };
|
|
37
|
+
}
|
|
38
|
+
const e = m.ends || [];
|
|
39
|
+
const z0 = e[0] && e[0].tos != null ? e[0].tos * IN_TO_MM : defaultTosMm;
|
|
40
|
+
const z1 = e[1] && e[1].tos != null ? e[1].tos * IN_TO_MM : defaultTosMm;
|
|
41
|
+
return { kind: 'beam', line: [[a[0], a[1], z0], [b[0], b[1], z1]] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** XY intersection point of segments a-b and c-d (within both), or null (parallel / outside). */
|
|
45
|
+
function segXY(a, b, c, d) {
|
|
46
|
+
const rx = b[0] - a[0], ry = b[1] - a[1], sx = d[0] - c[0], sy = d[1] - c[1];
|
|
47
|
+
const rxs = rx * sy - ry * sx;
|
|
48
|
+
if (Math.abs(rxs) < 1e-9) return null; // parallel/collinear
|
|
49
|
+
const qpx = c[0] - a[0], qpy = c[1] - a[1];
|
|
50
|
+
const t = (qpx * sy - qpy * sx) / rxs, u = (qpx * ry - qpy * rx) / rxs;
|
|
51
|
+
if (t < 0 || t > 1 || u < 0 || u > 1) return null; // crossing lies outside a segment
|
|
52
|
+
return [a[0] + rx * t, a[1] + ry * t];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Snap targets from the other members (the dragged one excluded). Beam → its top-flange centerline
|
|
57
|
+
* (the work-line at T.O.S) + endpoint vertices + a midpoint. Column → a vertical axis at its plan
|
|
58
|
+
* point + endpoint vertices. Plus member-member centerline INTERSECTIONS (Tekla-standard). Each
|
|
59
|
+
* tagged `fromId`. Generic `grid-line` targets (from plan grids) plug in here too.
|
|
60
|
+
*/
|
|
61
|
+
export function snapCandidates(members, ptPerFt, defaultTosMm, excludeId) {
|
|
62
|
+
const out = [];
|
|
63
|
+
const lines = []; // beam centerlines, for pairwise intersections
|
|
64
|
+
for (const m of members || []) {
|
|
65
|
+
if (!m || m.id === excludeId || !Array.isArray(m.wp) || m.wp.length < 2) continue;
|
|
66
|
+
const { kind, line } = memberGeometry(m, ptPerFt, defaultTosMm);
|
|
67
|
+
const [p0, p1] = line;
|
|
68
|
+
if (kind === 'column') { out.push({ type: 'vertical-axis', p: p0, fromId: m.id }); }
|
|
69
|
+
else {
|
|
70
|
+
out.push({ type: 'centerline', a: p0, b: p1, fromId: m.id });
|
|
71
|
+
out.push({ type: 'midpoint', p: [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2, (p0[2] + p1[2]) / 2], fromId: m.id });
|
|
72
|
+
lines.push({ id: m.id, p0, p1 });
|
|
73
|
+
}
|
|
74
|
+
out.push({ type: 'vertex', p: p0, fromId: m.id });
|
|
75
|
+
out.push({ type: 'vertex', p: p1, fromId: m.id });
|
|
76
|
+
}
|
|
77
|
+
for (let i = 0; i < lines.length; i++) {
|
|
78
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
79
|
+
const x = segXY(lines[i].p0, lines[i].p1, lines[j].p0, lines[j].p1);
|
|
80
|
+
if (x) out.push({ type: 'intersection', p: [x[0], x[1], (lines[i].p0[2] + lines[j].p0[2]) / 2], fromIds: [lines[i].id, lines[j].id] });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Distinct elevation levels (mm, sorted) among the OTHER members' endpoints — the snap targets for a
|
|
88
|
+
* vertical (Alt) drag, so a member can be raised/lowered onto another's T.O.S level. The caller turns
|
|
89
|
+
* each level into a point candidate at the dragged member's plan X/Y and reuses snapPoint().
|
|
90
|
+
*/
|
|
91
|
+
export function elevationLevels(members, ptPerFt, defaultTosMm, excludeId) {
|
|
92
|
+
const set = new Set();
|
|
93
|
+
for (const m of members || []) {
|
|
94
|
+
if (!m || m.id === excludeId || !Array.isArray(m.wp) || m.wp.length < 2) continue;
|
|
95
|
+
const { line } = memberGeometry(m, ptPerFt, defaultTosMm);
|
|
96
|
+
set.add(Math.round(line[0][2])); set.add(Math.round(line[1][2]));
|
|
97
|
+
}
|
|
98
|
+
return [...set].sort((a, b) => a - b);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Lower wins when two candidates are both within tolerance.
|
|
102
|
+
const PRECEDENCE = { vertex: 0, intersection: 1, midpoint: 2, centerline: 3, 'vertical-axis': 4, 'grid-line': 5, level: 1 };
|
|
103
|
+
|
|
104
|
+
function closestOnSeg(p, a, b) {
|
|
105
|
+
const ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
106
|
+
const len2 = ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2] || 1;
|
|
107
|
+
let t = ((p[0] - a[0]) * ab[0] + (p[1] - a[1]) * ab[1] + (p[2] - a[2]) * ab[2]) / len2;
|
|
108
|
+
t = Math.max(0, Math.min(1, t));
|
|
109
|
+
return [a[0] + ab[0] * t, a[1] + ab[1] * t, a[2] + ab[2] * t];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The 3D point a candidate proposes for the dragged point. */
|
|
113
|
+
function candidatePoint(c, dragged) {
|
|
114
|
+
if (c.type === 'vertex' || c.type === 'intersection' || c.type === 'midpoint' || c.type === 'level') return c.p; // fixed points
|
|
115
|
+
if (c.type === 'vertical-axis') return [c.p[0], c.p[1], dragged[2]]; // lock plan X/Y to the axis, keep dragged Z
|
|
116
|
+
return closestOnSeg(dragged, c.a, c.b); // centerline / grid-line
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Snap `dragged` (3D mm) to the nearest candidate within `tolPx` SCREEN pixels. `toScreen` maps a 3D
|
|
121
|
+
* mm point → `{x,y}` screen px (injected — keeps this DOM-free). Ties within tolerance break by
|
|
122
|
+
* PRECEDENCE (endpoint > intersection > centerline > axis > grid). Returns `{ snapped, candidate }`;
|
|
123
|
+
* candidate is null (and snapped === dragged) when nothing is in range.
|
|
124
|
+
*/
|
|
125
|
+
export function snapPoint(dragged, candidates, toScreen, tolPx) {
|
|
126
|
+
const ds = toScreen(dragged);
|
|
127
|
+
let best = null, bestPt = null, bestPrec = Infinity, bestD = Infinity;
|
|
128
|
+
for (const c of candidates || []) {
|
|
129
|
+
const pt = candidatePoint(c, dragged);
|
|
130
|
+
const s = toScreen(pt);
|
|
131
|
+
const d = Math.hypot(s.x - ds.x, s.y - ds.y);
|
|
132
|
+
if (d > tolPx) continue;
|
|
133
|
+
const prec = PRECEDENCE[c.type] ?? 9;
|
|
134
|
+
if (prec < bestPrec || (prec === bestPrec && d < bestD)) { best = c; bestPt = pt; bestPrec = prec; bestD = d; }
|
|
135
|
+
}
|
|
136
|
+
return best ? { snapped: bestPt, candidate: best } : { snapped: dragged, candidate: null };
|
|
137
|
+
}
|