Commit Graph
59 Commits
Author SHA1 Message Date
Hakan Seven e917c23b46 Merge pull request #1716 from schoeller/xrecord_crud
Xrecord crud
2026-10-12 00:21:14 +03:00
Hakan Seven 3cc3ead3a6 Merge pull request #1714 from yztink/fix/arc-tangent-line
fix(draw): support line tangency in arc construction and resolve trim contact precision
2026-10-11 23:42:48 +03:00
Sebastian ec96612388 fix: resolve the two clippy approx_constant denies
The informational clippy job fails on two pre-existing literals
(also failing on main, surfaced on this PR's merge): the trim
tangency test's angle list used 3.14 where pi was meant — use the
constant; the point-cloud light-direction fixture in render.rs uses
a hand-tuned 0.318 component that only looks like 1/pi — keep the
tuned value and allow the lint on that statement.
2026-10-11 21:30:54 +02:00
yztink 2f10a66bff fix(draw): support line tangency in arc construction and resolve trim contact precision
- Implement Apollonius PPC (Point-Point-Curve) and multi-tangent CCP solvers for lines in arc.rs
- Support tangent line snapping for all 3-input arc combinations (Tangent-Point-Point, Point-Tangent-Point, Tangent-Tangent-Point, Tangent-Point-Tangent, Point-Tangent-Tangent)
- Resolve TangentGeom::Line f32 truncation in snap.rs to preserve true f64 key vertices during tangent evaluation and TangentObject construction
- Preserve strict CUT_TOLERANCE (1e-7) in trim.rs while ensuring tangent contact points are detected accurately
- Add unit tests for line tangency solvers, tangent trim detection, and angular precision sweeps
2026-10-10 23:01:22 -07:00
Karim Jerbi 9cf9ff2c5c style: apply cargo fmt across workspace 2026-10-11 03:14:29 +01:00
Hakan SevenandClaude Opus 5.5 09cd50ea62 fix(trim): polyline vertices are not cutting edges; self-cuts only as an edge
#1567 made every vertex a trim limit, so a polyline no edge crosses lost
the clicked segment and a trimmed piece stopped at the next corner instead
of running on to the next cutting edge. Vertices are no cuts again, a
polyline with no cut is left alone, and its own crossings cut it only when
it is among the cutting edges. The crossing window's unused pick corner is
dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 10:06:10 +03:00
Hakan Seven 3bf8906ed3 Merge PR #1567 from gianlucafiore/fix/polyline-self-intersections: polyline self-intersection snapping and trimming 2026-10-02 10:04:11 +03:00
Karim Jerbi 02c470aac7 feat(pdf): searchable text layer with per-style subset font embedding
Real BT/Tj text objects ride alongside the vector outlines (Tr 3,
zero visual change): runs preserved at layout time, TTF styles embedded
as allsorts subsets with ToUnicode, width-matched Tm, clip-culled.
WireModel literals moved to ..Default::default() tails.
2026-10-01 23:30:35 +01:00
gianlucafiore 2131f4390f fix(trim): support polyline self-intersections and box trimming 2026-09-30 15:35:51 -03:00
Hakan Seven 66ac8c4ec0 Merge pull request #1511 from yztink/feat/fluid-tangent-arc
feat(draw): add fluid tangent arc, circle, and line construction and snap trim cuts to boundary contacts
2026-09-28 14:02:11 +03:00
yztink c827b87cf9 fix(trim): snap trim cut parameters to exact boundary contact points 2026-09-28 01:56:47 -07:00
ramox81andClaude Opus 5.5 7f8f8457c7 Frames: state-following ribbon list, DWF/DGN frames, image frame encoding and profile, frame-only picking
- The Frames list shows the drawing's frame state (hide / display and plot /
  display but not plot / frames vary), follows commands, undo and document
  switches, and lists the mixed state as a disabled item.
- DWFFRAME and DGNFRAME are frame variables of their own, kept in the header,
  and FRAME sets them with the others.
- Out-of-range frame values say "Requires an integer between 0 and 2." and
  ask again; a set value reports "Regenerating model.".
