@floless/app 0.62.0 → 0.64.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.62.0" : void 0,
53025
+ define: true ? "0.64.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.62.0" : void 0 });
53035
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.64.0" : void 0 });
53036
53036
  }
53037
53037
 
53038
53038
  // workflow-update.ts
@@ -64076,7 +64076,7 @@ async function startServer() {
64076
64076
  { bodyLimit: 25 * 1024 * 1024 },
64077
64077
  // contracts embed rasters (matches PUT /api/contract) — the editor POSTs the live contract on every 3D rebuild
64078
64078
  async (req, reply) => {
64079
- const doc = req.body && "contract" in req.body ? req.body.contract : readContract(req.params.appId);
64079
+ const doc = req.body && "contract" in req.body ? req.body.contract : readContractForApp(req.params.appId);
64080
64080
  if (doc == null) return reply.status(404).send({ ok: false, error: "no contract to render" });
64081
64081
  if (doc && typeof doc === "object" && doc.type === "drawing.vector/v1") {
64082
64082
  const cleaned = postProcess(doc);
@@ -43,6 +43,24 @@
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
44
  },
45
45
  "$defs": {
46
+ "grid": {
47
+ "type": ["object", "null"],
48
+ "required": ["origin"],
49
+ "additionalProperties": true,
50
+ "description": "Structural grid: X/Y are RELATIVE spacing strings (Tekla syntax — \"0 3*25' 20'-6\\\"\", n*d repeats a bay; tokens accept ft-in / inches / m / bare mm), Z is ABSOLUTE elevations. X entries space lines along +X (lines vertical on the plan, auto-labels 1,2,3…); Y entries march UP the sheet (lines horizontal, auto-labels A,B,C…). web/grid-core.js is the parser/geometry source of truth.",
51
+ "properties": {
52
+ "on": { "type": "boolean", "description": "Grid visible (2D + 3D). Default true." },
53
+ "origin": { "$ref": "#/$defs/point2", "description": "The [1,A] corner, display space (same space as member wp)." },
54
+ "x": { "type": "string", "description": "Relative spacings along +X, e.g. \"0 3*25'\"." },
55
+ "y": { "type": "string", "description": "Relative spacings up the sheet, e.g. \"0 4*30'\"." },
56
+ "z": { "type": "string", "description": "Absolute level elevations, e.g. \"0 16'-6\\\"\"." },
57
+ "labels_x": { "type": "string", "description": "Whitespace-separated label overrides for X lines; blank = auto 1,2,3…" },
58
+ "labels_y": { "type": "string", "description": "Whitespace-separated label overrides for Y lines; blank = auto A,B,C…" },
59
+ "ext": { "type": "number", "description": "How far lines run past the outermost line, in mm. Default 5'." },
60
+ "ends_x": { "type": "object", "description": "Per-line extent overrides for X lines: { \"<line index>\": [lo, hi] } display px along the line (a partial line like a 5.1 insert covering one wing). null / absent element = the auto full span. Set by dragging a bubble along its line." },
61
+ "ends_y": { "type": "object", "description": "Per-line extent overrides for Y lines — same shape as ends_x." }
62
+ }
63
+ },
46
64
  "filter": {
47
65
  "type": "object",
48
66
  "additionalProperties": true,
@@ -111,18 +129,21 @@
111
129
  },
112
130
  "elements": {
113
131
  "type": "array",
114
- "description": "Every drawing element: lines (with an SVG path `d`) and text spans (with `text`+`bbox`), each tagged layer/thickness/colour.",
132
+ "description": "Every drawing element: lines (with an SVG path `d`), text spans (with `text`+`bbox`) and circles (`cx`/`cy`/`r` — grid bubbles, callout balloons), each tagged layer/thickness/colour.",
115
133
  "items": {
116
134
  "type": "object",
117
135
  "additionalProperties": true,
118
136
  "properties": {
119
- "kind": { "enum": ["line", "text"] },
137
+ "kind": { "enum": ["line", "text", "circle"] },
120
138
  "layer": { "type": "string" },
121
139
  "w": { "type": "number" },
122
140
  "color": { "type": "string" },
123
141
  "d": { "type": "string", "description": "SVG path (line elements)." },
124
142
  "text": { "type": "string", "description": "Label text (text elements)." },
125
143
  "bbox": { "type": "array", "items": { "type": "number" }, "description": "[x0,y0,x1,y1] (text elements)." },
144
+ "cx": { "type": "number", "description": "Circle centre X (circle elements)." },
145
+ "cy": { "type": "number", "description": "Circle centre Y (circle elements)." },
146
+ "r": { "type": "number", "description": "Circle radius (circle elements)." },
126
147
  "dashed": { "type": "boolean" }
127
148
  }
128
149
  }
@@ -150,18 +171,21 @@
150
171
  "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." },
151
172
  "elements": {
152
173
  "type": "array",
153
- "description": "This sheet's drawing elements: lines (SVG path `d`) and text spans (`text`+`bbox`), each tagged layer/thickness/colour.",
174
+ "description": "This sheet's drawing elements: lines (SVG path `d`), text spans (`text`+`bbox`) and circles (`cx`/`cy`/`r` — grid bubbles feed the editor's grid-from-drawing detector), each tagged layer/thickness/colour.",
154
175
  "items": {
155
176
  "type": "object",
156
177
  "additionalProperties": true,
157
178
  "properties": {
158
- "kind": { "enum": ["line", "text"] },
179
+ "kind": { "enum": ["line", "text", "circle"] },
159
180
  "layer": { "type": "string" },
160
181
  "w": { "type": "number" },
161
182
  "color": { "type": "string" },
162
183
  "d": { "type": "string" },
163
184
  "text": { "type": "string" },
164
185
  "bbox": { "type": "array", "items": { "type": "number" } },
186
+ "cx": { "type": "number" },
187
+ "cy": { "type": "number" },
188
+ "r": { "type": "number" },
165
189
  "dashed": { "type": "boolean" }
166
190
  }
167
191
  }
@@ -236,7 +260,8 @@
236
260
  "tos_callouts": { "type": "array", "items": { "$ref": "#/$defs/tos_callout" } },
237
261
  "members": { "type": "array", "items": { "$ref": "#/$defs/member" } },
238
262
  "dims": { "type": "array", "items": { "$ref": "#/$defs/dim" }, "description": "Draft-only on-drawing dimensions (editor annotations). NOT baked into the lock / 3D / IFC / BOM." },
239
- "frame": { "$ref": "#/$defs/frame" }
263
+ "frame": { "$ref": "#/$defs/frame" },
264
+ "grid": { "$ref": "#/$defs/grid", "description": "Structural grid lines (Tekla-style). PER-PLAN — its origin/spacings live in this sheet's display space (like frame/dims). A plan REFERENCE the editor renders in 2D + 3D and snaps to — it never constrains members and is NOT baked into the lock / IFC / BOM." }
240
265
  }
241
266
  },
242
267
  "segment": {
@@ -2,7 +2,7 @@
2
2
  name: floless-app-steel-takeoff
3
3
  description: Read a structural steel drawing into an editable steel.takeoff/v1 contract in floless.app — the "Steel Takeoff" reader. Triggers on a pending floless `rebake` request for the `steel-model` app (queued from its "Re-read & re-bake ▸" button), or asks like "read this framing plan into a takeoff", "turn this structural drawing into a takeoff", "bake my drawing into steel-model", "extract members from this PDF". Teaches the host AI to READ a steel drawing (PDF/image) with its own vision at COMPOSE time, build a steel.takeoff/v1 contract (members + profiles + elevations + weights from AISC lookup), write it to the contract store, and hand control to the editor — no model in the run path, no API key.
4
4
  metadata:
5
- version: 0.3.0
5
+ version: 0.4.0
6
6
  ---
7
7
 
8
8
  # Steel Takeoff — read a drawing into a steel.takeoff/v1 contract
@@ -80,6 +80,11 @@ 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
+ 2b. **Circles** — a closed bezier path whose bbox is round (width ≈ height, ≥ ~8 pt across) becomes
84
+ `{ kind:"circle", cx:<centre x>, cy:<centre y>, r:<radius> }` in display space. These are the grid
85
+ bubbles and callout balloons; the editor's **grid-from-drawing** detector (`web/grid-core.js`
86
+ `gridFromDrawing`) anchors grid lines to them, so skipping circles silently disables the Grid
87
+ panel's "From drawing" button.
83
88
  3. **Per-sheet entry** — push `{ sheet:<number>, title:<name?>, page:{ w, h, bg_b64, rect }, steelBbox,
84
89
  elements:[…] }` onto `filter.sheets`. `bg_b64` is a dimmed raster of THIS sheet's framing area
85
90
  (**title block excluded** — CONFIDENTIAL, machine-local, never committed). **`page.rect` = the exact
@@ -96,6 +101,29 @@ native/display coords):
96
101
  5. Set `filter.mode: "layer"`, `filter.active: 0`. **PUT** the contract
97
102
  (`PUT /api/contract/steel-model`). On a re-read, preserve any existing per-facet `on` flags.
98
103
 
104
+ ### 1c. Grid lines — read the printed grid (quick, per sheet)
105
+
106
+ With the filter elements in hand, populate each plan's **`plan.grid`** (Tekla-style grid lines the
107
+ editor renders in 2D + 3D and snaps to). The geometry is deterministic — the editor's Grid panel has
108
+ a "From drawing" button running the same detector (`web/grid-core.js` `gridFromDrawing`: grid lines
109
+ are the long dash-dot line fields that AIM at a bubble circle's centre and STOP at its edge; bubbles
110
+ line up in a row along one sheet edge and columns along the sides). What the detector usually can't
111
+ get is the **labels** — CAD sets outline their text, so bubble marks like `5.1` / `K.1` aren't in the
112
+ text layer. You CAN read them: you already have vision on the sheet. So:
113
+
114
+ 1. Derive the line positions (or let the user press "From drawing" later — same result).
115
+ 2. **Vision-read each bubble's mark** from the sheet render, in line order.
116
+ 3. Write `plan.grid = { on:true, origin:[x,y], x:"0 3*25' …", y:"…", z:"0", labels_x:"K J H …",
117
+ labels_y:"0.5 1 1.1 …", ext:<mm> }` — origin = the left-most × bottom-most line crossing,
118
+ spacings are RELATIVE ft-in tokens (`n*d` repeats a bay), labels are whitespace-separated
119
+ overrides in line order (X: left→right; Y: bottom→up). Sanity-check a bay or two against the
120
+ printed dimension strings between grid lines — a mismatch means the sheet scale (`pt_per_ft`) is
121
+ wrong, which corrupts the whole takeoff, not just the grid. **Then PUT the contract again** —
122
+ the 1b PUT already happened, and the user's next Filter-node Save writes back the contract *it
123
+ loaded*; an un-PUT grid (and its vision-read labels) would be silently dropped.
124
+
125
+ Skip silently only when the sheet genuinely has no grid bubbles (rare for framing plans); say so.
126
+
99
127
  **Then STOP and hand off — checkpoint before the member read.** Tell the user to open the **Filter**
