Commit Graph
2031 Commits
Author SHA1 Message Date
Hakan Seven d80ee709ac fix(otrack): clear and localize tracking hint 2026-08-09 13:04:55 +03:00
Hakan Seven ae24f985f0 Merge pull request #714 from gianlucafiore/feature/tracking-reference-tooltip
Show active tracking reference tooltip
2026-08-09 12:57:50 +03:00
Hakan SevenandClaude Opus 5 1a37562a28 chore: take the stack with its solid layer and ACIS bridge
The kernel gained a B-rep layer — topology, analytic surfaces, surface
intersection, face splitting, point classification, booleans — and
acadifc gained the bridge that lifts an ACIS document into it and lowers
it back, provenance-driven so an untouched body saves byte for byte.

The reachability test is a compile-time one on purpose. A dependency
chain that stops resolving is caught where it breaks rather than the
next time somebody reaches for the far end of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 11:26:57 +03:00
Hakan Seven 2f60dd0f59 refactor(i18n): group Fluent attributes 2026-08-09 11:12:47 +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 <noreply@anthropic.com>
2026-08-09 10:02:23 +03:00
Hakan SevenandClaude Opus 5 ca85856f52 refactor(hatch): clip the pattern against the boundary in the kernel
The pattern's lines were clipped by sorting their crossings with the
boundary and taking every other span. That is right where a line cuts a
boundary cleanly and wrong wherever it grazes one: a line through a
corner crosses twice in effectively one place, and from there every span
is inverted — the fill lands in the holes and the solid comes out empty.
A 1e-5 dedup on the crossings hid some of it and could not fix the rest,
because which crossings to merge is not the question.

The kernel divides the line by its crossings and then asks each span
whether its middle is inside, which costs a containment test apiece and
cannot get out of step. It also accepts a curved boundary rather than
only a polygon, which this call site does not need yet and a section
fill will.

What stays is the PAT frame — the family angle, spacing, origin offset,
the k range and the dash walk — which is a format convention rather than
geometry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 09:59:19 +03:00
Hakan Seven 52799786f4 feat(i18n): complete locale catalogs 2026-08-09 09:44:54 +03:00
gianlucafiore a06e8a7a69 Show active tracking reference tooltip 2026-08-08 23:09:06 -03:00
Hakan SevenandClaude Opus 5 f1bc5f2cae fix(spline): reverse a spline without reshaping it
SPLINEDIT's REVERSE reversed the control points and then regenerated a
clamped *uniform* knot vector. That is only harmless for a spline whose
knots were evenly spaced to begin with; for any other — which is most of
them once a modeller has been near one — the curve came back a different
shape, bulging where it had been taut. It now mirrors the knots within
the domain, which is exact, and swaps the end tangents to match.

PEDIT's Spline option evaluated a uniform cubic B-spline from four
blending polynomials written out by hand, clamped the open case by
repeating end control points, and then pinned the two endpoints
afterwards to undo the drift that leaves. A clamped knot vector says all
of that directly, and the closed case gets a genuinely periodic curve
rather than one whose seam is a special case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:52:12 +03:00
Hakan SevenandClaude Opus 5 0677dd146c refactor(blend): evaluate the Bézier with de Casteljau from the kernel
BLEND summed Bernstein terms using a binomial-coefficient table with
entries for degrees three and five — the two it builds — and a fallback
of one for everything else. Correct today, and quietly wrong the moment
a third continuity mode is added. De Casteljau needs no coefficients at
all, works at any degree, and is steadier: the Bernstein terms are large
coefficients times small powers and they cancel, while repeated
interpolation only ever averages neighbours.