- IMAGEFRAME is stored in the image variables as bits (2 is written as 3) and
  a drawing without them uses the profile value, which setting IMAGEFRAME
  updates and the settings file keeps.
- Raster images, PDF underlays and wipeouts are picked on their frame only;
  underlay snap geometry is never picked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 10:53:53 +03:00
Hakan SevenandClaude Opus 5.5 f537aff75c feat: node graph overlay; move to opencadcodec/opencadkernel/opencadgraph
The codec and kernel repositories and crates are renamed opencadcodec and
opencadkernel. Inside OCS they are depended on under short keys, so code
reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node
graph engine). File-format and protocol spellings stay as they were: the
ACIS product id `acadrust`, the ACADRUST_* switches and the plugin
version keys `acadrust_version` / `acadrust_source`.

A button left of MCP on the command line opens a translucent node graph
over the viewport, with a palette of object, input, math, logic, list,
text and geometry nodes added by click or drag. An object node owns
drawing entities and its rows are the Properties panel's own rows for
them: the single-object page is extracted from refresh_properties as
entity_property_sections, values use the control protocol's
property_json, and writes go through set_property_value with a property
target override, so the panel, MCP and the graph share one read/write
path. A list reaching an object row makes one entity per element. Each
graph action is one undo step; the canvas pans and zooms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 07:34:23 +03:00
copilot-swe-agent[bot] 41101efaa5 fix(trim): skip empty preview polyline segment samples
Co-authored-by: dasmatus <[email protected]>
(cherry picked from commit fb13a7aaa6)
2026-09-20 21:29:15 +03:00
Claude e83c8a28a5 fix(trim): skip polyline segments with no plan-view shape
`sample_entity_xy` walks a polyline's exploded segments and drops the
vertex each segment shares with the previous one. A segment whose curve
plane is upright — any non-horizontal run of a 3D polyline — has no XY
curve, so `entity_curve_xy` declines it and the segment samples to
nothing. With nothing accumulated yet, the seam test compared the two
empty ends as equal and then sliced the empty sample from index 1:

    panicked at src/modules/draw/modify/trim.rs:3982:42:
    range start index 1 out of range for slice of length 0

On the web build that panic unwinds through winit's event-loop runner
and leaves its state borrowed, which is the "RefCell already borrowed"
follow-on in the same report.

Skip an empty sample instead, which leaves the seam handling to the
segments that do have a plan-view shape.

Fixes HakanSeven12/OpenCADStudio#1318

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TQzYZsXBwPZ5HwBaGitEUV
(cherry picked from commit ac9a6ce91a)
2026-09-20 21:29:15 +03:00
Karim Jerbi f79bad8adf fix(trim): handle polyline arc segments and preserve curvature
- Intersect cutting boundaries against polyline arc segments via KernelArc instead of chord lines
- Project click points onto BulgeArc geometry to evaluate accurate click parameters
- Proportionally scale and preserve bulges on surviving partial arc segments via extract_sub_polyline
- Update crossing trim and hover wire preview to support polyline arc geometry
2026-09-11 14:33:49 +01:00
PanosandClaude Opus 5 391a74a615 perf: let one tessellation serve every background
Switching to a Layout re-tessellated the whole model — `memo_miss=186455`,
2034 ms — to arrive at geometry identical to what was already in the memo.
The only thing that differed was colour.

`local_wires_for` already knows this: it stores the raw colour and drops
its background argument with `let _ = bg_color;`, noting that
`Batches::finalize` applies `adapt_to_bg` last, "so the cache no longer
has to rebuild on BACKGROUND / layout-switch". The tessellation memo's
guard had not been told, and still mixed the background in, so every
entry was discarded on a switch.

`finalize` now records what its colour resolution consumed — the raw
colour, the raw glyph colours, and the three flags it branches on — as
`WireModel::bg_adapt`, and `resolve_colors_for_bg` replays it for another
background. The guard drops the background, so a memo entry serves any
layout.

The resolution recomputes from the recorded raw colour rather than from
the resolved one, which makes it idempotent and therefore safe over a set
that mixes memo hits with freshly tessellated misses. Re-adapting the
resolved colour would not be safe: `adapt_to_bg` snaps a near-white colour
to pure black on a light background, and pure black comes back pure
*white* on a dark one, losing the tint. A test pins that asymmetry so the
raw colour is not optimised away later.

