@floless/app 0.67.0 → 0.69.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.
@@ -53022,7 +53022,7 @@ function appVersion() {
53022
53022
  return resolveVersion({
53023
53023
  isSea: isSea2(),
53024
53024
  sqVersionXml: readSqVersionXml(),
53025
- define: true ? "0.67.0" : void 0,
53025
+ define: true ? "0.69.0" : void 0,
53026
53026
  pkgVersion: readPkgVersion()
53027
53027
  });
53028
53028
  }
@@ -53032,7 +53032,7 @@ function resolveChannel(s) {
53032
53032
  return "dev";
53033
53033
  }
53034
53034
  function appChannel() {
53035
- return resolveChannel({ isSea: isSea2(), define: true ? "0.67.0" : void 0 });
53035
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.69.0" : void 0 });
53036
53036
  }
53037
53037
 
53038
53038
  // workflow-update.ts
@@ -53893,6 +53893,7 @@ function bakeContractIntoApp(sourcePath, contract) {
53893
53893
  const baked = { ...contract };
53894
53894
  delete baked.dims3d;
53895
53895
  delete baked.detail_placements;
53896
+ delete baked.prop_labels;
53896
53897
  if (Array.isArray(contract.plans)) {
53897
53898
  baked.plans = contract.plans.map((p) => {
53898
53899
  if (p && typeof p === "object") {
@@ -41,7 +41,18 @@
41
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)." },
42
42
  "joints": { "type": "array", "items": { "$ref": "#/$defs/joint" }, "description": "Placed connections (base plates, …) the connection engine expands into real 3D parts (plate/bolt/weld/cut) bound to their members. A joint may cite a `connections[]` row via `conn` for its type/detail identity; `params` carries the geometry settings (engine defaults when absent)." },
43
43
  "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." },
44
- "detail_placements": { "type": "array", "items": { "$ref": "#/$defs/detailPlacement" }, "description": "Draft-only placed 2D detail images (Slice 4 — insert a vectored detail into the model). Flat image planes the editor renders client-side against the model; metadata only — the image bytes live in custom_details. Model-global. NOT baked into the lock / 3D scene / IFC / BOM." }
44
+ "detail_placements": { "type": "array", "items": { "$ref": "#/$defs/detailPlacement" }, "description": "Draft-only placed 2D detail images (Slice 4 — insert a vectored detail into the model). Flat image planes the editor renders client-side against the model; metadata only — the image bytes live in custom_details. Model-global. NOT baked into the lock / 3D scene / IFC / BOM." },
45
+ "prop_labels": {
46
+ "type": "object",
47
+ "additionalProperties": true,
48
+ "description": "Draft-only display state (like dim_overlays): which member properties render as text-chip labels on the 2D/3D canvas, the anchor placement, and scope. Model-global. NOT baked into the lock / 3D scene / IFC / BOM.",
49
+ "properties": {
50
+ "props": { "type": "array", "items": { "type": "string" }, "description": "Checked property registry keys to show as labels (e.g. 'profile', 'tos_start')." },
51
+ "placement": { "enum": ["start", "mid", "end"], "description": "Anchor point of the label stack on each member." },
52
+ "selected_only": { "type": "boolean", "description": "When true, labels pin to `ids` (captured at check time); otherwise every member on the plan with a value is labeled." },
53
+ "ids": { "type": "array", "items": { "type": "string" }, "description": "Member ids the labels pin to, used only when selected_only is true." }
54
+ }
55
+ }
45
56
  },
46
57
  "$defs": {
47
58
  "detailPlacement": {
package/dist/web/app.js CHANGED
@@ -360,8 +360,8 @@ function cardEl(id, ports) {
360
360
  </div>
361
361
  <div class="title">${a.title}</div>
362
362
  <div class="subtitle">${a.subtitle}</div>
363
- ${a._runtimeModel ? '<div class="rt-model-badge" title="This node calls a model at run time (vision.extract). Output is content-cached and sits behind an approve gate — review the extraction before any write.">⚡ calls a model at run time</div>' : ''}
364
- ${a._frozen ? '<div class="frozen-badge" title="Frozen — its last result is pinned and Run skips this node. Right-click to unfreeze.">❄ Frozen</div>' : ''}
363
+ ${a._runtimeModel ? '<div class="rt-model-badge" data-tip="This node calls a model at run time (vision.extract). Output is content-cached and sits behind an approve gate — review the extraction before any write.">⚡ calls a model at run time</div>' : ''}
364
+ ${a._frozen ? '<div class="frozen-badge" data-tip="Frozen — its last result is pinned and Run skips this node. Right-click to unfreeze.">❄ Frozen</div>' : ''}
365
365
  <div class="blurb">${a.blurb}</div>
366
366
  <div class="footer-row">
367
367
  <span class="ports"><span class="ports-num">${ports.in}</span> in <span class="ports-arrow">·</span> <span class="ports-num">${ports.out}</span> out</span>
package/dist/web/aware.js CHANGED
@@ -1316,7 +1316,7 @@
1316
1316
  const v = String(vals[k] || '');
1317
1317
  if (!v) return `<span class="ni-pair ni-pair-file ni-pair-file-empty"><span class="ni-key">${escapeHtml(k)}</span><span class="ni-val ni-not-set">not set</span></span>`;
1318
1318
  const glyph = /\.pdf$/i.test(v) ? 'pdf' : 'img';
1319
- return `<span class="ni-pair ni-pair-file"><span class="ni-key">${escapeHtml(k)}</span><span class="ni-file-glyph">${glyph}</span><span class="ni-val ni-file-name" title="${escapeAttr(v)}">${escapeHtml(baseName(v))}</span></span>`;
1319
+ return `<span class="ni-pair ni-pair-file"><span class="ni-key">${escapeHtml(k)}</span><span class="ni-file-glyph">${glyph}</span><span class="ni-val ni-file-name" data-tip="${escapeAttr(v)}">${escapeHtml(baseName(v))}</span></span>`;
1320
1320
  }
1321
1321
  return `<span class="ni-pair"><span class="ni-key">${escapeHtml(k)}</span><span class="ni-val">${escapeHtml(String(vals[k]))}</span></span>`;
1322
1322
  }).join('');
@@ -2305,7 +2305,7 @@
2305
2305
  if (f.type === 'current') {
2306
2306
  const cext = (/\.([a-z0-9]+)$/i.exec(f.value || '') || [])[1];
2307
2307
  const cglyph = String(cext || '').toLowerCase() === 'pdf' ? 'pdf' : 'img';
2308
- return `<div class="rebake-current"><span class="rebake-current-label">${escapeHtml(f.label || 'Currently loaded')}</span><span class="fm-file-glyph">${cglyph}</span><span class="fm-file-name" title="${escapeAttr(f.value)}">${escapeHtml(f.value)}</span></div>`;
2308
+ return `<div class="rebake-current"><span class="rebake-current-label">${escapeHtml(f.label || 'Currently loaded')}</span><span class="fm-file-glyph">${cglyph}</span><span class="fm-file-name" data-tip="${escapeAttr(f.value)}">${escapeHtml(f.value)}</span></div>`;
2309
2309
  }
2310
2310
  let ctl;
2311
2311
  if (f.type === 'images') {
@@ -2420,7 +2420,7 @@
2420
2420
  return;
2421
2421
  }
2422
2422
  const glyph = st.ext === 'pdf' ? 'pdf' : 'img';
2423
- drop.innerHTML = `<span class="fm-file-glyph">${glyph}</span><span class="fm-file-name" title="${escapeAttr(st.value)}">${escapeHtml(st.name)}</span><span class="fm-file-actions"><button type="button" class="fm-file-replace">Replace</button><button type="button" class="fm-file-clear">Clear</button></span>`;
2423
+ drop.innerHTML = `<span class="fm-file-glyph">${glyph}</span><span class="fm-file-name" data-tip="${escapeAttr(st.value)}">${escapeHtml(st.name)}</span><span class="fm-file-actions"><button type="button" class="fm-file-replace">Replace</button><button type="button" class="fm-file-clear">Clear</button></span>`;
2424
2424
  drop.querySelector('.fm-file-replace').onclick = (e) => { e.stopPropagation(); fileInput.click(); };
2425
2425
  drop.querySelector('.fm-file-clear').onclick = (e) => { e.stopPropagation(); st.mode = 'empty'; st.value = ''; st.name = ''; st.ext = ''; render(); };
2426
2426
  };
@@ -2473,7 +2473,7 @@
2473
2473
  list.hidden = state.length === 0;
2474
2474
  list.innerHTML = state.map((s, i) => s.reading
2475
2475
  ? `<div class="fm-filechip is-reading"><span class="fm-file-glyph">${s.ext === 'pdf' ? 'pdf' : 'img'}</span><span class="fm-filechip-name">Reading…</span></div>`
2476
- : `<div class="fm-filechip"><span class="fm-file-glyph">${s.ext === 'pdf' ? 'pdf' : 'img'}</span><span class="fm-filechip-name" title="${escapeAttr(s.name)}">${escapeHtml(s.name)}</span><button type="button" class="fm-filechip-del" data-i="${i}" aria-label="Remove ${escapeAttr(s.name)}">×</button></div>`
2476
+ : `<div class="fm-filechip"><span class="fm-file-glyph">${s.ext === 'pdf' ? 'pdf' : 'img'}</span><span class="fm-filechip-name" data-tip="${escapeAttr(s.name)}">${escapeHtml(s.name)}</span><button type="button" class="fm-filechip-del" data-i="${i}" aria-label="Remove ${escapeAttr(s.name)}">×</button></div>`
2477
2477
  ).join('');
2478
2478
  list.querySelectorAll('.fm-filechip-del').forEach((b) => {
2479
2479
  b.onclick = () => {
@@ -375,10 +375,10 @@
375
375
  (unlike the opaque 3D viewer which uses a data: URL and allow-scripts only). -->
376
376
  <div id="contract-editor" class="contract-editor" hidden>
377
377
  <div class="contract-editor-bar">
378
- <button id="contract-editor-close" type="button">✕ Close</button>
379
378
  <span id="contract-editor-title" class="contract-editor-title"></span>
380
379
  <span style="flex:1"></span>
381
- <button id="contract-editor-approve" type="button" class="primary">✓ Approve &amp; bake lock</button>
380
+ <button id="contract-editor-close" type="button" data-tip="Close the editor and go back — your edits are already saved">Close</button>
381
+ <button id="contract-editor-approve" type="button" class="primary" data-tip="Locks in this version of the model so it can be Run. Your edits already auto-save — this is the sign-off, and Run stays disabled until you approve after each change.">✓ Approve</button>
382
382
  </div>
383
383
  <iframe id="contract-editor-frame" title="Contract editor"></iframe>
384
384
  </div>
@@ -34,6 +34,9 @@ let insertPending = null; // {name,url} — the detail the editor queue
34
34
  let labelsOnFlag = false; // member mark/id overlay labels toggle (a readability aid)
35
35
  let memberLabelHost = null; // fixed-position container for the member labels (positioned each frame)
36
36
  const memberLabelPool = []; // reused <div> labels, one per member, positioned in the loop
37
+ let propLabelHost = null; // fixed-position container for the right-click property labels (positioned each frame)
38
+ const propLabelPool = []; // reused multi-line <div> label chips, one per labelled member
39
+ let propLabelSpec = null; // { labels:[{id, lines:[...]}], placement } pushed from the editor (owns the text); this view owns projection/placement
37
40
  const EP_PX = 4; // end-dot radius in screen px (screen-constant via pxToWorld)
38
41
  let sceneBox = new THREE.Box3(); // current model bounds (Fit / ViewCube)
39
42
  let displayMode = 'solid'; // solid | wire | xray
@@ -156,6 +159,9 @@ function init(canvas, theApi) {
156
159
  memberLabelHost = document.createElement('div'); // member mark/id labels (Slice 4) — below the dim labels
157
160
  memberLabelHost.style.cssText = 'position:fixed;left:0;top:0;pointer-events:none;z-index:56;display:none';
158
161
  document.body.appendChild(memberLabelHost);
162
+ propLabelHost = document.createElement('div'); // right-click property labels — own host so they never clobber the mark/id pool
163
+ propLabelHost.style.cssText = 'position:fixed;left:0;top:0;pointer-events:none;z-index:56;display:none';
164
+ document.body.appendChild(propLabelHost);
159
165
  // persistent hover/selection status chip, bottom-centre of the canvas (mirrors the viewer's readout)
160
166
  hoverChip = document.createElement('div');
161
167
  hoverChip.style.cssText = 'position:fixed;display:none;pointer-events:none;z-index:55;background:var(--panel,#0f172a);color:var(--mut,#94a3b8);border:1px solid var(--line,#334155);border-radius:6px;padding:5px 12px;font:12px system-ui;white-space:nowrap;max-width:60vw;overflow:hidden;text-overflow:ellipsis;box-shadow:0 2px 8px rgba(0,0,0,.45)';
@@ -200,6 +206,7 @@ function loop() {
200
206
  positionDimLabels();
201
207
  positionOverlayLabels();
202
208
  positionMemberLabels();
209
+ positionPropLabels();
203
210
  renderer.render(scene, camera);
204
211
  if (overlayScene && overlayScene.children.length) { // 2nd pass with clipping OFF → the clip/work-area gizmos are never sectioned by any clip
205
212
  const saved = renderer.clippingPlanes;
@@ -553,6 +560,7 @@ function buildFromScene(sc) {
553
560
  buildStructGrid();
554
561
  buildRefLines();
555
562
  syncMemberLabels(); // refresh the member-label pool from the (possibly edited) member set
563
+ syncPropLabels(); // re-anchor the property labels too (member geometry may have moved)
556
564
  if (isolatedIds) { isolatedIds = new Set([...isolatedIds].filter((id) => meshById.has(id))); if (!isolatedIds.size) { isolatedIds = null; if (api && api.onIsolateChange) api.onIsolateChange(false); } } // drop ids an edit removed; if none survive, exit isolation + refresh the host's Show-all button
557
565
  if (connHidden.size) connHidden = new Set([...connHidden].filter((id) => meshById.has(id))); // drop per-part hides whose mesh an edit removed
558
566
  applyGroupVisibility(); applyDisplayMode();
@@ -661,6 +669,54 @@ function positionMemberLabels() {
661
669
  }
662
670
  }
663
671
 
672
+ // ---- Right-click property labels: the editor owns the TEXT (it has the property registry + formatters +
673
+ // scope) and hands us { labels:[{id, lines}], placement }; this view owns the ANCHOR (member start/mid/end
674
+ // via memberGeometry) + projection. Mirrors the member-label pool exactly, but stacks multiple lines per chip
675
+ // and lives in its own host so the mark/id "Labels" toggle and these can be on at once without fighting. ----
676
+ function setPropLabels(spec) {
677
+ propLabelSpec = (spec && Array.isArray(spec.labels) && spec.labels.length) ? spec : null;
678
+ syncPropLabels();
679
+ }
680
+ function syncPropLabels() {
681
+ if (!propLabelHost || !api) return;
682
+ const labels = propLabelSpec ? propLabelSpec.labels : [];
683
+ const place = propLabelSpec ? propLabelSpec.placement : 'mid';
684
+ while (propLabelPool.length < labels.length) {
685
+ const el = document.createElement('div');
686
+ el.style.cssText = 'position:absolute;transform:translate(-50%,-50%);pointer-events:none;background:#0b1220;color:#e2e8f0;border:1px solid var(--brand,#3b82f6);border-radius:4px;padding:2px 6px;font:11px system-ui;line-height:1.35;text-align:center;white-space:pre;box-shadow:0 0 0 1px rgba(0,0,0,.6),0 2px 8px rgba(0,0,0,.5)'; // multi-line via white-space:pre; brand-blue accent matches the 2D chips
687
+ propLabelHost.appendChild(el); propLabelPool.push(el);
688
+ }
689
+ const byId = new Map(members().map((m) => [m.id, m]));
690
+ const ppf = api.ptPerFt(), dtos = api.defaultTosMm();
691
+ for (let i = 0; i < propLabelPool.length; i++) {
692
+ const el = propLabelPool[i], L = labels[i], m = L ? byId.get(L.id) : null;
693
+ if (!m || !Array.isArray(m.wp) || m.wp.length < 2 || !L.lines || !L.lines.length) { el.style.display = 'none'; el._mid = null; el._memberId = null; continue; }
694
+ const g = memberGeometry(m, ppf, dtos), a = g.line[0], b = g.line[1];
695
+ el._mid = place === 'start' ? a : place === 'end' ? b : [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2];
696
+ el._memberId = m.id;
697
+ el.textContent = L.lines.join('\n');
698
+ el.style.display = 'block';
699
+ }
700
+ }
701
+ // Project each label's anchor → screen px (called from the render loop, like the dim + member labels).
702
+ function positionPropLabels() {
703
+ if (!propLabelHost) return;
704
+ if (!propLabelSpec || (canvasEl && canvasEl.style.display === 'none')) { propLabelHost.style.display = 'none'; return; }
705
+ propLabelHost.style.display = 'block';
706
+ const rect = canvasEl.getBoundingClientRect();
707
+ for (const el of propLabelPool) {
708
+ if (el.style.display === 'none' || !el._mid) continue;
709
+ const mesh = meshById.get(el._memberId);
710
+ if (mesh && mesh.visible === false) { el.style.visibility = 'hidden'; continue; } // member legend-hidden or isolated away → hide its labels too
711
+ if (isPointClipped(el._mid)) { el.style.visibility = 'hidden'; continue; } // sectioned away by a clip
712
+ const v = new THREE.Vector3(el._mid[0], el._mid[1], el._mid[2]).project(camera);
713
+ if (v.z > 1 || v.x < -1 || v.x > 1 || v.y < -1 || v.y > 1) { el.style.visibility = 'hidden'; continue; } // behind / off-screen
714
+ el.style.left = (rect.left + (v.x * 0.5 + 0.5) * rect.width) + 'px';
715
+ el.style.top = (rect.top + (-v.y * 0.5 + 0.5) * rect.height) + 'px';
716
+ el.style.visibility = 'visible';
717
+ }
718
+ }
719
+
664
720
  // ---- structural grid (Tekla-style): dashed lines per Z level live in the SCENE (so clips section
665
721
  // them, like Tekla); the label bubbles + level tags are sprites in the UNCLIPPED overlay pass, so the
666
722
  // wayfinding survives sectioning/work-area (the UX review's clip-regression guard). Data comes from
@@ -2479,6 +2535,7 @@ function hide() {
2479
2535
  cmClear3d(); drClear3d(); // and any 3D Move/Copy or draw draft/ghosts
2480
2536
  if (dimLabelHost) dimLabelHost.style.display = 'none'; // the loop that hides labels is about to stop — hide synchronously so none are stranded
2481
2537
  if (memberLabelHost) memberLabelHost.style.display = 'none'; // member labels ride the same about-to-stop loop — hide them too, or they strand over the 2D UI
2538
+ if (propLabelHost) propLabelHost.style.display = 'none'; // property labels ride the same loop — hide synchronously so none strand over the 2D UI
2482
2539
  clearCopyGhost(); // drop a stale Ctrl+drag copy ghost on the way out
2483
2540
  if (canvasEl) canvasEl.style.display = 'none'; if (hoverChip) hoverChip.style.display = 'none'; if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
2484
2541
  }
@@ -2497,6 +2554,8 @@ function dispose() {
2497
2554
  dimLabelHost = null; dimLabelPool.length = 0;
2498
2555
  if (memberLabelHost && memberLabelHost.parentNode) memberLabelHost.parentNode.removeChild(memberLabelHost);
2499
2556
  memberLabelHost = null; memberLabelPool.length = 0; labelsOnFlag = false; insertMode = false; insertPending = null;
2557
+ if (propLabelHost && propLabelHost.parentNode) propLabelHost.parentNode.removeChild(propLabelHost);
2558
+ propLabelHost = null; propLabelPool.length = 0; propLabelSpec = null;
2500
2559
  for (const w of [cube, triad]) { // both mini-widgets own a WebGL context — leak one and re-init eventually hits the browser's context cap
2501
2560
  if (!w) continue;
2502
2561
  w.scene.traverse((o) => { if (o.geometry) o.geometry.dispose(); const mm = Array.isArray(o.material) ? o.material : (o.material ? [o.material] : []); for (const m of mm) { if (m.map) m.map.dispose(); m.dispose(); } });
@@ -2580,6 +2639,8 @@ window.Steel3DView = {
2580
2639
  setInsertMode, insertMode: insertModeOn, // arm/query the detail-placement pick (Slice 4)
2581
2640
  setLabelsOn, labelsOn: () => labelsOnFlag, // member mark/id label overlay toggle
2582
2641
  syncMemberLabels, // editor calls after a mark/id edit to refresh labels
2642
+ setPropLabels, // right-click property labels: editor pushes { labels:[{id,lines}], placement }
2643
+ propLabelTexts: () => propLabelPool.filter((el) => el.style.display !== 'none' && el.style.visibility !== 'hidden').map((el) => el.textContent), // visible property-label chips — for tests
2583
2644
  refreshGrid: buildStructGrid, // grid edited in the panel → re-render without a full rebuild
2584
2645
  gridInfo: () => ({ lines: structGridGroup ? 1 : 0, labels: gridLabelGroup ? gridLabelGroup.children.length : 0 }), // test helper
2585
2646
  toggleGroup, setGroupsHidden, setIdsHidden, connHiddenIds: () => [...connHidden], soloToggle, setSoloGroups, showAllGroups, groupState, getGroups,