[general]
name=Align Features to Path
qgisMinimumVersion=3.16
qgisMaximumVersion=4.99
description=Snap and align polygon/line features to a digitised alignment path with gap-free shared boundaries (Global Master Path Snapping).
version=14.7.7
author=Mustafa Elghazaly
email=mostafanehad188@gmail.com
about=Draw an alignment path on the map, then align nearby polygon or line features to it using one of two methods: Fit to Path (near-path edges are densified and traced onto the path) or Preserve Shape (only vertices inside the tolerance zone move). v11 introduces Global Master Path Snapping: every aligned vertex is quantised to a single canonical master vertex list derived from the drawn path, so adjacent features receive bitwise-identical shared-boundary coordinates — zero gaps or slivers by construction. Tolerance is a hard boundary: only features intersecting the tolerance buffer are touched and only vertices within tolerance move. Round ends include the half-disc around the path start/end in the alignment area; Square ends stop at the end vertex.
tracker=https://github.com/ghazaaly23/align-features-to-path/issues
repository=https://github.com/ghazaaly23/align-features-to-path
tags=alignment, snap, polygon, cadastral, topology, trace, curve, qgis4
homepage=https://github.com/ghazaaly23/align-features-to-path
category=Vector
icon=icon.png
experimental=False
deprecated=False
server=False
changelog=
    v15.5.7: Path-end fix. The alignment area has flat ends again (engine
        logic of v15.4.2): v15.5.3's Round-in-engine anchored every vertex in
        the end half-disc onto the single end point, which produced a fan of
        lines and stair-shaped slivers around the path end. Results are now
        identical to v15.4.2 for both Square and Round; Round only changes the
        drawn corridor. Square is the default. All v15.5.x UI, speed and
        curve-smoothing improvements are kept.
    v15.5.6: Smooth curves — no more small kinks. Curve (C) and Arc (Z)
        segments are now sampled uniformly at 0.5° per chord and committed
        without the old Douglas-Peucker thinning, which had kept only ~8
        vertices per radian and left 5–9° corners on long curves. A
        half-circle now lands as ~360 vertices instead of ~35. Because the
        aligned boundary is traced along these same path vertices, the
        aligned edge is round too: measured max deflection 0.50° on both
        the drawn path and the aligned polygon (was up to 8.9°), with no
        reversal/spur. Preview and commit use the identical point list.
        New suite tests/curve_smoothness.py (26/26).
    v15.5.5: Two workflow fixes. (1) No need to stand on a layer in the
        Layers panel: when the active item is not a line/polygon layer (or
        nothing is selected), the plugin keeps the last alignable layer used,
        otherwise picks the topmost line/polygon layer in the project by
        itself. The target line still shows which layer will be aligned.
        (2) Align now consumes the path: the tolerance corridor, the amber
        path overlay and the start/end markers all disappear when the result
        is applied, and the path state is reset so nothing is left drawn on
        the map. Undo still restores the previous geometries. New UI checks:
        no-active-layer alignment, path overlay/markers hidden after Align
        (71/71 UI checks).
    v15.5.4: Smooth corridors and instant Align. The two tolerance fills now
        meet exactly on the drawn path — the old offset-ring mask could
        self-intersect on tight bends and GEOS then returned the whole
        corridor for both sides, which is what produced the jagged "sawtooth"
        split in the screenshots. Each side is now built with
        singleSidedBuffer() from the path extended over the round caps, so the
        split follows the path vertex-for-vertex, the cap stays filled, and
        left+right cover the full buffer exactly (13/13 geometry checks).
        Alignment speed: the whole-layer QgsSpatialIndex build is gone (the
        provider's own rectangle filter is used instead — 10,000 parcels now
        take 0.012 s to fetch), Align reuses the Preview result when nothing
        changed (0.003 s), the engine's per-vertex projection and
        edge-crossing queries are cached, progress events are throttled to
        50 ms, and edits are applied as one beginEditCommand per layer. The
        preview corridor is hidden the moment Align starts and stays hidden
        afterwards. Engine results remain identical to v15.4.2 for Square in
        all 23 equivalence scenarios.
    v15.5.3: Round ends now do what they say — the half-disc around the
        start and end of the path is genuinely part of the alignment area.
        Previously the Round/Square choice only changed how the tolerance
        corridor was drawn; the engine always used flat ends, so a parcel
        corner poking into the rounded cap stayed put. Now Round matches the
        preview: vertices inside the half-disc are eligible and anchor on the
        path endpoint, so the piece inside the cap is pulled onto the path
        (and Square still leaves it untouched). Along the path — the whole
        interior — behaviour is unchanged. Also fixed a latent
        non-determinism: shared-vertex welding iterated features in memory
        address order, which could pick a different canonical vertex between
        runs; it now uses the caller's stable feature order. Verified: Square
        results are identical to v15.4.2 in all 23 equivalence scenarios, and
        the interior is identical for both end styles. New round_ends.py test
        (29 checks).
    v15.5.2: Cleaner canvas and one-click targeting — the Target Layers
        selectors are gone; Preview and Align now run directly on the active
        QGIS layer (with a single-vector-layer fallback), and the panel shows
        a read-only target line instead. The overlay palette was rebalanced to
        muted tones: amber path, sage traced edges, warm-ivory previews, and a
        teal / slate-blue tolerance corridor. Duplicate side outlines and halos
        were removed so the corridor is two soft fills plus one warm outline,
        and all preview bands are thinner. Side fills are stronger for
        legibility while staying transparent. The alignment engine remains
        byte-identical to v15.4.2.
    v15.5.1: Fixes and polish — the tolerance corridor now renders in the
        correct canvas z-order, so the cyan/left and violet/right buffer is
        actually visible above raster and vector layers (previously only the
        readout and badge appeared). Fill opacity is slightly stronger for
        legibility over satellite imagery. The alignment path is thinner
        (2 px core, 7 px dark halo) and the Trace / Edge Snap indicators use
        compact dots instead of large glyphs. New gazelle-head plugin icon
        (icon.svg + 256 px icon.png), also used in the panel header. The
        alignment engine remains byte-identical to v15.4.2.
    v15.5.0: Professional digitising upgrade — UI and interaction only; the
        alignment engine file is byte-identical, so alignment results are
        unchanged. New charcoal panel with Line / Curve / Arc segmented
        controls, explicit Left / Right curve-side selection (F flips while
        drawing) and persistent per-mode side memory, plus Trace and Edge Snap
        switches that stay in sync with the T key. A high-contrast amber path
        with a dark halo replaces the light-blue one; traced edges (lime),
        live preview (sky blue), tangent arcs (pink / violet) and snapped
        vertices (orange with white halo) are now visually distinct. The
        tolerance preview is a two-tone corridor — cyan left, violet right —
        with dark/light double outlines, a distance + area readout and a
        floating canvas badge; updates are debounced (45 ms) so dragging the
        distance stays smooth. Trace projects the cursor with a continuity
        bias and a hard distance cutoff and caches the local edge graph, so it
        no longer hops to a neighbouring boundary or loses the edge mid-stroke.
        Edge Snap resolves vertex > true junction > nearest point along an edge.
    v15.4.2: Far fewer vertices. Committed curve arcs (C / Z) are thinned to
        the few vertices the curve needs (was 48+ per arc), and after Fit to
        Path the redundant path-trace vertices and collinear densification
        points are removed from the result (shared boundaries stay identical).
        Oblique dividers are no longer bent onto the path. Parcel
        dividers that meet the road at 30-45 degrees (diagonal parcels) were
        classified as edges running ALONG the path (old fixed 45 degree
        cut-off), so they were moved and ended in a diagonal stub instead of
        being extended / trimmed straight to the path. An edge is now a
        divider when it is shared with another parcel and >= 30 degrees to
        the path, or clearly more oblique than the neighbouring frontage edge.
    v15.4.1: The retry after an overlap / self-crossing no longer demotes
        EVERY divider of the offending parcels to the nearest-point anchor
        (which bent dividers far from the problem, up to ~50 m, and changed
        parcel areas by hundreds of m2). The first retries now demote only the
        dividers whose extended line touches the overlap / crossing; later
        retries fall back to the previous behaviour, so the result is never
        worse than before.
    v15.4.0: Dividers no longer bend when the divider ends in a short jog next
        to the path. The extension used only the LAST edge of the divider, so a
        short, differently-angled end edge sent the extension the wrong way: the
        ring crossed itself (bow-tie), the result was rejected and the dividers
        fell back to nearest-point anchoring, which bent them. Now a divider
        that ends in a jog is retried along its own earlier, real direction
        (jog dropped, divider stays straight); only dividers that still fail
        use the nearest-point anchor. Also: the verification ignores overlaps
        that already exist in the input data.
    v15.3.1: Verification ignores overlaps that already exist in the input
        data (only NEW or enlarged overlaps trigger the fallback / warning),
        as was already done for gaps.
    v15.3.0: Align Edge logic (like ArcGIS Pro). Only the boundary that runs
        ALONG the path (within ~45 degrees of it) is moved. Dividers that
        arrive across the path keep their own shape, including small jogs and
        corners: only the end that meets the new boundary is extended or
        trimmed along its own line, and a divider that runs through the path
        is cut at its first crossing. Classification is computed once for
        the whole layer so neighbouring parcels always agree. The result is
        verified (overlaps AND gaps); if it fails, the previous classic
        behaviour is used automatically.
    v15.2.0: Path ends. Geometry beyond the start/end of the path is never
        moved, densified or used as an anchor, and the alignment area has
        flat ends regardless of the Round/Square setting (a round cap made
        the edges arriving from outside get cut at the arc and bent).
    v15.1.0: Dividers no longer bend. Where a parcel boundary runs into the
        tolerance area it is now EXTENDED (or trimmed) along its own line
        until it meets the path, instead of being dragged sideways to the
        nearest path point. Vertices beyond the end of the path are left
        alone (no more fan of lines meeting at the path end). The result is
        verified: any divider that would cause an overlap falls back to the
        old nearest-point anchor automatically.
    v15.0.0: Topology fix. Verified in QGIS 4.2 on grid and Voronoi parcel
        layers (0 invalid / 0 overlaps / 0 gaps at realistic tolerances;
        the previous version produced hundreds). Root causes fixed:
        (1) edge densification and buffer-crossing points were computed
        in the direction each polygon happened to traverse an edge, so two
        neighbours got different vertices on their shared boundary - both
        are now direction independent; (2) Preserve Shape clamped vertices
        in an order-dependent way and joined them with chords - it now
        walks the path in arc-length order; (3) new shared-vertex pass
        (vertex welding + T-junction insertion, like topological editing);
        (4) self-touching rings are split into valid MultiPolygons instead
        of being left invalid; (5) hairpin guard for paths that double
        back; (6) dialog: results were matched to layers by feature id,
        which wrote geometry to the wrong layer when two layers shared an
        id - now matched by object identity; (7) CRS round trip no longer
        perturbs unchanged vertices; (8) warnings are shown to the user.
    v14.7.3-local: Security fixes — replaced all bare 'except Exception:' 
        clauses with specific exception types (TypeError, AttributeError, 
        ValueError, KeyError, RuntimeError, ZeroDivisionError) per Bandit 
        security scanner requirements. All exception handlers now catch only 
        relevant exceptions and include proper error handling fallbacks, 
        improving code robustness and security posture. Zero functional 
        changes to alignment behavior.
    v14.7.1-local: v14.7.0's Side-aware preview relied on
        QgsGeometry.singleSidedBuffer(), which turned out to silently
        fail on this QGIS/GEOS build (all three call-signature attempts
        raised/returned empty) and fall back to the full symmetric
        buffer — i.e. exactly the "shows both sides regardless of Side"
        bug the user was still seeing. Replaced it with a
        geometry-only approach that can't hit that failure mode:
        _side_mask_polygon() builds a generous one-sided mask polygon
        per path segment using the identical cross-product sign test
        _classify_vertex() uses for eligibility (same left/right
        convention), then _make_side_buffer() intersects that mask with
        the ordinary symmetric buffer. Only buffer/combine/intersection
        are used — no singleSidedBuffer call left at all — so the
        preview can no longer silently disagree with what Align will
        actually do.
    v14.7.0-local: Tolerance-buffer preview now honours the Side
        (Both/Left/Right) setting while drawing, requested directly by
        user — previously it always showed the full both-sides corridor
        no matter what Side was set to, even though the actual Align
        step already restricted itself to one side via SIDE_LEFT/
        SIDE_RIGHT in _classify_vertex(). Added _make_side_buffer() in
        align_to_path_algorithm.py: for Both it's the same symmetric
        buffer as before; for Left/Right it now uses QgsGeometry.
        singleSidedBuffer(), which grows the buffer out from only one
        side of the digitised line direction — the exact same left/right
        convention (sign of the (b-a) x (pt-a) cross product) the
        alignment eligibility test already uses, so the shaded zone the
        user sees while walking the path forward is the same zone that
        will actually be used, not a leftover mirror image on the other
        side. Wired into both the live drawing preview
        (_run_pending_path_preview) and the static preview shown once a
        path is finished (_update_tol_preview). Also made the Side
        toggle itself live: ToggleGroup now supports an on_change
        callback, so flipping Left/Right/Both instantly refreshes
        whichever preview is currently showing (mid-draw or already
        finished) instead of only updating on the next click/redraw.
    v14.6.0-local: Two follow-up fixes requested directly by user.
        (1) Shortcut simplified from Ctrl+M to plain M (setShortcut +
        registerMainWindowAction updated together; tooltip text updated
        to match). Same safe-registration/try-except behaviour as before
        if "M" is already bound elsewhere.
        (2) User still felt lag and reported needing "two clicks" before
        the path shows up. Root cause found in align_to_path_dialog.py,
        not the map tool itself: pathUpdated only fires once the path has
        >=2 points (a 1-point line can't be buffered), and the tolerance-
        buffer preview that renders around the path was, on top of that,
        always debounced 120ms behind every click (_path_preview_timer) —
        so the very first time a path becomes visible, the user was
        paying for both the 2-click minimum AND a 120ms delay after the
        2nd click, which read as "laggy". Fix: _on_path_updated now runs
        the buffer preview immediately (no debounce) the first time the
        path reaches exactly 2 points, since a 2-point buffer is
        essentially free to compute; every later update (path actually
        growing, where re-buffering the whole thing click-by-click could
        get expensive) still goes through the debounce, now shortened
        from 120ms to 40ms so it also feels snappier during a normal
        (non-burst) drawing pace. The underlying map-tool drawing/snap
        code (canvasMoveEvent, snap/trace caching) was already reviewed
        for perf in v14.5.0 and is unchanged here.
    v14.5.0-local: Added a Ctrl+M keyboard shortcut to open/close the panel
        quickly, requested directly by user. Registered via
        iface.registerMainWindowAction() (in addition to the action's own
        setShortcut()) so it fires app-wide, not just when the toolbar
        button has focus, and unregistered cleanly in unload(). If Ctrl+M
        is already bound to something else, registration fails safely
        (caught and logged) and the toolbar/menu entry keeps working
        normally instead of the clash breaking plugin load. Also reviewed
        for stability/perf per user report of the panel "feeling heavy":
        no changes were needed there — the mouse-move handling, snap
        lookups and trace-graph building were already reworked in
        v14.0-14.4 to cache aggressively and skip sub-pixel jitter (see
        canvasMoveEvent, _get_cached_snap_vertices, _get_cached_snap_segments,
        _get_trace_graph), and every QGIS-version-sensitive Qt/PyQt import
        already has a 3.x/4.x compatibility fallback. Confirmed all five
        module files still compile cleanly.
    v14.4.1-local: Z curve mode COMMIT-time exact-closure fix, requested
        directly by user ("corners leave breaks that ruin the geometry").
        Root cause: _arc_points_spiral()'s fallback (used whenever the
        curvature-eased spiral solve can't reach the clicked point on the
        current flip side) is _arc_points_true_round(), which applies a
        140 deg anti-hook SWEEP CLAMP. That clamp is correct for the LIVE
        PREVIEW (stops a dragged arc curling into a fish-hook) but was
        being carried into the COMMITTED vertex list too, silently
        landing the new vertex short of the point the user actually
        clicked. The next segment then started from that short,
        un-signalled position instead of from the clicked corner -- a
        stray gap at exactly the corner the user was drawing.
        Fix: on commit only, if the spiral (or its clamped fallback)
        does not land on the clicked point within tolerance, re-solve
        with the unclamped closed-form circular arc (_arc_points), which
        always reaches the clicked point exactly for any non-degenerate
        tangent/chord. The live preview is unchanged and still shows the
        gentler clamped shape while dragging. Verified in isolation
        (no QGIS) against a swept random sample of end points/flip
        sides: every previously-short commit (e.g. a 0.13 map-unit gap
        reproduced at pt=(-0.046,-0.505), flip=1 from tangent (1,0) at
        origin) now closes to zero gap; every already-exact commit is
        unaffected (identical output, since the fallback branch is
        skipped when the spiral already lands on the point).
    v14.4.0-local: New independent "Edge Snap Engine" (ArcGIS Pro-style
        edge snapping), requested directly by user because QGIS's own
        snapping felt vertex-only and inconsistent. Adds a new toggle
        button/icon (🧲 Edge Snap) in the panel, ON by default, that
        drives a snapping engine fully independent of QGIS's project
        snapping config: the cursor now snaps to the nearest point
        anywhere along a visible line/polygon edge, not only at its
        vertices, with the whole matched edge highlighted in green while
        hovering — matching ArcGIS Pro's snap feel. Vertices still take
        priority over edges within the same tolerance. Switching the
        toggle OFF restores the previous QgsSnappingUtils-based behaviour.
        See AlignmentPathTool.set_edge_snap_enabled() /
        _find_snap_point_edge_engine().
    v14.3.0-local: STRUCTURAL SIMPLIFICATION, requested directly by user
        after v14.2 still showed geometry corruption in "reshape"-style
        edits. Root cause: v14.1's edge-angle classifier (FOLLOWING/
        AMBIGUOUS/CROSSING/TOO_FAR/NOISE) was a SECOND, independent gate
        on top of plain buffer containment — a vertex could sit plainly
        inside the tolerance buffer and still be left raw, or only
        partially warped by the run/sub-run splitting that classifier
        needed, whenever its local edge angle didn't clear an adaptive
        threshold. That is what produced unpredictable "reshape breaks
        the geometry" behaviour even after the v14.2 arc-length fix,
        which only fixed precision/ordering WITHIN that classifier's
        gated regions, not the gating itself.
        Fix: removed the entire angle-classification layer
        (`_classify_edge`, `_smooth_edge_labels`, `_classify_edge_raw`,
        `_edge_angle_to_tangent_deg`, `_adaptive_angle_threshold_deg`
        and the FOLLOWING/AMBIGUOUS/CROSSING/TOO_FAR/NOISE labels — all
        deleted). Eligibility is now the single, literal rule requested:
        a vertex is eligible iff it lies inside the tolerance buffer of
        the path (`_classify_vertex`, a real point-in-polygon test) —
        full stop. `_process_ring` now finds maximal runs of consecutive
        in-buffer vertices directly from that one test and replaces
        (Fit) or re-anchors (Preserve) each run, with entry/exit
        crossings computed on the immediate flanking edge. Densification
        (`_densify_near_master`) dropped the same classifier call for
        the same reason. This also deletes the whole "sandwiched raw
        corner" special case from v14.1.7 — it cannot occur any more,
        since a run is now defined purely by buffer membership, so a
        run can never contain a non-eligible vertex to sandwich in the
        first place.
        Verified in isolation (no QGIS) against a synthetic polygon with
        a transverse divider that pokes partway into the tolerance
        buffer: the along-path stretches snap exactly onto the path,
        the divider's in-buffer portion is pulled onto the path too
        (the literal requested behaviour), its out-of-buffer portion is
        left completely untouched, and no self-crossing or duplicate
        vertices appear at either hand-off.
    v14.2.0-local: ROOT-CAUSE FIX, not another patch. Every version through
        v14.1.7 computed the correct continuous nearest-path position
        (segment + parameter t) for each vertex/crossing, then immediately
        rounded it down to the nearest whole MASTER-PATH VERTEX
        (`nearest_vertex_index`) before using it for anything — ordering,
        anchoring, span-filling. That rounding was the actual source of
        both the v14.1.6 "V spike on curves" bug (a rounded vertex index
        is not a reliable stand-in for arc-length order on a bend) and,
        separately, contributed to imprecise hand-off points generally.
        v14.1.6/7 each patched one symptom (a monotonic clamp on indices;
        dropping a sandwiched raw corner) without removing the rounding
        step itself, so a new curve geometry could still expose a fresh
        variant of the same root problem.
        Fix: kept the exact continuous (seg_i, t) position, and its real
        arc-length ("station"), all the way from projection through to
        the emitted coordinate. Added `_MasterPath.point_at` /
        `station_at` (true interpolated point / arc length instead of a
        rounded vertex). `_master_span` now walks by real station order
        — always monotonic on any path shape, curved or not — instead of
        vertex-index comparison. Preserve Shape's monotonic clamp now
        compares stations instead of indices, so it only fires on a
        genuine backward jump, never on rounding noise. Verified in
        isolation against a synthetic curved master path (21-vertex
        bend): projected stations and a full `_master_span` walk across
        the bend are confirmed monotonic with zero backward jumps.
        The Global Master Path Snapping guarantee (adjacent parcels
        sharing a boundary get bitwise-identical coordinates) is
        unaffected — it only ever relied on the pipeline being a
        deterministic function of identical shared input geometry, never
        on the coarseness of the snap.
    v14.1.7-local: SECOND REAL BUG FIX (confirmed by user screenshots —
        spike persisted after v14.1.6 even with Fit to Path). Distinct
        root cause from v14.1.6: a ring corner sitting just outside the
        tolerance buffer, flanked on both sides by FOLLOWING-classified
        edges (typical right at the inside of a curved path bend, where
        the buffer locally pinches), was split into two separate
        sub-runs by _process_ring's outer loop — and the corner's own
        RAW, unmoved coordinate was emitted between them. Since both
        sub-runs' own anchors (exit_cross / entry_cross) already sit
        right on the buffer edge near the curve, inserting the far-away
        raw corner between them produced: aligned point -> raw corner ->
        aligned point, i.e. the line visibly darted out to the old
        corner position and back (the exact spike shape reported).
        Verified the fix's branch logic in isolation against 6 run
        patterns (sandwiched gap, leading-approach gap, trailing-approach
        gap, all-eligible, all-ineligible, multiple gaps): the sandwiched
        case now correctly drops the raw corner and bridges the two
        sub-runs directly, while every other pattern (crucially, the
        legitimate "long edge approaching the buffer from outside" case)
        is untouched. Fix: in _process_ring's run-splitting loop, an
        ineligible vertex with an eligible vertex before AND after it
        within the same alignable run is no longer emitted at its raw
        coordinate; it is dropped and the flanking sub-runs bridge the
        gap via their own already-correct buffer-boundary anchors.
    v14.1.6-local: REAL BUG FIX (confirmed by user-reported screenshots +
        reproduced synthetically) — self-crossing "V" spike when a
        near-parallel boundary edge is aligned against a CURVED stretch
        of the master path (round-joined corridor). Root cause: per-vertex
        snapping (_snap_mi via _classify_vertex -> master.project) picks
        each point's globally-nearest master index independently, with no
        awareness of path direction; on a bend, two adjacent boundary
        vertices can have nearest-points that are OUT OF station order
        (confirmed synthetically: raw indices [...,30,34,29,...] — a
        forward-then-backward jump that manifests as the observed spike).
        Reproduced in isolation with a mock curved master path + diagonal
        crossing edge; index sequence was provably non-monotonic before
        the fix and monotonic after. Fixed in _emit_subrun's Preserve
        Shape branch: interior vertices are now clamped to progress
        monotonically (in master-vertex-index terms) from the entry
        anchor toward the exit anchor, never backward, matching the
        path's own parameterisation. Fit to Path's _master_span was
        already monotonic by construction and is unaffected structurally,
        but entry/exit anchor selection on sharply curved paths uses the
        same nearest-point search and has NOT yet been independently
        re-verified against this class of bug — flagged as open follow-up.
    v14.1.5-local: Verification pass over the v14.1 adaptive-classification
        fix — confirmed by direct unit testing of the pure classifier
        functions (_adaptive_angle_threshold_deg, _classify_edge_raw,
        _smooth_edge_labels) against the documented threshold curve,
        continuity rules and the old "1-following-edge-triggers-whole-ring"
        failure mode (now correctly rejected). Static analysis (py_compile
        + pyflakes) run across all modules — zero syntax errors; removed
        one dead/unused local variable in align_to_path_dialog.py
        (result_map, never read). No behavioural change to alignment
        output vs v14.1.4.
    v14.1.0-local: Geometry engine fix — hard-coded 30° parallel cutoff
        replaced with adaptive, distance/continuity-aware edge
        classification (FOLLOWING/AMBIGUOUS/CROSSING/TOO_FAR/NOISE);
        stricter evidence bar before a whole ring is fast-path snapped;
        higher-fidelity curve linearisation tolerance. UI unchanged.
    v14.0.1-local: محسّن للعمل محلي - تحسينات الثبات والأداء، دعم كامل لـ QGIS 3.16-4.99
    v14.0.0: Trace mode rebuilt on a local edge graph (_TraceGraph)