The contract is tested against the real `finalize` rather than a copy of
its logic: for every background pair, building under one and resolving for
the other lands exactly where building under the other would have.

`bg_adapt` is `None` for wires built anywhere else, and the resolution
skips them, so nothing outside the block cache changes colour. It is also
`None` when the resolution would be a no-op — a preserved colour with no
canvas fill looks the same under every background — which keeps the common
case at no cost.

Ordering is unchanged: the pass runs inside the builder, before
`append_scene_markers` and `apply_refedit_fade`, both of which have their
own separate background dependency.

`wires-build` gains `colors=` so the pass has a number of its own.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-05 18:53:16 +03:00
gianlucafiore 2885898da6 fix(trim): honor preselected cutting boundaries 2026-09-03 17:02:06 -03:00
Hakan Seven 4209c35ba3 fix(polyline): render fill mode boundaries
Use kernel-produced band outlines when FILLMODE is off. Preserve centerline linetype stations through inserts, clipping, projection, and PDF export.
2026-08-24 22:18:44 +03:00
Hakan Seven 7293bec1b9 fix(point): preserve relative marker sizing
Keep rendering, picking, block transforms, and plotted output on the
same viewport-relative PDSIZE geometry without overloading wire width
fields.
2026-08-22 22:22:01 +03:00
Hakan Seven c22f6cde60 fix(trim): preserve source handle in fence pass
Temporary fence pieces used a sentinel handle, so the source spline was included as its own cutting boundary and kernel intersection recursed indefinitely.

Refs #830
2026-08-22 01:11:37 +03:00
Hakan Seven 44efb8fd53 feat(frame): complete frame controls
Preserve frame geometry for selection while separating viewport and plot visibility across supported reference entities.

Refs #642
2026-08-20 23:37:02 +03:00
Hakan Seven 7c818d15e9 fix(geometry): honor polyline OCS in commands
Route editing and derived geometry through kernel world-space curves.\n\nRefs #797\nRefs #798\nRefs #799
2026-08-18 22:09:48 +03:00
Hakan Seven f9c3546551 fix(trim): keep retained geometry continuous
Use kernel trim spans so unrelated boundary crossings do not split survivors.
2026-08-18 20:12:30 +03:00
Hakan Seven 4fd9b98da4 perf(render): instance block geometry
Share source buffers across block placements and render nested content with GPU instance transforms.\n\nChunk silhouettes and compact mesh material data so large drawings stay under device buffer limits and remain responsive.
2026-08-12 02:33:10 +03:00
Hakan Seven ac8183ec91 refactor(cad): use direct stack dependencies 2026-08-10 15:03:31 +03:00
Hakan SevenandClaude Opus 5 f3c2e061e0 refactor(trim): find an extension's boundary with the kernel
EXTEND asked where a line reaches by matching over every Geo variant and
running its own line-line, line-circle and line-ellipse test — twice
over, once for a LINE and once for a polyline's terminal segment, about
two hundred and sixty lines between them. A boundary spline was walked
as sixty-four chords there rather than intersected, so an extend landed
on the chord nearest the spline instead of on the spline.

Both are one call now: shoot the segment's own infinite line and take
the nearest crossing on the side being extended. That is `cut_params`,
which every other cut in the file already goes through.

With those two gone, the sampled copy carried beside each boundary
spline had no remaining reader, so a spline boundary is now only ever
the exact NURBS. The line-ellipse adapter lost its last caller with it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-09 10:02:23 +03:00
Hakan SevenandClaude Opus 5 c2cc6fd7c4 refactor(trim): read boundaries and samples through the entity curve
Three per-type matches went; what is left is only the shape of the
boundary record, which the edit options need to mutate — imply_edge_geos
turns a segment into an infinite line and opens an arc into a full
circle, neither of which a bare curve says.

Two things change with it. A boundary spline is carried as a NURBS
rather than as 64 chords, so a trim cuts where the spline actually runs;
the sampled copy stays only for the two hand-rolled ray walks in the
polyline-extend paths. And an extruded arc or circle is read in its own
OCS instead of having its stored coordinates taken for world ones, so it
acts as a boundary where it is drawn.

