Drawing Viewer — Layer Classification & Coverage¶
| Status | Shipped |
|---|---|
| Linear | PIN-365 ↗ PIN-146 ↗ |
| Module | Progress Drawings ↗ |
Every DWG that lands in Pinley is drawn by a different engineering firm, and each firm names its layers its own way (
H-DUCT,SM-SA-LP,ACME-M-SUPPLY,|m-piping|…). Before the Drawing Viewer can colour, filter, or quantify anything by trade, every layer has to be mapped to a discipline. That mapping is the job of the classification rules engine. The two admin tabs are the two sides of that engine: Layer Classification is where you configure the mapping, and Classification Coverage is where you monitor how well it is working — and where you spot the firms whose drawings need new rules.
Concepts (read this first)¶
A few terms recur throughout both tabs:
| Term | What it means |
|---|---|
| Discipline | The trade a layer belongs to. Canonical codes: M (mechanical / HVAC), P (plumbing, incl. fire sprinkler), E (electrical), S (structural), A (architectural), FP (fire protection), MISC (unclassified). |
| Segment | Optional sub-type for HVAC air: SUPPLY, RETURN, EXHAUST, OUTSIDE, TRANSFER. Only meaningful on M layers. |
| Rule | One row mapping a layer-name regex → a discipline (+ optional segment). Carries a scope, a sort order, and an enabled flag. |
| Scope | Where a rule applies: Global (everywhere) → Provider / firm (all projects tied to one drawing-file provider) → Project (one project only). |
| MISC / FALLBACK | When no rule matches a layer, it falls back to MISC. A high share of MISC layers = low coverage. |
| Firm | In the admin copy, "firm" = a DrawingFileProvider. There is always an (unaffiliated) bucket for drawings whose project has no provider. |
How a winner is chosen for any layer name (same Python classifier.py the parse worker uses):
- Most specific scope wins — Project override beats Firm/Provider beats Global.
- Within a scope, lowest
sort_orderwins (1 = first, 100 = default baseline). - Patterns match by
re.search— the pattern can match a substring anywhere in the layer name. - If nothing matches → MISC (FALLBACK).
Rules are append-only: editing a rule never mutates the row — it writes a new version and marks the old one superseded (the old row stays on disk for audit).
Prerequisites¶
- Role: logged in as Admin (the two pages live under the sidebar ADMIN & FINANCE section; gated for Admin / Finance / Executive).
- Data: at least a handful of drawings ingested to READY status. The Coverage dashboard shows "No drawings ingested yet" until drawings reach READY. The Rules admin works with the seeded ruleset even before any drawing exists.
- A provider with drawings if you want the per-firm coverage rows to be non-empty (otherwise everything lands in the
(unaffiliated)bucket). - Permissions cached? If you logged in months ago, force a relogin to refresh
accountpermissionsin localStorage before testing.
1. Open the Layer Classification admin¶
Sidebar → ADMIN & FINANCE → Layer Classification (route /admin/layer-classification).
The page has three columns:
- Left — Providers (firms). Lists every drawing-file provider. Selecting one filters the rules table to that firm's scope. Selecting Global shows only rules with no provider/project scope.
- Centre — Rules table. All rules in the selected scope, with the pattern, discipline, segment, sort order, and enabled state. Toggle Include disabled to see deactivated rows; Pending drafts shows the misclassification-feedback queue (see §5).
- Right — Preview classification. Paste any layer name → Run → see which rule wins in the current scope.

2. Preview how a layer classifies¶
In the right-hand Preview classification panel, type a layer name (e.g. sm-salpduct, M-HVAC-DUCT-1, or CITY-Duct-Supply) and press Run. The result grid shows the winning Discipline, Segment, Source (RULE / FALLBACK / MANUAL_OVERRIDE), and the Matched rule id + version.
This uses the same classifier.py engine as the real parse worker, so the preview is faithful — it is the fastest way to check whether a layer is covered before touching any drawing.
3. Add a new rule¶
When a new firm's layer naming shows up unclassified, add a rule. In the Rules table click Add rule (the new rule is created in the currently-selected scope — pick the firm on the left first, or leave Global for an everywhere rule). Fill the modal:
- Pattern (Python regex) — e.g.
(?i)(?:^|[-_|\s])ducts?(?:[-_]|$|\s). Use(?i)for case-insensitive. - Discipline — the target trade.
- Segment (optional) — only for HVAC air layers.
- Sort order —
1wins first,100is the default baseline. Lower a rule's sort order to make it win over a broader rule in the same scope.

