Commit Graph
535 Commits
Author SHA1 Message Date
fa03bf2a29 feat(engine): INSERT of a drawing file keeps an attribute definition's field (#2619)
* feat(engine): INSERT of a drawing file keeps an attribute definition's field

The attribute definitions of a drawing file read by INSERT name=file lost their
extension dictionary, so the ACAD_FIELD dictionary and the FIELD objects of an attribute
whose value is a field (the file name in a title block) were gone from the block.
adopt() now copies that object tree to the drawing under new handles, with the FIELD
class and a field list if the drawing has none.

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

* foreign: index the kept objects once when walking and copying a dictionary tree

A file can keep many objects; a linear lookup per pointer grew with their product.

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

---------

Co-authored-by: andrewbrake <6925387+andrewbrake@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-02 05:31:18 -05:00
2ez4gcxandClaude Opus 5.5 b006110dc5 OOPS: bring back what -WBLOCK erased; keep the last erase through REFEDIT; the Block Editor (#2625)
* OOPS brings back what -WBLOCK erased

-WBLOCK erases the objects it writes, and OOPS brings back what the
last ERASE or BLOCK removed; the two landed separately, so OOPS never
saw what -WBLOCK erased. -WBLOCK now erases through erase_kept, as ERASE
and BLOCK do.

As AutoCAD 2024 does (accoreconsole, the drawing's entnext order read
back): OOPS after -WBLOCK brings its objects back, each where it was and
with its handle; only the last erase counts, -WBLOCK's or ERASE's; and
-WBLOCK * erases nothing, so OOPS still brings back the erase before it.

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

* OOPS: keep the last erase through a refused REFEDIT command; Block Editor

During REFEDIT, a command that removes an object outside the working set
is undone (refedit::after_command), but erase_kept had already replaced
what OOPS kept: the objects came back, and the erase before them was
lost. erase_kept now leaves OOPS as it was when an object it removes is
outside the working set. That covers BLOCK, -WBLOCK, ERASE and the
Delete key.

In the Block Editor, its objects keep the handles they have in the block
definition, so in_blocks found them there and OOPS brought back nothing
erased in the editor. in_blocks now skips the block open there.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 05:31:14 -05:00
2034d6899e Polyface meshes stay one object through DXF and DWG (#2600)
* Polyface meshes stay one object through DXF and DWG

A polyface mesh (POLYLINE flag 64) was read as loose 3D faces. It is now
read, drawn, exploded into its 3D faces and written back as a POLYLINE.
Hidden edges aren't drawn. A mesh too large for the 16-bit groups 71-74
is saved as 3D faces.

Split from #1005.

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

* A polyface face ends at its first zero index

A face record with a gap ([1, 2, 0, 3]) has two corners and is dropped,
not read as a triangle.

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

* Polyface meshes: write only faces that name their vertices; ID gives their Z

The writer drops the faces the reader would drop, so no record names a
missing vertex, and the 16-bit check only needs the counts. ID and DIST
give a mesh the Z of its nearest visible edge, as they do for 3D faces.

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-11-01 14:40:30 -05:00
e8bcc38e34 OOPS: bring back the last erase, each object where it was (#2605)
There was no OOPS. It now brings back what the last ERASE removed (the
Delete key too), or BLOCK deleting the objects it makes a block of, as
AutoCAD 2024 does (accoreconsole, the drawing's entnext order read
back):

- Each object keeps its handle and goes back directly above the object
  that was under it, or under the one that was above it when that is
  gone. An erase records both neighbours (EntityStore::neighbours).
- Only the last erase counts; entdel doesn't. OOPS says nothing and is
  one undo step.
- An object stays out when it is already in the drawing, when an undo
  took the drawing back to before it was made, when its layer, block or
  layout is gone, and, during REFEDIT, when it isn't in the working set.
  REFCLOSE forgets the working copies; the Block Editor has an OOPS of
  its own.
- The command's label is in the 17 interface catalogs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-01 14:40:15 -05:00
152617f4df Plot: a layout's saved page setup is used by default (part of #2493) (#2586)
* Plot: a layout's saved page setup is used by default (#2493)

A layout whose page setup says plot window, scale to fit or a plot scale was plotted 1:1 from
the sheet origin unless the caller passed fit. plot, exportpdf, the PNG device and preview now
read the layout's saved window, scale to fit, scale and centring when the options don't say
(render::paper::saved_plot); explicit options win, and fit: false without a scale is still the
plain 1:1 sheet. Tests: saved_setup_tests in pdf.rs (four), and
preview_uses_the_layouts_saved_page_setup_by_default.

* Parity checklist: regenerated after merging main

* Parity checklist: regenerated after merging main

* Plot: saved page setup in preview and PNG matches the PDF (review on #2586)

Preview honours the saved centre when fitting a window and an explicit scale beats a saved scale to fit (PDF too); PNG places a saved window without fit and clips to the printable area.

* Preview: a saved window clips to the printable area (review on #2586)

The preview drew the placed plot over the margins while the PDF and PNG blank them. The blanking helper now lives in the render crate and both the PNG plot and the preview use it. Test: preview_clips_a_saved_window_to_the_printable_area.

* Preview: pin hostile saved page setups and the default setup (review on #2586)

Degenerate or far-away saved windows and saved scales of 0 or not finite already fall back to the plain plot in the PDF, PNG and preview; tests now pin that, and that a layout with no saved setup plots as before.

* Preview: the printable clip follows the view's fit (review on #2586)

The clip assumed the sheet filled the image; when the height cap applies
View::fit scales and centres it. The printable corners now go through
View::to_screen. Test: preview_clip_follows_the_view_when_the_height_cap_applies.

* docs: leave the generated parity checklist to maintainers

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-11-01 14:38:34 -05:00
f0b46e34dc Hit tests don't lay out dashes: moving the mouse over a block stays smooth (#2603)
* Hit tests don't lay out dashes: moving the mouse over a block stays smooth (#1285)

Every mouse move hit-tests the block references under the cursor, and
hit_polylines built them with a full-detail display list, one primitive per
dash. A fine pattern on long lines in a block inserted at 0.001 made millions
of dashes per move.

Options::linetypes off draws every line whole and continuous, and
hit_polylines uses it for blocks, dimensions and multileaders. A dashed line in
a block is now hit across its gaps, as one outside a block already was.

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

* render: with linetypes off, a lightweight polyline is one continuous stroke

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

---------

Co-authored-by: Steffen <camperking@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-01 14:38:27 -05:00
2ez4gcxandClaude Opus 5.5 fb05c78590 Read bearings and compass points as AutoCAD does (#2590)
Measured in AutoCAD 2024 (accoreconsole):
- N, E, S and W are taken on their own in any angle format (LINE to
  @10<N goes north at AUNITS 0, 1 and 4).
- A bearing or a compass point is an angle like a number, measured from
  ANGBASE in the ANGDIR sense: with ANGBASE 90 and ANGDIR 1, @10<N45dE
  draws at 45 degrees and @10<N east; (angtof "N") is 0 there.
- <<< takes them from east counterclockwise; << (degrees) refuses them.

main took the letters in surveyor's units only, and read them and
bearings as absolute directions. One cardinal() now serves every angle
format, and the test that pinned the surveyor-only reading expects the
measured values. -UNITS uses the shared parser.

Also write the -UNITS tests' scripts with \n escapes again; they had
become multi-line strings.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:38:14 -05:00
2ez4gcxandClaude Opus 5.5 5e9c478bfb U inside a LISP routine takes back the routine's last command (#2610)
Inside a routine, commands recorded no undo step (the routine becomes
one when it ends), so U inside a routine took back the step from before
the routine: after drawing a line D, ERASE B then U in one routine
brought B back and removed D.

As AutoCAD 2024 does (accoreconsole, the drawing read back after one
routine), U inside a routine now takes back the routine's last command,
with what the routine did after it (entdel, entmake); U again, UNDO n
and REDO go on through its commands. With none left, U takes back the
drawing's step before the routine, as before, and the routine's step
then starts from there. Once the routine ends it is still one undo step.

The routine's steps live in the session's LISP state (RoutineSteps), not
in the drawing's undo stack, so a long routine doesn't push the drawing's
earlier steps out. A command inside the routine ends what REDO could
bring back; UNDO Control One and None apply. DocState::swap_in is the
one restore step undo, redo and the routine share.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:38:10 -05:00
Andrea FicarraandClaude Opus 5.5 c72c0eebfb SAVEAS portableFonts: Windows fonts for Linux text style fonts (#2576)
* SAVEAS portableFonts: Windows fonts for Linux text style fonts

A text style using a Linux TrueType font (Liberation, DejaVu, Open Sans)
shows in AutoCAD on Windows with a substitute it picks. A save now names
those styles on the command line (and as `nonPortableFonts` in the
result), and SAVEAS {"portableFonts": true} writes the Windows equivalent
in the file (Liberation -> Arial, Times New Roman, Courier New; DejaVu
Sans -> Verdana; Open Sans -> Arial) while the drawing keeps its own
fonts. Narrow and condensed families have no Windows stand-in (Arial
Narrow comes with Office) and are kept and named. QSAVE takes no SAVEAS
options.

Tests: stand_in_tests in cmd/file.rs, tests/save_portable_fonts.rs.

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

* Portable fonts kept in the file, applied by the writer

Reworked after review. SAVEAS `portableFonts` writes a copy of the drawing
with `Drawing::portable_fonts` set, and the DXF/DWG writer turns each
style's Linux font into its Windows font, keeping the style's own font in
the CADCraft FONT xdata already used for the built-in font. Reading the
file gives the Linux fonts back (unless the font was changed since, in
AutoCAD); the drawing itself is never changed.

It applies to that save: a later QSAVE writes the drawing's fonts and
says so, naming the styles and SAVEAS portableFonts. The family table
moves to `cadcraft_fonts::windows`, used by the writer and the note.
Styles from an xref aren't saved and aren't named. The parity docs are
left to the maintainers (CLAUDE.md).

Tests: crates/io/tests/it/portable_fonts.rs (DXF, DWG, a font changed in
AutoCAD), crates/engine/tests/save_portable_fonts.rs, windows.rs tests.

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

* Portable fonts: match the Windows font by file name

A STYLE whose Windows font carries a path (C:\Windows\Fonts\…) still
gets its own font back.

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

* Portable fonts: say the font list again only when it changes

A save still returns nonPortableFonts, but the command line note isn't
repeated on every QSAVE of the same drawing.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:38:04 -05:00
2ez4gcxandClaude Opus 5.5 0e2bff3b2b LEADER at the command line, as AutoCAD runs it (#2596)
LEADER had only its JSON form, so a script's LEADER drew nothing. This
adds the command-line command with AutoCAD 2024's prompts: the points,
Format (Spline/STraight/Arrow/None) and Undo, then annotation lines, or
the options None, Block, Copy, and Mtext and Tolerance (which come back
to the first line at the command line, as in accoreconsole).

Measured, and followed:
- A hook line, a horizontal last segment DIMASZ long, is added when the
  last segment is straight and more than 15 degrees off horizontal and
  text follows: 14.9 and 15 degrees keep the end, 15.1 hooks. A spline,
  no annotation and a block take none.
- The text sits DIMGAP past the end on the side the leader runs to:
  above it, bottom-attached, with DIMTAD on; centred on it with DIMTAD
  0. Sizes are DIMSCALE times the style's, with SETVAR overrides.
- A copied text keeps its contents, height and width, is centred on its
  line, and with DIMTAD on the leader ends half the text height lower.

The JSON form now draws through the same code. The drawing model has no
link from a leader to its annotation, so the two are separate objects.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:37:56 -05:00
2ez4gcxandClaude Opus 5.5 210cfc2a1d -WBLOCK at the command line, as AutoCAD runs it (#2594)
* -WBLOCK at the command line, as AutoCAD runs it

WBLOCK had only its JSON form, so a script's -WBLOCK did nothing: the
objects it should write and erase stayed in the drawing. This adds the
command-line command, with AutoCAD 2024's prompts:

- "Current settings: Object conversion=Delete", then the output file. A
  name without .dwg or .dxf gets .dwg. A DXF file asks for its decimal
  places first. A file that exists asks "... already exists, do you want
  to replace it? [Yes/No] <N>"; No ends the command.
- An existing block by name, "=" (the block named as the file), "*" (the
  whole drawing), or Enter for objects. A name that isn't a block is
  answered Block "NAME" not found. and asked again.
- For objects: the base point (with mOde), then the objects, which are
  written and erased. AutoCAD erases them whatever mOde says, and keeps
  Delete as the setting. Nothing selected writes nothing.

Its JSON form, -wblock, erases what it writes. WBLOCK's JSON form moves
into the new module unchanged, sharing the writer. The web build refuses
-WBLOCK rather than erase objects it can't save.

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

* -WBLOCK's JSON form: nothing to write writes no file

With no handles, an empty selection or only handles of objects that
aren't there, the JSON form wrote an empty drawing. It now refuses with
"no objects selected" before writing or erasing anything, as the command
line writes nothing when nothing is selected.

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

* -WBLOCK: only Yes replaces an existing file

At "... already exists, do you want to replace it? [Yes/No] <N>", any
text starting with "y" counted as Yes, so "yodel" replaced the file, and
any other text ended the command. Now only the Yes keyword replaces it,
No and Enter end the command, and anything else gets "Invalid option
keyword." and the prompt again.

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

* WBLOCK and -WBLOCK write their file through a temporary file

Replacing an existing file truncated it first, so a write that failed
part way lost it. The file now goes through atomic_write::write_atomic,
as SAVE, EXPORT and autosave write theirs.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:37:52 -05:00
2ez4gcxandClaude Opus 5.5 520394858c -ATTDEF at the command line: attribute modes, AFLAGS, and the text's prompts (#2597)
* -ATTDEF at the command line: attribute modes, AFLAGS, and the text's prompts

-ATTDEF asked for the tag first. A script's mode answer ("_V", "_C",
"_P") became the tag; the attribute kept no mode; AFLAGS never changed.
The definition was made at the start point, so the height and rotation a
script gives next ran as commands. It now asks as AutoCAD 2024 does:

- "Current attribute modes:" from AFLAGS, then "Enter an option to
  change [Invisible/Constant/Verify/Preset/Lock position/Annotative/
  Multiple lines] <done>", each answer toggling a mode, kept in AFLAGS.
- The tag (spaces taken, upper-cased; an empty one gets "Tag cannot be
  null"), the prompt and default value, or a constant attribute's value.
- "Current text style: ...", the point with Justify and Style, the
  height and the rotation.

The definition takes Invisible, Constant, Verify, Preset and Lock
position from the modes; a constant attribute stays in its block.

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

* -ATTDEF: a typed height is a distance, as TEXT takes it

The height was read with str::parse, so 1'6" (or any distance TEXT
takes) was refused as "Requires a positive height." It now goes through
the shared distance parser, `number`.

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

* -ATTDEF: a wrong answer at Justify is asked again

A wrong answer at "Enter an option [Left/Center/...]", or Align or Fit
(not available here), went on to the point prompt with the justification
unchanged, so a script's next answers landed one prompt off. Only a
justification goes on to its point now; anything else is asked again.

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

* -ATTDEF test: the modes start from AFLAGS, not from the default

The other cases start from AFLAGS 16, which is also the default, so
reading the modes from the drawing went untested. This starts from 0.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:37:49 -05:00
b6331c8603 -ARRAY: the classic command-line array, as AutoCAD lays it out (#2538)
* Add -ARRAY, the classic command-line array, as AutoCAD lays it out

Rectangular arrays move the copy in row r and column c by (c * column distance, r * row
distance), asking for a distance only when there is more than one row or column; polar arrays
space n items 360/n apart around a full circle and fill/(n - 1) otherwise, turning the copies or
moving them with the group 10 point of the object selected last. The copies are separate
objects; the type asked defaults to the one used last.

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

* -ARRAY: refuse an unknown type and oversized arrays before making transforms

The JSON form errors on a type other than rectangular or polar, and checks MAXARRAY before the
copies' transforms are made; the command line bounds the columns by the rows (CodeRabbit).

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-31 10:34:44 -04:00
2ez4gcxandClaude Opus 5.5 87f8ce22f6 CHANGE: change objects by a change point or their properties, as AutoCAD does (#2534)
* Add CHANGE, changing objects by a change point or their properties as AutoCAD does

A change point moves each line's nearer end to it (level or plumb under ORTHOMODE), passes a
circle through it, and places a text, attribute definition or block reference by it when that is
the one object selected; otherwise "Change point ignored" and the object's questions follow, in
selection order. Properties is CHPROP's loop with Elev, applied as the command ends. Objects on
locked layers are left out, through selection helpers now shared with CHPROP.

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

* CHANGE: refuse a JSON call without a readable point or elevation

A `point` or `elevation` that can't be read is an error, and so is a call with neither, instead
of changing nothing.

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

* CHANGE: Elev on every kind AutoCAD sets it for; Aligned and Fit text; rotation default

Measured in AutoCAD 2024: Elev also sets the z of ellipses, multiline texts, solids and flat
3D polylines (a 3D polyline between heights keeps them), and of hatches, which stop being
associative ("Hatch boundary associativity removed."). Aligned text isn't asked its height or
rotation and Fit text its rotation, their two points setting them. The rotation default shows
as the drawing measures angles (ANGBASE 90 shows a text at 0 as 270.00). Shared z is compared
to rounding, and the radius and height prompts say what they need.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:33:00 -04:00
2ez4gcxandClaude Opus 5.5 ac886483d1 -INSERT: ATTREQ, Preset and Verify attributes as AutoCAD asks them (#2535)
* -INSERT: ATTREQ, Preset and Verify attributes as AutoCAD asks them

ATTREQ 0 inserts blocks with their attributes' default values without asking (a session
setting, as AutoCAD keeps it in the registry). Preset attributes are never asked for, "Enter
attribute values" heads the questions, and Verify attributes are asked again after "Verify
attribute values", each with the value typed.

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

* -INSERT: a click doesn't answer a Verify attribute

Only typed text or Enter answers "Verify attribute values"; anything else asks again (CodeRabbit).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:32:56 -04:00
2ez4gcxandClaude Opus 5.5 a626b71284 fix(draw): RECTANG and POLYGON vertices in AutoCAD's order; RECTANG remembers its modes (#2554)
RECTANG's polyline now starts at the first corner and runs along X to
the other corner, as AutoCAD 2024 draws it; each chamfer cuts the edge
into its corner by the first distance and the edge out by the second,
and fillet arcs start on the edge into their corner (clockwise outlines
get negative bulges). RECTANG keeps its chamfer, fillet, width,
elevation, thickness and rotation for the next rectangle, lists them as
"Current rectangle modes:" and offers the rotation as "<50.50>"; chamfer
and fillet clear each other. A circumscribed POLYGON starts at the end
of the picked edge counter-clockwise from its midpoint.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:32:53 -04:00
2ez4gcxandClaude Opus 5.5 81e93f2caf Scale heights with SCALE and keep them with OFFSET (#2558)
AutoCAD 2024's SCALE is a uniform 3D scale about the base point: every Z
and elevation scales about the base point's Z (the current ELEVATION for a
point given without one) and every thickness by the factor. cadcraft
scaled X and Y only. The JSON form also takes a Z with the base point.

OFFSET's copy now keeps the source's height: a line's end Zs, a circle's
or arc's centre Z, a polyline's elevation. A circle's or arc's copy has no
thickness and a polyline's keeps it, as AutoCAD makes them.

Grip scaling, ALIGN's scale option and MOCORO stay 2D: their base point is
often a grip or snapped point whose Z cadcraft doesn't keep.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:32:49 -04:00
f18cdcbcac Add -UNITS; show due surveyor bearings as one letter (#2562)
* Add -UNITS; show due surveyor bearings as one letter

-UNITS asks for the length format and precision, the angle format and
precision, the direction of angle 0 and whether angles turn clockwise,
with AutoCAD 2024's prompts, listings, defaults and messages. As in
AutoCAD, each answer is kept as it is given, and choosing another format
resets its precision to 4. The direction is typed in the chosen angle
format and turns as ANGDIR says.

Surveyor's units show a bearing that is due north, south, east or west as
that letter ("E"), except to whole degrees ("N 90d E"), as AutoCAD's
ANGTOS does.

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

* units: read a due bearing typed as one letter (E, N, W, S) in surveyor's units

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-31 10:32:46 -04:00
fc9d056576 Refuse mirror lines whose points are closer than the geometry EPS (#2555)
* Refuse mirror lines whose points are closer than the geometry EPS

Found while testing crates/engine/src/cmd/modify/transform.rs:run_mirror (coincidence check `a.near(b, 1e-12)`), with crates/geom/src/mat.rs:43-47 (Mat3::mirror)

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

* mirror: the interactive prompt also refuses points closer than the geometry EPS

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

---------

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-31 10:32:42 -04:00
NetterSilverandClaude Haiku 5.5 0575a76291 Reject out-of-range OSMODE values in SETVAR instead of wrapping (#2531)
Found while testing crates/engine/src/sysvars.rs:282 (set, "OSMODE" branch)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-31 10:32:39 -04:00
NetterSilverandClaude Haiku 5.5 add1fd3528 Fix LISP atof returning a wrong value for out-of-range exponents (#2533)
Found while testing crates/engine/src/lisp/builtins/strings.rs:64-72 (atof)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-31 10:32:32 -04:00
NetterSilverandClaude Haiku 5.5 c254c51474 Reject nan and inf text values in SETVAR for real variables (#2546)
Found while testing crates/engine/src/sysvars.rs:~404-425 (generic fallthrough in set: String branch, then the `Some(HVal::Real(_))` arm)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-31 10:32:28 -04:00
NetterSilverandClaude Haiku 5.5 b0b2aba23e Reject non-finite arguments in CAL min() and max() (#2547)
Found while testing crates/engine/src/cmd/utility/calc.rs:~355-360 (call, "min" | "max" branch using f64::min/f64::max)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-31 10:32:24 -04:00
a83af92eb2 wblock: write the source drawing's $INSUNITS and $MEASUREMENT (#2002) (#2023)
* wblock: write the source drawing's $INSUNITS and $MEASUREMENT

run_wblock built the written drawing from Drawing::new_imperial() and
never copied the source's units, so every WBLOCK file declared inches
($INSUNITS 1) and imperial measurement ($MEASUREMENT 0) whatever the
source: a block written from a metric drawing came back at 0.0254 of
its size in any program that honours $INSUNITS on insert. The written
drawing now takes the source's values for both variables, as AutoCAD's
-WBLOCK does. A unitless source ($INSUNITS 0, kept by the reader per
#1209) writes unitless.

Co-Authored-By: GLM-5.3 <noreply@z.ai>

* wblock: start from the source drawing's header and styles

Clone the source header (resetting $INSBASE as before) and copy the
multileader, table and multiline styles and the annotation scale list
along with the other tables, so a block written from a metric drawing
keeps its text size, current dimension style, limits and scales, not
the imperial template's (review on #2023).

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

* wblock_units: each WBLOCK call writes into its own TempDir

The two tests shared one temp path and raced when cargo ran them in
parallel (review on #2023).

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

---------

Co-authored-by: GLM-5.3 <noreply@z.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:34:23 -07:00
Tanish Mankuandoctopoulpos e60d546ce4 Picking after an edit no longer ends the app on an rstar panic (#2539)
rstar 0.13 can panic when inserting after removals in a bulk-loaded tree, and picking runs outside the command guard. The patch now runs on a copy under catch_unwind and falls back to a bulk load. The web build can't catch a panic, so it always bulk-loads. Diagnosis, reproduction and fix from #2468.

Fixes #2468

Co-authored-by: octopoulpos <32617732+octopoulpos@users.noreply.github.com>
2026-10-31 00:34:14 -07:00
Tanish MankuandFilippo Reimer 42be8540a9 Backups of drawings sharing a name no longer overwrite each other (#2537)
plan.dwg and plan.dxf in one folder shared plan.bak. A DWG keeps <name>.bak, as AutoCAD names it, and any other drawing appends .bak. The naming comes from #1006.

Co-authored-by: Filippo Reimer <49290121+filforce95@users.noreply.github.com>
2026-10-31 00:26:50 -07:00
b35f00847a Validate new linetype names in the rename command (#2519)
* Validate new linetype names in the rename command

Found while testing crates/engine/src/cmd/props.rs:1154-1200 (run_rename, "linetype" branch)

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

* rename: a linetype's new name is stored trimmed, as -RENAME types it (CodeRabbit)

---------

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-31 00:45:52 -04:00
Brandon ThomasandClaude Opus 5.5 c6e49b177c test(autosave): fix a flake: wait for the background write to finish, not just for its file
Since #2518 the write syncs the folder after the rename, so the autosave
file exists while the write is still running. Until it finishes the drawing
is busy and the next change isn't planned, so the test's tick() returned
None. Autosaver::writing() says whether a write is still running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:44:27 -04:00
Tanish Manku d79f4f5732 Merge main into #2201 2026-10-30 19:20:04 -07:00
Tanish Manku b187af646a Save helpers used only by the file system build stay out of the wasm one (#2521) 2026-10-30 19:19:06 -07:00
assamaralandClaude Fable 5.1 9b7149676b fix(io): MTEXT legacy column groups no longer leak into the entity (#2247) (#2416)
* test(io): MTEXT legacy column groups stay in the AcDbMText subclass (#2247)

`legacy_column_groups_stay_in_the_mtext_subclass` (red: the column width 48 is read as the
entity's ltscale, 12.5; the height count 50 as a column height).

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

* fix(io): MTEXT legacy column groups no longer leak into the entity (#2247)

`common()` read the linetype scale (48) and the other AcDbEntity groups from the whole record,
so the column width 48 of an MTEXT's legacy column groups became its ltscale. They are now read
from the AcDbEntity subclass only. `read_columns` took the height count (the first 50) for a
column height and `column_tags` did not write it; both follow the DXF layout now.

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

* test(engine): the MTEXT entmake column heights carry their count (#2247)

Group 50 after the column type is the height count, then the heights. Thanks to austin62jake for the report and the fix.

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

* test(io): a marker-less MTEXT with an embedded object keeps its own ltscale (#2247)

Case from #2276 (Zaxaerith): without subclass markers, the 48 of an embedded object (101) was read as Common.ltscale.

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

* fix(io): stop the entity's own groups at an embedded object (#2247)

Without subclass markers, an embedded object's 48 was read as Common.ltscale. Suggested by austin62jake; case and test from #2276 (Zaxaerith).

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

* style(io): rustfmt the new MTEXT test

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

* fix(io): keep layer and colour that follow a subclass marker in a list with no AcDbEntity marker

entity_groups ended the common region at the first subclass marker other than
AcDbEntity, so an entmake list such as (0 . "LINE") (100 . "AcDbLine") (8 . "Walls")
was read on layer 0. The region now ends at a marker only when the record has an
AcDbEntity marker (files from R13 on always do); other records are read as a
whole up to an embedded object (101), as before the column change.

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

* fix(io): markers inside an application-defined group do not end the entity's groups

A `102 {...102 }` group before AcDbEntity may hold any code, 100 and 101 included: `entity_groups` stopped there and the layer and colour that followed fell back to defaults. The scan now skips the contents of such groups when looking for the AcDbEntity marker and for the end of its groups. Review of #2416.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(io): keep every column height of files written before the height count

CADCraft up to f536bf4 wrote group 76 = n and n group-50 heights, no count.
Reading those with the counted format dropped the first height and rewrote
76 as n - 1. The first 50 is a count only when there are 76 + 1 of them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(io): MTEXT column heights skip application-defined groups; no count for an empty list

A `102 {...}` group inside the MTEXT record may hold a 50 of its own: it
counted as a height and made a legacy record (76 = n, n heights, no count)
look counted, dropping the first height. `read_columns` now filters the
record with `outside_app` (shared with `entity_groups`).

`column_tags` wrote a lone `50 = 0` for a dynamic manual column with no
heights, which read back as one height of 0; the count is omitted for an
empty list, so `heights: []` round trips. ZWCAD 2025 also read that lone
50 as the rotation and zeroed it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 19:16:42 -07:00
midasmatterhorn dc6a0e9ec3 engine: renaming a layer repoints every viewport's frozen list and colour overrides (#1264) 2026-10-30 19:16:38 -07:00
Tanish MankuandAldo Zarate 3d623fc4d4 Saving over a DWG asks before it loses content, and saves keep a .bak (#2518)
Includes the .bak work from #1003.

Co-authored-by: Aldo Zarate <azarate@azf.com.mx>
2026-10-30 19:10:57 -07:00
Tanish Manku 04e74a165c Panic hunt covers every command, and a panic after a command restores the drawing (#2517)
panic_hunt runs every command in a fresh session and fails on real panics. The guard now covers the steps after a command, so a late panic restores the drawing instead of ending the app.
2026-10-30 19:10:52 -07:00
18d3aa4ed8 CHPROP: change properties at the command line, as AutoCAD asks for them (#2497)
* Add CHPROP, asking for properties as AutoCAD does

CHPROP selects objects, then asks "Enter property to change
[Color/LAyer/LType/ltScale/LWeight/Thickness/TRansparency]" until Enter,
applying each value through properties.set. Measured from AutoCAD 2024
through AutoLISP's command: colours 1-255, names, BYLAYER, BYBLOCK or
Truecolor; 0, 256 and 300 refused; a layer must exist; a linetype not
loaded is loaded from the standard file; a lineweight goes to the nearest
standard one with a note.

Keyword prefixes now have to reach past the keyword's last capital, so
"L" alone matches none of LAyer, LType, ltScale and LWeight, as in
AutoCAD.

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

* CHPROP tests: transparency, and refused true colours and transparencies

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

* CHPROP leaves objects on locked layers out, as AutoCAD does

Each pick and the pickfirst selection lose the objects on locked layers,
followed by "1 was on a locked layer." or "N were on a locked layer.";
with none of the pickfirst selection left, CHPROP asks for objects. The
JSON chprop leaves them out too and returns {changed, locked}.

The keywords test also pins QDIM's DA and PEDIT's vertex E, which AutoCAD
2024 refuses although only one keyword starts with them.

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

* chprop: look up locked handles in a set, and always report "locked" (CodeRabbit)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-30 15:46:27 -04:00
2ez4gcxandClaude Opus 5.5 29584fa4c4 SOLID: draw 2D solids, chained as AutoCAD chains them (#2489)
* SOLID: draw 2D solids, chained as AutoCAD chains them

CADCraft had the SOLID entity but no command to draw it. As AutoCAD
2024 asks from AutoLISP: first, second, third point, then "fourth
point or <exit>" (Enter makes a triangle); further solids take the
previous one's last two corners and ask their third and fourth points
until Enter. Corners stay in the order given.

The command (alias SO) has a JSON form taking the points chained the
same way. solid_chains_its_corners_as_autocad pins seven measured
calls to 1e-9.

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

* SOLID: offer it in the Draw menu

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:46:16 -04:00
2ez4gcxandClaude Opus 5.5 50df1284a3 -HATCH: Properties asks the pattern's scale and angle, and takes Solid (#2481)
* -HATCH: Properties asks the pattern's scale and angle, and takes Solid

The pattern option asked only for a name, so a script answering it as
AutoCAD asks (`_P ANSI31 2 45`) got "Unknown pattern 2" and "Unknown
pattern 45" and a hatch at scale 1, angle 0, and `_P _S` reported
"Unknown pattern _S".

As AutoCAD 2024's -HATCH Properties does, the pattern name is now
followed by its scale and then its angle (Enter keeps them), Solid
(S, _S or SOLID) sets a solid fill and asks neither, and an unknown
name or a refused value is asked again. The sCale and Angle options
and the pattern picker are unchanged.

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

* -HATCH: ask for the pattern again when a .pat file loads nothing

A .pat file that cannot be read, or holds no pattern, now repeats the
pattern prompt instead of going on to Scale and Angle with the
previous pattern.

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

* -HATCH: a pick at a Properties prompt asks again

Enter keeps the current pattern, scale or angle; a point or an object
(an entity name from LISP) answers none of those prompts, so the prompt
comes again instead of going on with the value unchanged.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:46:08 -04:00
2ez4gcxandClaude Opus 5.5 16ae23424d -INSERT: X, Y and Z presets, XYZ, and the Z factor AutoCAD stores (#2480)
* -INSERT: X, Y and Z presets, XYZ, and the Z factor AutoCAD stores

The insertion point prompt offers X, Y and Z: the factor for that axis
alone, the others 1, and no scale prompts after the point. The X scale
prompt offers XYZ, which asks X, Y and Z in turn. A Z factor not asked
is the size of the X factor: X -1 with Y as X stored Z -1, where AutoCAD
stores +1. Basepoint picks on the block dropped at the cursor, as
AutoCAD drops it where it is being dragged (the origin in a script),
instead of at its definition position.

Each was measured in AutoCAD 2024 from AutoLISP's command and is pinned
in insert_options_match_autocad.

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

* -INSERT: store the Z factor as AutoCAD does after Scale, Z and XYZ

Measured in AutoCAD 2024: Scale -2 gives X and Y -2 and Z 2; a Z preset
of -3 gives Z 3; XYZ with X -2 and Enter at Y and Z gives Z 2; a Z typed
at XYZ's Z prompt (-4) stays as typed. XYZ followed by a corner, with
Corner or a point straight away, still asks for Z, which defaults to
the size of the corner's X factor.

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

* -INSERT: an axis preset after Scale changes that axis alone

Measured in AutoCAD 2024: Scale -3 then X 2 gives 2/-3/3; X 2 then
Scale -3 gives -3/-3/3; Scale 2 then Z -5 gives 2/2/5. Scale now sets
all three presets, so a later X, Y or Z keeps the other two.

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

* -INSERT test: Basepoint with the cursor away from the origin

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:46:05 -04:00
2ez4gcxandClaude Opus 5.5 dda6562a97 Pick the object drawn last, not the nearest one (#2488)
select::pick said it returns the topmost entity within the aperture,
but it returned the nearest, using the draw order only to break ties.
AutoCAD 2024 picks the object drawn last among those under the pickbox,
and DRAWORDER changes which: a circle and a line drawn after it, 0.05
and 0.45 from the pick, give the line.

Candidates already come topmost first, so pick_with returns the first
within the aperture; pick_all keeps that order for selection cycling.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:46:01 -04:00
2ez4gcxandClaude Opus 5.5 2be869a154 Units and rtos: round a half away from zero, at 15 significant digits (#2484)
Decimal and scientific numbers went through Rust's formatting, which
rounds the exact binary value and a half to even: (rtos 1500.5 2 0)
gave 1500, (rtos 0.125 2 2) 0.12 and (rtos 1.005 2 2) 1.00.

AutoCAD 2024 gives 1501, 0.13 and 1.01: it rounds at 15 significant
digits, with a half away from zero, for scientific notation too
(2.5 to none is 3E+00). units::fixed does that on the decimal digits;
format_distance's decimal and scientific forms and rtos use it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:45:57 -04:00
2ez4gcxandClaude Opus 5.5 6ec6d97ea0 LISP: getvar and setvar take angle variables in radians (#2482)
* LISP: getvar and setvar take angle variables in radians

CADCraft keeps ANGBASE, SNAPANG, POLARANG, HPANG, CHAMFERD and
DIMJOGANG in degrees as far as sysvars::get/set go, which is right for
the SETVAR command, but AutoLISP's getvar returned those degrees and
setvar took them: (getvar "DIMJOGANG") gave 45.0, and
(setvar "ANGBASE" (/ pi 2)) set 1.57 degrees.

AutoCAD 2024 gives and takes them in radians from AutoLISP: each set to
30 by SETVAR, or to (/ pi 6) by setvar, reads 0.5235987755982987. The
LISP bridge now converts these six; the SETVAR command is unchanged.
getangle_is_relative_to_angbase sets ANGBASE in radians accordingly.

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

* SETVAR: keep real dimension variables real

A dimension variable whose style field is a real (DIMJOGANG, DIMSCALE
and the other real DIM* fields) is now stored as a real header value,
even when the first value set is a whole number. SETVAR DIMJOGANG 45
followed by (setvar "DIMJOGANG" 0.5) no longer rejects the fractional
degrees the radian value gives.

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

* SETVAR: store the value the dimension style took

A real dimension variable now stores the value its style field took after validation, so any input the style accepts (true for DIMSCALE gives 1) is stored as it was before.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:45:53 -04:00
2ez4gcxandClaude Opus 5.5 3ba2362ec9 LISP: entity names answer prompts that pick one object (#2477)
* LISP: entity names answer prompts that pick one object

`command` fed an entity name, alone or as `entsel` returns it (name and
point), to the running command as a pick. Prompts that pick a single
object with a point (MEASURE, DIVIDE, LENGTHEN, PEDIT, BREAK, TRIM and
EXTEND in Standard mode, FILLET, OFFSET, REVCLOUD Object, dimensions…)
only take points, so routines got "Invalid input" or nothing at all.

At such a prompt the name is now entered as its point, and a pick at
exactly that point finds the named object (Session::pick_object), so
the point says where on it, as in AutoCAD. A bare name stands at the
object's DXF group 10 point, the point AutoCAD picks it at: a line's
start, a circle's or an arc's centre, a polyline's first vertex. At an
arc's centre MEASURE runs from the arc's end, as AutoCAD does.

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

* Keep a named object for its own input and its own space

A name given at a prompt picking one object now holds only while that
input is taken: a later pick at the same point finds what lies there.
A name for an object in another space picks nothing, instead of the
object under its point in the current space.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:45:50 -04:00
Dylan M. Taylor d6de2db6db Update prompt shows the app icon and accent color instead of a letter (#2479)
UPDATE_APP (native builds; craft-update is a native-only dependency) carries our 128 px icon and accent (#3ea6e8, its colour in ArtCraft Launcher). The updater, the self-update
helper and craft-update's startup prompt all get them through crate::UPDATE_APP; the
prompt draws the icon, the accent and "by ArtCraft" (craft-update's default developer).

craft-update is pinned to storytold/craft-libs main at 1065653e7 (#434).
Clippy fixes for Rust 1.99 in untouched code (same logic):
- crates/constraints/src/auto.rs
- crates/engine/src/audit.rs
- crates/engine/src/cmd/inquiry.rs
- crates/engine/src/mtext_edit.rs
- crates/geom/src/nurbs.rs
- crates/io/src/dxf_raw.rs
- crates/io/src/dxf_read/document.rs
- crates/render/src/text.rs
- crates/ui-egui/src/cmdline.rs
- xtask/src/layers.rs
2026-10-30 01:28:46 -04:00
2d2be155dd fix(engine): refresh attribute baselines after value changes (#2214)
* xtask: the test suite and clippy pass on Windows

Two Windows-only failures stopped `cargo xtask ci` before it got going on
a Windows checkout (CI runs the gate on Linux only, so neither showed):

- parity::tests::civil_dates built a SystemTime 115,740,740 days after
  the epoch (year 318857). Windows' SystemTime ends in the year 30828, so
  the addition panicked ("overflow when adding duration to `SystemTime`").
  That case now checks `civil_from_days` on the day count, which is what
  it was about, on every platform.
- stats.rs imported `walk` for a test that only exists on Unix, so the
  import was unused on Windows and `clippy -D warnings` refused it. The
  Unix test calls `super::walk`, as its neighbour calls `super::walk_inner`.

Checked on Windows 11 x64: `cargo test --workspace` and
`cargo clippy --workspace --all-targets -- -D warnings` both pass.

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

* fix(engine): refresh attribute baselines after value changes

CC-046: invalidate the derived XY baseline for changed justified attribute
values, including small changes inside the writer's imported-font tolerance.
Preserve no-op placement, alignment anchors and Aligned/Fit endpoints.

Regression reproduced on current upstream; all three focused tests pass.
Full cargo xtask ci passed all seven stages on Windows (2558 passing test
executions, zero failures, one ignored). Release Trunk web build passed.
Browser interaction verification is pending user dismissal of a Windows
Security prompt; do not treat the build as a runtime pass.

Windows validation prerequisite: original upstream PR #2198 commit a2dd814.
Independent of editor/ribbon changes; reverting restores only this setter,
its regression tests and corresponding file-format parity record.

* fix(io): preserve insertion elevation when refreshing text baselines

CC-046 review follow-up to PR #2214 comment 4377706777. Regression reproduced insertion Z 9 becoming 7. Preserve group 10 Z; test DXF independent elevations and document the DWG bridge shared elevation. Full cargo xtask ci passes all seven gates. This separate commit is reversible; reverting it restores elevation loss during baseline recomputation. Existing browser UI verification predates this writer correction.

* test(engine): cover global attribute no-ops and two-point exports

CC-046 review 5653054817: verify equal-value global JSON and scripted
-ATTEDIT preserve imported placement, and Aligned/Fit DXF save/reopen
retain both placement points and justification.

Validation: cargo xtask ci passed all seven gates on this branch;
git diff --check passed. Test-only follow-up, no application change.
Depends on CC-046 and unchanged Windows prerequisite #2198.
Revert this commit independently to remove only these assertions.
Runtime evidence remains separately recorded for the production commits.

---------

Co-authored-by: 2ez4gcx <108074836+2ez4gcx@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:38:15 -04:00
9b4c1fb840 Reject negative or non-finite JSON chamfer distances (#2406)
Found while testing crates/engine/src/cmd/modify/fillet.rs:120-121 (run_chamfer) and 53-54 (fillet_lines)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-29 21:31:33 -04:00
42667d1332 ELLIPSE from axis endpoints matches what AutoCAD stores (#2197)
* ELLIPSE from axis endpoints matches what AutoCAD stores

Typed ELLIPSE input was checked against AutoCAD 2024 black-box (synthetic
input in accoreconsole, read back with entget); three rules differed:

- The major axis pointed at the second axis endpoint; AutoCAD points it at
  the first. A full ellipse looks the same, but an elliptical arc's angles
  are measured from that axis, so the arc was drawn on the opposite side of
  the center, and the stored major vector, start point and quadrant order
  all differed.
- A point at "Specify distance to other axis" gave its distance from the
  first axis; AutoCAD takes its distance from the center (the rubber band),
  then swaps the axes when it is the longer one.
- The end-angle prompt had no Included angle option, so `_I` was rejected
  and the next number became an absolute end angle. AutoCAD adds the
  included angle (default 180) to the start angle.

The JSON form (ellipse.axis / ellipse.arc with p1, p2, p3) follows the
same rules. The regression test pins ten AutoCAD-measured cases, among
them survey (VN-2000) coordinates, a near-millimetre ellipse and both
Included angle cases.

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

* docs: geometry parity says which part was measured against AutoCAD

The "How this was measured" section still said no numeric comparison
against AutoCAD had been run, next to the new ELLIPSE revision row that
reports one. It now names the 11 measured ELLIPSE cases and the test
that pins them, and keeps the rest of the document marked as read from
the source. Pointed out in review.

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

* ellipse: drop the import the axis-endpoint change left unused

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-29 21:31:26 -04:00
BooksandBrandon Thomas d15de75406 Dimensions: report the measurement as soon as one is created (#2421) (#2438)
* Dimensions: fill in the measurement when one is created (#2421)

A dimension made in the session kept measurement 0.0 until it was saved and reopened.
add_dim now sets it with the geometry call the DXF/DWG writer uses for group 42.
Block names are still assigned at save time. Test: a_new_dimension_reports_its_measurement.

* Parity checklist: regenerated after merging main

* Dimensions: a jogged radius dimension stores its radius as the measurement (#2421)

draw_jogged_radius set the text but not DimGeometry.measurement, so DIMJOGGED dimensions
kept measurement 0 (and the DXF/DWG group 42 was 0). It now sets the radius.
Test: jogged_radius asserts the stored measurement.

* Parity checklist: regenerated after merging main

* Dimensions: fill in a new dimension's text position when it is created (#2421)

A dimension made in the session kept text_mid (0,0) until it was saved and reopened.
add_dim now sets it from the same dimension_geometry result as the measurement, the
position the writer puts in group 11, unless the user placed the text.
Test: a_new_dimension_reports_its_text_position.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-29 21:31:22 -04:00
dcbcb6ab32 fix: keep WIPEOUT closure coherent across grips and outlines (#2284)
* fix: keep WIPEOUT closure coherent across grips and outlines

Treat WIPEOUT as a closed polygon with an optional exact terminal closure marker, not as an open polyline. Centralize logical vertices, explicit outlines and closure-preserving vertex edits in the document type. Reuse this contract in grips, geometric primitives, rendering and DXF export without changing the stored schema or writer closure convention.

Add six-version DXF ASCII/binary and DWG regressions for implicit/explicit closure, both windings, all corners, API/interactive edits, undo/redo and repeated saves. Keep open/closed polylines and their coincident endpoints distinct. Cover multiple hot grips, hostile inputs and equivalent mask/frame output.

Validation: cargo xtask ci, all seven stages passed (2571 tests passed, one pre-existing skip). Two unrelated network tests failed transiently in earlier runs; each passed in isolation and the complete final run passed without exclusions. Direct and reopened grip edits produce byte-identical CLI PNG output.

Fixes #2256; related test-only matrix PR #2237 and tracker #2238 B3.

* fix(doc): keep coincident WIPEOUT endpoints separately editable

When an edit makes the endpoints of an implicit boundary coincide, add a separate closing marker rather than reclassifying the edited corner as that marker. Retain the logical vertex count and allow either coincident corner to move independently afterwards.

The document regression fails before the fix. Add API/interactive coverage for both endpoint directions and both closure forms, undo/redo, and subsequent separate edits across all six DXF/DWG versions including binary DXF and save-after-edit. Update the documented editing contract.

Validation: cargo xtask ci passed all seven stages; 2573 tests passed, one pre-existing skip.

Addresses the coincident-endpoint review on PR #2284; related #2256.

---------

Co-authored-by: cloudj <jonbahnes@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-29 21:31:17 -04:00
3243271d4e Reject zero distance in JSON offset command (#2190)
Found while testing crates/engine/src/cmd/modify.rs:791 (run_offset)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-29 21:31:09 -04:00
NetterSilverandClaude Haiku 5.5 b9b7b3d439 Refuse invalid count in JSON copy instead of clamping it (#2294)
Found while testing crates/engine/src/cmd/modify/transform.rs:69 (run_copy)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-29 21:21:58 -04:00