Commit Graph

190 Commits

Author SHA1 Message Date
Brandon Thomas 0ef61cdff5 cargo fmt after the community PR batch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:44:34 -04:00
Valentin Klein 242a00923d
Object snap overrides at point prompts, Shift/Ctrl+right-click snap menu, Tab cycling (fixes #393) (#409)
Typed END, MID, CEN, GCEN, NOD, QUA, INT, APP, EXT, INS, PER, TAN, NEA,
PAR (any start of the word from three letters, with `_`/`'` prefixes)
and NON push a one-pick snap frame on the point-modifier layer: the
prompt reads "Endpoint of:", a typed point is snapped with that mode
alone within the aperture (or rejected with "No Endpoint found for
specified point."), and the canvas snaps the cursor with it
(Session::snap_override). A command's own keyword still wins (ARC/ZOOM
`cen`); `_cen` always snaps. Overrides nest with FROM and the filters.

Shift+right-click or Ctrl+right-click on the canvas opens the object
snap menu (From, Mid Between 2 Points, Point Filters, the snap modes,
None, Osnap Settings...), whose items type the same overrides. Tab over
a snap marker with nothing typed cycles the candidate snaps
(pointmod::snap_candidates: the running snap first, then the closest of
each mode).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:44:21 -04:00
Valentin Klein 2f9ebc6f63
PLINE arc options and Length, XLINE polyline segments (fixes #397) (#407)
PLINE arc mode answered "not available yet" for Angle, CEnter, Direction,
Radius and Second pt, and line mode for Length. Each now asks its questions
(included angles and directions read the drawing's angle settings) and adds
the segment. XLINE's options were already in draw2's XlineM2; the dead
keyword stub in draw.rs's RAY machine goes, and Offset/Reference use the
polyline segment picked rather than the first one.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:44:08 -04:00
Valentin Klein 7965f8728b
Object snap tracking, Extension, Parallel and Apparent Intersection snaps (fixes #379) (#405)
Resting the cursor on a snap point acquires it for tracking (seven at
most; resting again releases it). Acquired points send horizontal and
vertical alignment paths, or every polar angle with POLARMODE 2; the
cursor snaps where two paths cross, where a path crosses an object, or
onto the nearest path. EXT runs a line or arc on from an acquired end,
PAR gives a path parallel to a rested-on line through the base point,
APP snaps where objects would meet if extended. Direct distance entry
follows the path the cursor is on. POLARMODE is now AutoCAD's bitcode;
POLARADDANG and AUTOSNAP are new.

The engine owns all of it (Session::snap_cursor, snap/tracking.rs); the
canvas draws the + markers, dotted paths and tooltip.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:43:33 -04:00
Valentin Klein 1961a3186a
TRIM/EXTEND: cutting edges, Fence, Crossing, mOde, Project, Edge, eRase, Undo (part of #397) (#403)
The options TRIM and EXTEND list at their prompt all printed "not available
yet". Quick and Standard modes follow TRIMEXTENDMODE, Edge extends lines and
arcs virtually (EDGEMODE), Project is kept in PROJMODE. The JSON forms take
fence, crossing, edgeMode and (TRIM) mode.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:43:28 -04:00
Valentin Klein 65db0c2f6a
Point prompts: FROM, M2P/MTP, point filters, * prefix and <angle override (fixes #392) (#402)
A point modifier typed at any point prompt pushes a frame on the session
(Session::point_mods) that asks for what it needs ("Base point:",
"<Offset>:", "First point of mid:", ".X of", "(need YZ):") and hands one
point to the command, so every command gets them from the command line
and in scripts, and they nest (FROM then M2P). `*x,y`, `*@` and `@*`
parse as world coordinates (the same as the plain forms: no UCS yet).
`<a` locks the next point to that angle from the base point: a typed
distance follows it, a pick is projected onto it, typed coordinates win.
TK is left to object snap tracking (#379).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:43:24 -04:00
Valentin Klein 156bd0304e
Dimension text: DIMPOST's suffix also goes on tolerances (fixes #390) (#394)
With DIMTOL on, the DIMPOST suffix was added to the measurement only.
As in AutoCAD, the suffix (what follows `<>`, or the whole value when
there is no `<>`) now also follows each tolerance value: symmetric
(10.0mm±0.1mm) and stacked deviations alike. The prefix stays on the
measurement; limits are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:43:20 -04:00
Valentin Klein 9e19a10b29
Layouts: a viewport on a frozen layer hides its contents (fixes #395) (#396)
Freezing a viewport's own layer hid only its border; the model space
seen through it still showed on screen and in plots. As in AutoCAD, a
frozen viewport layer now hides the contents too, while turning the
layer off still hides only the border. The check is in the shared
layout path, so display, PDF/SVG/PNG export and plotting agree.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:43:15 -04:00
Nigel Ihza Firdausy 95dc3f22a7
fix(geom): keep finite-scale tolerance and midpoint math stable (#400) 2026-10-10 18:43:10 -04:00
Nigel Ihza Firdausy bc2c80289c
fix(select): include solid interiors in crossing-window selection (#401)
* fix(select): include solid interiors in crossing-window selection

* test(select): keep solid crossing regressions in an isolated module
2026-10-10 18:43:06 -04:00
Brandon Thomas 964eb0fb40 cargo fmt after the community PR batch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:11:18 -04:00
Valentin Klein d461b95872
ZOOM: Center, Scale, Dynamic and a plain n; All/Extents fit the sheet in paper space (fixes #388) (#391)
Center, Scale and Dynamic all zoomed to the extents, so the point or
factor typed after them ran as a command. Center now asks for the centre
point and a magnification or height (a height, nX or nXP), Scale for a
factor, and Dynamic, which needs a drag, asks for a window. A plain n
(at the first prompt and at Scale) is relative to the limits, as in
AutoCAD: 1 fits them. ZOOM All and Extents with nothing drawn used the
model limits in paper space too; Session::zoom_limits gives the sheet
there.

Builds on #372 (ZOOM inside a viewport).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:11:05 -04:00
Valentin Klein 304e56ba84
Attributes keep their own layer, colour, linetype and lineweight (fixes #358) (#359)
`Attrib` gains optional per-attribute properties (`props`), each overriding
the attribute definition's. DXF reading takes them from the ATTRIB's groups
8/6/62/420/370; writing gives each ATTRIB the properties it is drawn with.
The renderer prefers them over the definition's. -ATTEDIT's Layer and Color
options set them; ATTSYNC resets them to the definitions, as in AutoCAD.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:10:40 -04:00
Valentin Klein 19f7bc39ed
VPLAYER: option prompts at the command line (fixes #381) (#387)
Typed VPLAYER ran its JSON form with no parameters and ended, so its
options ran as commands (F started FILLET). It now loops on
?/Color/Freeze/Thaw/Reset/Newfrz/Vpvisdflt until Enter, asks for layer
names (wildcards, comma lists) and then which viewports to change:
All/Select/Current/Except current (Current by default inside a viewport,
Select by point on the sheet in paper space). Changes go through
viewport.set, layer.new and layer.set; the session is one undo step.
The Model tab refuses it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:10:35 -04:00
Valentin Klein 157ea6290d
LAYOUT / -LAYOUT: option prompts at the command line (fixes #378) (#380)
Typed LAYOUT ran its JSON default and ended, so the option and the
layout names typed after it ran as commands. It now asks for an option
(Copy/Delete/New/Template/Rename/SAveas/Set/?, Enter = Set) and the
layout name(s) the option needs, with the current layout as the default.
The options run the layout.* command bodies, so a refused name re-prompts
and the command is one undo step. Template and SAveas report that they
are not available yet and end.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:10:30 -04:00
Valentin Klein 1496d704b0
ZOOM inside a layout viewport acts on the viewport; nXP sets its scale (fixes #370) (#372)
In MSPACE, Extents/All/Window/Object fitted the drawing window's aspect and
paper-view height, In/Out/nX zoomed about the screen centre, and nXP was read
as nX. Now ZOOM works on the active (unlocked) viewport's model view: fits use
the viewport's aspect, zooms keep its centre, and nXP sets the viewport scale
to n paper units per model unit. Locked viewports and the Model tab are
unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:09:31 -04:00
Valentin Klein 54d75a1623
Control channel: cmdline.key replies like cmdline.input (fixes #360) (#361)
When Enter was refused (Enter repeats a command that isn't available),
headless failed the cmdline.key call while the app dropped the error,
and pressing Enter in the app's command line echoes it. Both now echo
the error to the history and reply with the command-line state, the
`output` lines the key added and `error` only when Enter was refused,
the shape #351 gives cmdline.input.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:09:26 -04:00
Valentin Klein de8ed87a6e
MLEADER remembers the placement order and Options (fixes #362) (#365)
The next MLEADER started again with "arrowhead first", Maxpoints 2 and
a landing line. As in AutoCAD, the order (arrowhead, landing or content
first) and the Options values (Maxpoints, leader landing, content type,
first and second angle) are now kept in Session::last_used for the
rest of the session and are where the next MLEADER starts. JSON calls
keep their documented defaults.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:09:20 -04:00
Valentin Klein 8db51c2766
Exploded dimension text and DXF dimension blocks match the drawn text (fixes #367) (#369)
EXPLODE and the DXF *D block took the text height from the bare style's
DIMTXT × DIMSCALE, ignoring the dimension's overrides (SETVAR DIM*, DIMOVERRIDE),
a fixed-height text style and the DIMSCALE 0 fallback (text ~1e-10 high), and
EXPLODE always wrote rotation 0, so aligned text turned horizontal. Both now
use the geometry the renderer draws (dimension_in): its text height, angle and
the overridden text style; EXPLODE adds no MTEXT for suppressed text.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:09:15 -04:00
Valentin Klein 409968ecbb
DIMTEDIT: command-line prompts (fixes #382) (#383)
Typed at the command line or run from a script, DIMTEDIT ran the JSON
form at once and failed with "select dimensions". It now asks "Select
dimension", then "Specify new location for dimension text or
[Left/Right/Center/Home/Angle]": a point moves the text there (the text
follows the cursor in the preview), Left/Right/Center justify it,
Home restores the default position and Angle asks for the text angle.
A single preselected dimension is taken at once. Each answer runs the
existing JSON form on the picked dimension. DIMTED is its alias.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:09:10 -04:00
Valentin Klein 1418130d45
DIMRADIUS/DIMDIAMETER: a location inside the circle puts the text inside (fixes #385) (#389)
Only the direction of the picked dimension line location was used, so
the text always went outside beyond the arrowhead. A location inside
the circle (typed, picked or JSON `at`) now places the text there: the
dimension stores it as its text position, and the dimension line runs
from the text out to the arrowhead; a diameter's spans the circle,
broken around the text. DIMTIH (not DIMTOH) decides whether inside
text is horizontal. Outside locations keep the default placement.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:09:05 -04:00
Valentin Klein 2f4ceea7a9
Dimension text: DIMRND rounds only the measurement; no DIMPOST on angles (fixes #373) (#376)
Tolerances and alternate units copied the primary number format, so they were
rounded by DIMRND and took DIMZIN's zero suppression: DIMRND 0.5 turned a
±0.1 tolerance into ±0.0000, and 10.3 in showed as [261.50] mm. Tolerances are
no longer rounded and use DIMTZIN. Alternate units use the new DIMALTRND
(rounding), DIMALTU (unit format) and DIMALTZ (zero suppression). The new
variables can be set with SETVAR, DIMSTYLE and overrides. A DXF keeps them as
DIMSTYLE group codes 148, 273, 284 and 285.

DIMPOST is a prefix/suffix for linear dimensions, so angular dimensions no
longer get it ("90°mm"). A text override still wraps the angle.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:08:59 -04:00
Valentin Klein a119873e20
Angular dimensions keep each leg's extension line; centre mark only outside (fixes #374) (#377)
When the arc location lies outside the sweep from the first leg to the second,
the arc runs from leg 2 to leg 1. The extension lines, DIMSE1/DIMSE2 and
DIMBLK1/DIMBLK2 still took the arc start as leg 1, so each extension line was
sized from the other leg. A short leg got no extension line, a leg already
crossing the arc got one drawn along it, and DIMSE1/DIMSE2 suppressed the
wrong line. The arc's ends are now paired with their legs.

Radius and diameter dimensions drew the DIMCEN mark even when the dimension
line is inside the circle. As in AutoCAD, the mark is now drawn only when the
line is outside: a radius led out to its text. A diameter line runs through
the centre, so it gets no mark.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:08:55 -04:00
Valentin Klein 179ecc1f0b
DIMBASELINE spacing follows DIM* overrides and DIMSCALE 0 (fixes #364) (#366)
Baseline dimensions were spaced by the bare style's DIMDLI × DIMSCALE, so
SETVAR DIMSCALE/DIMDLI (stored as overrides since #129) had no effect and a
style DIMSCALE of 0 collapsed the spacing to ~0. Use the continued
dimension's style with its overrides and the effective scale (the drawing's
DIMSCALE when the style's is 0), as drawing does; DIMSPACE's default spacing
uses the same scale.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:08:50 -04:00
Valentin Klein 7f5b39fb25
Undo/redo of LAYOUT Rename/New keeps the session on a layout that exists (fixes #371) (#375)
Undo and redo swapped the drawing but left the session on the current
layout's name. After undoing a rename or the creation of the current layout,
that layout no longer existed: nothing was drawn and commands failed with
"no such space". Now the session follows a renamed layout to its other name
(same tab order), keeping its paper view, and returns to the Model tab when
the layout is gone.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:08:45 -04:00
MarekSpodBiedry 6d8af2e5a4
Keep egui on the System theme preference so the OS appearance is followed live (#301) (#363) 2026-10-10 18:08:41 -04:00
Nigel Ihza Firdausy 35a8617855
fix(geom): use scale-aware circle-circle intersections (#384) 2026-10-10 18:08:37 -04:00
Nigel Ihza Firdausy 7bb64e5fce
fix(geom): preserve boundary vertices far from the origin (#386) 2026-10-10 18:08:32 -04:00
Brandon Thomas 1ff426dbe7 PURGE test: add the zero-length line directly
#344 made JSON LINE refuse coincident points; files can still hold them, so
the test that checks PURGE Zero-length adds one to the drawing directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:45:43 -04:00
Valentin Klein 9b7d7bd090
STRETCH: repeat Select objects until Enter, C/W windows, Displacement (fixes #228) (#231)
STRETCH gets its own prompt machine instead of a special case inside the
shared MOVE/ROTATE/SCALE one. "Select objects" now repeats until Enter:
implied windows (crossing right to left), `C` and `W` windows, single
picks and All/Last/Previous (moved whole), Remove/Add. Vertices inside any
of the windows move. Enter or `Displacement` at the base point asks for a
displacement, and Enter at the second point uses the base point as the
displacement, as in AutoCAD. Polygon and fence selection say they are not
available yet instead of being misread.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:39 -04:00
Valentin Klein 93152afa8e
Copy and paste between drawings; PASTEORIG, PASTEBLOCK; right-click menu (part of #325) (#355)
Copying in one drawing and pasting in another lost what the objects
refer to: a line on a layer the target lacked landed on a new default
layer (colour and linetype gone), and a block reference pointed at a
block the target didn't have. Cmd/Ctrl+C, X and V did nothing on the
desktop, because egui delivers them as clipboard events, not key
presses. PASTEORIG had no checks, and the canvas had no right-click
menu.

- Engine (cmd/clipboard.rs): the session clipboard keeps, with the
  copied objects, the layers, linetypes, text/dimension/multileader/
  table styles and blocks (nested too) they use. Pasting adds the ones
  the target lacks; names it has keep the target's definitions, as in
  AutoCAD; generated (*U, *D) blocks whose name is taken get a fresh
  one; objects get new handles.
- PASTEORIG pastes at the original coordinates, only into a drawing
  other than the one copied from (greyed and refused there, as in
  AutoCAD). PASTECLIP and PASTEBLOCK are greyed with an empty clipboard.
- PASTEBLOCK (Edit > Paste as Block, Cmd+Shift+V): the clipboard
  becomes an A$C… block inserted at a point. AI_DESELECT.
- UI: Cut/Copy/Paste events run the clipboard commands (Shift: Copy
  with Base Point / Paste as Block). Each copy is also put on the system
  clipboard as DXF text, and pasted DXF text that isn't our own last
  copy becomes the clipboard, so another CADCraft window can paste it.
- Right-click on the canvas with no command running opens a shortcut
  menu: Repeat <last>, Clipboard (Cut, Copy, Copy with Base Point,
  Paste, Paste as Block, Paste to Original Coordinates), the
  selection's editing commands and Deselect All, Undo/Redo, Pan/Zoom,
  Quick Select, Find. During a command it is still Enter.
- Ukrainian labels for the new items.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:34 -04:00
Valentin Klein 4010b94919
INSERT inserts exploded (*NAME) and drawing files (fixes #340) (#341)
At the block name prompt, *NAME inserts the block's objects: after the
insertion point INSERT asks one scale factor and the rotation, and no
attribute values. A path (C:\parts\bolt.dxf), NAME=path or NAME= reads
the drawing through the io hooks and defines a block named after the
file (or NAME) from its model space, with the file's $INSBASE as the
base point, adding the layers, linetypes, styles and blocks it needs and
keeping this drawing's definitions. A plain path whose block already
exists asks whether to redefine it; NAME=path redefines without asking.
A file that can't be read is reported at the name prompt. The JSON form
takes `file` (optional `name`) and "*NAME".

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:29 -04:00
Valentin Klein dacd8ece49
Control channel: headless and app reply alike to cmdline.input, state and app.open (fixes #351) (#352)
Both backends now return the same command-line state (prompt, running,
keywords, accept, buffer, history, historyExpanded). cmdline.input echoes
a refused line to the history and `output` and also reports it in `error`
on both sides. Headless app.open without a path answers "missing path"
like the app instead of reaching OPEN. Documented in control-protocol.md.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:24 -04:00
Valentin Klein cdd25840cc
Control channel: a timed-out request is cancelled, and the reply says whether it ran (fixes #345) (#348)
The control server gave up after 60 s with "timeout" but left the request
queued, so the app ran it later and a client retry applied it twice.
Requests now carry a shared state: on timeout the server cancels a request
the app has not started (reply state "not-run", safe to retry) and the app
skips it; one already started replies state "may-have-run". The MCP bridge
passes that on in its error.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:19 -04:00
Valentin Klein 45755697b6
Angle prompts read AUNITS, ANGBASE and ANGDIR (fixes #353) (#354)
ROTATE's angle and the hatch pattern angle are rotations (AUNITS, clockwise
when ANGDIR is 1). The dimension line (Rotated) and dimension text angles are
directions from ANGBASE. CHAMFER's angle and the MLEADER angle constraints are
amounts in AUNITS. The ROTATE and HATCH defaults are offered as they would be
typed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:13 -04:00
Valentin Klein 937dca6590
DWG and DXF keep ANGBASE/ANGDIR, fillet/chamfer, elevation and the snap grid (fixes #349) (#350)
acadrust's DXF writer leaves ANGBASE, ANGDIR, FILLETRAD, CHAMFERA/B,
ELEVATION and THICKNESS out of the HEADER although its DWG reader fills
them in; the DWG bridge now adds them to the DXF it hands our reader.
SNAPBASE/SNAPANG are written to and read from the *ACTIVE VPORT record
(groups 13/23 and 50), where R13+ files keep them.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:07 -04:00
Valentin Klein 7bed82838d
JSON draw/annotation commands refuse invalid sizes and coincident points (fixes #343) (#344)
CIRCLE/ARC no longer make a negative radius positive; POLYGON refuses a
non-integer sides count and a zero/negative radius or zero-length edge;
DONUT, RECTANG, PLINE, HATCH and TABLE refuse negative or zero sizes
instead of clamping them; LINE drops repeated points like the interactive
command; SPLINE needs two different fit points; DIMRADIUS/DIMDIAMETER/
DIMANGULAR refuse coincident points; DDPTYPE refuses an invalid PDMODE;
DIMSTYLE/DIMOVERRIDE list a non-positive DIMTXT/DIMTFAC or a negative
DIMASZ/DIMSCALE/DIMTSZ as ignored.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:01 -04:00
Valentin Klein 71706f8e6a
TL/ML/BL text is placed by its alignment point (fixes #342) (#346)
For every justification but baseline-left (and aligned/fit) the DXF
Reference makes group 11 the point the text is placed by; group 10 is a
computed point on the baseline. Left-justified text with a vertical
alignment was drawn, measured and picked from group 10 and then shifted
by the alignment again: TL and ML too low, BL too high. The renderer
(fonts::place) and text extents (doc text_box, used for picking) now
place it from the alignment point; a mirrored (extrusion 0,0,-1) TL/ML/BL
text keeps its alignment point when it turns right-justified; and the
Properties position moves justified text by its alignment point.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:31:56 -04:00
Valentin Klein 2f7a1e60d7
QDIM: one snap index per call, cap the input (fixes #356) (#357)
With DIMASSOC on, every dimension QDIM added looked up the objects under
its definition points by scanning the whole space, and each dimension
adds an entity, so QDIM over many objects took quadratic time (20,000
lines: 28 s). QDIM now builds the SnapIndex DIMCONTINUE/DIMBASELINE use
(#281) once per call; dimensions are no snap targets, so it stays valid
while QDIM adds them. A call naming more than 100,000 objects, or whose
objects give more than 100,000 distinct points, is refused before
anything is created.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:31:50 -04:00
Valentin Klein 776d99a1e0
FILLET/CHAMFER: Trim, Multiple, Undo; CHAMFER Angle, mEthod, Polyline (fixes #248) (#251)
* Pick box at object-selection prompts; prompts remember the last option

Part of #34.

The crosshair showed the pick box only at "Select objects" prompts. The
commands that pick one object themselves (FILLET, CHAMFER, OFFSET, TRIM,
EXTEND, BREAK, the dimension and constraint picks...) ask with a plain
point prompt, so the cursor stayed a bare crosshair there and hitting an
object was guesswork. The idle crosshair also showed the box with
PICKFIRST off.

- Engine: Prompt::picks_objects() (a selection prompt, or a point prompt
  that starts with "Select") and Session::picking_objects() (that, or no
  command with PICKFIRST on). The canvas uses it for the pick box and the
  rollover highlight.
- The box is sized from PICKBOX as the pick aperture the click uses
  (PICKBOX x 1.5 px half side), hidden at PICKBOX 0, and drawn with a new
  `crosshair` theme token.
- HATCH/GRADIENT: "Select objects" mode is a real mode now ("Select
  objects or [picK internal point/...]", Enter creates the hatch) and the
  next HATCH starts in the mode used last. Pattern, scale and angle
  sub-prompts show the current value as <default>. At select prompts P, L
  and ALL stay selection modes even when a keyword shares the letter.
- POLYGON offers the last Inscribed/Circumscribed choice, ROTATE the last
  angle, SCALE the last factor (Session::last_used, session only).
- FILLET shows the radius as <default>; CHAMFER asks for the second
  distance too, defaulting to the first (FILLETRAD, CHAMFERA/B).

JSON calls are unchanged: explicit params and documented defaults win.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* FILLET/CHAMFER Trim, Multiple, Undo; CHAMFER Angle, mEthod, Polyline (fixes #248)

Trim asks for the trim mode (TRIMMODE); with No trim the fillet arc or
chamfer line is added and both objects are kept. Multiple repeats until
Enter and Undo takes back the last fillet/chamfer of the command. CHAMFER
Angle asks for a length on the first line and an angle from it (CHAMFERC,
CHAMFERD, CHAMMODE = 1), mEthod picks Distance or Angle, and Polyline
chamfers every straight corner of a 2D polyline. The JSON forms take
`trim`, and CHAMFER `length`/`angle` and `polyline`.

Keyword matching now prefers a whole keyword or its capital-letter
shortcut over a prefix of another keyword, so `M` is Multiple rather than
the start of mEthod.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:56:01 -04:00
Valentin Klein c26150ec07
Dimension and multileader options take effect (fixes #243) (#247)
DIMORDINATE Xdatum/Ydatum/Mtext/Text/Angle, DIMRADIUS/DIMDIAMETER
Mtext/Text/Angle, DIMANGULAR Mtext/Text/Angle/Quadrant and DIMARC
Mtext/Text/Angle/Partial were listed but ignored (any DIMORDINATE
keyword other than Xdatum forced a Y datum). DIMLINEAR/DIMALIGNED
Angle rotated the dimension line instead of the text.

A shared DimText handles Mtext/Text (override text, <> = measurement)
and Angle (text rotation) for every placement prompt. Quadrant locks
the measured angle to a picked point's side; Partial dimensions the
part of the arc between two picked points. After Horizontal/Vertical/
Rotated only the text options remain listed.

MLEADER: leader Landing first / Content first switch the placement
order, Enter or Options opens the options loop (Leader type, leader
lAnding, Content type, Maxpoints, First/Second angle). Spline leaders,
leaderless and block content say "not available yet". Angle prompts
take numbers so scripts keep splitting the following tokens.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:55 -04:00
Valentin Klein d433112f1e
Draw hatch island styles, backgrounds, two-colour gradients and file pattern definitions (fixes #289, fixes #290) (#302)
- Island detection style (group 75) picks the loops that take part: outer keeps the
  outermost loops and their first islands, ignore only the outermost loops.
- A pattern hatch's background colour is filled under its pattern lines.
- Gradients blend colour 1 to colour 2 (LINEAR, CYLINDER, CURVED, SPHERICAL,
  HEMISPHERICAL and the INV forms) along their angle, shifted when not centred, drawn as
  bounded colour steps; ByLayer / ByBlock gradient colours resolve to the hatch's layer /
  block colour instead of colour 7.
- DXF HATCH pattern definition lines (78, 53, 43-46, 79, 49) of a pattern the library
  doesn't define are kept on the hatch (new `patternLines` field), drawn and written back;
  hostile counts are capped.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:49 -04:00
Valentin Klein 3e4eb7a0e9
-LAYER: command-line option loop for layers (fixes #254) (#260)
Typed -LAYER (and LAYER in scripts and headless sessions) now starts the
option loop instead of listing the layers and ending: ?, Make, Set, New,
Rename, ON, OFF, Color (with Truecolor), Ltype, LWeight, TRansparency,
Plot, Freeze, Thaw, LOck, Unlock and Description, until Enter. MATerial,
stAte and rEconcile report "not available yet" without taking the next
token. Every option runs the layer.* command bodies, so the session is
one undo step and refusals match the JSON form. Turning the current
layer off asks first; freezing it is refused.

layer.new and layer.set now reject a linetype that is not loaded (or
ByLayer/ByBlock), and layer.set refuses to rename a layer onto the name
of another existing layer.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:44 -04:00
Valentin Klein 2483676ba6
-PURGE: type, names and verify prompts; purge by type (fixes #262) (#263)
Typed -PURGE (PURGE has no dialog yet) now asks for the type of unused
objects, the names to purge <*> and whether to verify each name, instead
of purging everything at once and leaving the rest of the line to run as
commands. Verify Yes confirms each name; No purges every match. Types
CADCraft does not have read their answers and report nothing to purge;
Zero-length geometry and Empty text objects purge at once.

The JSON form takes {type, names}: LA purges only layers. All covers
every named type, so unused text, dimension, multileader and table
styles and empty groups are purged as the params doc promised. Layers and
text styles go through layer.delete and style.delete. Linetypes used
only inside block definitions, or set as CELTYPE, are kept.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:39 -04:00
Valentin Klein 7a876c74c9
ATTEDIT prompts at the command line; -ATTEDIT; tags in any case (fixes #273, #274) (#280)
ATTEDIT / EATTEDIT typed at the command line ran the JSON form and failed
with "`handle` is required". It now asks "Select block reference:" and then
"Enter value for <TAG> <current>:" for each attribute (Enter keeps the
value); the answers are applied together as one undo step.

-ATTEDIT is a command of its own (Modify > Object > Attribute > Global):
one at a time (Value: Change/Replace, Next) or global string replacement,
over the attributes chosen by block name, tag and value specifications.
Session::start now prefers an id that keeps its leading dash, so -ATTEDIT
is not taken for ATTEDIT; dashed aliases (-INSERT...) resolve as before.

INSERT `attribs` and ATTEDIT `values` match attribute tags in any case and
report the keys no attribute has in `unknownTags`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:33 -04:00
Valentin Klein 5b286119d2
ATTDISP and ATTSYNC; the display follows ATTMODE (fixes #275, #276) (#282)
ATTDISP asks "Enter attribute visibility setting [Normal/ON/OFF]" (default:
the current setting) and sets ATTMODE to 1 / 2 / 0. The renderer now reads
ATTMODE: OFF hides every attribute, ON also draws invisible ones, Normal
draws the visible ones as before.

ATTSYNC ("Enter an option [?/Name/Select] <Select>") brings every reference
of a block, in model space, layouts and other block definitions, in line
with the block's attribute definitions: missing attributes are added with
their defaults, ones whose definition is gone are removed, values are kept
for tags still defined (any case), and position, height, style and
visibility come from the definition.

Both live in the new cmd/blocks/attribs.rs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:27 -04:00
Valentin Klein cf9fbb8bf5
LAYMRG and LAYDEL: merge layers, delete a layer with its objects (fixes #286) (#296)
LAYMRG moves every object on the source layers (model space, layouts
and block definitions) to the target layer, drops the sources from
viewport freeze lists and colour overrides, and deletes the source
layers. LAYDEL deletes the layers and every object on them the same
way. Typed, both ask "Select object on layer to merge/delete or
[Name]:" until Enter (Undo drops the last one), LAYMRG then asks for
the target layer, and both warn and ask [Yes/No] <No> before changing
anything. Scripts answer Y. The JSON forms laymrg {from, to} and
laydel {names} never prompt.

Layer 0, Defpoints and xref-dependent layers are refused, and so is
merging a layer into itself. LAYDEL refuses the current layer; LAYMRG
merges it and makes the target current. Each run is one undo step.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:21 -04:00
Valentin Klein e9ec19b138
Midpoint, Perpendicular and Tangent snaps on elliptical arcs and splines; Quadrant stays on the arc (fixes #332) (#333)
Midpoint snaps halfway along the length of an elliptical arc or open spline.
Perpendicular and Tangent from a base point find every foot point and
tangent point on ellipses, elliptical arcs and splines (the one nearest the
cursor wins, as for circles): tangents to an ellipse exactly through the unit
circle, the rest by sampling the parameter range and bisecting each sign
change, with fixed iteration bounds. Quadrant on an elliptical arc offers only
the quadrant points the arc passes through.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:16 -04:00
Valentin Klein 6a15ba2a7d
Dimensions snap; justified text snaps and grips at its alignment point; grid snap follows SNAPBASE/SNAPANG; OSNAPHATCH (fixes #334) (#337)
- A dimension snaps to what it draws: the objects of its block (read from a
  file), expanded like block references, or else the lines generated from it
  (Endpoint on their ends, Midpoint, Intersection, Nearest); Node on its
  definition points.
- TEXT justified other than left-baseline (as read from DXF, group 11)
  snaps (Insertion) and grips at its alignment point; attributes too.
- The snap grid starts at SNAPBASE and turns by SNAPANG (drawing variables,
  defaults 0,0 and 0 degrees). SETVAR takes `x,y` for point variables.
- Hatches are left out of object snaps unless OSNAPHATCH is 1 (new session
  variable, default 0 as in AutoCAD).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:12 -04:00
Valentin Klein b1a78fbb8f
Typed angles follow ANGBASE, ANGDIR and AUNITS (fixes #326) (#328)
Polar points and angle prompts read bare numbers in AUNITS (explicit
d/r/g, deg-min-sec and bearings still work) and measure directions from
ANGBASE in the ANGDIR sense; `d<<a` (degrees) and `d<<<a` (AUNITS)
measure from +X counterclockwise. XLINE Reference turns per ANGDIR;
included angles and sweeps take the units only. ANGBASE is held in
radians like $ANGBASE in DXF; SETVAR reads and reports it in degrees.
JSON params keep their meaning.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:07 -04:00