symbol_sweep
Finds every instance of a repeated plan symbol from one example by matching vector geometry with rotation and mirroring options, counting matches per sheet or entire set while disclosing near-misses and exclusions.
Instructions
Find EVERY instance of a repeated plan symbol from ONE example — drains, thresholds, fixtures, transition markers: marquee a tight seed_rect around a single instance and the vector linework is searched for every other placement of that same segment cluster. Deterministic geometry, not vision: each placement scores as the length-weighted fraction of the seed's segments reproduced within tolerance_px, under translation plus 0/90/180/270 rotation and mirroring (symbols rotate on plans — both ON by default; turn them off to pin orientation). Score ≥ 0.92 is a match; the 0.75–0.92 band comes back in withheld with a reason — a near-match is a question you answer by LOOKING (view_sheet at its at), never a silent commit and never a silent drop. The seed's own location is reported in seed and never double-committed. Every proposed placement is scored up to a hard work ceiling sized for pathological sheets, and the reply says which it was: complete true means the count is a total; complete false (with candidates.dropped > 0) means the count is a FLOOR — some placements were never scored — so tighten the seed rect around more distinctive geometry rather than trusting it as a total. Marquee discipline: the rect must hug ONE instance — only segments FULLY inside it define the symbol, so a loose rect that swallows wall linework fingerprints the wall, not the symbol. scope "set" sweeps the WHOLE working set, counting on PLAN-role sheets only (the sheet graph decides): a symbol drawn in a detail, legend, or schedule is a reference drawing and never counts itself — which is also how you seed from one: marquee the assembly on the detail sheet and its plan-sheet occurrences are counted while the detail stays excluded (the exclusion disclosed in skipped, per-sheet results with per-sheet caps and wall-clock in sheets). Scale across sheets: the fingerprint is size-true and is never scale-SEARCHED, so a detail drawn at 1-1/2" = 1'-0" is 12× the size of the same mark on a 1/8" plan — when BOTH sheets have a scale set, the exact ratio is computed from them and the seed is resized before matching (reported per sheet as scaled); when a scale is missing, the sweep runs at 1:1 and SAYS so (scale_assumed), because an unknown ratio plus a zero count is not evidence of absence. Seeding from a detail/legend/schedule sheet REFUSES outright until both scales are set — that is the case where an unstated ratio silently finds nothing. commit: true (requires condition) commits every match center as an EA count marker through the same path as place_count — the whole sweep (set-wide included) is ONE undo step, each marker carries origin.method "symbol_sweep" with its score, transform, and seed source, and withheld placements are NEVER committed. The SEED instance is not in that count (#296) — in sheet scope it is almost always installed work, so pass commit_seed: true to mint it into the same batch (the reply reminds you whenever a sheet-scope commit leaves it out; ea_total one short of the hand tally is exactly this). The COUNT is scale-free (EA), but matching across sheets of different scales is not — set_scale on the sheets involved is what turns the ratio from an assumption into arithmetic. Counter-examples (#259): drafting reuses one generic shape for different devices — a wall-mounted data outlet drawn as a plain triangle, the flush-floor variant the SAME triangle inside a square, keynote callouts a triangle with a letter in it — so the seed legitimately matches things you do not mean, and seeding more geometry only works where the drawing offers more to capture. exclude takes rects around instances you do NOT mean, marqueed exactly like the seed. You never choose a mechanism; the rect's contents decide, because both are the same gesture: a rect holding EXTRA linework beyond the seed rejects placements where that extra linework is present too (the box, the letter), and a rect holding no extra linework of its own is read as the line running THROUGH it — a bare ceiling-grid tile whose grid line a real fixture, drawn over it, would BREAK. That second mechanic is not expressible as a seed: only segments fully INSIDE a rect define a symbol, and background structure is long by nature. Every rejection is disclosed in rejected[] — which negative, what fraction of its evidence was found, and the placement — and NEVER counted in found: an exclusion is a judgement, so look at it and reinstate any you disagree with using place_count at its at. A counter-example that holds no instance of the seed, or holds the seed with nothing extra, is REFUSED rather than silently doing nothing. Stroke luminance (#260): a flattened export strips the layer tree and flattens every pen, but the file still STATES stroke color — a black fixture outline over a grey ceiling grid is unambiguous there even when the geometry is identical (two empty 2 ft grid tiles reproduce a 2×4 fixture's outline exactly). luminance_tolerance (0–254) gates on it: a sheet segment only answers for a seed segment when their stroke luminances are within the stated tolerance (Rec. 709, 0 = black, 255 = white; 32–64 separates black from grey without touching anti-aliasing wobble). OPT-IN and disclosed, in the spirit of tolerance_px — omitted, sweeps score exactly as before; stated, the reply's lum_gate says the seed's own luminance band and names every placement the geometry would have committed and the pen did not, so you can LOOK at what a stated gate cost. Prefer geometry (a counter-example, a tighter seed) where the drawing offers it — color is the fallback for exports where nothing else survived. Labels (#308): for a LABELED family — fixtures, tagged equipment, keyed devices — the drawing already names every instance, and the sweep reads those names: a fixture token written beside a placement, or connected to it by a drawn leader line (leader-following arms only on multi-pen sheets, where the annotation pen separates from the work), comes back as label + label_via on the row, and the seed's own tag rides seed.label. Disclosure in both directions, never a recount: a committed match with NO label while the family is labeled was counted on shape alone (measured case: two 0.97 matches that were valve internals, not drains — LOOK at those first), a withheld row carrying the seed's own tag is the drawing vouching for a near-miss (look, then place_count), and a withheld row named a DIFFERENT tag is a sibling fixture answered, not a missed count. After any batch commit, LOOK at what landed — view_sheet {overlay: true} over the swept area — and audit the markers against the drawing before trusting the EA total. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| scope | No | "sheet" = this sheet only; "set" = every PLAN-role sheet in the working set (needs a text layer for the sheet graph; non-plan sheets are excluded and disclosed) | sheet |
| sheet | Yes | The sheet the seed rect sits on — in scope 'set' it may be ANY sheet (a detail/legend seed sheet is fingerprint source only, never counted) | |
| commit | No | Commit every MATCH center as one EA count marker (withheld placements never commit) | |
| mirror | No | Also match mirrored placements | |
| exclude | No | Counter-examples: rects around instances you do NOT mean, same gesture as seed_rect — 'count the triangles, not the keynote ones'. Marquee the LOOKALIKE ITSELF (the flush-floor variant with its box, the keynote triangle with its letter) or an EMPTY position whose background line a real instance would break (a bare ceiling grid tile). You never say which kind it is: the rect's own contents decide. Every rejection comes back in rejected[] with which negative did it and what it saw | |
| condition | No | Finish tag to commit match markers under (minted on first use), e.g. 'FD-1'. Required when commit is true | |
| rotations | No | Also match 90/180/270-rotated placements | |
| seed_rect | Yes | Marquee around ONE example instance, [[x0,y0],[x1,y1]] in image px — tight: segments fully inside define the symbol | |
| commit_seed | No | Sheet scope + commit only (#296): also commit the SEED instance — in sheet scope the seed is almost always installed work, and a count that excludes it bids one short. Joins the same one-undo-step batch, origin score 1. Refused in set scope, where a detail/legend seed is a reference drawing | |
| tolerance_px | No | Endpoint match tolerance in image px (default 2 — CAD jitter, not drift) | |
| luminance_tolerance | No | Stroke-luminance gate, 0–254 (#260): a sheet segment only answers for a seed segment when their stroke luminances (Rec. 709, 0 black – 255 white) are within this. For flattened exports where a black device and its grey background twin are geometrically identical — 32–64 separates black from grey. Omit to score on geometry alone; stated, the reply's lum_gate discloses the seed's luminance band and every placement the gate pulled under the commit bar |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| note | No | ||
| seed | Yes | ||
| found | Yes | Placements that cleared the commit bar — across every swept sheet in set scope | |
| scope | Yes | "sheet" = the swept sheet alone (matches/withheld/candidates at top level); "set" = every PLAN-role sheet in the working set (per-sheet results in sheets[], exclusions in skipped[]) | |
| sheets | No | Set scope only: one entry per swept PLAN-role sheet, load order | |
| matches | No | Sheet scope only. Deterministic reading order (y, then x). The seed's own location is never listed here | |
| skipped | No | Set scope only: every sheet excluded from counting, with role and reason — including the seed's own sheet when it is not a plan | |
| warning | No | Present when the work cap dropped candidates — what a tighter seed rect would recover | |
| complete | Yes | True when every proposed placement was scored (every swept sheet, in set scope) and the count is a total. FALSE MEANS THE COUNT IS A FLOOR — acknowledge it before trusting found (#261) | |
| ea_total | No | commit mode: the condition's total EA after this call | |
| lum_gate | No | Sheet scope only. The stated stroke-luminance gate's accounting (#260): the tolerance, the seed's own luminance band, and every placement the geometry would have committed that the pen pulled under the bar — NEVER counted in found, never silent. Set scope accounts per sheet in sheets[] | |
| rejected | No | Sheet scope only. Placements the geometry accepted and a counter-example refused (#259) — NEVER counted in found, and never silent: each says which negative did it and what it saw. Reinstate one by hand with place_count at its `at` if you disagree | |
| withheld | No | Sheet scope only. Near-matches in the [0.75, 0.92) band — reported with a reason, NEVER committed. A withheld placement is a question you can answer with view_sheet; a hidden one is a miscount | |
| committed | No | commit mode: count shapes committed — one per match | |
| condition | No | commit mode: the finish tag the markers counted under | |
| negatives | No | What each `exclude` rect was read as, in the order you passed them (#259) | |
| shape_ids | No | ||
| candidates | No | Sheet scope only — set scope accounts per sheet in sheets[] | |
| rejected_total | No | Set scope: placements counter-examples rejected across every swept sheet | |
| seed_committed | No | Present when commit_seed: true minted the seed instance into the batch (#296) — ea_total then includes it |