sample_entity_xy is now one tessellation at the density the renderer
uses, so a fence pass and a preview cut follow the drawn geometry rather
than a coarser approximation of it. It declines to guess a length for a
ray or an infinite line; fence_sample_xy, which has an answer, still
supplies one.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-09 00:43:53 +03:00
Hakan SevenandClaude Opus 5 3f103d2f8b refactor(trim): give rays and infinite lines their real parameters
Trimming a Ray or an XLine mapped it onto a virtual 0..=1 across a
million-unit stand-in for infinity, and read the surviving ends back with
a threshold — 0.9999 of that stand-in meant "still infinite". Both now use
the curve's own parameter, `t = 0` at the base and advancing by one
direction vector per unit, with no upper limit. An end that reaches
infinity says so, and `cut_intervals` bounds the target by its extent
instead of always by 0..=1.

The stand-in was not merely inelegant. A million units is the same order
as the coordinates themselves in a survey drawing, so a boundary beyond it
was silently missed, and the threshold meant a cut very near the far end
was rounded to "unbounded" and kept the wrong shape.

Implied and extended boundary edges become infinite lines rather than
segments stretched by the same constant. That is what they always meant —
the boundary cuts wherever its line reaches — and the kernel solves it
exactly.

The hover preview shared the virtual mapping and would have been left
feeding the old parameters into the new interpretation, so it is converted
with the rest.

TRIM_EXTENT survives only in `fence_sample_xy`, where a finite polyline is
genuinely what the fence test needs.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 20:04:08 +03:00
Hakan SevenandClaude Opus 5 dde5ac07cf refactor(trim): find boundary crossings with cadkernel
`line_seg_ts`, `arc_seg_ts`, `ellipse_seg_ts` and `spline_seg_ts` each
walked every boundary kind and solved that pair by hand — four nested
matches over the same seven cases. They are now four descriptions of the
target as a `Curve` handed to one `cut_params`, which asks the kernel.
Their signatures are unchanged, so the twenty call sites are untouched.

Three things follow from it.

Boundary splines and target splines stop being sampled. A cut on a spline
now lands where the boundary actually crosses rather than where a fixed
sampling said it did, because the kernel subdivides and converges to a
tolerance instead.

Rays and infinite lines as boundaries are handled by the kernel's extent
rather than by a case here — a ray rejecting what lies behind its origin
is the kernel's job now.

`extend_spline` shoots a real ray. It used to build a line a million units
long and cut against that, which both invents a limit and, at survey
coordinates, is comparable to the coordinates themselves. The ray's
direction is a unit vector, so what comes back is the distance to the
boundary. That also fixes the hit position, which multiplied by the
million-unit extent a second time and landed the extended end far past
where it was supposed to.

`cut_params` clamps to 0..=1 only for a bounded target; a ray has to keep
the parameter it comes back with, since the distance is the answer.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 19:53:55 +03:00
Hakan SevenandClaude Opus 5 042a479244 fix(spline): edit fit-point splines, and stop deleting the kept half
Two faults, reported together.

Fit-point splines could not be edited at all. A SPLINE is stored either as
a control polygon the curve approaches or as fit points it must pass
through with optional end tangents, and `spline_to_nurbs` only read the
first. A fit-point spline has no control points, so every command that
touched one — TRIM, BREAK, LENGTHEN, OFFSET — got nothing back and did
nothing. Two fit points with start and end tangents, which is what a
drawing stores for the simplest curve, was the clearest case. Fit points
now interpolate into an exact curve through cadkernel, so they are as
editable as any other spline.

Trimming deleted the whole spline when the surviving piece reached the
curve's own start. `trim_spline` cut away everything before the interval
and everything after it, and a cut at either end is a no-op that reports
nothing to do — which the end case handled and the start case propagated
as failure, dropping the piece. That is why one side of the cutting line
worked and the other emptied the drawing.