100
128
  node, flip through the sheets, narrow to the steel, and **Save**. The saved `on` flags + `mode` are
101
129
  what step 2 reads — **per sheet** (each sheet's elements passing `mode` + the global `on` flags). Only
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: floless-app-vectorize
3
- description: Vectorize a drawing into an editable drawing.vector/v1 contract in floless.app — the "Vectorize" reader. Triggers on a pending floless `rebake` request for the `vectorize` app (queued from its "Re-read & re-bake ▸" button), or asks like "vectorize this PDF", "turn my sketch into clean CAD linework", "read this detail into vectors", "trace this photo". Teaches the host AI to read a drawing at COMPOSE time — a vector PDF via ONE deterministic PyMuPDF pass (no LLM), a scan/photo/hand sketch via AWARE's fenced vision.extract (schema-bound, cached) — emit the drawing.vector/v1 contract, PUT it to the contract store, and hand the 2D vector editor to the user.
3
+ description: Vectorize a drawing into an editable drawing.vector/v1 contract in floless.app — the "Vectorize" reader. Triggers on a pending floless `rebake` request for the `vectorize` app (queued from its "Re-read & re-bake ▸" button), or asks like "vectorize this PDF", "turn my sketch into clean CAD linework", "read this detail into vectors", "trace this photo", "make this drawing 3D". Teaches the host AI to read a drawing at COMPOSE time — a vector PDF via ONE deterministic PyMuPDF pass (no LLM), a scan/photo/hand sketch via AWARE's fenced vision.extract (schema-bound, cached) — emit the drawing.vector/v1 contract, PUT it to the contract store, and hand the 2D vector editor to the user; and, for a multi-view set (plan + elevation), to classify views, read datums, and declare entities so the server reconstructs a viewable 3D model.
4
4
  metadata:
5
- version: 0.3.0
5
+ version: 0.4.0
6
6
  ---
7
7
 
8
8
  # Vectorize — read a drawing into a drawing.vector/v1 contract
@@ -26,8 +26,14 @@ The drawing is read **at compose time**, by one of two paths:
26
26
  There is **no model in the run path** either way. The app ships with a tiny hard-coded
27
27
  example; this skill is how the user's OWN drawing replaces it.
28
28
 
29
- **Later slices (mention as "next", do not attempt here):** multi-view 3D reconstruction,
30
- DXF export.
29
+ A **multi-view set** (a plan plus an elevation/section of the same object) can additionally be
30
+ **reconstructed in 3D**: the skill's multi-view pass (step 3c) adds view classification, datums
31
+ and entity declarations to the contract, and the server's deterministic reconstruction renders
32
+ it in the **View 3D** viewer — members and extruded bodies, with everything unplaceable listed
33
+ honestly with a reason.
34
+
35
+ **Later slices (mention as "next", do not attempt here):** DXF export, placing details into a
36
+ live model.
31
37
 
32
38
  > **Pasted a request?** If the user pastes a message beginning with a `[floless-request
33
39
  > type=rebake id=…]` marker, that's a request copied from the FloLess Dashboard — resolve it
@@ -119,6 +125,50 @@ For each raster image (or rasterized PDF page exported to an image):
119
125
  the vision output matches what the drawing actually says. A wildly wrong trace usually means
120
126
  the image is too low-contrast — say so rather than shipping garbage.
121
127
 
128
+ ### 3c. Multi-view pass — make a plan + elevation set 3D (optional)
129
+
130
+ Run this ONLY when the set contains **multiple views of one object** and the user wants 3D.
131
+ Prerequisite discipline: `reading-structural-drawings` — **massing + coordinate frame FIRST**,
132
+ entities last, cross-validate every view. All fields below are additive on the contract the
133
+ 2D pass (3a/3b) already produced; the server's reconstruction (`views-to-scene`) is
134
+ deterministic and **refuses to guess** — anything you leave unresolved lands in the 3D view's
135
+ "Not shown in 3D" list with a reason, never at a made-up position.
136
+
137
+ 1. **Classify every sheet** — set `sheets[].view`:
138
+ `{ kind: 'plan'|'elevation'|'section'|'detail', dir, datum }`. `dir` is the observer look
139
+ direction in world axes (a plan looks `-z`; a south elevation looks `+y`; a west-looking
140
+ section `+x`). **Cross-validate the scale of each axis pair with one known dimension**
141
+ (a labeled length that appears in both views) before trusting it — a wrong `dir` or scale
142
+ silently mirrors or misplaces the model. **Show the user the classification and get an OK**
143
+ ("s1 = plan at 1:50, s2 = south elevation — reconstruct from these?") before writing 3D
144
+ fields — this is the same confirm gate the steel reader uses before its heavy pass.
145
+ 2. **One world frame.** Give every sheet a `transform` (`scale` = world units per display unit,
146
+ `origin` = the display point that is world [0,0], `rotation` if the frame is turned) chosen
147
+ so the views AGREE: pick one shared reference (a grid intersection, a corner that appears in
148
+ both views) as the common origin. World units are the contract's `units` (mm canonical).
149
+ 3. **Datums** — read the height ladder off the elevation (level lines + labels) into the
150
+ top-level `datums: [{ id:'lv1', kind:'level', axis:'z', value:<world units>, label, sheet }]`.
151
+ Grid lines go in the same array with `axis:'x'|'y'`. Only `z` datums can drive heights.
152
+ 4. **Entities** — group the linework into `sheets[].groups`:
153
+ - a **member** (beam/bar/stud seen along its axis): `axis: [[x0,y0],[x1,y1]]` (this sheet's
154
+ display coords) + `section: { w, d }` in WORLD units — from a callout or a dimension, never
155
+ eyeballed;
156
+ - an **extruded body** (plate/wall/slab footprint): `footprint: [[x,y]…]` +
157
+ `extrude: { from, to }` where each end is a **z-datum id** or a world number. A present
158
+ extrude that doesn't resolve is an ERROR (surfaced, not guessed) — omit `extrude` entirely
159
+ if the height must come from the elevation instead;
160
+ - annotation groups (labels, dims) get neither — they simply don't build.
161
+ 5. **Links** — where a section mark or the drawing itself says two groups are the same object in
162
+ two views, add `links: [{ kind:'same-entity', a:{sheet,id}, b:{sheet,id} }]` (ids are only
163
+ sheet-unique — always the composite). A linked elevation entity gives its plan twin the
164
+ Z extent directly; unlinked entities are matched by their shared horizontal span, and
165
+ **anything ambiguous (two elevation entities over the same span) is skipped until you link it**.
166
+ 6. **PUT** the contract (step 4) and **open View 3D ▸** on the read node. Work the
167
+ "Not shown in 3D" panel like a worklist: each row says exactly what is missing (a datum, a
168
+ link, an extrude); fix the contract, re-PUT, reopen. Reconstructed members export through the
169
+ existing 3D/IFC path later; extruded bodies render in FloLess's own viewer only for now —
170
+ say so if the user asks about IFC.
171
+
122
172
  ### 4. Write to the contract store
123
173
 
124
174
  ```
@@ -147,6 +197,9 @@ Tell the user:
147
197
  - **Approve & bake lock**: when satisfied, the user clicks Approve in the editor header — it
148
198
  bakes the (edited) contract into the `read` node's `config.takeoff` and recompiles, arming
149
199
  the Run gate. Never auto-approve on their behalf.
200
+ - **View 3D ▸** (after a multi-view pass, step 3c): the node action opens the read-only
201
+ reconstruction viewer — orbit/pan/zoom, solid/wire/x-ray, per-group toggles, and the
202
+ "Not shown in 3D" panel listing every entity that couldn't be placed, with the reason.
150
203
 
151
204
  ### 6. Clear the request
152
205
 
package/dist/web/aware.js CHANGED
@@ -1360,6 +1360,11 @@
1360
1360
  // openContractEditor() returns false and the click would silently do nothing.
1361
1361
  if (contractType && window.CONTRACT_RENDERERS && window.CONTRACT_RENDERERS[contractType]) {
1362
1362
  addNodeAction(card, 'Edit contract ▸', () => openContractEditor(currentId, contractType));
1363
+ // A renderer with a LOCAL 3D viewer (the drawing.vector reconstruction) gets View 3D via
1364
+ // that page — distinct from the steel `scene` chain below (AWARE exporter companions).
1365
+ if (window.CONTRACT_RENDERERS[contractType].viewerUrl) {
1366
+ addNodeAction(card, 'View 3D ▸', () => openLocalViewer(currentId, contractType));
1367
+ }
1363
1368
  // The 3D/IFC/BOM/Tekla chain derives a scene from the contract — only contract types that
1364
1369
  // declare `scene` support it (a drawing.vector node would 4xx on these endpoints).
1365
1370
  if (window.CONTRACT_RENDERERS[contractType].scene) {
@@ -1770,6 +1775,21 @@
1770
1775
  $contractEditor.hidden = false;
1771
1776
  return true;
1772
1777
  }
1778
+ // A renderer-declared LOCAL viewer page (e.g. the drawing.vector 3D reconstruction) — reuses the
1779
+ // contract-editor overlay/iframe. Read-only: no Approve (the editor owns that gate).
1780
+ function openLocalViewer(appId, type) {
1781
+ const r = window.CONTRACT_RENDERERS && window.CONTRACT_RENDERERS[type];
1782
+ if (!r || !r.viewerUrl) return false;
1783
+ const ap = document.getElementById('contract-editor-approve'); if (ap) ap.hidden = true;
1784
+ $contractEditorTitle.replaceChildren(
1785
+ Object.assign(document.createElement('span'), { textContent: appId, style: 'color:var(--text);font-weight:600' }),
1786
+ Object.assign(document.createElement('span'), { textContent: ' · 3D view', style: 'color:var(--text-muted)' }),
1787
+ );
1788
+ $contractEditorFrame.onload = () => { try { $contractEditorFrame.contentWindow.focus(); } catch (_) {} };
1789
+ $contractEditorFrame.src = r.viewerUrl(appId);
1790
+ $contractEditor.hidden = false;
1791
+ return true;
1792
+ }
1773
1793
  // The steel filter pre-stage view — reuses the contract-editor overlay/iframe to load the served
1774
1794
  // steel-filter.html (layer/thickness/colour facets + eyedropper). Same seam as the editor: the
1775
1795
  // iframe talks to /api/contract directly and Save writes contract.filter.