@floless/app 0.59.0 → 0.59.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
CHANGED
|
@@ -53022,7 +53022,7 @@ function appVersion() {
|
|
|
53022
53022
|
return resolveVersion({
|
|
53023
53023
|
isSea: isSea2(),
|
|
53024
53024
|
sqVersionXml: readSqVersionXml(),
|
|
53025
|
-
define: true ? "0.59.
|
|
53025
|
+
define: true ? "0.59.1" : 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.59.
|
|
53035
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.59.1" : void 0 });
|
|
53036
53036
|
}
|
|
53037
53037
|
|
|
53038
53038
|
// workflow-update.ts
|
|
@@ -61,11 +61,12 @@
|
|
|
61
61
|
"properties": {
|
|
62
62
|
"w": { "type": "number", "minimum": 1 },
|
|
63
63
|
"h": { "type": "number", "minimum": 1 },
|
|
64
|
-
"bg_b64": { "type": "string", "description": "Base64 JPEG of the page (title block excluded). CONFIDENTIAL — machine-local, stripped from the baked .flo (contract-bake.ts), never committed." }
|
|
64
|
+
"bg_b64": { "type": "string", "description": "Base64 JPEG of the page (title block excluded). CONFIDENTIAL — machine-local, stripped from the baked .flo (contract-bake.ts), never committed." },
|
|
65
|
+
"rect": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4, "description": "[x0,y0,x1,y1] in vector-frame coords: the region `bg_b64` was clipped to, so the viewer registers the raster to it (not the whole page). Absent = covers the steelBbox (legacy)." }
|
|
65
66
|
}
|
|
66
67
|
},
|
|
67
68
|
"steelBbox": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4, "description": "[x0,y0,x1,y1] of the steel linework, for Zoom-to-steel." },
|
|
68
|
-
"mode": { "enum": ["thickness", "layer", "color", "thickness+layer", "thickness+color"], "description": "Active match
|
|
69
|
+
"mode": { "enum": ["", "thickness", "layer", "color", "thickness+layer", "thickness+color", "layer+color", "thickness+layer+color"], "description": "Active match dimensions, '+'-joined and AND-ed (canonical order: thickness, layer, color). Any combo of layer/thickness/color; \"\" = match nothing (all boxes unchecked — a valid saved state)." },
|
|
69
70
|
"hide_title_block": { "type": "boolean" },
|
|
70
71
|
"layers": {
|
|
71
72
|
"type": "array",
|
|
@@ -142,7 +143,8 @@
|
|
|
142
143
|
"properties": {
|
|
143
144
|
"w": { "type": "number", "minimum": 1 },
|
|
144
145
|
"h": { "type": "number", "minimum": 1 },
|
|
145
|
-
"bg_b64": { "type": "string", "description": "Base64 JPEG of this sheet (title block excluded). CONFIDENTIAL — stripped before bake, never committed." }
|
|
146
|
+
"bg_b64": { "type": "string", "description": "Base64 JPEG of this sheet (title block excluded). CONFIDENTIAL — stripped before bake, never committed." },
|
|
147
|
+
"rect": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4, "description": "[x0,y0,x1,y1] in the same vector-frame coords as element `d` paths: the region `bg_b64` was clipped to. The viewer registers the raster to THIS rect (not the whole page) so it aligns 1:1 with the linework. Absent = the raster covers the steelBbox (legacy)." }
|
|
146
148
|
}
|
|
147
149
|
},
|
|
148
150
|
"steelBbox": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4, "description": "[x0,y0,x1,y1] of this sheet's steel linework, for Zoom-to-steel." },
|
|
@@ -80,10 +80,13 @@ native/display coords):
|
|
|
80
80
|
pt>, color:<#rrggbb stroke>, d:<SVG path>, dashed:<bool> }`.
|
|
81
81
|
2. **Text** — `page.get_text("dict")`: per span keep `{ kind:"text", layer:<OCG>, w:0, color:<#hex>,
|
|
82
82
|
text:<string>, bbox:[x0,y0,x1,y1] }`.
|
|
83
|
-
3. **Per-sheet entry** — push `{ sheet:<number>, title:<name?>, page:{ w, h, bg_b64 }, steelBbox,
|
|
83
|
+
3. **Per-sheet entry** — push `{ sheet:<number>, title:<name?>, page:{ w, h, bg_b64, rect }, steelBbox,
|
|
84
84
|
elements:[…] }` onto `filter.sheets`. `bg_b64` is a dimmed raster of THIS sheet's framing area
|
|
85
|
-
(**title block excluded** — CONFIDENTIAL, machine-local, never committed)
|
|
86
|
-
|
|
85
|
+
(**title block excluded** — CONFIDENTIAL, machine-local, never committed). **`page.rect` = the exact
|
|
86
|
+
clip `[x0,y0,x1,y1]` you rendered `bg_b64` from, in the SAME display-space coords as the element `d`
|
|
87
|
+
paths** — the viewer registers the raster to `rect` (not the whole page), so it aligns 1:1 with the
|
|
88
|
+
linework. `w`/`h` stay the full display-space page size (the vectors' coordinate frame). `steelBbox`
|
|
89
|
+
= the bbox of this sheet's steel-layer elements (Zoom-to-steel).
|
|
87
90
|
4. **Global facets (union across sheets)** — aggregate distinct `layers` / `thicknesses` / `colors`
|
|
88
91
|
with a summed `count` each, ACROSS all sheets, into the top-level `filter.layers/thicknesses/colors`
|
|
89
92
|
(NOT per-sheet). Round each element's `w` and the facet `w` the **same** way (matching is
|
|
@@ -10,24 +10,37 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Shapes:
|
|
12
12
|
* element : { kind:'line'|'text', layer:string, w:number, color:string, d?:string, text?:string, bbox?:number[], dashed?:boolean }
|
|
13
|
-
* mode : '
|
|
13
|
+
* mode : any '+'-joined combo of 'layer'/'thickness'/'color', AND-ed (e.g. 'thickness+layer').
|
|
14
|
+
* '' = match nothing. Canonical order = MODE_ORDER (keeps legacy 'thickness+…' strings).
|
|
14
15
|
* sel : { layers:Set<string>, thicknesses:Set<number>, colors:Set<string> }
|
|
15
16
|
* filter : { mode, layers:[{name,count,steel?,drawable?,on?}], thicknesses:[{w,count,on?}], colors:[{hex,count,on?}], elements:[…] }
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
|
-
/**
|
|
19
|
+
/** Canonical dimension order for a mode string — keeps legacy 'thickness+layer'/'thickness+color'. */
|
|
20
|
+
export const MODE_ORDER = ['thickness', 'layer', 'color'];
|
|
21
|
+
|
|
22
|
+
/** The dimensions a mode activates, order-agnostic: 'thickness+layer' -> ['thickness','layer']. */
|
|
23
|
+
export function modeDims(mode) {
|
|
24
|
+
return String(mode || '').split('+').filter((d) => MODE_ORDER.includes(d));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Build the canonical mode string from a set/list of checked dimensions. */
|
|
28
|
+
export function canonicalMode(dims) {
|
|
29
|
+
const set = new Set(dims);
|
|
30
|
+
return MODE_ORDER.filter((d) => set.has(d)).join('+');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The active match predicate — an element passes when it matches EVERY checked dimension (AND).
|
|
35
|
+
* Zero checked dimensions match nothing (a valid, visible "0 shown" state, not a fall-through to all).
|
|
36
|
+
*/
|
|
19
37
|
export function matchesActive(el, mode, sel) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
case 'color': return byColor();
|
|
27
|
-
case 'thickness+layer': return byThk() && byLayer();
|
|
28
|
-
case 'thickness+color': return byThk() && byColor();
|
|
29
|
-
default: return false;
|
|
30
|
-
}
|
|
38
|
+
const dims = modeDims(mode);
|
|
39
|
+
if (!dims.length) return false;
|
|
40
|
+
return dims.every((d) =>
|
|
41
|
+
d === 'layer' ? sel.layers.has(el.layer)
|
|
42
|
+
: d === 'thickness' ? sel.thicknesses.has(el.w)
|
|
43
|
+
: sel.colors.has(el.color));
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
/**
|
|
@@ -66,6 +79,24 @@ export function countShown(elements, mode, sel) {
|
|
|
66
79
|
return n;
|
|
67
80
|
}
|
|
68
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Where a sheet's background raster (`page.bg_b64`) maps, in the same vector-frame coords as the
|
|
84
|
+
* elements' `d` paths — so the raster registers 1:1 with the linework. The producer clips the raster
|
|
85
|
+
* to the framing area and records that clip in `page.rect` ([x0,y0,x1,y1]); the renderer places the
|
|
86
|
+
* <image> there (preserveAspectRatio="none") instead of stretching it across the whole page.
|
|
87
|
+
*
|
|
88
|
+
* ponytail: `steelBbox` fallback bridges pre-`rect` contracts (the producer clipped the raster to the
|
|
89
|
+
* steel bbox, so it aligns exactly). Upgrade path: drop it once every baked contract carries page.rect.
|
|
90
|
+
*/
|
|
91
|
+
export function bgRect(sheet) {
|
|
92
|
+
const pg = (sheet && sheet.page) || {};
|
|
93
|
+
const valid = (r) => Array.isArray(r) && r.length === 4 && r[2] - r[0] > 0 && r[3] - r[1] > 0;
|
|
94
|
+
// Try each candidate in order — a degenerate page.rect must NOT skip a valid steelBbox.
|
|
95
|
+
const r = valid(pg.rect) ? pg.rect : (valid(sheet && sheet.steelBbox) ? sheet.steelBbox : null);
|
|
96
|
+
if (r) return { x: r[0], y: r[1], w: r[2] - r[0], h: r[3] - r[1] };
|
|
97
|
+
return { x: 0, y: 0, w: pg.w || 0, h: pg.h || 0 };
|
|
98
|
+
}
|
|
99
|
+
|
|
69
100
|
/**
|
|
70
101
|
* Write the live selection + mode back onto the filter block's `on` flags so it persists on the
|
|
71
102
|
* contract (what Save PUTs, and what the reader reads). Mutates and returns `filter`.
|
|
@@ -38,11 +38,9 @@ header select#sheetSel{font-weight:600;min-width:200px;max-width:320px}
|
|
|
38
38
|
.opts label em{margin-left:auto;color:var(--mut);font-style:normal;font-size:11px}
|
|
39
39
|
.opts .sw{width:12px;height:12px;border-radius:3px;flex:0 0 12px;border:1px solid var(--line)}
|
|
40
40
|
.opts label.textlayer{opacity:.55;cursor:default}
|
|
41
|
-
/* Fully custom checkbox
|
|
42
|
-
input[type=
|
|
43
|
-
input[type=radio]{border-radius:50%}
|
|
41
|
+
/* Fully custom checkbox — the native unchecked widget is a white/grey square that breaks the dark baseline. */
|
|
42
|
+
input[type=checkbox]{-webkit-appearance:none;appearance:none;width:14px;height:14px;flex:none;border:1px solid #475569;border-radius:3px;background:transparent;cursor:pointer;margin:0;vertical-align:middle}
|
|
44
43
|
input[type=checkbox]:checked{background:var(--brand);border-color:var(--brand);background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 10 8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 4l3 3 5-5' stroke='%23fff' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:center}
|
|
45
|
-
input[type=radio]:checked{background:var(--brand);border-color:var(--brand);box-shadow:inset 0 0 0 3px var(--panel)}
|
|
46
44
|
input:focus-visible{outline:2px solid var(--brand);outline-offset:1px}
|
|
47
45
|
.facet.dim{opacity:.4} .facet.dim .opts,.facet.dim .acts{pointer-events:none}
|
|
48
46
|
.hint{color:var(--mut);font-size:11px;line-height:1.5}
|
|
@@ -82,12 +80,11 @@ footer{margin-top:auto;display:flex;align-items:center;gap:6px;flex-wrap:wrap;bo
|
|
|
82
80
|
<div id="app">
|
|
83
81
|
<aside id="panel">
|
|
84
82
|
<section id="modes">
|
|
85
|
-
<h2>Match
|
|
86
|
-
<label><input type="
|
|
87
|
-
<label><input type="
|
|
88
|
-
<label><input type="
|
|
89
|
-
<
|
|
90
|
-
<label><input type="radio" name="mode" value="thickness+color" /> Thickness + Color</label>
|
|
83
|
+
<h2>Match on</h2>
|
|
84
|
+
<label><input type="checkbox" name="dim" value="layer" checked /> Layer</label>
|
|
85
|
+
<label><input type="checkbox" name="dim" value="thickness" /> Thickness</label>
|
|
86
|
+
<label><input type="checkbox" name="dim" value="color" /> Color</label>
|
|
87
|
+
<p class="hint">Checked criteria must all match (AND).</p>
|
|
91
88
|
</section>
|
|
92
89
|
<section class="facet" id="facet-layers">
|
|
93
90
|
<h2>Layers <span class="acts"><button data-all="layers">all</button><button data-none="layers">none</button></span></h2>
|
|
@@ -128,7 +125,7 @@ footer{margin-top:auto;display:flex;align-items:center;gap:6px;flex-wrap:wrap;bo
|
|
|
128
125
|
</div>
|
|
129
126
|
|
|
130
127
|
<script type="module">
|
|
131
|
-
import { matchesActive, selFromFilter, eyedropperAdd, countShown, applySelToFilter, normalizeFilter } from './steel-filter-core.js';
|
|
128
|
+
import { matchesActive, modeDims, canonicalMode, selFromFilter, eyedropperAdd, countShown, bgRect, applySelToFilter, normalizeFilter } from './steel-filter-core.js';
|
|
132
129
|
|
|
133
130
|
const APP_ID = new URLSearchParams(location.search).get('app') || '';
|
|
134
131
|
const SVGNS = 'http://www.w3.org/2000/svg';
|
|
@@ -183,9 +180,11 @@ function showEmpty(msg){
|
|
|
183
180
|
}
|
|
184
181
|
|
|
185
182
|
function initFromFilter(){
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
// Canonicalize on load (drops unknown tokens, orders dims) so a facet-only save writes a valid
|
|
184
|
+
// enum string; a saved-empty '' round-trips as '', while an absent mode defaults to 'layer'.
|
|
185
|
+
mode = (F.mode == null) ? 'layer' : canonicalMode(modeDims(F.mode));
|
|
186
|
+
const on = new Set(modeDims(mode));
|
|
187
|
+
document.querySelectorAll('input[name=dim]').forEach(cb => { cb.checked = on.has(cb.value); });
|
|
189
188
|
// layer colours: steel layers vivid+distinct, others a single muted slate (GLOBAL facets)
|
|
190
189
|
let si = 0;
|
|
191
190
|
layerColor = {};
|
|
@@ -217,7 +216,16 @@ function setSheet(i){
|
|
|
217
216
|
const sh = sheets[active] || {};
|
|
218
217
|
const pg = (sh.page && sh.page.w > 0 && sh.page.h > 0) ? sh.page : { w: 1000, h: 1000 }; // guard empty page → no NaN viewBox
|
|
219
218
|
svg.setAttribute('viewBox', `0 0 ${pg.w} ${pg.h}`);
|
|
220
|
-
if(pg.bg_b64){
|
|
219
|
+
if(pg.bg_b64){
|
|
220
|
+
// Register the raster to its true extent (the framing-area clip), NOT the whole page — else a
|
|
221
|
+
// crop gets stretched over the full viewBox and floats off-scale from the vector linework.
|
|
222
|
+
const r = bgRect(sh);
|
|
223
|
+
bgImg.setAttribute('href', 'data:image/jpeg;base64,' + pg.bg_b64);
|
|
224
|
+
bgImg.setAttribute('x', r.x); bgImg.setAttribute('y', r.y);
|
|
225
|
+
bgImg.setAttribute('width', r.w); bgImg.setAttribute('height', r.h);
|
|
226
|
+
bgImg.setAttribute('preserveAspectRatio', 'none');
|
|
227
|
+
bgImg.style.display='';
|
|
228
|
+
}
|
|
221
229
|
else bgImg.style.display = 'none';
|
|
222
230
|
buildOverlay();
|
|
223
231
|
base = { x: 0, y: 0, w: pg.w, h: pg.h }; vb = { ...base }; setVB();
|
|
@@ -309,13 +317,17 @@ function apply(){
|
|
|
309
317
|
html += ' · <b>' + sa + '</b> of <b>' + ta + '</b> total (' + sheets.length + ' sheets)';
|
|
310
318
|
}
|
|
311
319
|
countEl.innerHTML = html;
|
|
312
|
-
|
|
313
|
-
document.getElementById('facet-
|
|
314
|
-
document.getElementById('facet-
|
|
320
|
+
const dims = new Set(modeDims(mode));
|
|
321
|
+
document.getElementById('facet-layers').classList.toggle('dim', !dims.has('layer'));
|
|
322
|
+
document.getElementById('facet-thicknesses').classList.toggle('dim', !dims.has('thickness'));
|
|
323
|
+
document.getElementById('facet-colors').classList.toggle('dim', !dims.has('color'));
|
|
315
324
|
}
|
|
316
325
|
|
|
317
326
|
// --- modes + all/none ---
|
|
318
|
-
document.querySelectorAll('input[name=
|
|
327
|
+
document.querySelectorAll('input[name=dim]').forEach(cb => cb.addEventListener('change', () => {
|
|
328
|
+
mode = canonicalMode([...document.querySelectorAll('input[name=dim]:checked')].map(c => c.value));
|
|
329
|
+
markDirty(); apply();
|
|
330
|
+
}));
|
|
319
331
|
document.querySelectorAll('[data-all]').forEach(b => b.addEventListener('click', () => setAllFacet(b.dataset.all, true)));
|
|
320
332
|
document.querySelectorAll('[data-none]').forEach(b => b.addEventListener('click', () => setAllFacet(b.dataset.none, false)));
|
|
321
333
|
function setAllFacet(facet, on){
|