v14.7.6: Fixed author contact email typo in metadata and source
headers.
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)
yes
MustafaELghazaly
2026-10-01T07:25:06.487670+00:00
3.16.0
4.99.0
None
no
Plugin Tags