Curvature through three points moves with it, since both are the 3D
arithmetic a blend needs and neither has a planar counterpart to
delegate to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:47:04 +03:00
Hakan SevenandClaude Opus 5 77a9167e34 fix(area): measure the curve, not a polygon through some of its points
A hatch's area was not the area of anything. Its boundary was flattened
into a ring by pushing an arc's *centre* into it, and a spline's control
points — neither of which is on the boundary — and the shoelace formula
was run over the result. Boundary edges now go through the kernel, which
measures each for what it encloses: an arc contributes its sector, a
spline is integrated.

AREA had three measurements between them. The bulged-polyline one
carried its own arc corrections; the planar entities it did not cover
fell through to mass properties. Both are now one call on the entity's
curve, which gets the bulges, the ellipse and the spline right in the
same place. What is left is the genuinely spatial — a 3D polyline, a
spline whose points wander off any plane — measured as rings of points
by Newell's method rather than by projecting them flat.

Regions were doing the same Newell sum by hand. They share it now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:44:04 +03:00
Hakan SevenandClaude Opus 5 9babf355b1 refactor(fillet): find the tangent arc in the kernel
FILLET enumerated its own candidates twice, once for a line against an
arc and once for two arcs, each with its own quadratic, its own four
sign combinations and its own conventions about which root meant what.
Both are the same construction: a circle of the given radius tangent to
a curve has its centre on one of that curve's offsets, so the fillet
centre is where an offset of one crosses an offset of the other.

That now comes from the kernel, which does it for any pair of straight
and circular curves and agrees with the corner formula where both apply.
What stays here is the part that genuinely belongs to the command —
which candidate the two picks meant, whether the touch lands on the
drawn part of an arc rather than elsewhere on its circle, and which side
of each entity to keep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:37:11 +03:00
Hakan SevenandClaude Opus 5 f9a0d73dc6 refactor(hatch): take the boundary arrangement from the kernel
624 lines become 70. What went is the planar arrangement itself —
splitting segments at their crossings, welding coincident endpoints and
tracing the bounded faces of the resulting graph — which is not a
drawing-specific problem. It is the same one a B-rep boolean solves in a
face's parameter space once the intersection curves are projected into
it, so solving it in the kernel means the boolean does not arrive with a
second copy.

What stays is reading the wires, which is where the drawing's own
conventions are: NaNs separating the runs inside one WireModel, and the
double-single coordinate pair that has to be recombined before anything
is measured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:30:32 +03:00
Hakan SevenandClaude Opus 5 d32f39221c fix(measure): space points along a curve by distance, not by parameter
DIVIDE and MEASURE did nothing at all on an ELLIPSE or a SPLINE. Their
length function answered `0.0` for both — the `_ => 0.0` arm — and the
command reported the entity type as unsupported. On a polyline they
measured the chords and ignored the bulges, so points landed short of
where they belonged; on an arc or a circle they read the centre as world
coordinates when the entity stores it in its OCS.

All three now walk the entity's own curve, which the kernel measures
properly: closed forms where they exist, and the integral of the speed
where they do not.

A path ARRAY had the same confusion in a different place. It sampled the
path at even *parameters*, which is the same as even distances on a line
and a circle and nothing like it on an ellipse or a spline — copies
bunched up wherever the parameter ran slow. It also now closes properly:
on a closed path the last copy no longer lands on the first.

LENGTHEN measured an ellipse with a hundred and twenty-eight chords and
then bisected on that, and a spline with forty rounds of thirty-two
chords apiece. A chord sum reads short, so "current length" was already
below the true one before any delta was applied. Both now ask the kernel,
which also makes the spline case one evaluation instead of about
thirteen hundred.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:24:29 +03:00
Hakan Seven e6345f89f7 feat(i18n): expand language coverage
Complete missing keys in the existing translations and add selectable Spanish, Brazilian Portuguese, Arabic, and Japanese resources.\n\nThe large compatibility catalogs for the new locales remain in progress.
2026-08-09 01:15:51 +03:00
Hakan SevenandClaude Opus 5 ece440bcab refactor(curve): draw a fit spline and an offset preview from the curve
Two places were left describing geometry for themselves.

