Commit Graph
66 Commits
Author SHA1 Message Date
Valentin KleinandClaude Opus 5.5 9d2ae09a79 OFFSET Erase/Layer/Multiple/Undo and MOVE/COPY Displacement take effect (fixes #244) (#246)
OFFSET's Erase and Layer keywords open their Yes/No and Current/Source
prompts (remembered in OFFSETERASE/OFFSETLAYER and shown in the current
settings line); Multiple keeps offsetting the newest copy until Enter and
Undo takes back the last offset. The JSON form takes `erase` and `layer`.
MOVE and COPY ask for a displacement after the Displacement keyword or
Enter at the base point prompt.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:12:14 -04:00
Valentin KleinandClaude Opus 5.5 c04a769ef2 -INSERT asks for X and Y scale; Basepoint/Scale/Rotate take effect (fixes #245) (#249)
After the insertion point, the command-line INSERT now asks "Enter X
scale factor, specify opposite corner, or [Corner] <1>", then "Enter Y
scale factor <use X scale factor>", then the rotation, so scripts like
`-INSERT B1 20,20 1 1 0` place the block as written instead of rotating
it by the Y scale and running the rotation as a command. A point (or
Corner) at the X scale prompt gives both factors from the opposite
corner.

The Basepoint, Scale and Rotate options at the insertion point prompt
now work: Scale and Rotate preset the value and skip that prompt later,
Basepoint picks a new base point on the block at its definition
position.

The JSON form takes `scale: [x, y]` and `basePoint` too.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:12:09 -04:00
Valentin KleinandClaude Opus 5.5 9524f9a5c2 -RENAME prompts; RENAME renames blocks (fixes #264) (#266)
Typed RENAME / -RENAME (and REN) asks for the object type
[Block/Dimstyle/LAyer/LType/Material/multileadeRstyle/Plotstyle/
textStyle/Tablestyle/Ucs/VIew/VPort], the old name and the new name,
then runs the rename command body, so the checks and the single undo
step match the JSON form. An unknown old name or a refused new name
re-prompts. Material, Plotstyle and VPort read both names and report
"not available yet".

The JSON form now renames blocks, as its params doc promised. Every
INSERT follows the new name, in model space, layouts and nested block
definitions, including inserts that carry attributes. INSNAME and
dimension-style arrowheads follow too. Duplicate names (any case),
invalid names and anonymous blocks are refused. Multileader styles,
table styles, named views and UCSs can be renamed as well. A layer
rename no longer records two undo steps.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:12:03 -04:00
Valentin KleinandClaude Opus 5.5 824ae58d91 Object snaps find the geometry inside block references (fixes #314) (#318)
Running snaps only saw a block reference's insertion point. Now the objects
inside it snap where they are drawn: nested references and MINSERT arrays are
expanded, placed exactly by the reference's transform (non-uniform scale
turns circles and arcs into ellipses), and hidden objects or objects on layers
that are off are left out. Insertion also snaps to visible attributes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:11:58 -04:00
Valentin KleinandClaude Opus 5.5 3e5e5ac3ce -BLOCK erases the selection and asks before redefining (fixes #307) (#310)
-BLOCK was an alias of BLOCK, so the selected objects were always
converted to a block reference; a script doing -BLOCK then INSERT ended
up with an extra reference. -BLOCK is now its own command: the command
line erases the selection, and its JSON form defaults to keep "delete".
BLOCK (menu Make..., typed BLOCK) and the "block" JSON form still
convert by default.

Both forms ask 'Block "<name>" already exists. Redefine it? [Yes/No]
<N>:' when the name is in use; No asks for the name again.

Session::start resolves a dashed name to a command with that id (the
same change as #280's -ATTEDIT).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:11:19 -04:00
Valentin KleinandClaude Opus 5.5 f55559e5bb OFFSET keeps a closed spline closed (fixes #303) (#305)
The offset copy of a spline was always rebuilt with the open fit-point
interpolation, so a closed spline came out open with a corner at its start
point. Use Spline::from_fit with the source's closed flag, which gives a
periodic spline for closed input.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:11:13 -04:00
Valentin KleinandClaude Opus 5.5 dd8764a7b5 WBLOCK honours base when writing a block (fixes #297) (#300)
wblock {name, base} ignored base and always measured from the block's
own base point. base (in block coordinates) now overrides it, the way
it already worked for selected objects. In both modes the base point
becomes the written drawing's origin, and $INSBASE is 0,0,0.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:11:07 -04:00
Valentin KleinandClaude Opus 5.5 b90d8cf644 Metric drawings load library linetypes in millimetres (fixes #294) (#299)
The linetype library is defined in inches, and LINETYPE loaded it
unchanged into metric drawings: DASHED was 0.5 mm on, 0.25 mm off at
LTSCALE 1, so dashed lines looked solid. CAD programs load ISO-scaled
definitions when MEASUREMENT is 1 (the same patterns with lengths times
25.4). library::standard_linetypes_for(d) does the equivalent with our
own definitions: in a metric drawing every length (dashes, gaps, text and
shape size, offsets) is scaled by 25.4 as it is loaded. LINETYPE and the
CENTER linetype that CENTERMARK/CENTERLINE load use it. Definitions
already in a drawing are not changed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:11:02 -04:00
Valentin KleinandClaude Opus 5.5 f5073a301e Guard every step of an interactive command against panics (#283) (#284)
Session::execute and interactive input already ran under catch_unwind;
the machine factory, begin (the first step of Session::start), prompt and
preview (called by the UI every frame) did not, so a panic there took
down the app. Session::start now wraps each machine in guard::Guarded,
which runs all of its steps under catch_unwind. A panic in the factory,
begin or input ends the command with an internal error and the session
restores the drawing, as for input. prompt and preview can't end the
command, so they fall back to an "Internal error" prompt and no preview,
and the next input ends the command the same way. AGENTS.md/CLAUDE.md
name the wider guard.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:10:51 -04:00
Valentin KleinandClaude Opus 5.5 fc1810f5d9 Array commands: ARRAY, and edit-array option loops for ARRAYRECT/ARRAYPOLAR (#230) (#232)
* Array commands: ARRAY, and the edit-array option loops for ARRAYRECT/ARRAYPOLAR (#230)

ARRAY (alias AR) selects objects, asks for the array type and continues as
ARRAYRECT, ARRAYPATH or ARRAYPOLAR. ARRAYRECT no longer creates a fixed 4x3
grid right after selection: it previews the array and loops on
ASsociative/Base point/COUnt/Spacing/COLumns/Rows/Levels/eXit. ARRAYPOLAR
asks for the center (or Base point), then loops on Items/Angle between/
Fill angle/ROTate items/eXit. The copies are created on eXit or Enter.
Associative arrays, levels and polar rows report "not available yet" and
return to the loop without consuming the next input.

The array commands move to cmd/array.rs. Arrays stay non-associative
copies, and the JSON forms are unchanged. A full clockwise polar fill
(-360) no longer puts the last item on top of the source.

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

* docs/gaps.md: narrow F5 (array option loops, #230)

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

* Arrays: refuse arrays over MAXARRAY instead of hanging or shrinking them (#230)

`arrayrect {"rows":1000000,"cols":1000000}` was clamped to 1000x1000 and
then created a million copies of every selected object, which hangs the
app. ARRAYRECT, ARRAYPOLAR and ARRAYPATH (JSON and interactive, also via
ARRAY) now count items x selected objects and refuse an array over the
new MAXARRAY session variable (default 100000, settable 100..10000000)
with a message naming the limit, instead of clamping the counts and
silently creating fewer copies. The interactive count prompts accept at
most what still fits under MAXARRAY.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:34:30 -04:00
Valentin KleinandClaude Opus 5.5 c55feb79da SPLINE Close is periodic, options report; POLYGON typed radius has a flat bottom; CIRCLE rejects bad input (fixes #238) (#240)
SPLINE Close now builds a smooth closed (periodic) curve through the fit points
instead of repeating the first point. The geometry gets Spline::from_fit_points_closed:
chord-length parameters including the closing chord, a periodic knot vector, and the
first three control points repeated at the end. The fit data no longer duplicates the
first point. DXF writes group 70 bit 2 (periodic) for it, and a file whose closed spline
needs refitting is refitted closed. SPLINEDIT Close/refit/move and fit-point grips keep
a closed spline periodic.

SPLINE Method asks Fit/CV, and CV continues as SPLINE by control vertices. Knots, Object,
start/end Tangency and toLerance report "not available yet" and stay at the prompt.
Garbage text reports "Point or option keyword required.".

POLYGON with a typed radius puts the bottom edge horizontal for Inscribed and for
Circumscribed. The JSON form uses the same orientation when no angle is given.

CIRCLE reports "Point or option keyword required." for an invalid token at the centre
prompt, and "Invalid point." at the 3P/2P point prompts, then asks again.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:34:24 -04:00
Valentin KleinandClaude Opus 5.5 a15d0e5b8e DIMCONTINUE/DIMBASELINE: one snap index per chain, cap the points (#279) (#281)
With DIMASSOC on, every chained dimension looked up the objects under
its definition points by scanning the whole space, and each point adds
an entity, so a long `points` list took quadratic time (16,000 points:
2.7 s continue, 10 s baseline). The chain now builds one x-sorted
SnapIndex up front. Dimensions are no snap targets, so the index stays
valid while the chain adds them, and each lookup is a binary search. The
index picks the same snap as find_snap: lowest rank first, then the first
object in space order. A call with more than 100,000 points is refused.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:34:07 -04:00
Valentin KleinandClaude Opus 5.5 1d1fe4fc6d BLOCK refuses a definition that references itself; names are case-insensitive (fixes #250) (#252)
Redefining a block with a selection that holds a reference to that block,
directly or through nested blocks, is now refused with "Block "B"
references itself." and changes nothing. Before, the block ended up
containing itself and lost its original geometry. At the command line
the message ends -BLOCK, as for any definition that can't be made.

Redefining with a name that differs only in case (Door, then DOOR)
replaces the existing block instead of adding a second one.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:34:01 -04:00
Valentin KleinandClaude Opus 5.5 5eb7fd452a Programmatic SPLINE limits the degree to 1..10 (fixes #268) (#269)
spline {control, degree} took any degree up to the control-point count
minus one; tessellation and extents grow with its square, so a few
thousand control points with a high degree hung the app on every redraw.
The Degree prompt, spline.cv and DXF input already limit it to 1..10.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:33:31 -04:00
MarekSpodBiedry 7f84f9c874 Linetype rename keeps layer, object and current-linetype references (#74) (#195) 2026-10-10 14:40:13 -04:00
Valentin KleinandClaude Opus 5.5 3b73884abd Deliver browser saves as downloads; keep unsaved state when they fail (#133) (#193)
On wasm, saving skipped the file write but still marked the drawing saved,
renamed its tab and reported success, so nothing reached the user. Saving
now hands the bytes to a host delivery hook (`file::set_deliver`); the web
app installs a small web-sys download helper (Blob + object URL + temporary
<a download>), reusable for other files such as plots. Without the hook, or
when the download can't be started, save errors and the drawing stays dirty.
A started download counts as saved and keeps the name for later QSAVEs.
The web app's Save/Save As (menu, close prompt) download under the drawing's
name, as there is no file picker in the browser.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:39:58 -04:00
MarekSpodBiedry 3d125493f9 Layer rename re-points paper-space and block-definition entities (#75) (#191) 2026-10-10 14:39:43 -04:00
MarekSpodBiedry e13de2c2f1 Layer delete and purge keep layers used by block definitions (#72) (#189) 2026-10-10 14:39:28 -04:00
Valentin KleinandClaude Opus 5.5 6d7502abf9 UNDO asks for the number of operations; U undoes one (fixes #241) (#242)
UNDO now prompts "Enter the number of operations to undo or
[Auto/Control/BEgin/End/Mark/Back] <1>" and undoes that many; Enter undoes
one. Its other options say they are not available yet (Auto and Control
still take their value), so nothing that follows is run as a command.
The JSON form (`undo {count?}`) is unchanged.

U is its own command, as in AutoCAD: it undoes one operation without
asking and is what Edit > Undo, Cmd+Z and the toolbar button run.

An interactive command outside undo that changed the drawing (UNDO)
records no undo step when it finishes, matching `execute`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:37:58 -04:00
Valentin KleinandClaude Opus 5.5 ca882b38e2 TEXTEDIT prompt, Undo and Mode; MTEXT takes lines until an empty one (#234) (#239)
TEXTEDIT's prompt no longer reads "or or". Undo reverts the last edit made
in this TEXTEDIT, and Mode switches between Multiple (keep selecting) and
Single (end after one edit). At the command line and in scripts, MTEXT
takes one paragraph per input line until an empty line, instead of ending
after the first line and running the next one as a command. The Text
Editor window sends its contents and then Enter, so it still ends MTEXT.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:37:52 -04:00
Valentin KleinandClaude Opus 5.5 a6b728b0f7 AREA Object/Add area/Subtract area and DIST Multiple points take effect (#233) (#237)
AREA's Object option measures picked objects; Add area / Subtract area keep
a running "Total area" over areas measured by points or by object, and
Enter at the first-corner prompt ends the command. Length continues the
last segment by a typed length in AREA and DIST point sequences. DIST's
Multiple points measures a path and reports the running and total
distance. Arc now says it is not available yet instead of being ignored
(or ending AREA as Total).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:37:46 -04:00
Valentin KleinandClaude Opus 5.5 98a1be7455 ROTATE: Reference option (fixes #235) (#236)
"Specify the reference angle <0>" takes a typed angle or two points;
"Specify the new angle or [Points] <0>" takes a typed angle, a point
measured from the base point, or two points after `P`. The objects turn by
the new angle minus the reference angle. Previously Reference only said
"not available yet" and the values that followed were misread or run as
commands.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:37:40 -04:00
Valentin KleinandClaude Opus 5.5 ba6af44d36 ELLIPSE Rotation/Parameter and BREAK First point take effect (fixes #219) (#222)
ELLIPSE: after Rotation the next value is the rotation around the major
axis (ratio = cos angle, 0 to 89.4 degrees; a picked point gives the
angle from the centre), instead of the other-axis distance. At the
elliptical arc's start/end prompts, Parameter reads the values as
ellipse parameters and Angle switches back to true angles.

BREAK: First point asks for a new first break point; the selection pick
then only selects the object.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:37:34 -04:00
Valentin KleinandClaude Opus 5.5 89799f48d4 Keep raster images through DXF save; report entities a save can't keep (#208) (#217)
Raster images: the reader now follows an IMAGE's group 340 to its IMAGEDEF
and keeps the file path and the definition's name (its ACAD_IMAGE_DICT
key), pixel size and resolution unit. It also keeps the clip boundary (in
pixel coordinates), the clipping state, the display flags and
brightness/contrast/fade. The writer emits the IMAGE entity, plus one
IMAGEDEF per path and name, an IMAGEDEF_REACTOR per image,
RASTERVARIABLES, the ACAD_IMAGE_DICT and ACAD_IMAGE_VARS entries in the
named object dictionary, and the four CLASS records, as the DXF Reference
describes. An image without a clip boundary gets the full-image rectangle.
DWG save keeps the same data, except the resolution unit, which the DWG
bridge drops. The pixels are still not loaded.

Entity types CADCraft doesn't model (MLINE, MULTILEADER, 3DSOLID, REGION,
MESH, ...) are still not written. Writing their raw groups back into the
R2000 file would leave references to objects that aren't kept
(MLINESTYLE, MLEADERSTYLE, ACIS data in later versions), and some of the
types don't exist in R2000 at all. Another CAD program could then reject
the whole file. Instead, qsave, saveas and document.bytes now say on the
command line which ones weren't saved ("Not saved (CADCraft can't write
these object types yet): 1 3DSOLID, 2 MLINE") and list them by type in the
result's `notSaved`. `cadcraft-cli run --save` and `convert` print the
same line.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:37:26 -04:00
MarekSpodBiedry 932ba8cc58 Respect locked layers in Clear, Cut, Stretch, Trim, Extend, Fillet, Explode and SplineEdit (#87) (#196) 2026-10-10 14:36:48 -04:00
MarekSpodBiedry 6957ef8756 Keep the active drawing when closing an earlier inactive one (#76) (#192) 2026-10-10 14:36:37 -04:00
MarekSpodBiedry 5a214a0cac Reject layer.set atomically: a refused call no longer applies other changes (#71) (#188) 2026-10-10 14:36:18 -04:00
3e0adadf94 Dynamic Input: pointer input boxes with Tab, comma and relative entry (#60)
* Dynamic Input: pointer input boxes, Tab separator, relative entry

While a command asks for a point and Dynamic Input is on, the cursor
tooltip shows a pair of value boxes, following AutoCAD's pointer input:

- The first point of a command is X and Y, absolute.
- Second and next points default to polar (distance and angle) measured
  from the last point, so no `@` is needed.
- Typing `,` after a value locks it as X and moves to Y; `<` locks it as
  the distance and moves to the angle. A locked value holds the cursor and
  shows a lock.
- Tab types the separator for the format on show, so `40` Tab `90` is 40
  units at 90 degrees, and with the Cartesian setting `40` Tab `40` is 40
  over and 40 up.
- Enter or Space sends the entry with `@` in front, filling an empty box
  from the cursor. A plain number and Enter is still direct distance entry;
  `@`/`#` entries, keywords and command names go through untouched.

The command-line text is the single source of truth: the boxes are a view
of it, so the command line always shows exactly what will be sent
(`40,`, `40<90`) and Backspace simply edits it.

Tab is taken from egui in the raw input hook (desktop and web) while no
widget has focus. Otherwise egui moves keyboard focus to the next button on
Tab and the command line stops receiving keystrokes.

Two new settings mirror AutoCAD's: DYNPIFORMAT (1 = second points start
Cartesian) and DYNPICOORDS (1 = absolute), through SETVAR and in Drafting
Settings under Dynamic Input. The tooltip uses the theme tokens, matching
the command line.

Scripts, the control channel and MCP never pass through the boxes and keep
the plain command-line rules. The parsing lives in a small pure module
(dyninput.rs) with unit tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr

* Dynamic Input tooltip: border colour from theme tokens

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 13:21:39 -04:00
kaffeochknivarandClaude Opus 5.5 a43e88e17a UNITS opens a Drawing Units dialog; SETVAR prompts at the command line (#30) (#38)
Typing UNITS ran the JSON form with no parameters, which changed nothing
and printed nothing. Typed or menu-invoked UNITS (no parameters) now opens
a Drawing Units dialog like QSELECT does: length type and precision, angle
type and precision, insertion units and a sample output. OK applies the
values through the `units` command; JSON calls run the command as before.

SETVAR had no prompts either, so `SETVAR LUNITS 2` ran with no name and
the remaining inputs were dropped. Space acting as Enter was not the
cause. SETVAR now asks "Enter variable name or [?]" and then "Enter new
value for NAME <current>"; `?` lists the variables, unknown names end the
command, and a value of the wrong type re-prompts.

sysvars::set now refuses variables that `get` derives from state
(DWGNAME, DBMOD, EXTMIN, ...) and keeps integer header variables integer,
so a typo can no longer store text in LUNITS.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:16:16 -04:00
5c2a18638e DIMANGULAR: accept a circle (#20)
* DIMANGULAR: accept a circle

The first prompt offers "arc, circle, line" but picking a circle was
rejected. Picking a circle now uses its center as the angle vertex and
the picked point as the first endpoint, then asks for the second
endpoint.

* cargo fmt

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:15:47 -04:00
kaffeochknivarandClaude Opus 5.5 d144d7f371 SETVAR DIM* variables become style overrides for new dimensions (#66) (#129)
`SETVAR DIMSCALE 24` was stored in the header but never reached new
dimensions: they were created with no overrides, and the header DIMSCALE
was only a fallback for styles whose scale is zero.

As in AutoCAD, the header DIM* variables that differ from the current
dimension style are now its overrides, and new dimensions take them on
(`Drawing::dim_overrides`). Existing dimensions keep their size. Making a
style current, or editing the current style, sets the variables back to
the style's values (`Drawing::sync_dim_vars`), so
`dimstyle {"name": "Standard", "DIMSCALE": 24}` still works.
GETVAR/SETVAR read DIM* variables the header doesn't carry from the
current style.

Dimension extents now use the same scale as the geometry (the style with
the dimension's overrides) instead of multiplying by the header DIMSCALE.
A DIMSCALE of zero (AutoCAD: scale to the layout viewport) falls back to
the drawing's DIMSCALE and then 1 instead of 1e-9.

Reading DXF, $DIMDSEP becomes the separator character, as the DIMSTYLE
table already is, so AutoCAD's header variables don't turn into spurious
overrides.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:15:33 -04:00
by TruittandClaude Opus 5.5 d87950afe8 Press-and-drag selection windows (PICKDRAG = 2) (#63)
Selection windows only worked click-then-click: the canvas reacts to clicks,
and a press that moves before release is a drag, not a click, so a drag was
ignored entirely.

Now, while objects are being selected (no command running, or a command
asking for objects), a drag opens the window where the button went down and
letting go closes it, as with AutoCAD's default PICKDRAG = 2. Dragging right
is a window, dragging left a crossing window, Shift still removes, and
click-click works as before. Drags that start on a grip or while a command
asks for a point are left alone.

Session::begin_window opens the window at the drag's start, even over an
object (a click there would pick the object instead), with an engine test.


Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:15:20 -04:00
Brandon ThomasandClaude Opus 5.5 8fceaaff3a Fix the arc-length DWG round-trip test build; cargo fmt
The squash for #159 picked up an earlier revision of the #140 test, which
called as_bytes() on a Vec<u8>. Also apply rustfmt to code from #120 and #155.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:04:59 -04:00
3e7e0bdf2a Linux: default to XWayland for file drag-and-drop on Wayland desktops (#159)
* DIMANGULAR: accept polyline segments (fixes #10)

Picking a side of a rectangle or a PLINE segment was rejected with
"Select a line or an arc." because only LINE and ARC entities were
accepted. The picked object is now resolved to the segment under the
cursor: straight polyline segments work like lines and arc segments
like arcs.

* Redraw edits made while a command is still running (fixes #4)

LINE and PLINE add geometry on every point, but the canvas only rebuilt
its display list when the drawing revision or the document pointer
changed. The revision was bumped when a command finished, and the
pointer changed only on the first copy-on-write edit, so only the first
segment appeared until the command ended. Session::feed now bumps the
revision after each input once the running command has changed the
drawing.

* Fix #14: include DWG in the Open drawing filter

* Blocks window: click a block to insert it (#8)

Each block name in the Blocks window is now a button. Clicking it starts INSERT with that block already chosen, so the command line asks for the insertion point.

* SCALE: Reference option (#17)

SCALE > Reference printed "not available yet". It now asks for the reference length (typed, or two points) and the new length (typed, or a point measured from the base point), and scales by new / reference. Zero, negative or non-finite lengths are rejected and asked again.

* Print and Ctrl+P ask where to save the PDF (#5)

PLOT/EXPORTPDF started from the menu, the toolbar, Ctrl+P or the command line had no path,
so the PDF was built in memory and dropped. They now open the save dialog (PDF filter first)
and write the file there. JSON calls from scripts, the control channel and MCP still never
open a dialog. Ctrl+P was shown in the menu but not bound; it is now.

* Scripts: split a line at keyword prompts so POLYGON 6 200,20 I 15 works (#43)

* Layer Properties Manager: stop the window growing every frame (fixes #29)

The vertical separator between the filter tree and the table takes all the
height it is offered, and a resizable window offers its previous height, so
the window grew 30px per frame until it filled the screen. Cap the row at the
table's fixed height. Regression test runs the dialog headless for 30 frames
and checks the height stays put.

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

* ui.render: fit the drawing and return pngBase64 like the headless render (#49, #50)

* Linux: default to XWayland for file drag-and-drop on Wayland desktops

* Fonts: per-character fallback, FONTALT and .ttc for CJK text in SHX styles (#130)

CJK text in a style that uses an SHX font (romans.shx + chineset.shx big font)
drew every ideograph as an empty box: the SHX name resolves to our stroke font,
which has no CJK glyphs, and the font scan skipped .ttc collections, where most
CJK system fonts live.

- Per-character fallback: a character the text's font lacks (stroke font or
  TrueType) is drawn from an installed font that has it, checked by cmap.
  Wide-coverage fonts go first (Noto Sans CJK, Source Han, WenQuanYi, Droid
  Sans Fallback, PingFang, Microsoft JhengHei/YaHei, MS Gothic, ...), regular
  weights before others. Fallback glyphs are sized like TrueType text (cap
  height = text height). Lookups are cached; the search is bounded (24 files,
  160 MiB, 256 faces per character, files over 96 MiB never read) and skips
  spaces, controls and private-use code points. No fallback: the box as before.
- FONTALT (sysvar, seeded from CADCRAFT_FONTALT): font used for a style font
  that can't be found (an SHX file, a missing TTF). Empty = our stroke font.
- FONTFALLBACK (sysvar, seeded from CADCRAFT_FONTFALLBACK): fonts tried first
  for missing characters, e.g. "Noto Sans CJK TC".
- .ttc/.otc are scanned; TextFont::Outline carries a ttf::Face (bytes + face
  index). Fonts are also found by family name among files with a similar
  stem, so "Noto Sans CJK TC" picks face 3 of NotoSansCJK-Regular.ttc.
- Cap height falls back to the height of "H" when OS/2 has none (DejaVu
  Sans), which fixes ttf_tests::system_font_outlines_when_available and
  render's truetype_text_is_filled_or_outlined on machines without Arial.
- README: a Fonts section for the two settings; ROADMAP M4 note.

Native SHX / big-font rendering stays on the roadmap (M4).

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

* Fonts: cap the total bytes of system font files loaded from disk

A drawing naming many text styles could otherwise load every installed font
into memory through family-name lookups.

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

* Pan with a two-finger trackpad swipe; keep wheel and pinch zoom

Trackpad two-finger swipes arrive as scroll events, and the canvas turned
every scroll into a zoom, so trackpad users had no realtime pan (middle-drag
needs a middle button).

Scroll now follows the device: precise point deltas (trackpad, Magic Mouse)
pan the view by x and y; notched line deltas (mouse wheel) zoom about the
cursor as before. Pinch and Cmd+scroll still zoom through zoom_delta, and
middle-drag still pans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr

* Add Tru10 to contributors/people.toml

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr

* Trackpad pan: only on macOS, where point deltas mean a trackpad

Browsers and Wayland smooth-scroll mice report ordinary wheels in points too,
so the wheel keeps zooming there.

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

* Keep arc-length dimensions as arc-length through DXF save and reopen (#100)

* PLINE: Halfwidth is centre-to-edge, and Width answers taper the next segment (fixes #83, #104)

Interactive PLINE kept one width value: Halfwidth answers were stored as
the total width, and the ending-width answer overwrote the starting one,
so a taper became a constant width. The next segment's starting and
ending widths are now tracked separately, Halfwidth answers are doubled,
and each new segment (including the closing one) gets its widths on its
first vertex; later segments are uniform at the ending width, as in
AutoCAD. Uniform polylines are still stored as const_width, so their DXF
output is unchanged.

Fixes #83. Fixes #104.

* Arc-length dimensions: write a standard aligned record plus CADCraft xdata

DXF has no arc-length DIMENSION type, so write type 1 and mark the kind and
arc centre in CADCRAFT xdata (like ASSOC). Other programs read an aligned
dimension; CADCraft restores the arc length, through DXF and DWG.

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

* Desktop: write log records to standard error and a rotating log file

The desktop app installed no logger, so every log::warn!/log::error!/
log::info! in the workspace was discarded: a broken app icon, the GPU
canvas set-up, the engine's command-line echo at debug level. A launch
from a desktop menu has no terminal either, so there was nothing to
attach to a bug report.

apps/cadcraft/src/logging.rs (no new dependency):
- standard error plus <settings dir>/logs/cadcraft.log; each launch
  rotates the previous log to cadcraft.1.log and .2.log, so the log of a
  run that crashed survives the next start; the file is capped at 16 MiB
- records logged before the settings directory is known are buffered
  (bounded) and written once the file is attached
- default levels: info for cadcraft*, warn for everything else;
  RUST_LOG replaces them with env_logger-style directives
- never panics: an unusable directory or a failed write leaves standard
  error as the only sink; a poisoned lock is tolerated

CADCraft had no per-user directory yet. config_dir in main.rs adds one:
CADCRAFT_CONFIG_DIR, else $XDG_CONFIG_HOME/cadcraft or ~/.config/cadcraft
(Linux, BSDs), ~/Library/Application Support/CADCraft (macOS),
%APPDATA%\CADCraft (Windows).

The logger is installed first in main and the file is opened after
argument parsing (--version leaves no file). The control server's
eprintln!s now go through the logger. A --control or
CADCRAFT_CONTROL_PORT value that is not a port is now logged instead of
silently leaving the control channel off. Documented in README.md
(Quick start > Logs).

---------

Co-authored-by: SBaksa <sabaksa11@gmail.com>
Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
Co-authored-by: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com>
Co-authored-by: lloyd <34131158+LloydNicholson@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Tru10 <133776756+Tru10@users.noreply.github.com>
Co-authored-by: Gert Pellin <gert@pellin.be>
2026-10-10 12:58:26 -04:00
MarekSpodBiedry 415da86d39 Scripts: split a line at keyword prompts so POLYGON 6 200,20 I 15 works (#43) (#45) 2026-10-10 12:57:42 -04:00
MarekSpodBiedry a1df2f2c8c SCALE: Reference option (#17) (#27)
SCALE > Reference printed "not available yet". It now asks for the reference length (typed, or two points) and the new length (typed, or a point measured from the base point), and scales by new / reference. Zero, negative or non-finite lengths are rejected and asked again.
2026-10-10 12:57:31 -04:00
Books 093c87d08a DIMANGULAR: accept polyline segments (fixes #10) (#15)
Picking a side of a rectangle or a PLINE segment was rejected with
"Select a line or an arc." because only LINE and ARC entities were
accepted. The picked object is now resolved to the segment under the
cursor: straight polyline segments work like lines and arc segments
like arcs.
2026-10-10 12:57:13 -04:00
Books 698f2c8c36 Redraw edits made while a command is still running (fixes #4) (#22)
LINE and PLINE add geometry on every point, but the canvas only rebuilt
its display list when the drawing revision or the document pointer
changed. The revision was bumped when a command finished, and the
pointer changed only on the first copy-on-write edit, so only the first
segment appeared until the command ended. Session::feed now bumps the
revision after each input once the running command has changed the
drawing.
2026-10-10 12:57:07 -04:00
Books 3c577968f6 ARC: refuse zero-radius arcs instead of adding them (fixes #85) (#123)
The `arc` command checked its three-point form for collinear points but
added the arcs built by its start/center/end and center/radius forms
unchecked, so radius 0 (or a start point on the center) added a
degenerate Arc, dirtied the drawing and recorded an undo step. Every
form now goes through the existing finite_arc check and fails with the
same "these values do not define an arc" error the arc.* variants and
the interactive ARC already use, leaving the drawing unchanged.

Fixes #85.
2026-10-10 12:26:20 -04:00
MarekSpodBiedry 3e0de7e4eb Keep a free measured origin in place when its source moves (#109) (#152) 2026-10-10 12:26:01 -04:00
Valentin KleinandClaude Opus 5.5 6c145ed733 Menu items open their missing dialogs; COUNT shows its result; New Layout asks for a name (#126, part 1) (#180)
* Menu items open their missing dialogs; COUNT shows its result; New Layout asks for a name (#126)

Eight menu items ran their JSON form with no parameters and changed nothing.
Typed or chosen from a menu they now do what the menu promises; JSON calls
still run the command and never open a dialog.

- New crates/ui-egui/src/managers.rs: Linetype Manager (load from the
  library, set current, global scale, pattern preview), Point Style
  (PDMODE tiles drawn in code, size relative/absolute), Table Style and
  Multileader Style (list, set current, new, edit properties), Page Setup
  Manager (per layout: paper, orientation, plot area, fit/scale, center,
  lineweights) and Constraint Settings (infer, bars, transparency, dynamic
  constraints, AutoConstrain types and tolerances). All changes go through
  their commands (app.run), so they are undoable and scriptable.
- menus::run_ui_command routes LINETYPE/LT, DDPTYPE, TABLESTYLE/TS,
  MLEADERSTYLE/MLS, PAGESETUP and CONSTRAINTSETTINGS/CSETTINGS without
  parameters to the dialogs; dialogs::show dispatches them.
- COUNT returns a `message` (per-block counts and total) that the command
  line shows; Session::start echoes multi-line messages line by line.
- LAYOUT.NEW from the menu or command line prompts
  "Enter name of new layout <LayoutN>:"; Enter takes the default, a taken
  or invalid name is reported and asked again.

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

* Move COUNT test away from other PRs' hunks to avoid merge conflicts

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:15:42 -04:00
Valentin KleinandClaude Opus 5.5 8d71f7de4a Properties palette: every control acts or is visibly unavailable (#182)
* Properties palette: every control acts or is visibly unavailable (#6)

Several Properties palette controls looked clickable but did nothing: most
current-property rows with nothing selected (linetype, lineweight, thickness,
text/dimension/multileader/table style), the selection-type drop-down, a
circle's circumference and area, a line's length and angle, a dimension's
style, and the All/My switch.

- Nothing selected: Linetype runs `linetype {current}`, Lineweight `lweight`,
  Thickness `setvar THICKNESS`, the style rows `style.current`,
  `dimstyle.current`, `mleaderstyle` and `tablestyle`. Transparency,
  Annotation scale and Plot style(s) have no command yet: greyed with a
  "Not available yet" tooltip.
- Header: the type drop-down narrows the selection to one object type
  (`select {handles}`), plus Toggle PICKADD (`setvar PICKADD`), Select Objects
  (`select`) and Quick Select buttons, with icons drawn in code.
- Geometry: circle circumference/area, line length/angle and dimension style
  are editable through `properties.set` (new keys `circumference`, `area`,
  `dimStyle`); computed values (line deltas, polyline length/area, block
  name) are greyed read-only.
- PICKADD now changes clicking with no command running: off, each pick
  replaces the selection and Shift adds.
- Palette text fields keep typed text while the window itself is unfocused
  (they read focus from egui memory, not `Response::has_focus`).

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

* Move palette tests away from other PRs' hunks to avoid merge conflicts

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:15:34 -04:00
Valentin KleinandClaude Opus 5.5 306ba991a5 Selection commands prompt "Select objects" without a pickfirst selection (#126) (#176)
Edit > Cut / Copy / Copy with Base Point, View > Zoom > Object, Dimension >
Update, Tools > Draw Order > Bring to Front / Send to Back and the Format >
Layer Tools LAYCUR, LAYFRZ, LAYOFF, LAYLCK and LAYULK did nothing when started
from a menu, toolbar or the command line with nothing selected.

- machines::SelectRun: "Select objects:", then run the command's JSON body on
  the picked handles. With a pickfirst selection it runs at once, as before.
  It replaces the DIMREASSOCIATE/DIMDISASSOCIATE SelectThen in annotate.rs and
  calls the command body directly, so the command records one undo step.
- LAYFRZ/LAYOFF/LAYLCK/LAYULK pick one object ("Select an object on the layer
  to be frozen" etc.).
- COPYBASE asks for the base point first, then for objects (skipped with a
  pickfirst selection).
- Objects picked at the prompt are deselected afterwards.
- JSON/programmatic calls are unchanged and never prompt.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:15:26 -04:00
Valentin KleinandClaude Opus 5.5 7666c1cd68 LINE: deferred tangent/perpendicular for the first point (fixes #69) (#168)
TAN and PER snaps need the other end of the line, so the first point of a
LINE (no base point yet) could never snap tangent: a belt line tangent to
two circles was impossible to draw.

Now, as in AutoCAD, a point prompt can accept *deferred* snaps:

- snap: with no base and the new `deferred` flag, osnap offers "Deferred
  Tangent" (circles, arcs) and "Deferred Perpendicular" (circles, arcs,
  lines, xlines, rays) where no other snap wins. The hit carries the curve
  (`SnapHit::deferred`, `Deferred`, `SnapCurve`).
- `resolve_deferred` solves the line once the other end is known: tangent
  or perpendicular from a point, tangent-to-tangent (common tangents of two
  circles, new `Circle::common_tangents` in geom) and the perpendicular
  mixes. Among several solutions the one nearest the picks wins. No
  solution (point inside the circle, concentric or nested circles,
  non-finite input) is `None`, never a panic.
- prompt: `Prompt::deferred()` opts a prompt in; `Input::Deferred` reaches
  only such prompts, everywhere else the session turns it into the point it
  was picked at, so other commands snap as before.
- LINE opts in for its first point (and the next one while the first is
  still deferred). No solution reports and re-prompts; Undo drops the
  deferred point. The rubber band shows the resolved line, also the
  tangent-to-tangent one while hovering the second curve
  (`Session::cursor_deferred`, set by the canvas).
- canvas: passes the prompt's flag to osnap and sends a deferred pick as
  `Input::Deferred`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:15:18 -04:00
Valentin KleinandClaude Opus 5.5 4d61f57eec HATCH/GRADIENT/BOUNDARY: islands made of separate lines (fixes #106) (#163)
Point picks found the outer boundary by tracing the planar graph of all
curves, but islands only came from single closed objects (circles, closed
polylines, full ellipses, closed splines). A square drawn as four LINEs
inside a rectangle was ignored: HATCH drew through it, GRADIENT filled it
and BOUNDARY created only the outer polyline.

- geom::region::boundary_at returns the outer loop plus islands: every
  connected group of curves inside the face that doesn't touch its
  boundary and doesn't surround the pick point. Each island is the
  outline of the group's outside, traced with the same sharpest-turn
  walk from its leftmost vertex after dangling ends are pruned. Islands
  report which input curves they came from. Traces are bounded by the
  group's edge count and islands are capped at MAX_ISLANDS.
- loops_at (shared by HATCH, GRADIENT and BOUNDARY) adds those islands
  when every curve in the group is an open LINE, ARC, elliptical arc,
  open polyline or open spline. Groups containing a closed object keep
  the existing per-object islands, so closed-polyline and circle islands
  are unchanged (bulges kept).
- Tests: geom region tests for separate-line islands, dangling ends,
  open and attached curves, nesting and hostile input; engine tests for
  HATCH (rendered pattern stays out of the hole), GRADIENT (375 filled,
  hole clear) and BOUNDARY (two polylines) with four-line and
  closed-polyline islands.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:15:09 -04:00
Valentin KleinandClaude Opus 5.5 7497618f76 fix(engine): programmatic undo/redo ends a running command first (#178)
* Programmatic undo/redo ends a running command first (#177)

`undo` or `redo` run through engine.execute, MCP, the control channel or a
script while an interactive command was still running (e.g. TEXT waiting for
another line) swapped the drawing under that command. Interactive commands
push their undo step only when they end, so `undo` popped the previous
command's step instead: it removed the earlier geometry, dropped the running
command's entities, and left the command running from a stale state.

Both commands now first end the running command the way Esc does, exactly as
Session::start already does for Cmd+Z, the menu and the command line. The
command's work so far becomes one undo step, and `undo` then removes that step.

Regression test: programmatic_undo_during_a_command_ends_it_first.

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

* Move test away from other PRs' hunks to avoid merge conflicts

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:14:53 -04:00
Valentin KleinandClaude Opus 5.5 3fe93806b7 Entity transparency: set it, report it, round-trip DXF group 440 (#174)
properties.set documented `transparency` but never read it, nothing reported it,
and DXF dropped group 440. Fixes #132.

- properties.set takes "ByLayer", "ByBlock" or a percentage 0..90 (a number or
  "50"/"50%"); anything else (out of range, NaN, junk) is an error and leaves
  the selection unchanged.
- properties and drawing.inspect / drawing.entities report it in the same form.
- Properties palette: the Transparency row is a menu (ByLayer, ByBlock, a
  0..90 percentage) that acts through properties.set.
- DXF: entities write 440 unless ByLayer (ByBlock 0x01000000, a percentage its
  alpha | 0x02000000, the alpha mapping layers use); the reader parses it back,
  any other value reads as ByLayer.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:14:37 -04:00
MarekSpodBiedry df1c074fff Keep an ordinate dimension's datum fixed when its feature moves (#105) (#157) 2026-10-10 12:14:00 -04:00
MarekSpodBiedry 947535158a Store canonical autoTypes names so accepted spellings take effect (#95) (#156) 2026-10-10 12:13:53 -04:00
MarekSpodBiedry 0af1528f4c AREA of a normal hatch no longer counts unfilled islands (#91) (#155) 2026-10-10 12:13:47 -04:00