Commit Graph
565 Commits
Author SHA1 Message Date
STE1200andClaude Opus 5.5 5ec0d215d8 CLI: info and convert report what a damaged DWG left out (read notes, #859) (#2337)
A damaged DWG opens partially since #859 and the drawing's read notes
say how many object records were left out. OPEN echoes them and MCP
`open` returns them as `warnings`, but `cadcraft-cli info` and
`convert` stayed silent: a corpus round trip wrote thousands of skipped
records away without a word.

- convert: each read note goes to stderr as `IN: <note>`, next to the
  skipped-records warning (#872). The exit code stays 0.
- drawing.inspect (and so info, MCP inspect_drawing and the control
  channel): new field `readNotes`, next to `skipped`. Existing fields
  are unchanged.
- run already prints OPEN's notes with the session log.
- README and docs/control-protocol.md say where the notes appear.

Part of #1039.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 15:01:51 -07:00
Newton Kilgore Sightsinger 3bb515485d Xrefs: a saved path is found regardless of letter case on case-sensitive file systems (#1982) 2026-11-02 15:01:36 -07:00
Rahulandrahul05ranjan 03101e8d77 fix(io): a moved DIMENSION draws its block at its insertion point (group 12) (#2634) (#2636)
A DIMENSION moved by an older AutoCAD keeps its *D block contents where
they were and the move in DXF group 12, and is drawn translated by the
insertion point. Group 12 was never read into Dimension, so the block
rendered at its pre-move position and the extents reached there too.

Read group 12 into a new Dimension::insertion_point, draw the block
translated by it, move its bounds with it in the extents, test the
block's text where it draws, and write group 12 back whenever the block
written is the one read from the file. Regenerated geometry (edits drop
the block link) sits at its definition points, so no offset is kept or
written for it.

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
2026-11-02 15:01:26 -07:00
Tanish Manku cd19b205b7 Merge remote-tracking branch 'origin/main' into land-1978 2026-11-02 15:01:09 -07:00
Tanish Manku fbc47b48e8 Merge remote-tracking branch 'origin/main' into land-2474 2026-11-02 14:36:58 -07:00
Tanish Manku 86f3fdc9e9 Test: the DWG note's wording, and one-line fake bytes 2026-11-02 14:36:58 -07:00
Tanish Manku 7690d36bb0 fmt 2026-11-02 14:36:58 -07:00
7bc76f94ca OVERKILL: objects within the tolerance are duplicates wherever their numbers fall (#2653)
* OVERKILL: objects within the tolerance are duplicates wherever their numbers fall

OVERKILL compared objects by rounding every number to a multiple of the
tolerance. Two numbers closer than the tolerance on either side of a
rounding step round apart, so the objects were kept: at 0.01, circles at
x = 80.001 and 80.003 were merged but circles at 50.004 and 50.006 were
not. Arcs on one circle were bucketed the same way.

Duplicates are now found by shape (the JSON without its floating-point
numbers; counts and flags must match exactly) and a weighted sum of the
numbers on a grid of the tolerance, looking in the neighbouring cells too,
then compared pairwise to within the tolerance. Arcs and circles look in
the neighbouring cells of centre and radius and are grouped with a
union-find. Lines were already grouped by sorting and are unchanged.

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

* OVERKILL: within the tolerance doesn't chain

Arcs and circles were grouped with a union-find over pairwise matches,
so A ~ B and B ~ C put A and C in one group even when A and C are
farther apart than the tolerance: a circle C was deleted for matching
only the arc between it and the kept circle A, and arcs were combined
across the chain. Each object now joins the first group whose every
member is within the tolerance of it, or starts its own.

In the duplicates pass, a followed object displaced an earlier keeper
that later objects had already matched, leaving them deleted up to twice
the tolerance from what stays. Followed objects are now looked at first,
so every deletion is within the tolerance of an object that stays.

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

---------

Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:36:53 -07:00
2ez4gcxandClaude Opus 5.5 beb6c48a59 AREA, PERIMETER and DISTANCE system variables, as AutoCAD sets them (#2552)
* feat(inquiry): AREA, PERIMETER and DISTANCE system variables, as AutoCAD sets them

AREA, DIST and MEASUREGEOM now leave AREA, PERIMETER and DISTANCE for
getvar, read-only and not saved: the area just measured (the running
total in Add and Subtract mode) with the last area's perimeter, and the
distance (the total of Multiple points). After DIST's two points
LASTPOINT stays at the first point, as measured. AREA's Object option
measures splines, closed by their chord like open polylines.

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

* Address review: spline area over narrow spans, finite distances, list order

- AREA of a spline samples it span by span (Spline::tessellate_spans), so
  a narrow knot span no longer drops out of the area. The spread over the
  spans is one helper shared with Spline::bounds, whose boxes are
  unchanged.
- DIST refuses a distance that isn't finite on all three paths and leaves
  DISTANCE as it was.
- sysvars::list puts the session's values after the drawing's header, so
  it shows what get returns (AREA, ORTHOMODE, LWDISPLAY...).
- LASTPOINT after DIST's Multiple points is pinned to AutoCAD 2024's
  measured behaviour: the last point entered, also after Length and Undo.

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

* Address review: measure every knot span of a spline in AREA

A spline with more knot spans than half the sample cap fell back to an
even grid over the domain, which can step over a narrow span and measure
the wrong area: a 100 x 100 square as a degree-1 spline, its right and
top edges two spans a millionth wide after 4,500 spans along the bottom,
measured 4,998.75.

Spread the samples over the spans at any span count: at least one
segment per span, within the cap up to MAX_SAMPLES spans and one each
past it, as Spline::length integrates every span. One evaluation per
span costs 85-285 ms on a million spans in a release build, against
32-52 s for the length AREA already takes.

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

* Address review: share AREA's spline samples by how much each span bends

Past the sample cap, tessellate_spans gave each knot span one segment, so
a bent span among many straight ones was measured by its chord. Example:
a quadratic spline along the x-axis, one control point 1000 above it,
past 4,500 spans. It measured 750; the area is 1750/3.

Each span now keeps one segment. A span whose control points are
collinear is straight, so its chord is exact. tessellate's count is
shared among the spans that bend, by the cube root of a measure of their
control points' convex hull, which holds the span and its chord. The
shares are rounded on the running total, so they use the whole count
however many spans share it. The example now measures 583.33343, below
the cap and past it.

The spreading by parameter width that this replaces goes, and
span_params with it: bounds is main's code again.

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

* DIST: a measurement refused as too large leaves LASTPOINT at the first point

Session::feed sets LASTPOINT to the second point before DIST checks the
distance, and the refusal returned before DIST put the first point back,
as it does after a measurement. It now does so before refusing.

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

* AREA: a spline too long to measure is too large, not a perimeter of 0

Spline::length reads a length past f64's range as 0, and AREA took that
as the spline's perimeter. Spline::checked_length returns None there,
and AREA reports the perimeter too large, as it does a polyline's.

The overflow was hidden twice: in the sum, and before it in the
derivative, which read as zero speed once past f64's range. The length
integral now takes the derivative before that (Spline::velocity), so an
overflowing speed counts too. param_at_length keeps the speed it had,
and finite lengths are unchanged.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:36:42 -07:00
2ez4gcxandClaude Opus 5.5 990ff2ce9f Say "or" once in prompts that offer options (#2563)
* Say "or" once in prompts that offer options

Twelve prompt messages ended in "or", and the command line adds "or"
before a prompt's options, so they read "or or": HELIX height, XLINE angle
and offset, BLEND, four LENGTHEN prompts, two NCOPY prompts, PEDIT and
table breaks. They now read as AutoCAD's do.

The interface catalogs are keyed by these messages: each key loses its
"or" and each translation its trailing word for "or"; where the shorter
key already had a translation, the longer row is removed.

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

* Say "or" once in HELIX's radius prompts; rename the Vietnamese keys

HELIX's base and top radius prompts ended in "or" before their Diameter
option too, so they read "Specify base radius or or [Diameter]". They
now read "Specify base radius or [Diameter] <1.0000>:", and their keys
lose "or" in all 17 catalogs, as the other twelve did.

The Vietnamese catalog, merged from main, gets the same renames as the
other catalogs; where the shorter key already had a translation, the
longer row goes. The unused "Specify rotation angle or" row goes from
every catalog too: RECTANG has asked "Specify rotation angle" since
#2554.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:36:37 -07:00
2ez4gcxandClaude Opus 5.5 8de925e1d3 Write option prompts without "or", as AutoCAD does (#2540)
* Write option prompts without "or", as AutoCAD does

AutoCAD writes " or [" only when a prompt asks for something besides its keywords ("Enter block
name or [?]"); a prompt asking for one of them has none ("Enter an option [...]", "Enter layout
option [...]", "New color [Truecolor/COlorbook]"). The 27 messages read without "or" from
AutoCAD 2024's command line are listed in NO_OR and displayed that way.

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

* Address review: the window's command line drops "or" as the prompt does

The command line in the window draws a prompt's keywords itself and
wrote " or [" after every message, so -LAYER still read "Enter an option
or [?/Make/...]" there, and questions such as AUDIT's had "or" too. Only
the prompt text (the control channel, the state JSON, the tests) had
dropped it.

Prompt::keyword_lead gives what goes before the keywords; display() and
the command line both use it, and the command line translates only
" or [". A render test reads what the command line draws, in English and
German, for -LAYER, LINE and AUDIT. Suggested in review.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:36:32 -07:00
Tanish Manku 97679d8cff Ask before a save drops solids from a 2013+ DXF (#2334)
The interactive save only knew about DWG, so overwriting a 2013+ DXF lost SAT-text
solids on a command-line note. loss_on_save now lists them, and EXPORT reports them
with the DXF wording.
2026-11-02 14:30:30 -07:00
Tanish Manku 9972a739c5 Merge remote-tracking branch 'origin/main' into land-2474
# Conflicts:
#	crates/doc/src/foreign.rs
#	crates/engine/tests/save_reports_not_saved.rs
#	crates/io/src/dxf_raw.rs
#	crates/io/src/dxf_write/sections.rs
2026-11-02 14:29:22 -07:00
Tanish Mankuandassamaral 0f56c16d00 EXPORT and WBLOCK say what a DWG file leaves out (#2240) (#2660)
Co-authored-by: assamaral <217119886+assamaral@users.noreply.github.com>
2026-11-02 14:27:45 -07:00
Tanish MankuandMarekSpodBiedry a60d437f0a WBLOCK: new handles start above those of the copied blocks (#1996) (#2659)
Co-authored-by: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com>
2026-11-02 14:27:33 -07:00
SoldierPePe facae4a9a6 plot and exportpdf help name the device parameter (#2557) (#2567)
* plot and exportpdf help name the `device` parameter (#2557)

`cadcraft-cli commands plot` didn't list `device`, so the only way to find it was the `unknown plotter` error.

PLOT's help now lists `device` with the built-in devices (CADCraft PDF, PNG, SVG), PLOTTERMANAGER plotters and "None". It also says the default is the page setup's device and that the device picks the output format and the papers. EXPORTPDF's help names `device` too, and notes that its output stays PDF. A test checks that both help texts name `device` and that PLOT's names every built-in device.

* plot help: say what Model, "None" and an omitted paper do

In model space, and with "None", no device is used and the plot is a PDF. When `paper` is omitted, a layout keeps its page setup's paper and model space takes the device's default.
2026-11-02 14:26:48 -07:00
NetterSilverandClaude Haiku 5.5 e6352d955e Reject control characters in layer names (#2569)
Found while testing crates/engine/src/cmd/layer.rs:184 (valid_name, used by run_new)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-02 14:26:33 -07:00
NetterSilverandClaude Haiku 5.5 4a17ee78c2 Reject invalid fill angles in JSON arraypolar (#2655)
Found while testing crates/engine/src/cmd/array.rs:380 (run_arraypolar, `fill: f64_or(p, "angle", 360.0)`)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-02 14:26:28 -07:00
CAandClaude Fable 5.1 fbd6e676f8 DWG save keeps OLE objects (OLE2FRAME): stop reporting them as not saved (#2582)
* DWG save keeps OLE objects (OLE2FRAME): stop reporting them as not saved

acadrust reads OLE2FRAME from our DXF into its own entity and writes it to
DWG of every version we save, frame and embedded data unchanged; only the
"Not saved in the DWG file" warning (and the QSAVE loss prompt) listed it.
not_saved_as now skips the types the DWG bridge writes (DWG_WRITES).

Tests: a synthetic OLE2FRAME through DWG (resaved, and as 2000 and 2010
DWG) keeps every byte; OLE objects are not reported at any version.

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

* DWG_WRITES matches the type as stored, not case-insensitively

The DXF given to the DWG bridge carries an unknown entity's type as stored, and
acadrust reads only "OLE2FRAME" into its OLE entity: any other spelling becomes
its Unknown and is dropped, so it must stay in the "Not saved" report. The test
adds a lower-case "ole2frame" that is still listed. (Review comment on #2582.)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:26:14 -07:00
ab15e42c6c fix(io): a layout keeps its limits and paper image origin through a save (#2109) (#2623)
Every LAYOUT was written with limits 0,0 to 12,9 (AcDbLayout groups
10/11) and paper image origin 0,0 (groups 148/149), whatever the file
had, so another program drew the sheet elsewhere and a Layout plot came
out shifted. Layout::limits and PageSetup::image_origin keep what the
file had (finite, within 1e12) and a save writes it back; a layout
without them writes the old defaults, which read back as none.

Co-authored-by: austin62jake <340374338+austin62jake@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:25:55 -07:00
3146b47656 fix(engine): WBLOCK writes only what the block or selection uses (#2423) (#2621)
* fix(engine): WBLOCK writes only what the block or selection uses (#2423)

WBLOCK of a block, or of selected objects, copied the source drawing's
whole layer, linetype, style and block tables into the new file, so a
5-object symbol carried 40 unused layers and 20 blocks. The written
drawing is now purged of what nothing in it uses, as AutoCAD's WBLOCK
writes it: purge::purge_unused runs PURGE All's scan on a plain Drawing
until a pass removes nothing (a block can free its layer), keeping the
defaults and the current ones. Writing the entire drawing is unchanged.

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

* fix(engine): PURGE's scan counts leaders' dimension styles and styles used by other styles

From the review of #2621: PURGE (and now WBLOCK, which runs its scan)
took as unused a dimension style only a LEADER used, a linetype only a
dimension style or a dimension's override named (DIMLTYPE, DIMLTEX1,
DIMLTEX2), and a text style only a complex linetype's text used, and
removed them while the objects still named them. The scan counts all
three now; text_styles_in_use, which STYLE's delete also asks, includes
complex linetypes' text styles.

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

* fix(engine): PURGE's scan counts multiline styles' linetypes and table styles' text styles

From the review of #2621: a linetype only a multiline style's element
used, and a text style only a table style's cell style used, were taken
as unused and purged while the styles still named them.

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

---------

Co-authored-by: austin62jake <340374338+austin62jake@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:25:41 -07:00
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