A fit-point SPLINE was drawn from a C² cubic solved in spline.rs while
TRIM and snap cut a separate interpolation from the kernel. Near enough
to look right, far enough apart that a trim landed beside the line it
was aimed at. It now draws the curve those commands work on, and falls
back to the local solve only where the kernel declines — a fit spline
through points in space is not a planar curve, so there is nothing for
it to say.

Closed fit splines were worse than that: the kernel's interpolation is a
clamped solve with no wrap in it, so the curve TRIM used never returned
to its start and the seam cut nothing. Repeating the first fit point
closes it. The seam is C¹ where the rest of the curve is C², which is
the honest limit of a clamped solve — the alternative was a curve with a
gap in it.

OFFSET's preview sampling had drifted the same way TRIM's had: an arc
and a circle read `center.x/.y` as world coordinates when the entity
stores them in its OCS, so an extruded one previewed on the wrong side;
every curved kind was cut at a fixed twenty segments per radian whatever
its size; and an ellipse rebuilt its axis frame from the major axis' X
and Y alone, dropping the Z the entity stores. A hundred lines of that
become one tessellation. A RAY now previews as far as an XLINE does
rather than as a stub the length of its direction vector.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:08:45 +03:00
Hakan SevenandClaude Opus 5 90abfc21a9 refactor(render): tessellate curved entities from their own curve
The point lists were built three different ways and none of them scaled
with the geometry. A full ellipse was cut into sixteen segments per half
and an elliptical arc into thirty-two, whatever its size, so anything
large read as a polygon; a circle worked out its own segment count from
the chord tolerance; an arc was handed to truck's arc-through-three-
points builder, which cancels at survey coordinates.

All three now sample the entity's curve at the render pass's chord
tolerance, so how round something looks follows how big it is and how
far in the view is zoomed. The ellipse also stops casting its local
coordinates to f32 on the way in — the vertex path splits f64 into a
double-single pair precisely so that precision survives to the shader,
and throwing it away first defeated that.

What does not change is the kind of object each entity hands over.
EXTRUDE, REVOLVE and SWEEP read their profile and path out of Contour or
Curve and have no arm for a bare point list, so an arc or an ellipse
given as Lines would quietly stop being usable as either. The arc keeps
its circle_arc edge for that reason; only the ellipse's control points
change, and they keep the same two-half-edge wire.

The ellipse now decides it is closed from its own endpoints rather than
from its stored parameters, so a file that writes an end a hair short of
a full turn still produces a wire a plane can attach to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 00:55:29 +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 <noreply@anthropic.com>
2026-08-09 00:43:53 +03:00
Hakan SevenandClaude Opus 5 b36e85e46e refactor(snap): take an entity's snap points from its curve
Each entity built its own, and what they offered had drifted apart from
what the geometry actually has:

- An arc offered no quadrants at all. It now offers the ones its sweep
  covers, and no more.
- A spline offered its control points as endpoints. For a control-point
  spline those are not on the curve, so the cursor landed in empty space
  beside the geometry. It now offers its two real ends and its midpoint,
  with the fit points — which are on the curve — still on offer.
- An elliptical arc had no midpoint and no ends.

A new SnapHint::Endpoint carries an end that cannot go in key_vertices.
Consecutive entries there are taken to be joined by a straight segment
and their midpoint offered, which is right for a line or a polyline and
wrong for anything curved: an arc's two ends would also produce a snap
out on the chord. Splines lose their key_vertices for the same reason —
the chord between two fit points of a curvy spline does not run along
it.