Discipline vocabulary caveat
The standalone New classification rule modal currently offers the legacy long-form discipline names (HVAC / PLUMBING / ELEC / STRUCT / ARCH / MISC) instead of the canonical codes (M / P / E / S / A / FP / MISC) used everywhere else. Rules authored with the long-form names will not roll up into the canonical discipline buckets on the Coverage dashboard or the viewer's colour map. The reliable path that always uses canonical codes is promoting an override from the Coverage dashboard (§6). This is known pre-existing tech debt — tracked as a follow-on (see Known issues).
After saving, the new rule is live for future parses immediately, but existing drawings keep their old classification until you re-run the engine (§7).
4. Open the Classification Coverage dashboard¶
Sidebar → ADMIN & FINANCE → Classification Coverage (route /admin/layer-coverage), or the Coverage dashboard → link in the Rules-admin header.
The dashboard answers "how well is classification working, per firm?":
- Global summary at the top — total drawings, total layers, and overall MISC %.
- One row per firm, showing the active rule count, drawing count, MISC %, override %, and last-upload date.
- Expand a firm to see the discipline breakdown (a stacked bar across M / P / E / S / A / FP / MISC with a legend) and the Top overridden layer names table.
The two health metrics and their colour thresholds:
| Metric | Meaning | Green | Amber | Red |
|---|---|---|---|---|
| MISC % | Share of layers that matched no rule (the coverage gap) | < 15 % | 15–30 % | ≥ 30 % |
| Override % | Share of layers a user manually corrected (rules lagging reality) | < 3 % | 3–10 % | ≥ 10 % |

5. The misclassification feedback queue¶
There are three ways a layer's classification gets corrected, and they all feed back into the rules library:
- A PM/admin flags a layer as misclassified directly in the viewer's LayerPanel → it lands in the Rules-admin Pending drafts queue as a disabled draft rule the admin reviews and promotes.
- A user overrides a layer's discipline in the viewer → it surfaces in the Coverage dashboard's Top overridden table (§6).
- An admin adds a rule from scratch (§3).
6. Promote a frequent correction into a rule¶
This is the core loop. In an expanded firm row, the Top overridden layer names table is effectively a rule-authoring backlog — the layer names users keep correcting, with the most common FROM → TO direction. Click → Rule on a row to open Promote override to rule:
- The layer name and the
FROM → TOcorrection are shown at the top. - Pattern — pick Exact (this layer only), Same prefix + keyword (recommended), Keyword only (broad), or Custom.
- Discipline / Segment / Sort order — pre-filled from the correction (canonical codes here).
- Scope — Global or the specific Provider.
- Inline preview — test the candidate pattern against a layer name before saving.
- Reclassify after save — leave checked to immediately re-run the engine over the affected scope.
Click Create rule. The correction is now a permanent rule, and (if the checkbox was on) the affected drawings reclassify in the background.

7. Reclassify drawings after a rules change¶
Rule changes do not retroactively touch existing drawings — you must re-run the engine. Both admin pages expose:
- Reclassify all drawings — re-runs the classifier across every drawing.
- Reclassify this firm's drawings — scoped to the selected/expanded provider.
Reclassification only re-applies layer classification — it does not regenerate scenes (the rendered geometry is untouched). You get a toast with the enqueued count and a job id; the dashboard metrics refresh shortly after.
8. How an admin keeps coverage high (the short version)¶
The whole workflow is a watch → act → re-run loop:
- Watch the Coverage dashboard. A firm with a high (amber/red) MISC % is usually a new engineer whose layer naming the rules don't catch yet. A high override % means users are repeatedly hand-correcting the same layers.
- Act on whichever signal fired:
- High MISC % → layers aren't matching any rule → add patterns (§3), ideally scoped to that firm.
- High override % / a populated Top overridden table → the corrections are already telling you the right answer → one-click → Rule to codify them (§6).
- Anything in the Pending drafts queue → review and promote (§5).
- Re-run the classifier for that firm (§7). MISC % and override % drop, and coverage recovers.
Verification Checklist¶
- Both pages load under ADMIN & FINANCE for an Admin user; a non-Admin without
GetUsersdoes not see them. - Preview classification returns a discipline + matched rule for a known layer name (e.g.
sm-salpduct→ M / SUPPLY) and MISC / FALLBACK for a nonsense name. - Add rule creates a row visible in the current scope; the preview reflects the new rule.
- Coverage dashboard shows per-firm rows with MISC % / override % chips colour-coded by threshold.
- Expanding a firm renders the discipline stacked bar + legend and the Top overridden table.
- → Rule opens the promote modal pre-filled with the
FROM → TOcorrection; Create rule adds the rule and (with the checkbox) enqueues reclassification. - After Reclassify this firm's drawings, the firm's MISC % decreases on refresh.
Known Issues / Current Gaps¶
The standalone New classification rule modal (
RuleFormModal) offers legacy long-form discipline names (HVAC/PLUMBING/ELEC/STRUCT/ARCH) instead of the canonicalM/P/E/S/A/FP/MISCtaxonomy. Rules created this way don't roll up into the canonical buckets. Author canonical rules via the Coverage → Rule promote flow until this is unified — pre-existing tech debt surfaced in PIN-365.The Coverage discipline legend includes FP, but the seeded ruleset folds fire sprinkler into P, so the FP bucket is effectively always empty today.
Coverage drilldown is firm-level (expand a row); there is no per-drawing coverage route yet.
Related docs¶
- Takeoff — the other Drawing/PDF-driven walkthrough.
- Drawing Viewer (technical spec) — engine internals, scene pipeline, and the §7 classification design this UI sits on.
Updating this walkthrough
Run the full flow yourself end-to-end. If it passes, bump last_verified: in the frontmatter to today's date. If any step fails, fix the doc or the code in the same PR — drift is the enemy.