@floless/app 0.48.1 → 0.50.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.
package/dist/floless-server.cjs
CHANGED
|
@@ -52856,7 +52856,7 @@ function appVersion() {
|
|
|
52856
52856
|
return resolveVersion({
|
|
52857
52857
|
isSea: isSea2(),
|
|
52858
52858
|
sqVersionXml: readSqVersionXml(),
|
|
52859
|
-
define: true ? "0.
|
|
52859
|
+
define: true ? "0.50.0" : void 0,
|
|
52860
52860
|
pkgVersion: readPkgVersion()
|
|
52861
52861
|
});
|
|
52862
52862
|
}
|
|
@@ -52866,7 +52866,7 @@ function resolveChannel(s) {
|
|
|
52866
52866
|
return "dev";
|
|
52867
52867
|
}
|
|
52868
52868
|
function appChannel() {
|
|
52869
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.
|
|
52869
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.50.0" : void 0 });
|
|
52870
52870
|
}
|
|
52871
52871
|
|
|
52872
52872
|
// workflow-update.ts
|
|
@@ -212,6 +212,7 @@
|
|
|
212
212
|
"segments": { "type": "array", "items": { "$ref": "#/$defs/segment" } },
|
|
213
213
|
"labels": { "type": "array", "items": { "$ref": "#/$defs/label" } },
|
|
214
214
|
"details": { "type": "array", "items": { "$ref": "#/$defs/detail" } },
|
|
215
|
+
"callouts": { "type": "array", "items": { "$ref": "#/$defs/callout" }, "description": "Drawing cross-references drawn ON this plan that point to another sheet (section cuts, elevation marks, detail refs). The editor renders a clickable bubble at each `disp` that jumps to the referenced sheet." },
|
|
215
216
|
"tos_callouts": { "type": "array", "items": { "$ref": "#/$defs/tos_callout" } },
|
|
216
217
|
"members": { "type": "array", "items": { "$ref": "#/$defs/member" } },
|
|
217
218
|
"dims": { "type": "array", "items": { "$ref": "#/$defs/dim" }, "description": "Draft-only on-drawing dimensions (editor annotations). NOT baked into the lock / 3D / IFC / BOM." },
|
|
@@ -281,6 +282,26 @@
|
|
|
281
282
|
"disp": { "$ref": "#/$defs/point2" }
|
|
282
283
|
}
|
|
283
284
|
},
|
|
285
|
+
"callout": {
|
|
286
|
+
"type": "object",
|
|
287
|
+
"required": ["kind", "target"],
|
|
288
|
+
"additionalProperties": true,
|
|
289
|
+
"description": "A drawing cross-reference drawn ON this plan that points to another sheet — a section cut, an elevation mark, or a detail reference. Clicking its bubble in the editor jumps to the referenced sheet (a loaded plan → switch to it & frame it; otherwise the detail lightbox).",
|
|
290
|
+
"properties": {
|
|
291
|
+
"kind": { "enum": ["section", "elevation", "detail-ref"], "description": "section = a section cut symbol; elevation = an elevation / interior-elevation mark; detail-ref = a detail callout pointing to another sheet." },
|
|
292
|
+
"text": { "type": "string", "description": "The mark as drawn, e.g. \"A\", \"A-A\", \"3/S-501\"." },
|
|
293
|
+
"disp": { "$ref": "#/$defs/point2", "description": "Where the mark is drawn on THIS plan (display space) — the clickable hotspot anchor." },
|
|
294
|
+
"target": {
|
|
295
|
+
"type": "object",
|
|
296
|
+
"additionalProperties": true,
|
|
297
|
+
"description": "Where the mark points.",
|
|
298
|
+
"properties": {
|
|
299
|
+
"sheet": { "type": "string", "description": "Referenced sheet, e.g. S-301." },
|
|
300
|
+
"number": { "type": "string", "description": "Detail / section number on that sheet, e.g. \"A\" or \"3\"." }
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
},
|
|
284
305
|
"tos_callout": {
|
|
285
306
|
"type": "object",
|
|
286
307
|
"required": ["elev_in"],
|
|
@@ -223,6 +223,9 @@ Important optional fields:
|
|
|
223
223
|
"weights": { "W16X26": 26, "HSS6X6X3/8": 27.48 }, // profile → plf; null for unresolved
|
|
224
224
|
"moment_frames": [ … ], // from Frame Elevation sheets
|
|
225
225
|
"detail_bubbles": { "S-504": { "5": [0.32, 0.71] } }, // sheet → { number → [fx,fy] }
|
|
226
|
+
"connections": [ // vendor-neutral connection library
|
|
227
|
+
{ "id": "cx-m1", "type": "moment", "detail": "5-S504", "targets": { "tekla": "146" }, "source": "drawing" }
|
|
228
|
+
],
|
|
226
229
|
"plans": [
|
|
227
230
|
{
|
|
228
231
|
"sheet": "S-202",
|
|
@@ -234,6 +237,7 @@ Important optional fields:
|
|
|
234
237
|
"segments": [ { "id": "seg-1", "a": [x,y], "b": [x,y], "o": "H" } ],
|
|
235
238
|
"labels": [ { "text": "W16X26", "disp": [x,y] } ],
|
|
236
239
|
"details": [ { "text": "5-S504", "disp": [x,y] } ],
|
|
240
|
+
"callouts": [ { "kind": "section", "text": "A", "disp": [x,y], "target": { "sheet": "S-301", "number": "A" } } ],
|
|
237
241
|
"tos_callouts": [ { "elev_in": 198, "type": "TOS", "disp": [x,y] } ],
|
|
238
242
|
"members": [
|
|
239
243
|
{
|
|
@@ -245,7 +249,7 @@ Important optional fields:
|
|
|
245
249
|
"rfi": false,
|
|
246
250
|
"mf": false,
|
|
247
251
|
"ends": [
|
|
248
|
-
{ "tos": 198, "note": "moment", "tosDef": false, "detail": "5-S504" },
|
|
252
|
+
{ "tos": 198, "note": "moment", "tosDef": false, "detail": "5-S504", "conn": "cx-m1" },
|
|
249
253
|
{ "tos": 198, "note": "shear", "tosDef": true, "detail": "" }
|
|
250
254
|
]
|
|
251
255
|
},
|
|
@@ -282,6 +286,25 @@ Key field rules:
|
|
|
282
286
|
moment-frame member (persistent; survives AISC resolution).
|
|
283
287
|
- `raster_b64` is a base64 JPEG of the framing area (title block excluded). It is
|
|
284
288
|
**machine-local** and **never committed**.
|
|
289
|
+
- **Connection library (`connections[]`) + per-end refs.** A vendor-neutral top-level
|
|
290
|
+
`connections: [{ id, type, detail, targets:{ tekla:"146", … }, source }]` maps a connection
|
|
291
|
+
TYPE → the firm's design detail# → per-platform component ids. A member end references a row by id
|
|
292
|
+
via `ends[].conn` (or `col.conn` for a column); the legacy `note`/`detail` stay as the fallback
|
|
293
|
+
when `conn` is empty. `source` is `drawing` (read from a callout — best evidence), `company-rules`
|
|
294
|
+
(the firm's standard), or `user`. Prefer a `conn` reference over free text when the connection is
|
|
295
|
+
known — the editor's per-end picker and the Connections confidence card both key on it.
|
|
296
|
+
- **Company-rules pre-fill (§5.5).** If a firm-standard knowledge file is present (e.g.
|
|
297
|
+
`company-rules.md`: "moment → 50S504 → Tekla 146"), read it and emit those rows in `connections[]`
|
|
298
|
+
with `source: "company-rules"`, then PUT. The user reviews/edits them in the editor's
|
|
299
|
+
**Connections** library panel and assigns a row per member end. No new agent verb — it's contract data.
|
|
300
|
+
- **Clickable callouts (`callouts[]`).** Harvest the cross-reference marks drawn ON the plan that
|
|
301
|
+
point to ANOTHER sheet — **section cuts** (e.g. a circle with `A` over `A` and a sheet ref),
|
|
302
|
+
**elevation/interior-elevation marks**, and **detail references** — as per-plan
|
|
303
|
+
`callouts: [{ kind:"section"|"elevation"|"detail-ref", text:"A", disp:[x,y], target:{ sheet:"S-301", number:"A" } }]`.
|
|
304
|
+
`disp` is where the mark sits on THIS plan (display space — the clickable hotspot); `target` is where
|
|
305
|
+
it points. The editor draws a clickable bubble at each `disp`: clicking jumps to the referenced sheet
|
|
306
|
+
if it's loaded as a plan, else opens the detail lightbox. (Keep `details[]` as-is — those are the
|
|
307
|
+
detail callouts that already preview; `callouts[]` adds the section/elevation marks and off-sheet refs.)
|
|
285
308
|
|
|
286
309
|
### 5. Write to the contract store
|
|
287
310
|
|
|
@@ -68,7 +68,16 @@
|
|
|
68
68
|
rect.dethot{cursor:pointer;fill:rgba(168,85,247,.16);stroke:#a855f7;stroke-width:1;stroke-dasharray:3 3;vector-effect:non-scaling-stroke}
|
|
69
69
|
rect.dethot:hover{fill:rgba(168,85,247,.34)}
|
|
70
70
|
text.dettx{fill:#ddd6fe;font:bold 11px system-ui;text-anchor:middle;pointer-events:none}
|
|
71
|
-
|
|
71
|
+
/* clickable callout bubbles (Workstream B): section/elevation/detail-ref + plain details — navigable cross-references drawn on the plan in select mode. Existing palette only: purple #a855f7 (details), cyan #22d3ee (sections/elevations); solid vs dashed separates the kinds. */
|
|
72
|
+
g.cohot{cursor:pointer}
|
|
73
|
+
circle.cobub{stroke-width:1.5;vector-effect:non-scaling-stroke;opacity:.55}
|
|
74
|
+
circle.cobub.k-detail,circle.cobub.k-detailref{stroke:#a855f7;color:#a855f7;fill:rgba(168,85,247,.12)}
|
|
75
|
+
circle.cobub.k-section,circle.cobub.k-elev{stroke:#22d3ee;color:#22d3ee;fill:rgba(34,211,238,.10)}
|
|
76
|
+
circle.cobub.k-detailref,circle.cobub.k-elev{stroke-dasharray:3 3}
|
|
77
|
+
g.cohot:hover circle.cobub{opacity:1;stroke-width:2;filter:drop-shadow(0 0 4px currentColor)}
|
|
78
|
+
text.cotx{fill:#e2e8f0;font:bold 11px system-ui;text-anchor:middle;dominant-baseline:central;pointer-events:none;opacity:.6}
|
|
79
|
+
g.cohot:hover text.cotx{opacity:1}
|
|
80
|
+
#detailsModal,#framesModal,#rfiModal,#confModal,#askAiModal,#connModal{position:fixed;inset:0;z-index:20;display:none;align-items:center;justify-content:center}
|
|
72
81
|
#askAiDrop:hover{border-color:var(--brand);color:var(--text)} #askAiDrop.has{border-style:solid;border-color:var(--brand)}
|
|
73
82
|
.aithumb{position:relative;display:inline-flex} .aithumb img{height:60px;border-radius:4px;border:1px solid var(--line);background:#fff;display:block}
|
|
74
83
|
.aithumb button{position:absolute;top:-5px;right:-5px;width:16px;height:16px;padding:0;border-radius:8px;font-size:10px;line-height:16px;text-align:center;background:#7f1d1d;border-color:#991b1b;color:#fecaca}
|
|
@@ -93,7 +102,11 @@
|
|
|
93
102
|
.chip{display:inline-block;padding:1px 6px;border-radius:4px;font-size:10px;border:1px solid var(--line);color:var(--mut);margin:0 3px 3px 0}
|
|
94
103
|
.chip.assumed,.chip.weak{border-color:#713f12;color:#fbbf24} .chip.fail{border-color:#7f1d1d;color:#fca5a5}
|
|
95
104
|
tr.confrow{cursor:pointer} .conf-expand{color:var(--mut);font-size:12px;line-height:1.6;background:#0b1220}
|
|
96
|
-
.ftab td .combo{padding:4px 6px} .ftab .rea{color:var(--mut);font-size:12px}
|
|
105
|
+
.ftab td .combo{padding:4px 6px} .ftab .rea{color:var(--mut);font-size:12px}
|
|
106
|
+
.ftab-inp{background:#0f172a;color:var(--text);border:1px solid #475569;border-radius:4px;padding:4px 6px;width:100%;font:13px system-ui}
|
|
107
|
+
.ftab-inp:focus{outline:none;border-color:var(--brand)}
|
|
108
|
+
.ftab .cdel{padding:3px 7px;font-size:11px} .ftab .csrc{white-space:nowrap}
|
|
109
|
+
.connset{opacity:.55} .rfichip{display:inline-block;min-width:18px;height:18px;line-height:18px;text-align:center;background:#7f1d1d;color:#fecaca;border-radius:9px;font-size:11px;padding:0 5px}
|
|
97
110
|
.mbackdrop{position:absolute;inset:0;background:rgba(2,8,23,.62)}
|
|
98
111
|
.mpanel{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:10px;width:min(880px,92vw);max-height:86vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.6)}
|
|
99
112
|
.mhead{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--line)}
|
|
@@ -211,6 +224,7 @@
|
|
|
211
224
|
<div id=moreMenu role=menu>
|
|
212
225
|
<div class=mlabel>Dimensions</div>
|
|
213
226
|
<button id=dimToggleB title="Show or hide all placed dimensions on the plan">Hide dimensions</button>
|
|
227
|
+
<button id=calloutToggleB title="Show or hide the clickable callout bubbles (section / elevation / detail references) on the plan">Hide callouts</button>
|
|
214
228
|
<hr>
|
|
215
229
|
<div class=mlabel>Coordinate system</div>
|
|
216
230
|
<button id=csSetB title="Define a local X axis by clicking two points (origin, then X-direction). Y follows at 90°. Orthogonal drawing and X/Y dimensions then snap to this frame — for skewed framing.">↺ Set local axes…</button>
|
|
@@ -223,6 +237,7 @@
|
|
|
223
237
|
<hr>
|
|
224
238
|
<div class=mlabel>Reference</div>
|
|
225
239
|
<button id=detailsBtn>Details</button>
|
|
240
|
+
<button id=connBtn title="Connection library — map each connection type to its design detail# and per-platform component id; reference a row from each member end">Connections</button>
|
|
226
241
|
<button id=framesBtn>Frames</button>
|
|
227
242
|
<hr>
|
|
228
243
|
<div class=mlabel>Session</div>
|
|
@@ -272,6 +287,12 @@
|
|
|
272
287
|
<div class=hint id=dnHint>For anything the auto‑detection missed. The image stays on your machine.</div>
|
|
273
288
|
</div>
|
|
274
289
|
<div class=mgrid id=detailsGrid></div></div></div>
|
|
290
|
+
<div id=connModal><div class=mbackdrop id=connBackdrop></div>
|
|
291
|
+
<div class=mpanel><div class=mhead><b>Connection library</b><div style="display:flex;gap:8px;align-items:center"><button id=connNew>+ New row</button><button id=connClose>✕</button></div></div>
|
|
292
|
+
<div class=conf-filter><span class=glab>Platform</span><div class=grp id=connPlatGrp><button class=on data-plat=tekla>Tekla</button><button data-plat=sds2>SDS2</button><button data-plat=revit>Revit</button></div><span class=hint style="margin:0">The design detail# is platform-neutral; the component id is per-platform.</span></div>
|
|
293
|
+
<div style="overflow:auto;flex:1;padding:14px;scrollbar-width:thin">
|
|
294
|
+
<table class=ftab id=connTable><thead><tr><th style="width:24%">Type</th><th style="width:24%">Detail #</th><th id=connCompHead style="width:24%">Tekla id</th><th style="width:18%">Source</th><th style="width:10%"></th></tr></thead><tbody id=connBody></tbody></table>
|
|
295
|
+
</div></div></div>
|
|
275
296
|
<div id=framesModal><div class=mbackdrop id=framesBackdrop></div>
|
|
276
297
|
<div class=mpanel><div class=mhead><b>Moment frame schedule</b><button id=framesClose>✕</button></div>
|
|
277
298
|
<div id=framesGrid style="padding:14px;overflow:auto"></div></div></div>
|
|
@@ -427,6 +448,7 @@ function main() {
|
|
|
427
448
|
let P, X0,Y0,X1,Y1, FT, RB64, EXTX, EXTY, profs; // per-plan, set by setPlan()
|
|
428
449
|
let mode='sel', drag=null, picking=false, pickKind='profile', pickEnd=null, geoMode=null;
|
|
429
450
|
let dimMode=false, dimDraft=null, dimAxis='free', selDimIds=new Set(), dimsVisible=true; // Dimension tool: armed flag, in-progress {a,b,axis}, sticky axis, selected dim ids (multi-select), show/hide
|
|
451
|
+
let calloutsVisible=true; // Workstream B: show/hide the clickable callout bubbles (details + section/elevation/detail-ref) on the plan in select mode
|
|
430
452
|
let csaxisMode=false, csDraft=null; // Local coordinate system "set axes" tool: armed flag + in-progress origin [x,y] (null until click 1). P.frame={o,u} holds the committed local frame (null = global).
|
|
431
453
|
let dimChain=false, dimChainPrev=null, dimSeq=0; // chained "continuous" dimensioning: toggle, the running {point,axis,off,rot}, and a counter for unique ids on rapid clicks
|
|
432
454
|
let dimSplitMode=false; // "add split point" mode on a selected dim — each click inserts a point and splits the dim segment under it into two
|
|
@@ -739,6 +761,22 @@ function render(){
|
|
|
739
761
|
s+=`<rect class=lblhot data-prof="${esc(lb.text)}" x="${lb.disp[0]-w/2}" y="${lb.disp[1]-10}" width="${w}" height="20" rx="3"><title>${esc(lb.text)}</title></rect>`;}
|
|
740
762
|
if(picking&&pickKind==='detail'&&P.details) for(const d of P.details){const w=Math.max(34,d.text.length*8);
|
|
741
763
|
s+=`<rect class=dethot data-det="${esc(d.text)}" x="${d.disp[0]-w/2}" y="${d.disp[1]-9}" width="${w}" height="18" rx="3"><title>${esc(d.text)}</title></rect><text class=dettx x="${d.disp[0]}" y="${d.disp[1]+4}">${esc(d.text)}</text>`;}
|
|
764
|
+
// Workstream B: clickable callout bubbles in normal select mode — details[] + per-plan callouts[].
|
|
765
|
+
// data-co-sheet drives the smart jump (loaded plan → switch; else data-co-prev → detail lightbox).
|
|
766
|
+
// hidden while a sub-tool is armed (dim-split / geo extend-trim-split) so the bubbles never distract
|
|
767
|
+
// or shadow a tool click — those handlers run earlier in pointerdown, but don't draw navigables mid-tool.
|
|
768
|
+
if(calloutsVisible&&mode==='sel'&&!picking&&!dimMode&&!csaxisMode&&!dimSplitMode&&!geoMode){
|
|
769
|
+
const sheetLoaded=sh=>!!sh&&C.plans.some(p=>p.sheet&&p.sheet.toUpperCase()===String(sh).toUpperCase());
|
|
770
|
+
// honest destination hint: loaded → no suffix (jumps); a resolvable preview → "(lightbox)"; else "(not loaded)".
|
|
771
|
+
const hint=(sh,prev)=>sheetLoaded(sh)?'':(prev&&previewFor(prev)?' (lightbox)':' (not loaded)');
|
|
772
|
+
const cob=(kc,mark,disp,sheet,prev,tip)=>`<g class=cohot data-co-sheet="${esc(sheet||'')}" data-co-prev="${esc(prev||'')}"><title>${esc(tip)}</title><circle class="cobub k-${kc}" cx="${disp[0]}" cy="${disp[1]}" r="11"/><text class=cotx x="${disp[0]}" y="${disp[1]}">${esc(String(mark==null?'':mark).slice(0,3))}</text></g>`;
|
|
773
|
+
for(const d of (P.details||[])){if(!Array.isArray(d.disp))continue;const sh=sheetOf(d.text),nu=numOf(d.text);
|
|
774
|
+
s+=cob('detail',nu||d.text,d.disp,sh,d.text,'Detail '+(nu||d.text||'')+(sh?' → '+sh:'')+hint(sh,d.text));}
|
|
775
|
+
for(const co of (P.callouts||[])){if(!Array.isArray(co.disp))continue;const sh=co.target&&co.target.sheet,nu=co.target&&co.target.number;
|
|
776
|
+
const kc=co.kind==='section'?'section':co.kind==='elevation'?'elev':'detailref';const kl=co.kind==='section'?'Section':co.kind==='elevation'?'Elevation':'Detail ref';
|
|
777
|
+
const prev=(nu&&sh)?(nu+'-'+sh):(co.text||'');
|
|
778
|
+
s+=cob(kc,co.text||nu,co.disp,sh,prev,kl+' '+(co.text||nu||'')+(sh?' → '+sh:'')+hint(sh,prev));}
|
|
779
|
+
}
|
|
742
780
|
// numbered badges on the selected members (1..N in selection order; coincident ones fan out so duplicates are countable)
|
|
743
781
|
// sized in 1/zoom units → constant on screen at any zoom (updateBadges keeps them in sync while zooming)
|
|
744
782
|
const selM=selArr();
|
|
@@ -748,7 +786,7 @@ function render(){
|
|
|
748
786
|
s+=`<circle class=numbg ${d} cx="${x}" cy="${y}" r="${R}"/><text class=numtx ${d} x="${x}" y="${y}" style="font-size:${F}px">${it.idx+1}</text>`;});}}
|
|
749
787
|
s+=renderDims();
|
|
750
788
|
if(P.frame)s+=axisGlyphSvg(P.frame.o,P.frame.u,false); // local-axes glyph at the origin (only when a frame is set; removed on reset)
|
|
751
|
-
svg.innerHTML=s; document.getElementById('profiles').innerHTML=profs.map(p=>`<option value="${esc(p)}">`).join(''); document.getElementById('details').innerHTML=(P.details||[]).map(d=>`<option value="${esc(d.text)}">`).join(''); stats(); panel(); updUR(); updDup(); updConf(); updCS();
|
|
789
|
+
svg.innerHTML=s; document.getElementById('profiles').innerHTML=profs.map(p=>`<option value="${esc(p)}">`).join(''); document.getElementById('details').innerHTML=(P.details||[]).map(d=>`<option value="${esc(d.text)}">`).join(''); stats(); panel(); updUR(); updDup(); updConf(); updCS(); updConnBtn();
|
|
752
790
|
if(view3d&&window.Steel3DView)window.Steel3DView.setSelection(selIds); // keep the 3D highlight in sync with the 2D selection
|
|
753
791
|
}
|
|
754
792
|
function updDup(){const n=redundantDups().length;
|
|
@@ -901,12 +939,20 @@ function panel(){
|
|
|
901
939
|
return `<div class=elabrow><span class=elab>${label}</span><label class=defck><input type=checkbox id=${id}_ck ${def?'checked':''}>default</label></div><input id=${id} inputmode=decimal placeholder="5 3/4" · 1'-0 1/4"" value="${esc(fmtFtIn(v))}"${def?' disabled':''}><span class=edec>${esc(fmtDecIn(v))}</span>`;};
|
|
902
940
|
const nin=(id,v)=>`<input id=${id} class="f combo" data-src=conntypes placeholder="connection / note" value="${esc(v||'')}" autocomplete=off>`;
|
|
903
941
|
const dFld=(id,o)=>{const hasPrev=o.detail&&previewFor(o.detail);return `<div class=elab>Connection detail</div><div style="display:flex;gap:6px"><input id=${id} class=combo data-src=details placeholder="e.g. 5-S504" value="${esc(o.detail||'')}" style="flex:1" autocomplete=off>${hasPrev?`<button id=${id}_open class=ghost title="Open detail ${esc(o.detail)}">⤢</button>`:''}<button id=${id}_pk class="ghost${(picking&&pickKind==='detail'&&pickEnd===o)?' on':''}" title="Pick a detail callout from the drawing">⌖</button></div>`;};
|
|
942
|
+
// Per-end connection-library picker (sets o.conn = a connections[] row id). When set, the row's
|
|
943
|
+
// type/detail# are the source of truth (shown read-only below); the free-text note/detail dim to a
|
|
944
|
+
// fallback. Readonly combo → pick only, no free typing; the connrows source shows every row.
|
|
945
|
+
const cFld=(id,o)=>{const row=o.conn?connRowById(o.conn):null;
|
|
946
|
+
const cap=row?`<span class=edec style="text-align:left">${esc(row.type||'?')}${row.detail?' · '+esc(row.detail):''} — ${esc(row.source||'user')}</span>`
|
|
947
|
+
:(o.conn?'<span class=edec style="text-align:left;color:#fca5a5">references a missing row — re-pick</span>':'');
|
|
948
|
+
return `<div class=elab>Connection</div><input id=${id} class=combo data-src=connrows readonly placeholder="— none — (pick from the library)" value="${esc(row?connLabel(row):'')}" autocomplete=off>${cap}`;};
|
|
949
|
+
const fb=(o,html)=>`<div class="${o.conn?'connset':''}">${html}</div>`; // dim the note/detail fallback when a library row is referenced
|
|
904
950
|
const dotS='<span class=epdot style="background:#facc15"></span>',dotE='<span class=epdot style="background:#f472b6"></span>'; // start=yellow, end=magenta — matches the canvas end markers
|
|
905
951
|
const elev = col
|
|
906
|
-
? `<div class="sect f">Elevation</div>${ftFld('bos',dotS+'Bottom (BOS)',m.col.bos)}${tosFld('tosC',dotE+'Top (TOS)',m.col)}${nin('ntC',m.col.note)
|
|
907
|
-
: `<div class="sect f">${dotS}Start</div>${tosFld('tosA','TOS',m.ends[0])}${nin('ntA',m.ends[0].note)
|
|
952
|
+
? `<div class="sect f">Elevation</div>${ftFld('bos',dotS+'Bottom (BOS)',m.col.bos)}${tosFld('tosC',dotE+'Top (TOS)',m.col)}${cFld('cnC',m.col)}${fb(m.col,nin('ntC',m.col.note)+dFld('dtC',m.col))}`
|
|
953
|
+
: `<div class="sect f">${dotS}Start</div>${tosFld('tosA','TOS',m.ends[0])}${cFld('cnA',m.ends[0])}${fb(m.ends[0],nin('ntA',m.ends[0].note)+dFld('dtA',m.ends[0]))}
|
|
908
954
|
<div class=divrow><hr><button class=ghost id=matchEnds>Match → both ends</button><hr></div>
|
|
909
|
-
<div class=sect>${dotE}End</div>${tosFld('tosB','TOS',m.ends[1])}${nin('ntB',m.ends[1].note)
|
|
955
|
+
<div class=sect>${dotE}End</div>${tosFld('tosB','TOS',m.ends[1])}${cFld('cnB',m.ends[1])}${fb(m.ends[1],nin('ntB',m.ends[1].note)+dFld('dtB',m.ends[1]))}`;
|
|
910
956
|
p.innerHTML=`<h3>Member ${esc(m.id)}</h3>
|
|
911
957
|
<div class=row><label>Profile</label><div style="display:flex;gap:6px"><input id=pf class=combo data-src=profiles value="${esc(m.profile)}" style="flex:1" autocomplete=off><button id=pickProf class="ghost${(picking&&pickKind==='profile')?' on':''}" title="Pick profile by clicking a label in the drawing">⌖ pick</button></div>${(picking&&pickKind==='profile')?'<div class="hint" style="margin-top:4px;font-style:italic;color:var(--brand)">Click a profile label in the drawing…</div>':(picking&&pickKind==='detail')?'<div class="hint" style="margin-top:4px;font-style:italic;color:#a855f7">Click a detail callout in the drawing…</div>':''}</div>
|
|
912
958
|
<div class="seg2 f"><button id=rBeam class="${col?'':'on'}">Beam</button><button id=rCol class="${col?'on':''}">Column</button></div>
|
|
@@ -939,18 +985,19 @@ function panel(){
|
|
|
939
985
|
const wireDet=(id,o)=>{const i=document.getElementById(id);if(i)i.onchange=e=>edit(()=>{o.detail=e.target.value.trim();});
|
|
940
986
|
const pk=document.getElementById(id+'_pk');if(pk)pk.onclick=()=>{if(picking&&pickKind==='detail'&&pickEnd===o){picking=false;pickEnd=null;}else{picking=true;pickKind='detail';pickEnd=o;}render();};
|
|
941
987
|
const op=document.getElementById(id+'_open');if(op)op.onclick=()=>openPreview(o.detail);};
|
|
988
|
+
const wireConn=(id,o)=>{const i=document.getElementById(id);if(!i)return;i.onchange=e=>edit(()=>{const v=(e.target.value||'').trim();o.conn=(!v||v==='— none —')?'':v.split(' · ')[0];});};
|
|
942
989
|
if(col){
|
|
943
990
|
document.getElementById('bos').onchange=e=>edit(()=>{m.col.bos=parseLen(e.target.value);});
|
|
944
991
|
wireTos('tosC',m.col);
|
|
945
992
|
document.getElementById('ntC').onchange=e=>edit(()=>{m.col.note=e.target.value;});
|
|
946
|
-
wireDet('dtC',m.col);
|
|
993
|
+
wireDet('dtC',m.col);wireConn('cnC',m.col);
|
|
947
994
|
}else{
|
|
948
995
|
wireTos('tosA',m.ends[0]);
|
|
949
996
|
document.getElementById('ntA').onchange=e=>edit(()=>{m.ends[0].note=e.target.value;});
|
|
950
|
-
wireDet('dtA',m.ends[0]);
|
|
997
|
+
wireDet('dtA',m.ends[0]);wireConn('cnA',m.ends[0]);
|
|
951
998
|
wireTos('tosB',m.ends[1]);
|
|
952
999
|
document.getElementById('ntB').onchange=e=>edit(()=>{m.ends[1].note=e.target.value;});
|
|
953
|
-
wireDet('dtB',m.ends[1]);
|
|
1000
|
+
wireDet('dtB',m.ends[1]);wireConn('cnB',m.ends[1]);
|
|
954
1001
|
document.getElementById('matchEnds').onclick=()=>edit(()=>{m.ends[1]={tos:m.ends[0].tos,note:m.ends[0].note,tosDef:m.ends[0].tosDef,detail:m.ends[0].detail,conn:m.ends[0].conn,connVerified:m.ends[0].connVerified};});
|
|
955
1002
|
}
|
|
956
1003
|
document.getElementById('del').onclick=()=>edit(()=>{P.members=P.members.filter(x=>x.id!==m.id);selIds.clear();if(selDimIds.size){P.dims=P.dims.filter(d=>!selDimIds.has(d.id));selDimIds.clear();}}); // also drop any dims a mixed marquee selected
|
|
@@ -1157,6 +1204,12 @@ svg.addEventListener('pointerdown',e=>{if(e.button!==0)return;const t=e.target;
|
|
|
1157
1204
|
return;}
|
|
1158
1205
|
if(t.classList.contains('dethot')){const det=t.dataset.det;
|
|
1159
1206
|
if(picking&&pickKind==='detail'&&pickEnd){const o=pickEnd;picking=false;pickEnd=null;edit(()=>{o.detail=det;});}return;}
|
|
1207
|
+
{const cg=t.closest('g.cohot');if(cg){const sheet=cg.dataset.coSheet,prev=cg.dataset.coPrev; // Workstream B smart jump
|
|
1208
|
+
const idx=sheet?C.plans.findIndex(p=>p.sheet&&p.sheet.toUpperCase()===sheet.toUpperCase()):-1;
|
|
1209
|
+
if(idx>=0){if(idx!==C.active)setPlan(idx);else fitToWindow();} // referenced sheet is loaded → switch to it (or re-frame if already here)
|
|
1210
|
+
else if(prev&&previewFor(prev))openPreview(prev); // not loaded but we have the detail image → lightbox
|
|
1211
|
+
else toast(sheet?('Sheet '+sheet+' isn’t loaded — open it to jump there'):'No target sheet for this callout');
|
|
1212
|
+
return;}}
|
|
1160
1213
|
if(t.classList.contains('seg')&&mode==='add'){addFromSeg(t.dataset.seg);return;}
|
|
1161
1214
|
if(t.tagName==='image'&&mode==='add'){buildSnap(null);let q=toSvg(e),x0=q.x,y0=q.y;if(!e.altKey){const sn=snap(x0,y0);x0=sn.x;y0=sn.y;}const ln=document.createElementNS('http://www.w3.org/2000/svg','line');ln.setAttribute('class','draw');ln.setAttribute('x1',x0);ln.setAttribute('y1',y0);ln.setAttribute('x2',x0);ln.setAttribute('y2',y0);svg.appendChild(ln);drag={type:'draw',x0,y0,line:ln};svg.setPointerCapture(e.pointerId);e.preventDefault();return;}
|
|
1162
1215
|
if(t.classList.contains('member')&&mode==='sel'){const id=t.dataset.id;
|
|
@@ -1219,6 +1272,8 @@ document.getElementById('csSetB').onclick=()=>{csaxisMode=!csaxisMode;setCsMode(
|
|
|
1219
1272
|
document.getElementById('csResetB').onclick=()=>{resetFrame();render();};
|
|
1220
1273
|
function updDimToggle(){const b=document.getElementById('dimToggleB');if(b)b.textContent=dimsVisible?'Hide dimensions':'Show dimensions';}
|
|
1221
1274
|
document.getElementById('dimToggleB').onclick=()=>{dimsVisible=!dimsVisible;updDimToggle();render();};
|
|
1275
|
+
function updCalloutToggle(){const b=document.getElementById('calloutToggleB');if(b)b.textContent=calloutsVisible?'Hide callouts':'Show callouts';}
|
|
1276
|
+
document.getElementById('calloutToggleB').onclick=()=>{calloutsVisible=!calloutsVisible;updCalloutToggle();render();};
|
|
1222
1277
|
document.getElementById('dupB').onclick=()=>{const ids=redundantDups(); // re-scan on demand (also runs live after every edit)
|
|
1223
1278
|
if(!ids.length){const b=document.getElementById('dupB');b.dataset.flash='1';b.classList.add('ok');b.textContent='No duplicates ✓';
|
|
1224
1279
|
setTimeout(()=>{delete b.dataset.flash;b.classList.remove('ok');updDup();},1500);return;}
|
|
@@ -1240,6 +1295,7 @@ addEventListener('keydown',e=>{
|
|
|
1240
1295
|
if(e.key==='Escape'&&lightboxOpen()){closeLightbox();return;}
|
|
1241
1296
|
if(e.key==='Escape'&&askAiIsOpen()){askAiClose();return;}
|
|
1242
1297
|
if(e.key==='Escape'&&detailsOpen()){closeDetails();return;}
|
|
1298
|
+
if(e.key==='Escape'&&connLibOpen()){closeConnLib();return;}
|
|
1243
1299
|
if(e.key==='Escape'&&framesOpen()){closeFrames();return;}
|
|
1244
1300
|
if(e.key==='Escape'&&rfiOpen()){closeRFI();return;}
|
|
1245
1301
|
if(e.key==='Escape'&&confOpen()){closeConf();return;}
|
|
@@ -1428,6 +1484,37 @@ function detailsOpen(){return document.getElementById('detailsModal').style.disp
|
|
|
1428
1484
|
document.getElementById('detailsBtn').onclick=openDetails;
|
|
1429
1485
|
document.getElementById('detailsClose').onclick=closeDetails;
|
|
1430
1486
|
document.getElementById('detailsBackdrop').onclick=closeDetails;
|
|
1487
|
+
// --- connection library: edit connections[] (type → design detail# → per-platform component id) ---
|
|
1488
|
+
// The library is the vendor-neutral source; member ends reference a row via ends[].conn / col.conn.
|
|
1489
|
+
// connPlat scopes the editable "component id" column to one platform; the detail# is platform-neutral.
|
|
1490
|
+
let connPlat='tekla';const PLAT_LABEL={tekla:'Tekla',sds2:'SDS2',revit:'Revit'};
|
|
1491
|
+
function openConnLib(){if(!Array.isArray(C.connections))C.connections=[];renderConnTable();document.getElementById('connModal').style.display='flex';}
|
|
1492
|
+
function closeConnLib(){document.getElementById('connModal').style.display='none';}
|
|
1493
|
+
function connLibOpen(){return document.getElementById('connModal').style.display==='flex';}
|
|
1494
|
+
function updConnBtn(){const b=document.getElementById('connBtn');if(b)b.textContent='Connections'+((C.connections||[]).length?' ('+C.connections.length+')':'');}
|
|
1495
|
+
function renderConnTable(){const tb=document.getElementById('connBody');if(!tb)return;
|
|
1496
|
+
document.getElementById('connCompHead').textContent=(PLAT_LABEL[connPlat]||connPlat)+' id';
|
|
1497
|
+
const rows=C.connections||[];
|
|
1498
|
+
if(!rows.length){tb.innerHTML='<tr><td colspan=5 style="text-align:center;padding:32px;color:var(--mut);font-size:12px">No connections defined — click + New row to add the first.</td></tr>';updConnBtn();return;}
|
|
1499
|
+
const srcChip=s=>s==='drawing'?'<span class="chip assumed">drawing</span>':s==='company-rules'?'<span class=chip>company-rules</span>':'<span class=chip style="border-color:#1d4ed8;color:#93c5fd">user</span>';
|
|
1500
|
+
tb.innerHTML=rows.map(r=>`<tr data-id="${esc(r.id)}">`
|
|
1501
|
+
+`<td><input class="combo ftab-inp" data-src=conntypes data-f=type value="${esc(r.type||'')}" placeholder="moment" autocomplete=off></td>`
|
|
1502
|
+
+`<td><input class="combo ftab-inp" data-src=details data-f=detail value="${esc(r.detail||'')}" placeholder="e.g. 50S504" autocomplete=off></td>`
|
|
1503
|
+
+`<td><input class=ftab-inp data-f=target value="${esc((r.targets&&r.targets[connPlat])||'')}" placeholder="e.g. 146" autocomplete=off></td>`
|
|
1504
|
+
+`<td class=csrc>${srcChip(r.source||'user')}</td>`
|
|
1505
|
+
+`<td><button class="danger cdel" title="Remove this row">✕</button></td></tr>`).join('');
|
|
1506
|
+
tb.querySelectorAll('tr[data-id]').forEach(tr=>{const id=tr.dataset.id;const row=connRowById(id);if(!row)return;
|
|
1507
|
+
tr.querySelector('[data-f=type]').onchange=e=>edit(()=>{row.type=e.target.value.trim();});
|
|
1508
|
+
tr.querySelector('[data-f=detail]').onchange=e=>edit(()=>{row.detail=e.target.value.trim();});
|
|
1509
|
+
tr.querySelector('[data-f=target]').onchange=e=>edit(()=>{if(!row.targets)row.targets={};const v=e.target.value.trim();if(v)row.targets[connPlat]=v;else delete row.targets[connPlat];});
|
|
1510
|
+
tr.querySelector('.cdel').onclick=()=>{edit(()=>{C.connections=C.connections.filter(x=>x.id!==id);
|
|
1511
|
+
for(const pl of (C.plans||[]))for(const m of (pl.members||[])){if(m.col&&m.col.conn===id)m.col.conn='';for(const en of (m.ends||[]))if(en&&en.conn===id)en.conn='';}});renderConnTable();};});
|
|
1512
|
+
updConnBtn();}
|
|
1513
|
+
document.getElementById('connBtn').onclick=openConnLib;
|
|
1514
|
+
document.getElementById('connClose').onclick=closeConnLib;
|
|
1515
|
+
document.getElementById('connBackdrop').onclick=closeConnLib;
|
|
1516
|
+
document.getElementById('connNew').onclick=()=>{edit(()=>{if(!Array.isArray(C.connections))C.connections=[];C.connections.push({id:'conn'+Date.now(),type:'moment',detail:'',targets:{[connPlat]:''},source:'user'});});renderConnTable();const tb=document.getElementById('connBody');if(tb.lastElementChild)tb.lastElementChild.scrollIntoView({block:'nearest'});};
|
|
1517
|
+
document.getElementById('connPlatGrp').querySelectorAll('button').forEach(b=>b.onclick=()=>{connPlat=b.dataset.plat;document.getElementById('connPlatGrp').querySelectorAll('button').forEach(x=>x.classList.toggle('on',x===b));renderConnTable();});
|
|
1431
1518
|
// --- create your own detail (name + screenshot) — for anything the auto-detect missed ---
|
|
1432
1519
|
function readImageCompressed(file,cb){const r=new FileReader();r.onload=()=>{const im=new Image();im.onload=()=>{
|
|
1433
1520
|
const max=1400;let w=im.naturalWidth,h=im.naturalHeight;const sc=Math.min(1,max/Math.max(w,h));w=Math.max(1,Math.round(w*sc));h=Math.max(1,Math.round(h*sc));
|
|
@@ -1678,12 +1765,18 @@ lbView.addEventListener('dblclick',lbReset);
|
|
|
1678
1765
|
const COMBO_SRC={
|
|
1679
1766
|
profiles:()=>profs,
|
|
1680
1767
|
conntypes:()=>['moment','drag','shear','braced','pinned','fixed'],
|
|
1681
|
-
details:()=>{const s=new Set();(P.details||[]).forEach(d=>s.add(d.text));Object.keys(C.custom_details).forEach(n=>s.add(n));return [...s].sort((a,b)=>String(a).localeCompare(String(b),undefined,{numeric:true}));}
|
|
1768
|
+
details:()=>{const s=new Set();(P.details||[]).forEach(d=>s.add(d.text));Object.keys(C.custom_details).forEach(n=>s.add(n));return [...s].sort((a,b)=>String(a).localeCompare(String(b),undefined,{numeric:true}));},
|
|
1769
|
+
connrows:()=>['— none —',...(C.connections||[]).map(r=>connLabel(r))]
|
|
1682
1770
|
};
|
|
1771
|
+
// A connection-library row's one-line label for the per-end picker. The id is the prefix (parsed
|
|
1772
|
+
// back on select); type/detail follow for the human. Kept here so COMBO_SRC + cFld + wireConn agree.
|
|
1773
|
+
function connLabel(r){return r.id+' · '+(r.type||'?')+(r.detail?' · '+r.detail:'');}
|
|
1774
|
+
function connRowById(id){return (C.connections||[]).find(r=>r&&r.id===id)||null;}
|
|
1683
1775
|
const comboPop=document.createElement('div');comboPop.id='comboPop';document.body.appendChild(comboPop);
|
|
1684
1776
|
let comboInput=null,comboIdx=-1;
|
|
1685
1777
|
function showCombo(inp){const src=COMBO_SRC[inp.dataset.src];if(!src)return;comboInput=inp;
|
|
1686
|
-
|
|
1778
|
+
// connrows is a row PICKER (readonly), not a type-to-filter field — always offer every row.
|
|
1779
|
+
const q=inp.dataset.src==='connrows'?'':(inp.value||'').trim().toUpperCase();const opts=src().filter(o=>String(o).toUpperCase().includes(q));
|
|
1687
1780
|
if(!opts.length){comboPop.style.display='none';return;}
|
|
1688
1781
|
comboPop.innerHTML=opts.map(o=>`<div class=opt data-v="${esc(o)}">${esc(o)}</div>`).join('');
|
|
1689
1782
|
const r=inp.getBoundingClientRect();comboPop.style.left=Math.round(r.left)+'px';comboPop.style.top=Math.round(r.bottom+2)+'px';comboPop.style.minWidth=Math.round(r.width)+'px';
|