A polyline's midpoints and arc-segment centres stay out of snap_pts
deliberately. The engine already derives a chain's midpoints from its
vertices, and a wire carrying a centre is treated as round elsewhere,
which a polyline with one bulge in it is not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 00:43:44 +03:00
Hakan SevenandClaude Opus 5 57661c5754 feat(entities): give an entity its geometry as a curve
The same shape was being read out of an entity's fields in three places
and the three had drifted. An arc's tessellation cut it at 20 segments
per radian, TRIM's sampling at 16, and its snap points came from a
fourth expression again; a spline was sampled at a fixed 96 points
however long it was; TRIM read an arc's centre as world coordinates
when the entity stores it in its OCS, so an arc carrying the (0, 0, −1)
normal a MIRROR leaves behind acted as a boundary on the wrong side of
the drawing.

entity_curve returns a PlanarCurve: the shape, plus the plane its
coordinates are read in. The plane comes from the entity's OCS — its
extrusion normal through the DXF arbitrary-axis algorithm — not from the
UCS. A UCS is the working plane a user drew in and an entity does not
remember it: draw a circle in a UCS turned 30° about Z and the stored
normal is still +Z, which is why the header keeps the UCS separately.
The algorithm itself stays in transform.rs, where the format is
understood and where #142's degenerate case was fixed once.

Entities that store world coordinates and may not be planar at all —
LINE, SPLINE — are checked rather than assumed. A spline whose points
wander in space returns None instead of being flattened onto XY, which
would move it.

entity_curve_xy folds the plane away for the editing commands, which
work in plan view and need every boundary in one frame before two of
them can be intersected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 00:43:35 +03:00
Hakan Seven 8847724127 fix(cursor): restore default geometry
Keep CURSORSIZE 5 and PICKBOX 3 mapped to the pre-option 60 px arms, 15 px box, and 8 px selection aperture.\n\nRefs #413
2026-08-08 21:53:26 +03:00
Hakan Seven 1738f9a79b feat(cursor): add crosshair options
Add persistent cursor sizing, pick aperture, color, pointer mode, isometric drafting planes, and SNAPANG-aligned grid behavior.\n\nRefs #413
2026-08-08 21:32:44 +03:00
Hakan SevenandClaude Opus 5 188ac80b2e chore: take the stack without nalgebra
The codec declared nalgebra and never used it — its Vector2 and Vector3
are plain structs of its own. It was pulling eleven crates into every
build here for nothing.

Also picks up cadkernel's Vec2, which replaces the vector arithmetic the
kernel had been writing out by hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 21:15:24 +03:00
Hakan Seven adb8ced8b5 feat(view): complete ZOOM options
Add command-line navigation modes and persistent wheel controls.\n\nRefs #412
2026-08-08 20:37: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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
2026-08-08 19:53:55 +03:00
Hakan Seven 5bd5c884e0 fix(aliases): map CP to COPY 2026-08-08 19:20:34 +03:00
Hakan Seven 65b882dd76 fix(hatch): harden boundary extraction
Use a sweep broad phase, preserve f64 coordinates, and split collinear overlaps so large drawings remain responsive and precise.
2026-08-08 18:59:56 +03:00
Hakan Seven 598de3e47a Merge pull request #703 from gianlucafiore/feature/699-hatch-pick-boundary
Add internal-point hatch boundary detection
2026-08-08 18:36:46 +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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
2026-08-08 16:55:07 +03:00
Hakan SevenandClaude Opus 5 95e61f3e8d refactor(fillet): take the corner arc from cadkernel
`compute_fillet` keeps the entity work — resolving which side of each line
the pick kept, and trimming the lines back — and hands the geometry out.
The kernel takes the corner as an apex plus the two keep directions, which
is what the side resolution here already produces, and returns the tangent
points, centre and the endpoints in fill-the-corner order.

`line_circle_ts` and `circle_circle_pts` were further copies of maths the
kernel already had; the second now calls `circle_circle_points`, added for
callers that want the crossings as points rather than as angles to cut at.