Elevation is read from whichever point list the spline actually has. A
fit-point spline has no control points, so it was falling to z = 0 on the
first edit.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 17:08:17 +03:00
Hakan SevenandClaude Opus 5 0f8f4864ba refactor(spline): evaluate splines with cadkernel, weights and all
The spline path ran on truck's BSplineCurve<Point3>, which is not
rational. Weights were dropped on the way in and cleared on every split,
so a NURBS circle — which is how most circles arrive once they have been
through a modeller — was evaluated as a parabola and stayed one after a
break. cadkernel's NurbsCurve carries the weights and applies them in
homogeneous coordinates, and `spline_cut` splits by raising the cut to
full knot multiplicity, so both halves are exact and both stay rational.

`spline_ops` keeps the knot-domain call shapes the commands were written
against — `spline_range` replaces reaching for `range_tuple`, `spline_cut`
replaces `cut` plus the rebuild — so TRIM, BREAK, LENGTHEN and OFFSET
changed only where they named those.

Knot vectors for a rebuilt control polygon come from the kernel too, so
the clamped uniform vector is defined once rather than assumed in two
places.

truck is now absent from the whole spline path. It remains for 3D solids.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 16:55:07 +03:00
Hakan SevenandClaude Opus 5 06a0fc4ddf fix(trim): cut a closed ellipse at the right span
Two faults met on the same command, both of them older than the move to
cadkernel and both hidden until ellipse previews started rendering
truthfully.

The survivor came back in two pieces. `trim_ellipse` handed its cuts to
`trim_intervals`, which is the open-curve routine: it anchors on bounds at
0.0 and 1.0, so a closed ellipse gained invented cuts at the parameter
seam and the surviving arc arrived split either side of it. Worse, a click
in the wrapping piece landed in one of those two halves, so the wrong span
disappeared. A closed ellipse now takes the cyclic route circles already
used — find the cut-to-cut gap holding the click, keep everything else,
and let end < start signal the wrap.

The cut span was also short. That came from `line_ellipse`, fixed upstream
in cadkernel 46e76cc: it returned the geometric angle at the centre rather
than the ellipse parameter, which are the same thing only on a circle. On
anything eccentric the cuts sat tens of degrees off where the line
actually crossed, so the removed piece was consistently too small. The
lock bump brings that fix in.

Elliptical arcs, lines, rays, xlines and open splines keep using
`trim_intervals`; nothing about the open-curve path changes.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 15:42:30 +03:00
Hakan SevenandClaude Opus 5 d0cc3284d2 refactor(modify): take plane geometry from cadkernel
TRIM, FILLET and EXPLODE each carried their own copy of the same plane
maths. The copies had already drifted apart: `ll` differed only in
parameter names between trim.rs and fillet.rs, `arc_pts` only in which
angle helper it called, and `norm` meant angle normalisation in trim.rs
and explode.rs while meaning vector normalisation in two other files.

The implementations now live in cadkernel's geom2d. A new local `geom`
module adapts the call shapes — the kernel takes `[f64; 2]` points and
returns `[f64; 3]`, while these commands pass loose scalars and feed
`WireModel::points`, which is `[f32; 3]`. Narrowing stays on this side
because only the caller knows whether it is a preview wire, which leaves
`points_low` empty and accepts the loss, or resident geometry, which
splits the f64 into a high/low pair instead.

Call sites are untouched: the kernel functions are imported under the old
local names, so the signatures they were written against still hold.

This also fixes ELLIPSE trim and extend previews. `ellipse_pts` wrote its
components as [x, z, y] while `arc_pts` — reached from the same match in
`entity_pts`, feeding the same `[f32; 3]` — wrote [x, y, z], and
`WireModel::points` is documented as world-space positions. Ellipse
previews were collapsing onto y = 0 and displacing along Z. The kernel's
`ellipse_arc` orders them correctly, so delegating corrects it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 15:27:45 +03:00
Hakan SevenandClaude Opus 5 5c5c7606a1 feat(select): offer the selection modes at "Select objects"
Only Previous and Last were on offer, so Window, Crossing, Fence, the two
polygons and All could not be reached at all -- and with no keyboard to
type them into, the buttons were the only way in.

