@floless/app 0.59.0 → 0.60.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,289 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Vectorize — 2D editor</title>
7
+ <style>
8
+ /* Locked baseline: shadcn dark slate-blue (identical tokens + scrollbar theming as steel-editor.html). */
9
+ :root{--bg:#0f172a;--panel:#1e293b;--line:#334155;--text:#e2e8f0;--mut:#94a3b8;--brand:#3b82f6;--paper:#f8fafc}
10
+ *{scrollbar-width:thin;scrollbar-color:#475569 transparent}
11
+ *::-webkit-scrollbar{width:10px;height:10px}
12
+ *::-webkit-scrollbar-track{background:transparent}
13
+ *::-webkit-scrollbar-thumb{background:#475569;border-radius:6px;border:2px solid transparent;background-clip:content-box}
14
+ *::-webkit-scrollbar-thumb:hover{background:#5b6b85;background-clip:content-box}
15
+ *::-webkit-scrollbar-corner{background:transparent}
16
+ *{box-sizing:border-box}
17
+ body{margin:0;background:var(--bg);color:var(--text);font:13px system-ui;height:100vh;display:flex;flex-direction:column;overflow:hidden}
18
+ header{display:flex;align-items:center;gap:14px;padding:8px 14px;background:var(--panel);border-bottom:1px solid var(--line);flex:none}
19
+ header b{font-size:14px}
20
+ header .spacer{flex:1}
21
+ .stat{color:var(--mut)} .stat b{color:var(--text);font-variant-numeric:tabular-nums}
22
+ button{height:28px;padding:0 12px;background:var(--line);color:var(--text);border:1px solid var(--line);border-radius:6px;cursor:pointer;font:inherit}
23
+ button:hover{background:#475569}
24
+ button.primary{background:var(--brand);border-color:var(--brand);color:#fff}
25
+ button.primary:hover{filter:brightness(1.08)}
26
+ #wrap{flex:1;display:flex;min-height:0}
27
+ #stage{flex:1;position:relative;background:var(--paper);overflow:hidden;min-width:0}
28
+ #svg{position:absolute;inset:0;width:100%;height:100%;touch-action:none;cursor:grab}
29
+ #svg.panning{cursor:grabbing}
30
+ #svg path{stroke-linecap:round;stroke-linejoin:round}
31
+ #svg .weak{stroke:#f59e0b!important} #svg text.weak{fill:#f59e0b!important}
32
+ #svg .hidden{display:none}
33
+ #state{position:absolute;inset:0;display:none;align-items:center;justify-content:center;flex-direction:column;gap:10px;background:var(--bg);color:var(--mut);text-align:center;padding:24px}
34
+ #state.show{display:flex}
35
+ #state .spin{width:26px;height:26px;border:3px solid var(--line);border-top-color:var(--brand);border-radius:50%;animation:spin .8s linear infinite}
36
+ @keyframes spin{to{transform:rotate(360deg)}}
37
+ aside{width:244px;flex:none;background:var(--panel);border-left:1px solid var(--line);padding:12px;overflow:auto;display:flex;flex-direction:column;gap:14px}
38
+ aside h3{margin:0;font-size:10px;color:var(--mut);text-transform:uppercase;letter-spacing:.06em}
39
+ .ck{display:flex;align-items:center;gap:8px;padding:5px 6px;border-radius:6px;cursor:pointer;color:var(--text)}
40
+ .ck:hover{background:var(--line)}
41
+ .ck input{accent-color:var(--brand);cursor:pointer;margin:0}
42
+ .ck .ct{margin-left:auto;color:var(--mut);font-variant-numeric:tabular-nums;font-size:11px}
43
+ .muted{color:var(--mut);font-size:12px;line-height:1.5}
44
+ hr{border:0;border-top:1px solid var(--line);margin:0}
45
+ #zoombar{position:absolute;left:12px;bottom:12px;display:flex;align-items:center;gap:8px;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:6px 10px;box-shadow:0 4px 14px rgba(0,0,0,.45);z-index:5}
46
+ #zoombar input[type=range]{width:120px;padding:0;accent-color:var(--brand)}
47
+ #zoombar #zpct{min-width:42px;text-align:right;color:var(--mut);font-variant-numeric:tabular-nums}
48
+ #zoombar .ghost{height:24px;padding:0 8px;background:transparent;border:1px solid var(--line);color:var(--mut);border-radius:5px;font-size:11px}
49
+ #zoombar .ghost:hover{color:var(--text);border-color:var(--brand)}
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <header>
54
+ <b>Vectorize</b>
55
+ <span class="stat" id="title">—</span>
56
+ <span class="spacer"></span>
57
+ <span class="stat"><b id="nEl">0</b> elements · <b id="nTx">0</b> text</span>
58
+ <button class="primary" id="exportBtn" disabled>Export SVG</button>
59
+ </header>
60
+ <div id="wrap">
61
+ <div id="stage">
62
+ <svg id="svg" xmlns="http://www.w3.org/2000/svg"></svg>
63
+ <div id="state" class="show"><div class="spin"></div><div id="stateMsg">Loading drawing…</div></div>
64
+ <div id="zoombar">
65
+ <button class="ghost" id="fitBtn" title="Fit to view">Fit</button>
66
+ <input type="range" id="zoom" min="10" max="800" value="100" aria-label="Zoom">
67
+ <span id="zpct">100%</span>
68
+ </div>
69
+ </div>
70
+ <aside>
71
+ <div>
72
+ <h3>Show</h3>
73
+ <label class="ck"><input type="checkbox" id="kLines" checked><span>Lines</span><span class="ct" id="cLines">0</span></label>
74
+ <label class="ck"><input type="checkbox" id="kCurves" checked><span>Curves</span><span class="ct" id="cCurves">0</span></label>
75
+ <label class="ck"><input type="checkbox" id="kText" checked><span>Text</span><span class="ct" id="cText">0</span></label>
76
+ </div>
77
+ <hr>
78
+ <div>
79
+ <h3>Layers</h3>
80
+ <div id="layers"><div class="muted">No layers in this drawing.</div></div>
81
+ </div>
82
+ <hr>
83
+ <div>
84
+ <h3>Confidence</h3>
85
+ <label class="ck" id="weakRow" hidden><input type="checkbox" id="weakOnly"><span>Isolate weak traces</span><span class="ct" id="cWeak">0</span></label>
86
+ <div class="muted" id="weakNote">No weak traces — read deterministically.</div>
87
+ </div>
88
+ </aside>
89
+ </div>
90
+ <script>
91
+ (function () {
92
+ 'use strict';
93
+ const $ = (id) => document.getElementById(id);
94
+ const SVGNS = 'http://www.w3.org/2000/svg';
95
+ const svg = $('svg');
96
+ const params = new URLSearchParams(location.search);
97
+ const app = params.get('app');
98
+ const src = params.get('src') || (app ? ('/api/contract/' + encodeURIComponent(app)) : '/vector-example.json');
99
+ const WEAK = 0.5;
100
+
101
+ let sheet = null;
102
+ let view = { x: 0, y: 0, w: 100, h: 100 };
103
+
104
+ function showState(msg) { $('stateMsg').textContent = msg; $('state').classList.add('show'); $('state').querySelector('.spin').style.display = msg ? 'none' : ''; }
105
+ function hideState() { $('state').classList.remove('show'); }
106
+
107
+ // Build an SVG polyline path from explicit points (a line/polyline carrying pts but no d).
108
+ function ptsPath(pts) {
109
+ if (!pts || pts.length < 2) return '';
110
+ return pts.map((p, i) => (i === 0 ? 'M' : 'L') + (+p[0] || 0) + ' ' + (+p[1] || 0)).join(' ');
111
+ }
112
+ // Build one element as a real SVG DOM node (safe — setAttribute / textContent, no innerHTML).
113
+ function makeNode(el) {
114
+ const weak = (el.confidence != null && el.confidence < WEAK);
115
+ if (el.kind === 'text') {
116
+ if (!el.text) return null;
117
+ const ox = (el.origin && el.origin[0] != null) ? el.origin[0] : (el.bbox ? el.bbox[0] : 0);
118
+ const oy = (el.origin && el.origin[1] != null) ? el.origin[1] : (el.bbox ? (el.bbox[3] != null ? el.bbox[3] : el.bbox[1]) : 0);
119
+ const t = document.createElementNS(SVGNS, 'text');
120
+ t.setAttribute('x', ox); t.setAttribute('y', oy);
121
+ t.setAttribute('font-size', el.size != null ? el.size : 4);
122
+ t.setAttribute('fill', el.color || '#334155');
123
+ if (el.angle) t.setAttribute('transform', 'rotate(' + el.angle + ' ' + ox + ' ' + oy + ')');
124
+ t.textContent = el.text;
125
+ tag(t, el, 'text', weak);
126
+ return t;
127
+ }
128
+ const d = el.d || ptsPath(el.pts);
129
+ if (!d) return null;
130
+ const p = document.createElementNS(SVGNS, 'path');
131
+ p.setAttribute('d', d);
132
+ p.setAttribute('fill', 'none');
133
+ p.setAttribute('stroke', el.color || '#334155');
134
+ p.setAttribute('stroke-width', el.w || 1);
135
+ p.setAttribute('vector-effect', 'non-scaling-stroke');
136
+ if (el.dashed) p.setAttribute('stroke-dasharray', '3 2');
137
+ tag(p, el, (el.kind === 'line' || el.kind === 'polyline') ? 'lines' : 'curves', weak);
138
+ return p;
139
+ }
140
+ function tag(node, el, group, weak) {
141
+ node.setAttribute('class', 'el' + (weak ? ' weak' : ''));
142
+ node.dataset.kind = el.kind;
143
+ node.dataset.group = group;
144
+ node.dataset.layer = el.layer || '';
145
+ node.dataset.id = el.id || '';
146
+ }
147
+
148
+ function render() {
149
+ while (svg.firstChild) svg.removeChild(svg.firstChild);
150
+ const frag = document.createDocumentFragment();
151
+ for (const el of sheet.elements) { const n = makeNode(el); if (n) frag.appendChild(n); }
152
+ svg.appendChild(frag);
153
+ applyVisibility();
154
+ }
155
+
156
+ function counts() {
157
+ let lines = 0, curves = 0, text = 0, weak = 0;
158
+ for (const el of sheet.elements) {
159
+ if (el.kind === 'text') text++;
160
+ else if (el.kind === 'line' || el.kind === 'polyline') lines++;
161
+ else curves++;
162
+ if (el.confidence != null && el.confidence < WEAK) weak++;
163
+ }
164
+ return { lines, curves, text, weak };
165
+ }
166
+
167
+ function applyVisibility() {
168
+ const showLines = $('kLines').checked, showCurves = $('kCurves').checked, showText = $('kText').checked;
169
+ const off = new Set([...document.querySelectorAll('#layers input:not(:checked)')].map((i) => i.dataset.layer));
170
+ const weakOnly = $('weakOnly').checked;
171
+ for (const node of svg.children) {
172
+ const g = node.dataset.group;
173
+ let vis = g === 'text' ? showText : g === 'lines' ? showLines : showCurves;
174
+ if (vis && node.dataset.layer && off.has(node.dataset.layer)) vis = false;
175
+ if (vis && weakOnly) vis = node.classList.contains('weak');
176
+ node.classList.toggle('hidden', !vis);
177
+ }
178
+ }
179
+
180
+ function pageW() { return (sheet.page && sheet.page.w) || 100; }
181
+ function pageH() { return (sheet.page && sheet.page.h) || 100; }
182
+ function setViewBox() { svg.setAttribute('viewBox', view.x + ' ' + view.y + ' ' + view.w + ' ' + view.h); }
183
+ function baseScale() { const r = svg.getBoundingClientRect(); return Math.min(r.width / (pageW() * 1.08), r.height / (pageH() * 1.08)) || 1; }
184
+ function curScale() { const r = svg.getBoundingClientRect(); return (r.width / view.w) || 1; }
185
+ function syncZoom() { const pct = Math.round((curScale() / baseScale()) * 100); $('zoom').value = Math.max(10, Math.min(800, pct)); $('zpct').textContent = pct + '%'; }
186
+ function fit() { const m = Math.max(pageW(), pageH()) * 0.04; view = { x: -m, y: -m, w: pageW() + 2 * m, h: pageH() + 2 * m }; setViewBox(); syncZoom(); }
187
+
188
+ svg.addEventListener('wheel', (e) => {
189
+ e.preventDefault();
190
+ const r = svg.getBoundingClientRect();
191
+ const px = view.x + ((e.clientX - r.left) / r.width) * view.w;
192
+ const py = view.y + ((e.clientY - r.top) / r.height) * view.h;
193
+ const f = e.deltaY < 0 ? 0.9 : 1.1;
194
+ view.w *= f; view.h *= f;
195
+ view.x = px - ((e.clientX - r.left) / r.width) * view.w;
196
+ view.y = py - ((e.clientY - r.top) / r.height) * view.h;
197
+ setViewBox(); syncZoom();
198
+ }, { passive: false });
199
+
200
+ let pan = null;
201
+ svg.addEventListener('pointerdown', (e) => { pan = { x: e.clientX, y: e.clientY, vx: view.x, vy: view.y }; svg.setPointerCapture(e.pointerId); svg.classList.add('panning'); });
202
+ svg.addEventListener('pointermove', (e) => {
203
+ if (!pan) return;
204
+ const r = svg.getBoundingClientRect();
205
+ view.x = pan.vx - (e.clientX - pan.x) * (view.w / r.width);
206
+ view.y = pan.vy - (e.clientY - pan.y) * (view.h / r.height);
207
+ setViewBox();
208
+ });
209
+ const endPan = () => { pan = null; svg.classList.remove('panning'); };
210
+ svg.addEventListener('pointerup', endPan); svg.addEventListener('pointercancel', endPan);
211
+
212
+ $('zoom').addEventListener('input', (e) => {
213
+ const pct = +e.target.value, target = baseScale() * (pct / 100), r = svg.getBoundingClientRect();
214
+ const cx = view.x + view.w / 2, cy = view.y + view.h / 2;
215
+ view.w = r.width / target; view.h = r.height / target; view.x = cx - view.w / 2; view.y = cy - view.h / 2;
216
+ setViewBox(); $('zpct').textContent = pct + '%';
217
+ });
218
+ $('fitBtn').addEventListener('click', fit);
219
+ ['kLines', 'kCurves', 'kText', 'weakOnly'].forEach((id) => $(id).addEventListener('change', applyVisibility));
220
+
221
+ // Export the visible drawing as a standalone SVG — clone the live nodes and serialize (no string HTML).
222
+ $('exportBtn').addEventListener('click', () => {
223
+ if (!sheet) return; // nothing loaded yet — guard against a null-sheet deref
224
+ const out = document.createElementNS(SVGNS, 'svg');
225
+ out.setAttribute('xmlns', SVGNS);
226
+ out.setAttribute('viewBox', '0 0 ' + pageW() + ' ' + pageH());
227
+ out.setAttribute('width', pageW()); out.setAttribute('height', pageH());
228
+ for (const node of svg.children) {
229
+ if (node.classList.contains('hidden')) continue;
230
+ const c = node.cloneNode(true);
231
+ c.removeAttribute('class');
232
+ out.appendChild(c);
233
+ }
234
+ const doc = new XMLSerializer().serializeToString(out);
235
+ const blob = new Blob([doc], { type: 'image/svg+xml' });
236
+ const a = document.createElement('a');
237
+ a.href = URL.createObjectURL(blob);
238
+ a.download = String((sheet.label || 'drawing')).replace(/[^a-z0-9._-]+/gi, '-') + '.svg';
239
+ document.body.appendChild(a); a.click(); a.remove();
240
+ URL.revokeObjectURL(a.href);
241
+ });
242
+
243
+ function buildLayers() {
244
+ const box = $('layers');
245
+ while (box.firstChild) box.removeChild(box.firstChild);
246
+ const layers = (sheet.layers || []).filter((l) => l && l.name);
247
+ if (!layers.length) {
248
+ const d = document.createElement('div'); d.className = 'muted'; d.textContent = 'No layers in this drawing.'; box.appendChild(d); return;
249
+ }
250
+ for (const l of layers) {
251
+ const lab = document.createElement('label'); lab.className = 'ck';
252
+ const inp = document.createElement('input'); inp.type = 'checkbox'; inp.checked = true; inp.dataset.layer = l.name;
253
+ inp.addEventListener('change', applyVisibility);
254
+ const nm = document.createElement('span'); nm.textContent = l.name;
255
+ const ct = document.createElement('span'); ct.className = 'ct'; ct.textContent = String(l.count || 0);
256
+ lab.append(inp, nm, ct); box.appendChild(lab);
257
+ }
258
+ }
259
+
260
+ function load(contract) {
261
+ if (!contract || !Array.isArray(contract.sheets) || !contract.sheets.length) { showState('This app has no vectorized drawing yet. Drop a PDF and re-bake to read one.'); return; }
262
+ sheet = contract.sheets[contract.active || 0] || contract.sheets[0];
263
+ if (!sheet || !Array.isArray(sheet.elements)) { showState('The drawing has no elements.'); return; }
264
+ const c = counts();
265
+ const name = (contract.source && contract.source.name) ? contract.source.name : (sheet.label || 'drawing');
266
+ $('title').textContent = name;
267
+ document.title = 'Vectorize — ' + name;
268
+ $('nEl').textContent = sheet.elements.length; $('nTx').textContent = c.text;
269
+ $('cLines').textContent = c.lines; $('cCurves').textContent = c.curves; $('cText').textContent = c.text;
270
+ $('cWeak').textContent = c.weak;
271
+ $('weakNote').textContent = c.weak ? (c.weak + ' trace' + (c.weak === 1 ? '' : 's') + ' below ' + WEAK + ' confidence.') : 'No weak traces — read deterministically.';
272
+ $('weakRow').hidden = c.weak === 0; // hide the filter when there's nothing to filter (not disabled)
273
+ buildLayers();
274
+ render(); fit(); $('exportBtn').disabled = false; hideState();
275
+ }
276
+
277
+ const ctrl = new AbortController();
278
+ const timer = setTimeout(() => ctrl.abort(), 15000);
279
+ fetch(src, { headers: { accept: 'application/json' }, signal: ctrl.signal })
280
+ .then((r) => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
281
+ .then(load)
282
+ .catch((e) => showState(e.name === 'AbortError' ? 'Loading timed out — is the server reachable?' : ('Could not load the drawing (' + e.message + ').')))
283
+ .finally(() => clearTimeout(timer));
284
+
285
+ window.addEventListener('resize', () => { if (sheet) syncZoom(); });
286
+ })();
287
+ </script>
288
+ </body>
289
+ </html>
@@ -0,0 +1,107 @@
1
+ {
2
+ "type": "drawing.vector/v1",
3
+ "units": "mm",
4
+ "source": {
5
+ "name": "vectorize-detail.pdf",
6
+ "kind": "pdf",
7
+ "extractor": "pymupdf@compose-time"
8
+ },
9
+ "sheets": [
10
+ {
11
+ "id": "s1",
12
+ "label": "page 1",
13
+ "page": {
14
+ "w": 300,
15
+ "h": 220
16
+ },
17
+ "elements": [
18
+ {
19
+ "kind": "polyline",
20
+ "d": "M90.0 70.0 H210.0 V170.0 H90.0 Z",
21
+ "color": "#000000",
22
+ "w": 1.4,
23
+ "id": "e1"
24
+ },
25
+ {
26
+ "kind": "polyline",
27
+ "d": "M120.0 100.0 H180.0 V140.0 H120.0 Z",
28
+ "color": "#667084",
29
+ "w": 1,
30
+ "id": "e2"
31
+ },
32
+ {
33
+ "kind": "spline",
34
+ "d": "M100.0 85.0 C100.0 87.8 102.2 90.0 105.0 90.0 M105.0 90.0 C107.8 90.0 110.0 87.8 110.0 85.0 M110.0 85.0 C110.0 82.2 107.8 80.0 105.0 80.0 M105.0 80.0 C102.2 80.0 100.0 82.2 100.0 85.0",
35
+ "color": "#db2626",
36
+ "w": 1,
37
+ "id": "e3"
38
+ },
39
+ {
40
+ "kind": "spline",
41
+ "d": "M190.0 85.0 C190.0 87.8 192.2 90.0 195.0 90.0 M195.0 90.0 C197.8 90.0 200.0 87.8 200.0 85.0 M200.0 85.0 C200.0 82.2 197.8 80.0 195.0 80.0 M195.0 80.0 C192.2 80.0 190.0 82.2 190.0 85.0",
42
+ "color": "#db2626",
43
+ "w": 1,
44
+ "id": "e4"
45
+ },
46
+ {
47
+ "kind": "spline",
48
+ "d": "M100.0 155.0 C100.0 157.8 102.2 160.0 105.0 160.0 M105.0 160.0 C107.8 160.0 110.0 157.8 110.0 155.0 M110.0 155.0 C110.0 152.2 107.8 150.0 105.0 150.0 M105.0 150.0 C102.2 150.0 100.0 152.2 100.0 155.0",
49
+ "color": "#db2626",
50
+ "w": 1,
51
+ "id": "e5"
52
+ },
53
+ {
54
+ "kind": "spline",
55
+ "d": "M190.0 155.0 C190.0 157.8 192.2 160.0 195.0 160.0 M195.0 160.0 C197.8 160.0 200.0 157.8 200.0 155.0 M200.0 155.0 C200.0 152.2 197.8 150.0 195.0 150.0 M195.0 150.0 C192.2 150.0 190.0 152.2 190.0 155.0",
56
+ "color": "#db2626",
57
+ "w": 1,
58
+ "id": "e6"
59
+ },
60
+ {
61
+ "kind": "line",
62
+ "d": "M210.0 70.0 L250.0 55.0",
63
+ "color": "#667084",
64
+ "w": 0.8,
65
+ "id": "e7"
66
+ },
67
+ {
68
+ "kind": "text",
69
+ "text": "BASE PLATE PL1/2",
70
+ "bbox": [
71
+ 96,
72
+ 50.3,
73
+ 177,
74
+ 62.7
75
+ ],
76
+ "origin": [
77
+ 96,
78
+ 60
79
+ ],
80
+ "size": 9,
81
+ "color": "#000000",
82
+ "font": "Helvetica",
83
+ "id": "e8"
84
+ },
85
+ {
86
+ "kind": "text",
87
+ "text": "4x Ø22",
88
+ "bbox": [
89
+ 214,
90
+ 43.4,
91
+ 239.8,
92
+ 54.4
93
+ ],
94
+ "origin": [
95
+ 214,
96
+ 52
97
+ ],
98
+ "size": 8,
99
+ "color": "#db2626",
100
+ "font": "Helvetica",
101
+ "id": "e9"
102
+ }
103
+ ],
104
+ "layers": []
105
+ }
106
+ ]
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.59.0",
3
+ "version": "0.60.0",
4
4
  "type": "module",
5
5
  "description": "Thin localhost host for floless.app — serves web/ and shells the aware CLI. No engine, no LLM.",
6
6
  "bin": {