`arc_span` and `clamp_angle_to_arc` stay put on purpose. This file's
`arc_span` treats any span under 1e-6 as a full turn, where the kernel's
only does so at zero or below. That difference is real for a degenerate
arc, and picking a winner is a separate question from moving the code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 16:20:34 +03:00
Hakan SevenandClaude Opus 5 397fb38292 refactor(offset): take polyline offsetting from cadkernel
The offset core moves out: normalising the source, the cavalier_contours
call, the sharp-corner join fixup and the conversion back. What stays is
entity work — reading an LwPolyline in, writing offset LwPolylines out,
and the per-type offsets for lines, arcs, circles, ellipses and splines,
which are analytic and never needed the polyline machinery.

`BulgeArc` moves with it, since the offset preprocessing splits over-half-
turn arcs and cannot work without it. `entities::common` now re-exports it
from the kernel, so the twelve modules already reaching for
`entities::common::BulgeArc` are untouched.

`norm_rad` was a fourth copy of angle normalisation, after the three
removed from trim, fillet and explode. It now aliases the kernel's.

cavalier_contours leaves this crate's manifest: nothing here calls it any
more, and it arrives through the kernel's `offset` feature instead, which
acadifc forwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 16:01:43 +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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
2026-08-08 15:27:45 +03:00
gianlucafiore 7d5818d5e0 Add internal-point hatch boundary detection 2026-08-08 09:09:43 -03:00
Hakan SevenandClaude Opus 5 051272ad71 build: reach the CAD stack through acadifc
The DWG/DXF/ACIS codec now lives in its own crate and acadifc re-exports
it alongside the geometry kernel, so this is the single dependency:

    OpenCADStudio -> acadifc -> { cadcodec, cadkernel }

acadifc is aliased to `acadrust`, which leaves the 2569 `use acadrust::…`
paths across 253 files untouched — the alias is a manifest-level rename,
not a source change.

The old `[patch.crates-io]` entry goes away with it: the dependency is a
git revision now rather than a crates.io version being redirected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 13:11:33 +03:00
Hakan SevenandClaude Opus 5 3d5f400b9d fix(statusbar): keep the cursor visible over a disabled menu row
A greyed-out option in a status-bar menu made the mouse pointer vanish.
A disabled button reports no mouse interaction at all, and a menu is an
overlay: when the overlay reports none, the cursor is decided by whatever
lies beneath it, which over the drawing is the canvas hiding the pointer
so it can draw the crosshair itself. Enabled rows were fine only because
they ask for a pointer and so never fall through.

Every row now answers for the cursor over it, doing something or not.
`mouse_area` fills in only where the content stays silent, so an enabled
row keeps the pointer it asked for.

Fixed for every status-bar menu rather than the one that showed it: they
all build their rows through the same place, and the next menu with a
disabled row would have had the same hole.

Closes #684

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 09:56:58 +03:00
Hakan SevenandClaude Opus 5 6e3356fb27 fix(statusbar): let the cursor reach a menu without closing it
A status-bar menu closed as soon as the cursor left its own rectangle.
These menus hang off a pill barely wider than its icon, so reaching for a
row's text means moving up and across at once, and the diagonal passes
through the gap between the pill and the menu. Moving straight up worked;
moving toward the thing you were aiming at did not.

The safe area now extends one row of the bar in every direction, which is
the room that diagonal needs. The bar's row height stops being a literal
at its own call site and becomes ROW_HEIGHT, so the two stay together if
the bar is ever resized.

Closes #682

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 09:45:35 +03:00
Hakan SevenandClaude Opus 5 6ca4463a26 fix(web): give the browser build a load_file to fail through
Reading a drawing by path is gated off on wasm, and two features added
since 0.9.3 call it unconditionally: importing a drawing as a block, and
taking layer standards from one. Both compile into the web build, so the
web bundle stopped building and the v0.9.4 Pages deploy failed.