Window and Crossing fix the sense of the next box. Dragging derives that
from the direction the corner travels and would overwrite it the moment
the corner moved, so the derivation stands down while a keyword holds it.
Add and Remove decide what a pick does for the rest of the round: Remove
subtracts the way Shift already does, rather than through PICKADD, which
decides whether a pick *replaces* the set and so would have wiped
everything gathered so far. Both are dropped when the next command
starts, since neither outlives the selection that asked for them.

Fence and the polygons are picked point by point, which TRIM and EXTEND
already ask for. That gesture -- the points, the rubber-banded dashed
preview, whether the path closes -- moves into one place both bind to,
and the two preview builders duplicated in trim.rs go with it.

A fence is not the polygon test with crossing set: that closes the point
list and counts whatever lies inside, so a line drawn past a row of
objects would take everything behind it too. It gets a hit test that only
counts a real intersection, and skips the area-based geometry entirely.

The lasso gathered its hits from wires, hatches, block hatches, meshes and
block meshes inline; the new modes need the same list, and a second copy
would drift. Both now come through `Scene::path_hit_handles`.

Closes #596

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 13:32:11 +03:00
Hakan Seven b92b8df997 fix(trim): handle lasso and box selection
Keep selection gestures in the active trim flow so crossed segments are modified instead of selected.\n\nRefs #487
2026-08-05 00:26:29 +03:00
Hakan Seven b1d55c6a7e feat(view): add visual style previews
- preview render modes on hover without committing
- distinguish 2D and 3D wireframe fill and depth behavior

Refs #483
2026-08-04 21:23:25 +03:00
Hakan Seven 3e6693b93f feat(locale): localize full interface 2026-08-02 22:09:25 +03:00
Hakan Seven c5b5cd9dee fix(trim): preserve uncut polyline segments
Refs #588
2026-07-30 21:21:40 +03:00
Hakan Seven deec48ad99 perf: keep heavy work off UI thread 2026-07-25 18:49:18 +03:00
Hakan SevenandClaude Fable 5 9b28170d4b fix(undo): grip-drag history entry + mid-command Ctrl+Z steps (#332)
Grip drags mutated the document without ever pushing history — dragging
a polyline vertex (or any grip) could not be undone. The release-commit
now swaps the pre-drag backup in, snapshots that PRE state as a "GRIP"
entry, swaps the dragged result back and clears the redo stack; the
Esc-cancel path is unchanged and a no-move engaging click records
nothing.

Ctrl+Z during a drawing command steps the command instead of the
document undo swallowing the in-progress object (which also killed the
active command): new CadCommand::on_undo_step hook, consulted before
undo_active_tab. PLINE pops its last vertex (shared with the U option),
LINE reverts the last committed segment and keeps drawing from the
previous point (rides its existing U/UndoDocument path), and ARC
Continue reverts the last arc and resumes from the prior anchor with
the correct tangent via a new anchor history. With nothing placed yet
each falls through to the document undo.

Also drops the two warnings the #336 work left behind (unused Entity
import, dead Snapper::override_active).

