seg_intersect_xy found where two segments' XY projections cross and returned
the point at z=0, ignoring height. Two lines at different Z whose plans cross
therefore fired a real Intersection snap when they only *appear* to cross in
top view — that case belongs to Apparent Intersection (view-space).
Rename to seg_intersect_3d and require the segments to be at the same height
at the crossing: compute Z on each at its crossing parameter and reject when
they differ (tolerance scaled to coordinate magnitude). A genuine crossing
now snaps at its true height instead of z=0; a plan-only crossing is left to
Apparent Intersection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
extension_bases_screen only skipped closed curves (circle/ellipse, keyed on
the Quadrant hint) when finding which endpoints' extension rays a snapped
point lies on. An arc is an open curve with no Quadrant hint, so its
tessellation chords were scanned; one straight chord-extension grazes a
nearby line-line intersection (beyond the arc endpoint, within 4px), drawing
a spurious dashed extension guide radiating from the arc.
Skip every round curve by the Center hint (which arcs, circles and ellipses
all carry and straight lines never do) — the same signal nearest_segment
uses. The intersection snap point itself was already correct; only the stray
guide line is removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Snap and hover hit-testing scanned the whole model wire set every cursor move; a heavily block-instanced drawing explodes to millions of wires, so this stalled for over a second per move. Add a cached spatial grid (wire_grid) keyed on the geometry epoch and query only the cursor neighbourhood via Scene::hit_test_wires_near. Gate the O(in-range segments squared) intersection passes on the in-range POINT count, not the wire count — a handful of curved wires, each hundreds of tessellated points, blew the quadratic up while the wire count looked modest. Give zero-size entities away from the origin (POINTs) a real cullable AABB, and fall back to a block sub-wire's own world points when its entity bounding box is degenerate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Object snap ran several full scans of the drawing on every cursor move,
and the Intersection / Apparent-Intersection passes compare pairs of
in-range segments — O(k^2) in the number of wires whose bounding box
overlaps the aperture. Zoomed out over a large drawing (or with long
lines whose AABB spans the view) that pairwise scan grows to the whole
document and freezes point picking, so MOVE/COPY/paste placement hung.
Count the in-range wires once and gate only the pairwise passes on it:
above the cap they are skipped (you cannot resolve a single intersection
when the aperture spans thousands of segments anyway), while every
single-wire snap (endpoint, midpoint, centre, perpendicular, nearest)
keeps working — so point picking still snaps to corners at any zoom.
Once the first point of a segment is placed, OTRACK now offers an
alignment along the line joining that base point and an acquired
tracking point — the direction *between* them, not only the ortho/polar
axes or the corner's own edges. Acquire an existing corner while drawing
and the cursor locks onto the base→corner line, with typed-distance
measured from the base point.
Adds a base→corner ray per acquired point in otrack_snap, grouped with
that corner so it never self-intersects with the corner's other rays.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).
- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
cached by the tess memo, cloned on hit, and transformed by the block
expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
the strokes; render gathers text_verts from the viewport wire set
(`gather_text_verts`, cached on wire content id) — the old collector is
removed. Covers model, dimension, table, block-internal and paper-sheet
text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
entity_aabb which mis-places the box for MTEXT; composite paths no longer
clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
highlight, keyed on selection_generation) mirroring the selected-wire
xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the forces_endpoint_snap mechanism merged in #293:
CadCommand::forces_endpoint_snap, Snapper::snap_forced_corners /
forced_corner_modes, the PlotWindowCommand override, the on_tick +
click-commit wiring, and the associated tests. PLOTWINDOW now uses the
normal snapper like every other command.
Rationale: forcing Endpoint snap on regardless of the global OSNAP master
overrides an explicit user setting and is surprising — a plot window is a
coarse print region that usually wants a margin, not a corner lock, and
the forced set only caught Endpoint (not center/mid/intersection) so it
was a partial, opinionated helper. Parking it pending a design discussion
with the PR author; can be reinstated behind a clearer UX (e.g. respecting
OSNAP, or an opt-in modifier) if justified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PLOTWINDOW / PW now forces Endpoint object-snap while picking its two
corners, independent of the global OSNAP toggle, so the plot area lands
exactly on frame corners. The snapper's master toggle and configured
modes are saved and restored around the pick, so the user's OSNAP
settings are unchanged afterward.
- CadCommand::forces_endpoint_snap() opt-in (default false; PlotWindowCommand true).
- Snapper::snap_forced_corners() forces Endpoint only, then restores state.
Intersection is deliberately excluded: it is O(n^2) over the hit-test wire
set and froze the UI on large drawings when a plot window is picked
zoomed-out. Frame corners are endpoints, so Endpoint alone suffices.
- the cursor-move handler routes the plot-window pick through the forced path.
Works in model and paper space (shared pick path). Tests: opt-in flag;
forced mode set is endpoint-only; snapper state is restored after the call.
Two refinements to Parallel snap. The acquired reference now keeps a
point on the line and draws a ∥ marker there, so the user sees which line
the parallel is measured from. And dwelling on that same reference line
again removes it (toggle), matching how OTRACK tracking points clear;
dwelling on a different line switches the reference. Same-line detection
uses parallel direction plus perpendicular screen distance, so it holds
across zoom.
Refs #277
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parallel snap did nothing — the type was in the osnap set, priority and
marker, but there was no logic behind it. Implement it: hovering a line
(or polyline segment) for a short dwell acquires its direction as a
reference; then while drawing from the last point, when the cursor runs
parallel to that reference the point locks onto the line through the last
point parallel to it, showing the ∥ marker and the alignment guide.
Curves (circle/arc/ellipse) are ignored — parallel to a curve is
undefined. Works with only the Parallel object snap on, independent of
OTRACK; the reference clears with the command's tracking state.
Closes#277
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The earlier #274 fix only corrected the tangent point's coordinate — the
point was still the circle edge facing the cursor, so Tangent snapped
like Nearest. Compute the true tangent from the point the command is
drawing from (from_point): the tangent point's radius is perpendicular to
the line from that point, giving two solutions at acos(r/|CP|) either
side of the C→P direction — take the one nearer the cursor. Falls back to
the facing point for a deferred tangent (object picked first) or a point
inside the circle. A line is tangent to itself everywhere, so the Line
case stays nearest-on-line. Covered by a perpendicularity unit test.
Refs #274
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Catching a plain line-line Intersection re-runs extension_bases_screen to
draw the dashed guide(s) for an *extended* intersection (#247/#259). It
scanned every wire including tessellated circles, and a full circle's
chords point in every direction, so one almost always has an outward
extension that grazes the crossing — drawing spurious guide lines
radiating from nearby circles.
Skip closed curves (full circle / ellipse) in that scan, identified by
their Quadrant snap hints (arcs never carry them, so arc extension guides
are unaffected).
Closes#276
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two object-snap fixes:
- Tangent on a circle built the snap point as
`(cx + r·nx, cy, cy + r·ny)` — the y-offset landed in Z and Y was stuck
at the centre, so the tangent point sat at the wrong place. Build it as
`(cx + r·nx, cy + r·ny, cz)`.
- Endpoint snapped to a full circle / ellipse: for tessellated curves it
snaps the first/last point, but a closed curve's first/last is a seam,
not an endpoint. Skip Endpoint for closed curves — identified by their
Quadrant snap hints, which arcs never carry — so arcs still snap their
real endpoints.
Closes#274, closes#275
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extension-driven acquisition (OTRACK off) acquired any dwelled snap
point, including the extension foot the cursor paused on while tracing an
extension. Each such spurious acquisition filled the 4-point buffer and
evicted, oldest-first, the endpoint the user had actually acquired — so
after a few pauses its + marker and its extension disappeared.
Restrict Extension-driven acquisition to real segment endpoints (points
with an incident edge); midpoints, centres, intersections and extension
feet are ignored. OTRACK still acquires any snap point.
Refs #262
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extension snap extended every segment's line in the whole drawing, so in
a larger project an extension foot was live almost everywhere and the
cursor snapped to it constantly.
Extension now behaves like a temporary tracking point: an endpoint must
first be acquired by dwelling on it, and only then does that segment's
line extend. Acquisition — and the tracking-point marker — is active
whenever OTRACK is on OR the Extension snap is on, so Extension works
independently of OTRACK's on/off state (and is not tied to POLAR). Both
the per-segment extension and the extended-intersection candidates are
gated on an acquired endpoint, and the whole scan is skipped when nothing
is acquired (the common case), keeping large drawings responsive.
Closes#262
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#218: with Ortho on and a base point the axis is now a hard lock — a lone
tracking ray can't pull the cursor off it. Only a tracking ray crossing
the ortho axis locks (the corner-finding case); otherwise the ortho
constraint applies as the fallback, so Ortho reliably locks when OTRACK
is also on.
#219: an acquired corner now contributes an alignment ray along its own
edge direction(s) (captured once at acquisition), so OTRACK follows a
segment's extension, not just the ortho/polar axes. Acquisition is also
captured on leave — a perfectly still cursor emits no move events, so
"pause on a corner, then drag" now reliably acquires it. The acquired
marker projects through the active pane's camera + rect (canvas offset
included) instead of vanishing, and the active alignment path is drawn
as a full-length dashed guide through the corner.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extension osnap now offers a ray off every polyline vertex, not just the
first and last, and snaps to where two segments' extension lines would
cross. That crossing is emitted as an Intersection so it outranks the
per-segment extension feet on their own lines. When it wins, both
contributing extensions are re-detected — off-ray distance measured in
screen space so the tolerance holds at UTM coordinates — and the overlay
draws a dashed guide from each endpoint through the crossing, so both
extension paths remain visible instead of vanishing when caught.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Extension object snap worked but drew no tracking path, so it was
easy to miss — users had to keep the cursor exactly on the invisible
extension line without any visual cue.
When an Extension snap is active, draw a dashed guide line from the
endpoint it extends from, through the snap point and a little beyond,
and orient the three-dot marker along that direction. SnapResult carries
the endpoint screen position (filled once the winning snap is known);
the overlay renders the guide.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Delete src/modules/draw/modify/extend.rs (a 1-line placeholder with no
tool()/logic; `extend::` was referenced nowhere) and its `mod extend;`.
- Flatten single-file module dirs to the modern flat style:
src/command/mod.rs -> src/command.rs, src/snap/mod.rs -> src/snap.rs.
Parent `mod command;`/`mod snap;` are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>