Neither feature can work in the browser — a file arrives through the page,
not from a filesystem the app can open — so the fix is not to make them
work but to let them say so. `load_file` now exists on wasm and returns
that as its error, which both callers already surface. Keeping the
signature means neither has to know that the web has no files, and the
next feature that reads a second drawing will not break the bundle
either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 03:34:57 +03:00
Hakan SevenandClaude Opus 5 17b2f158f4 chore(release): prepare v0.9.4
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.9.4
2026-08-08 03:23:27 +03:00
Hakan SevenandClaude Opus 5 2feac73eec fix(locale): put TCOUNT's new prompts back in the catalog
The placement options arrived with their prompts and messages written as
plain Rust strings, so they were the only command left speaking English
whatever the interface was set to.

They go through t! / tf! now like every other prompt, and the strings are
registered so the call actually reaches a translation: a source string
only resolves if the generated lookup knows it, and t! on an unknown
string just hands the source back. Keys follow what the table already
uses — the slug of the source, cut to forty-six characters, and the first
ten hex digits of its SHA-1 — which reproduces 2978 of the 3161 entries
exactly; the rest are hand-written ones predating the generator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 03:19:54 +03:00
Hakan Seven 7ef5b431e1 Merge pull request #698 from gianlucafiore/fix/697
Fix TCOUNT numbering and add placement options
2026-08-08 03:07:09 +03:00
Hakan SevenandClaude Opus 5 7307ea3136 fix(dim): measure a two-line angle between the lines that define it
A two-line angular dimension stores two lines, not two rays:
first_point→second_point is one, angle_vertex→definition_point the other,
and the angle is the one they make where they cross. The renderer read
angle_vertex as the centre and the other two points as the rays from it,
which measures something else entirely — a ten degree angle came out as
two hundred and seventy, and a shallow one as very nearly a full circle.

The lines are now intersected for the real vertex, and the arc point
picks which of the four angles at that crossing is meant. Each line
offers its direction and its reverse, and the sweep is tried from either
line: taking only the first→second order leaves out half the angles, and
the half left out is the one wanted whenever the lines are nearly
parallel. Parallel lines cross nowhere, so only the extension lines are
drawn.

A zero sweep was also being turned into a full turn, because the wrap
that pushes a negative sweep forwards ran on zero too. Two rays pointing
the same way enclose no angle and now draw none.

Checked against the three angular dimensions in a drawing that carries
its own baked arcs: the vertex and sweep computed from the stored line
endpoints reproduce the baked centre and angle to the decimal — 7.48,
10.61 and 2.29 degrees — from entirely separate inputs.

This is the path taken when a dimension has no baked block: one OCS drew
itself, or one whose block was dropped by an edit. Both paths now agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 02:59:34 +03:00
Hakan SevenandClaude Opus 5 db43c21d55 fix(annotative): scale text by its own annotativeness, not its style's
An annotative object is drawn at the annotation scale only for the scales
it has a representation for. Text was being scaled whenever its text
style carried the annotative flag, whether or not the text itself had any
representation at all.

A drawing of twenty floors here has 1144 pieces of text on a style
someone flagged annotative and no object contexts anywhere. At 1:100 they
were drawn a hundred times too tall — a 411-unit height reaching 41067 —
while every other style in the same drawing stayed put.

`is_annotative` already said the right thing, and said why: a text style
can be turned annotative long after text was drawn in it, and that text
keeps its stored height until it is explicitly converted. The scale
resolver contradicted it. Text now scales on its own annotativeness
alone: an object context, a legacy XDATA marker, or the entity's flag.

Dimensions, leaders, tolerances and tables are unchanged — there the
style is the standard signal and still counts on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 02:45:59 +03:00
Hakan SevenandClaude Opus 5 2ea3bca093 feat(properties): stop offering a height the style has fixed
A text style with a non-zero height fixes the size of everything drawn in
it — the CAD that writes the file skips the height prompt for such a
style, and shows the height without letting it be changed. OCS offered
the field either way, so a drawing could be edited into disagreeing with
its own style.