Closes #332

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-22 15:48:22 +03:00
Hakan SevenandClaude Fable 5 9b40881ab1 feat(trim): TRIM/EXTEND options — edges, fence, crossing, edge, erase (#336)
Both commands gain a shared option mode machine:

- Cutting/Boundary edges (T/B): pick which objects act as the boundary
  (picked edges highlight yellow, the hovered candidate half-strength);
  Enter rebuilds the boundary set, empty = everything (quick mode).
- Fence (F): draw a dashed fence; every object crossing it is trimmed /
  extended at each crossing, re-applied to the surviving pieces so a
  fence across both overhanging ends clears both. Live red/cyan result
  preview while placing points.
- Crossing (C): crossing-selection semantics — the dashed green window
  picks objects crossing its edges AND wholly inside; runs through the
  fence machinery with a synthesized inside click.
- Edge (E): boundaries extrapolate (lines run past their ends, arcs
  close to full circles). Cuts that land on an implied extension draw a
  dashed yellow guide from the boundary's drawn end to the cut point,
  so the user sees which edge causes the cut. Extend previews also show
  the added tail as a bright dashed slice of the true curve.
- Erase (R, TRIM): picks delete without needing an intersection, with a
  red hover preview.
- Shift+click swaps trim<->extend per pick (new set_shift plumbing into
  the entity-pick and hover paths); the hover preview follows the held
  state. Project is out of scope for the 2D pipeline.

The per-type pick bodies moved to shared free fns (pick_trim_at /
pick_extend_at) used by direct picks, hover previews and the fence
pass; EXTEND's snapshot bookkeeping unified with TRIM's placeholder
flow. Option keywords now take precedence over the hex-handle reading
in feed_active_cmd ("F"/"C"/"E" are valid hex), and fence geos carry
sentinel handles — probing with NULL against NULL-handle geos excluded
the whole fence. Regression test: fence_trims_x_arm.

Closes #336

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-22 14:53:50 +03:00
Hakan SevenandClaude Fable 5 41f7e2becf fix(extrim): polyline cutting boundaries and polyline targets (#340)
Boundary: a picked polyline explodes into per-segment Line geos, and
extend_line_geos then blew every one of them up to +-TRIM_EXTENT — a
closed rectangle boundary became a grid of infinite lines and the
parity side-test broke. Only an actual LINE entity boundary is treated
as infinite now.

Targets: the sampled trim path sampled polylines at their vertices
only, so a straight segment crossing the boundary was cut at the wrong
place — or not at all when both endpoints were on the kept side.
insert_boundary_crossings splices the exact line_seg_ts intersections
into the sample list and extrim_sampled classifies per segment
midpoint (a vertex exactly on the boundary can't flip the parity
test). Ellipse and spline targets gain the exact cut points too. The
removal preview subdivides straight polyline segments so the red/blue
split follows the boundary instead of jumping at vertices.

Closes #340

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-22 12:04:30 +03:00
Hakan SevenandClaude Fable 5 bd27ee31d0 fix(trim): arc-arc intersections tested against wrong circle (#370)
cc_angles returns angles on the TARGET circle, but the arc-boundary arm
of arc_seg_ts filtered them against the BOUNDARY arc's angular span —
wrong circle, so arc×arc (and circle×arc, EXTEND-to-arc, bulge-polyline
segment) intersections were dropped and TRIM neither highlighted nor
cut. Re-express the intersection point as an angle on the boundary
circle before the span test, matching line_seg_ts/ellipse_seg_ts.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-21 22:45:28 +03:00
Hakan SevenandClaude Fable 5 e177f92ff5 fix(xref): blocks respect their internal draw order
Xref merge now inserts entities in the source block's chain order and
carries SortEntitiesTable objects across (remapped to merged handles).
draw_depth_map values become [depth, half] so exploded block/xref fills
and wide-polyline band wires both compose child depth as
insert_depth + in-block rank x half, recursing into nested inserts with
the same divisor - bands and fills from one block interleave by the
block's internal order instead of stacking at the insert's single depth.
Band batches split per rank via WireModel.depth_override; thin wires
stay merged. The wire arena falls back to a full rebuild when a slab
carries mixed depths so refreshes don't flatten band overrides.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-21 15:53:28 +03:00
Hakan SevenandClaude Fable 5 8ff7b6b25c feat(extend): arcs extend to their boundaries
EXTEND's pick dispatcher covered lines, ellipses, polylines and
splines but never arcs, though TRIM already handled them (#409).
extend_arc gathers every boundary crossing around the arc's full
circle (reusing arc_seg_ts against all boundary kinds) and walks the
clicked end to the nearest one — CCW for the end angle, CW for the
start. The hover preview draws the extended result like the other
types, and the replace-snapshot patches the new Arc's handle.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-21 14:58:56 +03:00
Hakan SevenandClaude Opus 4.8 b3016ca892 feat(polyline): smooth per-segment taper for varying-width wide polylines
A wide polyline with per-vertex varying widths rendered at its widest edge
(uniform-width ones were already exact). Draw the true taper instead.

`WireModel.taper_widths` carries a per-point full band width. The wire
instance (native slim + web fat) gains per-endpoint `world_hw_a/b`, and both
wire shaders interpolate `mix(hw_a, hw_b, which_end)` so each segment's band
narrows/widens linearly — depth-tested, and the linetype dash still applies.

Alignment (widths must match the tessellated points) is solved by building
both together: a new `TruckObject::TaperedLines(points, widths)`, filled by
`common::tapered_band_points` (straight = endpoints, arc = 16 samples with the
width lerped along it). to_truck routes a varying-width polyline here —
LwPolyline before the PLINEGEN split, Polyline2D at its return — while a
uniform-width one keeps the constant-band Contour path unchanged. `world_width`
stays the widest edge as the PDF-stroke + zoom-floor fallback.

Blocks fall back to the constant-widest band (`LocalWire` carries no
taper_widths).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-20 16:36:06 +03:00
Hakan SevenandClaude Opus 4.8 bd1a5affd7 feat(lwpolyline): render the wide-polyline band via the wire shader
A wide polyline's flat band was drawn by the hatch pipeline: a solid fill
with only draw-order depth. It ignored the entity linetype (the width
"crushed" the dash pattern) and showed through 3-D geometry in a tilted view.

Draw the band from the polyline's own centre-line wire instead. A new
`WireModel.world_width` (drawing units) makes the wire vertex shader expand
the quad by `world_half_width / world_per_pixel` (clamped to a hairline), so
the band is sized in world units and tracks zoom. Because it is now a wire:

- the GPU dash shader dashes the band by arc-length — the linetype applies;
- it is depth-tested at the polyline elevation — no draw-order see-through;
- no CPU band triangles or per-dash geometry are generated.

`world_half_width` rides in `WireConst` (native, replacing a pad) and the web
`WireInstance` (loc 14); both wire shaders branch on it. The tessellator sets
`world_width` from the LwPolyline width on the Contour / SegmentedLines arms,
and the model-space fill loop no longer emits a hatch band for any LwPolyline.
Pick still uses the band polygon (`pick_tris`).

Blocks keep the hatch band: `LocalWire` carries no `world_width` (finalize
zeroes it), so a scaled insert still scales its band correctly.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-20 14:50:27 +03:00
Hakan SevenandClaude Opus 4.8 626e8091f7 refactor(viewport): unify content-viewport clipping on the stencil path
Rectangular paper viewports now go through the same GPU stencil clip as
non-rectangular ones: viewport_clip_boundary_ndc returns the four rect
corners (a full-target mask) instead of nothing, so the render path has
no rect/non-rect special case — every content viewport carries a clip
boundary and draws with stencil reference 0xFF.

With a single clip path, the old per-wire / -image / -wipeout scissor
mechanism is fully dead and removed: compute_*_scissors, the
*_pixel_scissors buffers, the project_scissor helper, the per-pass
set_scissor_rect blocks, the vp_scissor field threaded through
WireModel / ImageModel / HatchModel and their GPU mirrors (WireGpu /
ImageGpu / WipeoutGpu / HatchWebGpu), from_run's scissor parameter, the
scissor grouping in build_wire_buffers, and the is_arena_eligible gate.
Net -245 lines across 54 files.

Rectangular viewports still clip exactly via their own render rectangle;
the stencil mask is a redundant full-target no-op there, kept only so
the clip code path stays uniform. XCLIP keeps its CPU wire clip.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-20 11:06:28 +03:00
Hakan SevenandClaude Opus 4.8 50f6196594 fix(render): flag 2D overlay fills so they show in every view mode
A SOLID's filled quad and a dimension/MultiLeader text background are flat 2-D
overlays that should draw in wireframe and shaded alike, but face3d_gpu
classified fills by `!fill_tris_low.is_empty()` — and a 2-D fill at UTM scale
carries a double-single low residual for precision just like a real 3-D mesh
face. So the arrowhead fill and the "5;8" cyan text background were routed to
the 3-D buffer and only appeared in shaded mode, vanishing back to nothing on
return to wireframe.

WireModel now carries an explicit `fill_is_3d`: true only for a real surface
(PolyfaceMesh / PolygonMesh face, set in the tessellator), false for every flat
overlay fill. face3d_gpu classifies on the flag, so 2-D fills stay in the
always-visible buffer while keeping their low residual (and UTM precision).

Verified on budweiser: the `_BoxFilled` arrow SOLID and the dim '5;8' MTEXT
background both tessellate with fill_is_3d = false.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 15:15:24 +03:00