You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The heuristics exist to recover information the file already states
classifyHatchSegs in web/src/lib/oneclick.ts is a wall of tuned constants — HATCH_MIN_RUN, pitch-regularity bands, SPAN_PROTECT_RATIO, the filled-not-stroked poché exemption added after the VA plan drew its walls as solid shapes — and its entire job is to guess which segments are boundary and which are pattern.
A construction document exported from Revit, AutoCAD, or Archicad usually says so outright. PDF Optional Content Groups carry the CAD layer structure through the export, most often on the AIA CAD Layer Guidelines convention:
OpenTakeoff reads none of it. grep -r optionalContent web/src mcp/src returns zero hits. Every path is flattened into one undifferentiated 1-bit mask and then partially un-flattened by heuristics.
Structure can't be excluded. Columns inside a room are on S-COLS, not the floor layer, but the mask can't tell.
Demolition linework is indistinguishable from new work.A-WALL-DEMO traced as a boundary is a wrong number produced confidently.
The work
1. Capture the OCG id during extraction. pdf.js exposes layer membership through marked-content operators in the operator list (beginMarkedContentProps carrying an OCG ref) and the id→name/visibility mapping through page.getOptionalContentConfig(). extractVectorGeometry already walks the operator list for constructPath; it emits a layerId alongside the existing meta bits and nothing else changes. The marked-content stack nests — this is a small state machine, not a lookup.
2. A layer-name normalizer. New pure module, web/src/lib/layers.ts: raw layer name → { role, confidence } where role is boundary | finish-pattern | annotation | structure | demolition | unknown. The AIA table is the easy half. The real work is the field:
xref-prefixed names (ARCH-FLOOR|A-WALL-FULL)
vendor dialects (Bentley, Archicad, Vectorworks)
truncated and renumbered variants (A-WALL-1, WALL, A-WALL FULL HT)
the degenerate case where everything sits on layer 0
non-English layer names
This module is where the RFC actually lives. It's self-contained, pure, table-driven, and testable without any geometry background.
3. buildMask consumes roles instead of guessing. When a page has usable layers: boundary roles go in hard, pattern/annotation/demolition are excluded outright, structure becomes a subtractive hint, and classifyHatchSegs runs only over unknown. When a page has no layers, today's path runs completely untouched. This is a short-circuit, never a replacement — a regression on unlayered sheets is a failed PR.
4. Surface it.sheet_info (MCP) returns the layer table. one_click and detect_rooms accept an optional layers: { include?, exclude? }. The canvas gets a layer visibility panel — an estimator toggling A-FLOR-PATT off to see the room boundary underneath is worth shipping on its own merit, independent of the engine work.
5. Provenance. A trace bounded by declared layers is categorically stronger evidence than one bounded by a pitch heuristic. layer_bounded: true rides the contribution.v2 wire and feeds the confidence field #60 calls for.
Known hard parts
pdf.js's OCG plumbing is thinly documented and the marked-content stack nests.
Visibility state matters: a path can be present-but-hidden in the default config. Honor it or you will trace demolition.
Non-conforming layer names are the majority of real sets, not the exception.
Some exporters flatten layers entirely. The fallback must be invisible, not a degraded mode with a warning.
Finish line
Against the benchmark corpus (#60): layered fixtures show a measurable IoU floor lift and a refusal-rate drop; unlayered fixtures show exactly zero delta. Both numbers reported in the PR, per fixture.
Explicitly out of scope
Native DWG and IFC ingestion. Both need non-JS dependencies and break the client-only architecture. That's a real lane and a separate decision — it is not this issue, and PRs that start there will be redirected.
Constraints
oneclick.ts and layers.ts stay pure — no DOM, no React. The MCP server runs the identical engine headlessly. Refusal over guessing. New failure modes ship with named regression fixtures.
The heuristics exist to recover information the file already states
classifyHatchSegsinweb/src/lib/oneclick.tsis a wall of tuned constants —HATCH_MIN_RUN, pitch-regularity bands,SPAN_PROTECT_RATIO, the filled-not-stroked poché exemption added after the VA plan drew its walls as solid shapes — and its entire job is to guess which segments are boundary and which are pattern.A construction document exported from Revit, AutoCAD, or Archicad usually says so outright. PDF Optional Content Groups carry the CAD layer structure through the export, most often on the AIA CAD Layer Guidelines convention:
OpenTakeoff reads none of it.
grep -r optionalContent web/src mcp/srcreturns zero hits. Every path is flattened into one undifferentiated 1-bit mask and then partially un-flattened by heuristics.Where that costs us
A-FLOR-PATTis the answer, stated by the file, requiring no inference.S-COLS, not the floor layer, but the mask can't tell.A-WALL-DEMOtraced as a boundary is a wrong number produced confidently.The work
1. Capture the OCG id during extraction. pdf.js exposes layer membership through marked-content operators in the operator list (
beginMarkedContentPropscarrying an OCG ref) and the id→name/visibility mapping throughpage.getOptionalContentConfig().extractVectorGeometryalready walks the operator list forconstructPath; it emits alayerIdalongside the existing meta bits and nothing else changes. The marked-content stack nests — this is a small state machine, not a lookup.2. A layer-name normalizer. New pure module,
web/src/lib/layers.ts: raw layer name →{ role, confidence }where role isboundary | finish-pattern | annotation | structure | demolition | unknown. The AIA table is the easy half. The real work is the field:ARCH-FLOOR|A-WALL-FULL)A-WALL-1,WALL,A-WALL FULL HT)0This module is where the RFC actually lives. It's self-contained, pure, table-driven, and testable without any geometry background.
3.
buildMaskconsumes roles instead of guessing. When a page has usable layers: boundary roles go in hard, pattern/annotation/demolition are excluded outright, structure becomes a subtractive hint, andclassifyHatchSegsruns only overunknown. When a page has no layers, today's path runs completely untouched. This is a short-circuit, never a replacement — a regression on unlayered sheets is a failed PR.4. Surface it.
sheet_info(MCP) returns the layer table.one_clickanddetect_roomsaccept an optionallayers: { include?, exclude? }. The canvas gets a layer visibility panel — an estimator togglingA-FLOR-PATToff to see the room boundary underneath is worth shipping on its own merit, independent of the engine work.5. Provenance. A trace bounded by declared layers is categorically stronger evidence than one bounded by a pitch heuristic.
layer_bounded: truerides the contribution.v2 wire and feeds the confidence field #60 calls for.Known hard parts
Finish line
Against the benchmark corpus (#60): layered fixtures show a measurable IoU floor lift and a refusal-rate drop; unlayered fixtures show exactly zero delta. Both numbers reported in the PR, per fixture.
Explicitly out of scope
Native DWG and IFC ingestion. Both need non-JS dependencies and break the client-only architecture. That's a real lane and a separate decision — it is not this issue, and PRs that start there will be redirected.
Constraints
oneclick.tsandlayers.tsstay pure — no DOM, no React. The MCP server runs the identical engine headlessly. Refusal over guessing. New failure modes ship with named regression fixtures.