The height row is now read-only when the style fixes it, for TEXT, MTEXT
and both attribute kinds. `num_prop` already draws a numeric row either
way, so the value still reads the same, just grey.

The panel builders are handed an entity, not the document it came from,
and widening that trait would touch every entity type. The lookup rides
beside the unit context instead, seeded from the same place, since it
answers the same kind of question: something the drawing settles that a
row needs in order to render itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 02:45:46 +03:00
Hakan SevenandClaude Opus 5 188d1a9ccb fix(dim): draw a dimension from the picture it carries
A DIMENSION references the block "that contains the entities that make up
the dimension picture", and that block is the picture: AutoCAD requires
it and draws it, BricsCAD draws it when present and only renders from the
dimension variables when it is missing. OCS re-derived the picture from
DIMVARS every time and ignored the block entirely.

That works while a style agrees with what the drawing actually drew. A
Revit-exported schedule here does not: its style stores DIMTXT already in
drawing units beside a DIMSCALE meant to scale it, so the text came out
three hundred times too tall, and each dimension carries a DIMFXL
override that is likewise already in drawing units, so the extension
lines ran nearly a million units past the points they measure.

Neither is guesswork about units now. The block holds the geometry the
drawing was authored with, so it is drawn: the contents are already in
world space and the block's base point is the origin, and the identity
instance that carries them exists only to reuse the scene graph's
ByBlock / layer-0 colour resolution. Every wire answers to the dimension,
so a pick still selects the dimension rather than one of its pieces.
Re-deriving stays as the fallback for a dimension with no block.

Annotative dimensions keep the old path: their several representations
are separate blocks and choosing between them is what the annotation
machinery already does, while the doctrine above assumes one picture.

A picture is only good while its inputs hold, so it is dropped when they
change: editing a dimension's properties, and saving the dimension style
it is drawn with. Entity edits already dropped it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 02:44:30 +03:00
gianlucafiore b704a43872 Fix TCOUNT numbering and placement options 2026-08-07 19:50:11 -03:00
Hakan SevenandClaude Opus 5 039f703d7f fix(input): stop drafting-aid keys from cancelling the command
F8 reached ORTHO as an ordinary command, and dispatching a command tears
down whatever is running first: the active command dropped, its preview
cleared, its base point forgotten. So pressing F8 partway through a LINE
abandoned the line, and during a MOVE it took the dragged ghost with it.

A function key gets pressed mid-command — that is what it is for.
Toggles that only flip a flag now skip the teardown: ORTHO, GRID, SNAP,
POLAR, and the drafting-settings popup. None of them starts a command,
opens a document or reads geometry, so there is nothing for the teardown
to protect.

F7, F9 and F10 came through the same path and were broken the same way;
they are fixed with F8. F3, F11 and F12 already had messages of their
own and were never affected.

Closes #677

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 00:48:25 +03:00
Hakan SevenandClaude Opus 5 eb513ac5f8 fix(stretch): let the selection stage behave like every other one
STRETCH ended itself when a crossing window caught nothing, so a missed
aim meant restarting the command to try again. A missed aim is not a
decision to stop: say what happened and ask for the corner afresh, the
way a selection that picks nothing leaves MOVE still asking.

Enter now ends the selection stage too. With objects already picked the
window is only there to narrow them, so there is nothing left to choose
— Enter takes a window containing all of them, which stretches every
vertex, which is the whole selection moving. That is what a
pick-selected stretch means. With nothing picked there is nothing to
end, so Enter still cancels. The prompt says so.

Not changed: with a prior selection, a window that misses every vertex
still goes on to ask for the base point. The command holds wires, not
entities, and the entity rules do not follow the wires — a small window
inside a circle catches its centre, and so the whole circle, while no
tessellated vertex is inside. Guarding on the wires would refuse that
stretch.

Closes #676

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 00:42:16 +03:00