Commit Graph
723 Commits
Author SHA1 Message Date
Dylan M. Taylor ca62686e55 style: fmt 2026-10-24 08:36:02 +00:00
Dylan M. TaylorandClaude Sonnet 5.5 884ed1c86c refactor: replace wildcard imports in two test modules (S2208)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 08:35:52 +00:00
63394e872d chore: resolve SonarCloud maintainability issues (#2025)
* chore: resolve SonarCloud maintainability issues

* style: format with rustfmt

* fix(engine): reference DocState instead of DrawingState

* fix: restore cmd wildcard imports and remove unused Arc import

* Unit tests for the extracted mleader alignment steps and spacemouse filter

* test: fix assertions flagged in review and apply rustfmt

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

* test: initialise drift filter in struct literal for clippy

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

---------

Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 04:34:49 -04:00
Nicholas GowerandBrandon Thomas e59195edb8 usage: a read-only cross-reference of layers, blocks, styles and linetypes (#1889)
* usage: a read-only cross-reference of layers, blocks, styles and linetypes

USAGE reports, for every layer, block, text style and linetype, how many
objects use it in model space and the layouts, how many inside block
definitions, whether it is used only inside block definitions, and whether
it is empty or unused. Its unused lists are PURGE's own scan (candidates and
block reachability), shared read-only; PURGE's behaviour is unchanged.

The JSON form takes {type?, limit?, offset?}, returns schema 1 with a paged
items list, per-kind summary and PURGE's unused names in table order.

Fixes #1439

* docs: document the functions touched by #1439

* usage: mark a scan cut at its limit as incomplete (#1439)

USAGE's one pass over model space, the layouts and the block definitions was
capped at 5,000,000 objects with `take`, so a larger drawing silently omitted
later references while the counts, statuses, summary and message looked
complete — an unused layer or block could be reported as used, or vice versa.

The limit is now `MAX_SCAN`, and `Scan::new` takes it as a parameter: it counts
the drawing's objects first and records an `Incomplete { scanned, total }` when
the limit cuts the scan short. The report carries `incomplete: true` and an
`incompleteReason` naming both counts, and the message ends with a note, so a
partial report can never be mistaken for a complete one. A drawing holding
exactly the limit is still complete.

PURGE's scan limit and candidate behaviour are untouched; USAGE only reads
PURGE's unused lists as before.

Tests: unit tests cut a small drawing at a small limit (a 5,000,000-object
drawing is impractical) and check the boundary and the miscount the signal
prevents; the integration test checks a complete drawing says so. Also adds the
missing Ukrainian translation for the command label, which the i18n gate
requires.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 02:04:10 -04:00
Brandon ThomasandClaude Opus 5.5 5e39af933f test(autosave): fix a flake: the owner-only folder test uses its own instance id
Two tests that start in the same millisecond got the same instance_id(), so this test's
shutdown() removed the other test's lock from the process-wide SAVEFILEPATH (after #1868).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 02:02:53 -04:00
c66f6f45b8 Reject bare or signed second token in mixed distances (#2011)
Found while testing crates/engine/src/units.rs:176-179 (parse_mixed)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:59:50 -04:00
b062030a03 Reject a leading '+' in ACI indices and RGB triples in Color::parse (#1878)
Found while testing crates/color/src/lib.rs:115 and 126

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:59:45 -04:00
Nicholas GowerandBrandon Thomas 7aa952b8da DWG: write AutoCAD's drawing variable dictionary (AC1018 output) (#1872)
* DWG: write AutoCAD's drawing variable dictionary (fixes #1813)

A 2004 (AC1018) DWG of a drawing without an AcDbVariableDictionary is read as
empty by Vectric Aspire 10.5, though the same drawing as DXF imports. AutoCAD
always writes the dictionary; CADCraft's and ezdxf's DXF writers don't.

The DWG bridge now adds it, with AutoCAD's usual variables, when the drawing
has none, for DWG versions before 2018 (AC1032 output reads fine without it).
A dictionary the drawing already has keeps its entries and values.

* docs: document the functions touched by #1813

* DWG: keep the drawing's DIMASSOC and HIDETEXT in the variable dictionary

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:59:40 -04:00
Nicholas GowerandBrandon Thomas 5cfe1a4149 appload: load a .lsp file from a path or the file picker (#1903)
* appload: load a .lsp file from a path or the file picker (#1820)

APPLOAD had no user interface: typed or chosen from Tools > Load Application it
started a command-line prompt instead of a file browser, and the JSON form did
not say which commands the file defined.

- The desktop's APPLOAD opens an rfd file browser filtered to .lsp, starting in
  the folder the last routine came from (kept in the preferences as lispDir).
  A cancelled picker is a no-op; the web (no native dialog) keeps the engine's
  name prompt.
- The JSON form `appload {path}` loads to the end, never opens a dialog and now
  returns `commands`, the `c:` commands the file defined; a non-.lsp path is
  refused and unreadable or unparsable files return an ordinary error.
- Tests: a synthetic .lsp in a temp folder loads by path and its command runs;
  the picker path, its cancelled answer and the remembered folder; refused,
  missing and broken files.

* appload: load a picked file interactively; keep path whitespace; web hint

Reviewer follow-ups on #1820:

- A picked file now loads through the interactive APPLOAD command (started
  and answered with the picked path, as a typed name is), so a routine it
  starts that calls getpoint or another get… waits for the user instead of
  receiving nil. JSON calls keep the blocking run.
- A JSON `path` is read as given: only a path of blanks is refused, so a
  relative name with leading or trailing spaces is not trimmed into another
  file.
- The web has no file system, so APPLOAD no longer asks for a file name it
  cannot read: it says to drop a .lsp file on the window, which loads the
  file's contents. The host declares this through Services::no_file_system.

Tests: a picked file whose load asks for a point waits for it; a relative
path in a folder whose name starts with a space loads; on a host without a
file system APPLOAD gives the drop hint instead of prompting.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:57:06 -04:00
Nicholas GowerandBrandon Thomas 9f2060d674 analysis: report found/analysed/skipped counts, caps and implausibility (#1890)
* analysis: report found/analyzed/skipped, caps and implausible values (fixes #1440)

* docs: document the functions touched by #1440

Add doc comments to the self-check helpers, the COUNT/SHAPES/MASSPROP entry points and helpers, and the tests and fixtures the analysis self-check change touches, so CodeRabbit's docstring coverage check passes.

* analysis: tolerate rounding past a bound, and skip area checks without extents (#1440)

The self-check's implausible() compared value > bound exactly, so a border
rectangle whose shoelace area equals the extents' width * height could be
flagged by one ulp of rounding. Allow a 1e-9 relative slack past the bound
(the roundings of two ways of measuring the same thing), while a clearly
larger value is still flagged.

MASSPROP and SHAPES fell back to an extents area of 0 when nothing visible
gave the drawing extents, so every region or shape with area got a false
"area is larger than the drawing extents" flag. Pass f64::INFINITY instead:
implausible() already skips non-finite bounds, because no bound applies.

Tests: one ulp past the bound is not flagged while 1e-6 past is; an
extents-filling rectangle is not flagged; invisible-only masses/shapes with
no visible extents are not flagged.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:56:51 -04:00
9fc626a33b MCP: the open tool description names DWG as well as DXF (#1887)
* MCP: the open tool description names DWG as well as DXF

The open tool reads both DXF and DWG through the io layer, but
tools/list advertised it as "Open a DXF file." so clients reading
the descriptions would not try a DWG. Say "Open a DXF or DWG file."

Adds a regression test that finds open in the tools/list reply and
asserts its description still mentions DXF and now mentions DWG.

Fixes #1875

* contributors: credit nicholasgower by GitHub profile name

* docs: document the functions touched by #1875

---------

Co-authored-by: nicholasgower <nicholasgower@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:56:34 -04:00
fef32761af DXF/DWG: angular, diametric and radial dimensions get DIMASSOC (#1651) (#1874)
* DXF/DWG: angular, diametric and radial dimensions get DIMASSOC (#1651)

std_assoc_refs had slots only for linear/aligned and ordinate dimensions, so
every other associative kind was saved without a DIMASSOC object: AutoCAD
did not follow angular, diameter or radius dimensions, although CADCraft
itself did through its xdata.

Write the references AutoCAD writes for each kind: both lines of an angular
dimension (4 point references; the 3-point kind its three definition points)
and, for radial and diametric dimensions, the single centre reference to the
circle or arc (associativity flag 2), which is how AutoCAD files store them.
Read the same references back, with the circle's definition points
reconstructed, so files from other writers associate those kinds too.

Curve start references now use osnap 13 as AutoCAD writes them (1 is the
end), which also lets a stored point of 0,0,0 resolve to the right end.

Tests: the issue's commands through the engine, and a DXF/DWG round trip
with CADCraft's xdata stripped so only the standard objects remain.

* docs: document the functions touched by #1651

* DXF read: a line's osnap 1 with a stored point still picks the nearer end (other writers)

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:56:17 -04:00
Brandon ThomasandClaude Opus 5.5 9562d951df contributors: repair people.toml entries for ffishman, aussieboer61 and nicholasgower (merge of #1735/#1792/#1863)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:55:51 -04:00
f05fa9cec3 DWG read: keep a polyline's vertex order when handles are not ascending (#1863)
* DWG read: keep a polyline's vertex order when handles are not ascending (fixes #1856)

acadrust 0.6.4 assembled a polyline's VERTEX records in handle order and discarded the polyline's owned vertex-handle list, so a vertex inserted into an existing polyline (and therefore given a higher handle) moved to the end of the outline. 0.6.5 reads the owned list and orders each polyline's vertices by it.

Adds a regression test that writes a DWG whose second 3D-polyline vertex has the highest handle, reads it through the DWG bridge, and checks the order for AC1018 and AC1032.

* contributors: credit @nicholasgower by GitHub profile name

* docs: document the functions touched by #1856

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:55:23 -04:00
73e7486dc9 DXF: proxy objects and entities keep their class through a save (#1812) (#1858)
* dxf: proxy objects and entities keep their class through a save (#1812)

A proxy (ACAD_PROXY_OBJECT, ACAD_PROXY_ENTITY) names its class by number
in group 91: 500 plus the class's place in the CLASSES section (DXF
Reference). Saving rewrites CLASSES (CADCraft's own classes, then the kept
ones), but kept proxies were written with their read number, which then
named another class: an R14 file's visual styles came back as SCALE,
TABLESTYLE or MLEADERSTYLE, and numbers past the last class stayed.

The reader now keeps the file's class order (Foreign::class_order) when
the file has proxies, and the CLASS records its proxies name. The writer
renumbers each kept proxy to its class's place in the written CLASSES and
drops a proxy whose number names no class (with what it owns and its
dictionary entry). The DWG bridge writes through the same path.

Test proxies_keep_their_class_when_classes_are_rewritten (a file with
VISUALSTYLE and WIDGETX first in CLASSES, proxies of both and of a number
past the last class, and a proxy entity; also through a second save).

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

* docs: the proxy class fix is for DXF save (#1812)

From the local CodeRabbit review: DWG save drops proxy entities (acadrust)
and proxy objects through DWG weren't checked, so the progress log says DXF.

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

* dxf: a proxy's class number is found outside 102 groups (#1812)

From the PR review: an application's 102 {NAME ... 102 } group before a
proxy's own groups can hold a group 91, which was read as the class number
and overwritten on save. class_group finds the first 91 outside 102 groups
(before xdata), for reading and renumbering alike (test
proxies_keep_their_class_when_classes_are_rewritten, now with a {MYAPP
group holding a 91 that stays as it was).

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

---------

Co-authored-by: Tyrel Haveman <1434399+tyrel@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:55:07 -04:00
fde62a39ab MEASUREINIT: choose the units of new drawings, kept in the profile (#1792)
* MEASUREINIT: choose the units of new drawings, kept in the profile

NEW, QNEW and the first drawing at startup were always imperial unless a
template or {"metric": true} was given, and nothing kept a metric choice
across restarts. MEASUREINIT (0 imperial, the default; 1 metric) is now a
profile variable: NEW without `metric` or a template follows it, the app's
startup drawing follows it, and Options > Files shows it as "Units of new
drawings". QNEWTEMPLATE and an explicit `metric` still win.

Part of #1182.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxn7PD9WzP4ET2LU7BRLDK

* contributors: add Richard Sauer (aussieboer61)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxn7PD9WzP4ET2LU7BRLDK

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:54:51 -04:00
7db656fdc7 Write a layout's paper-space viewport (id 1) ahead of its other viewports (#1782)
DWG stores no viewport ids; readers number a layout's viewports in order. A drawing whose draw
order (SORTENTS) put the paper-space viewport behind a model viewport came back from DWG with
that model viewport as the sheet.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:54:36 -04:00
MarekSpodBiedryandBrandon Thomas 9d9e4c3b0d STRETCH moves only the multiline vertices inside the area (#1712) (#1739)
* STRETCH moves only the multiline vertices inside the area (#1712)

* STRETCH selects a multiline when a crossing area holds one of its vertices (#1712)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:54:20 -04:00
c92bb5cdad Close hatch boundaries where a line ends just short of a flattened ellipse (#1690) (#1738)
* Close hatch boundaries where a line ends just short of a flattened ellipse (#1690)

* Bound the hatch endpoint snap scan by segment count and visited segments (#1690)

* test: LINE returns its new objects as `handles`

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:54:04 -04:00
1c35d7f8c1 Keep the active layout when saving DWG/DXF (#1735)
* Keep the active layout when saving DWG/DXF

The layout a file marks active (block *Paper_Space) was not kept on read, and on write *Paper_Space always went to the first tab, so every save made the first sheet active. Store it as Drawing::active_layout and write it as *Paper_Space again; tab order is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Match the active layout by name without regard to case

paper_space_layouts and layout.rename now compare the active layout's
name case-insensitively, as Drawing::layout() does, so an active layout
stored as "s2" still resolves the "S2" tab.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Test case-insensitive active layout matching

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: ffishman <20211677+ffishman@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:53:49 -04:00
BooksandBrandon Thomas a94fc0898b Diameter dimensions: the *D block's text keeps the sign as %%c (fixes #1645) (#1696)
The *D block and EXPLODE's MTEXT took the dimension's decoded text, so
%%c arrived as U+2300, which no SHX font has: other programs plotted a
diameter dimension in an SHX style without its sign. They now write
DimGeometry::mtext_contents(): the sign as %%c, which every font draws
(and CADCraft decodes), a % that would run into a code as %%%.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:53:32 -04:00
WillandBrandon Thomas a0dced6d45 fix(render): honor DIMDSEP in angular dimensions (#1670)
Use the effective dimension style's decimal separator when formatting
angular labels, preserving the public formatter's decimal-point default.

Add rendering and stored DXF/DWG block regression tests, and update the
file-format progress documentation.

Fixes #1646.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:53:16 -04:00
532c4ccc1c HATCH treats rooms enclosed by multilines as islands (#1642)
A pick inside a multiline wall found the band between the wall's lines
as the outer boundary, but the rooms the inner lines enclose were
dropped as islands: island_curve accepted lines, arcs, polylines and
splines, not multilines. The rooms were filled along with the walls. A
multiline's lines can now make up an island like the other open curves.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:53:00 -04:00
CapriandGPT-6 Luna 88a73b7618 Render: Match AutoCAD clearance for DIMTAD 1 text (#2004) (#2027)
Co-authored-by: GPT-6 Luna <noreply@openai.com>
2026-10-24 01:52:16 -04:00
NetterSilverandClaude Haiku 5.5 0fc8e5241c Reject surveyor bearing angles outside 0-90 degrees (#2024)
Found while testing crates/engine/src/units.rs:279-294 (bearing, via parse_dms 297-317)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-24 01:52:11 -04:00
Marco Arnulfo cff2eb7262 Dimensions: text that does not fit follows DIMTMOVE, over the dimension line (with a leader for 1) instead of past the second extension line (#2015)
* Dimensions: text that does not fit follows DIMTMOVE, over the dimension line (with a leader for 1) instead of past the second extension line (fixes #1998)

* Dimensions: tighter check that the line is not carried out to moved text; drop a guard that never changed the outcome (#1998)
2026-10-24 01:52:05 -04:00
Marco Arnulfo ab6bff5e4e SETVAR CECOLOR takes the colour names and indexes the color command takes (fixes #1999) (#2014) 2026-10-24 01:52:00 -04:00
MarekSpodBiedry 1a76e10c20 hatch: list patternFile/patternText in the catalog and point to them from the unknown-pattern error (#2000) (#2007) 2026-10-24 01:51:53 -04:00
Jonasandcloudj 941ea341bc fix(ui): toggle QuickCalc with Ctrl/Cmd+8 (#2001)
Co-authored-by: cloudj <jonbahnes@gmail.com>
2026-10-24 01:51:47 -04:00
Antoine Boxho 71882cf5e6 constraints: bound combined expression and parameter depth (#1959) 2026-10-24 01:51:42 -04:00
64c35cadff Distinguish polyline midpoint grips from vertices (#1950) (#1958)
* ui: distinguish polyline midpoint grips

* docs: the midpoint-grip row belongs in the revision history (CodeRabbit)

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:51:36 -04:00
MarekSpodBiedry 63dec85749 Let getkword return arbitrary input when initget bit 128 is set (#1945) (#1953) 2026-10-24 01:51:31 -04:00
MarekSpodBiedry 1c3c25035c List binary fractions in the Units precision drop-down for Architectural and Fractional (#1861) (#1952) 2026-10-24 01:51:26 -04:00
2bc3b02ea1 Make LISP angtof honour its units argument (#1949)
* Make LISP angtof honour its units argument

Found while testing crates/engine/src/lisp/drawing.rs:962-966

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

* angtof: a units argument outside 0..=4 gives nil instead of reading degrees (CodeRabbit)

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-24 01:51:21 -04:00
Kshitij VarshneyandClaude Opus 5.5 6842b8774b File tabs: stop the close button from flickering (#1914)
* File tabs: the close button no longer flickers under the pointer

An inactive drawing tab drew its X only while the tab was hovered. Once
the pointer reached the X, egui gave the hover to the X, so the next frame
left the X out, the tab got the hover back, and the frame after drew the X
again. The tab background and its X flickered for as long as the pointer
stayed there, and a click could land on the tab instead of the X.

The tab now asks whether the pointer is inside its rectangle
(`Ui::rect_contains_pointer`), which the X does not change, both for the
hover fill and for showing the X.

Regression test: tests/file_tab_close.rs points at an inactive tab's X,
checks the X is there on each of the next frames, and clicks it to close
that drawing. It failed on the first frame before the fix.

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

* File tabs: plainer comment on why the tab checks the pointer

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:51:16 -04:00
78c1653f50 loops: implied closed loops from loose LINE and ARC chains (#1906)
* loops: implied closed loops from loose LINE and ARC chains

LOOPS is a read-only inquiry command that chains LINE and ARC endpoints
meeting within a tolerance. Closed loops report their member handles,
perimeter, area and winding (exact with arcs, like SHAPES); open chains
report the endpoint gap, where it is and their length; branch junctions
(three or more ends at one point) are reported but excluded from the
chains rather than resolved.

The tolerance defaults to 1e-9 x the extents diagonal and is an option.
The quadratic endpoint pairing is capped (maxPairs, 1,000,000 by default)
and reports capped/pairTestsNeeded instead of guessing from a partial
pairing. Degenerate lines and arcs are counted under skipped; full-circle
arcs close on their own. The command is read-only.

Fixes #1437

* loops: cap maxPairs at 100,000,000 so a caller can't ask for unbounded work

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:51:11 -04:00
fda82da3df RECTANG: add a Center Rectangle construction mode (#1895)
* RECTANG: add a Center Rectangle construction mode (#1127)

The first point of RECTANG was always a corner. A new Center option makes it
the rectangle's centre, so the second point (or Dimensions/Area) builds it
outward evenly: centre 10,10 with a corner at 14,16 gives 6,4 to 14,16.

- interactive: a Center keyword at the first prompt (Chamfer keeps the c
  shortcut; Center matches ce or the full word), which also changes the
  prompt to Specify center point for rectangle;
- JSON: center: true on rectang, keeping p1/p2 and the default corner mode
  unchanged;
- rotation turns about the centre; chamfer, fillet, width, elevation and
  thickness apply as before; the rubber band previews the centred rectangle;
- params documentation updated.

Tests: centre-point geometry for p2, dimensions and area, rotation, fillet,
chamfer, width, thickness, the interactive keyword and preview, and the
unchanged default corner mode.

Fixes #1127

* docs: document the functions touched by #1127

* RECTANG: the Center option is CEnter, so C stays Chamfer's shortcut (as PLINE)

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:51:06 -04:00
NetterSilverandClaude Opus 5.5 2c59eb19c5 Fix LISP getangle subtracting ANGBASE in degrees from radians (#1883)
Found while testing crates/engine/src/lisp/input.rs:256-258 (lisp_angbase, used at :127 and :183); cause is crates/engine/src/sysvars.rs:141

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:50:56 -04:00
0299a3eaa0 fix(canvas): keep the OS cursor visible over menus and panels (#1879)
* fix(canvas): keep the OS cursor visible over menus and panels (fixes #1701)

The drawing area hid the OS cursor with `CursorIcon::None` whenever the
canvas response was hovered and the pointer lay inside the canvas rect.
egui still reports the canvas as hovered when the pointer is over a menu
popup's frame (the padding between its items and its border, and its
separators): only widgets claim the hover, so an open menu drawn over the
drawing left the canvas "hovered" and the cursor invisible there. The
same happened over a dialog's padding or a floating palette over the
canvas.

Require the topmost layer under the pointer to be the canvas's own layer
before treating the pointer as inside the drawing. Then menus, tooltips,
dialogs and floating panels keep a visible cursor (and the crosshair
stops under them, as over the menu bar already).

Tests: `crates/ui-egui/tests/cursor_over_menus.rs` (headless egui_kittest)
pins the canvas hiding the cursor, the menu bar and docked palettes
keeping it, the Show System Cursor option restoring the arrow off the
canvas, and the frame of an open menu over the drawing keeping the
cursor: the last case failed before this change. The whole
`cadcraft-ui-egui` test suite passes.

* docs: document the functions touched by #1701

* fix(canvas): keep the OS cursor visible over canvas-painted menus

Selection cycling's rows and Dynamic Input's options rows are menus, but
the canvas paints them itself: they share the canvas's layer, so
`layer_id_at` still reports the drawing and the OS cursor was hidden over
them (the review of #1701 caught it).

Exclude their rectangles from the crosshair's cursor contract, leaving the
canvas's input and the Dynamic Input tooltip under the pointer untouched
(so the list keeps highlighting and a click still picks its row).

Tests: two cases in `crates/ui-egui/tests/cursor_over_menus.rs` hover a
selection cycling row and a Dynamic Input options row and assert the OS
cursor stays visible; the drawing beside the list still hides it. Both
failed before this change.

* The painted menu frame counts as over the menu (CodeRabbit)

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

* cargo fmt

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:50:51 -04:00
Nicholas Gower 1c7d3f0fae CLI: a --cmd can act on a handle an earlier --cmd returned ({last}) (#1871)
* cli: act on a handle an earlier --cmd returned ({last}) (#1663)

A run's --cmd steps were independent: mview answered {"viewports":["100"]}
but viewport.set could not use that handle, so it failed with "no viewport
given".

The batch runner now remembers the last object handle a --cmd returned (the
strings in its result that name an entity of the active drawing, so counts and
names are not mistaken for handles). A parameter value of exactly {last} in a
later --cmd, at any depth, is replaced with it; every other value is passed
through unchanged. With no handle yet the run fails with a clear error instead
of passing a bad handle to the command.

Fixes #1663

* cli: keep {last} to the drawing and handle fields that returned it

Read handles only from result handle fields, never arbitrary strings: a group named like another object's handle is a name, not a reference. Clear the remembered handle when new, open, document.switch (or a script running one) makes another drawing current, and keep a handle the new drawing's own command returns.
2026-10-24 01:50:40 -04:00
Nicholas Gower 4f282b30cc OFFSET uses a single preselected object (#1870)
* OFFSET uses a single preselected object (#1610)

With exactly one offsettable object selected before OFFSET, take it and go
to the side prompt instead of asking to select an object again; a multiple
selection keeps the select-object loop. AutoCAD ignores preselection here,
but this is the requested behaviour for users coming from other tools.

* docs: document the functions touched by #1610
2026-10-24 01:50:34 -04:00
Nicholas Gower b9d59c64ab web: report the open drawing in the viewport title (#1869)
* web: report the open drawing in the viewport title

* docs: document the functions touched by #1717

* web: write the browser document title directly

eframe's web runner ignores egui::ViewportCommand::Title, so the tab and
an embedding page kept showing CADCraft when a drawing was opened,
renamed or became dirty. Write the title through web_sys instead, and
route the change decision through a writer function so the changed-title
path is covered by a native test.
2026-10-24 01:50:29 -04:00
oracola91 4c327cccd0 Autosave: a new autosave folder is owner-only on Unix (#1868)
Autosave files are unsaved drawings. The autosave folder was made with
create_dir_all, by the first write or for the lock file, so on Unix it
got the default mode and other accounts on the machine could read the
files. A folder the autosaver creates is now 0o700. A folder that already
exists keeps its mode, since SAVEFILEPATH can be any folder the user
picked.

The same change as storytold/pdfcraft#3320 and storytold/vectorcraft#3406.
2026-10-24 01:50:24 -04:00
587d1dcd69 Spline extents: evaluate on the stack and sample relative to the spline (#1076) (#1834)
* Evaluate spline basis functions on the stack

`basis()` allocated three Vecs for every evaluated point, and a large
drawing evaluates millions of points while computing its extents on
open: `basis` was 10% and `calloc` 6% of `cadcraft-cli info` on the
1.1 M-entity drawing of #1076. `eval` now works in fixed arrays up to
degree 10 (the cap of the DXF reader and the SPLINE command) and falls
back to the heap above it. Results are bit-identical to the old code,
which the test keeps as its reference.

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

* Validate a spline once per tessellation

`tessellate` called `eval` for every sample, and `eval` re-ran
`is_valid` (a walk over the whole knot vector) and `domain()` each
time, so a spline with many control points cost O(knots) per point.
Validate and read the domain once, then evaluate the samples with the
checked variant. Part of the open time of large drawings (#1076).

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

* Size spline bounds sampling relative to the spline

`Spline::bounds` tessellated at an absolute tolerance of 1e-3, so every
spline of a drawing in millimetres hit the 4000-point cap: the extents
computed on open evaluated 12 M points for the 3072 five-point cubics
of the 1.1 M-entity drawing of #1076, a third of its open time in the
CLI and the GUI alike. Sample at 1e-3 of the spline's own control-box
diagonal instead, with at least four samples per knot span, straight
into the box without an intermediate Vec. The box of a typical spline
now takes a few hundred evaluations and stays within 1e-3 of the
spline's size of the finely sampled one (3.4e-4 at worst over the test
set); a degree-1 spline gets its exact control box.

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

* Give a spline with a non-finite control point its control box

`Bounds2::from_points` skips a non-finite point, so the control box
and its diagonal were finite while a NaN or infinite control point
poisoned every sample (`c * 0.0` is still NaN): `bounds` returned the
empty box instead of the box of the finite control points that the
other degenerate cases get. Check the control points before sampling.

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

* Size spline bounds sampling by a dimensionless ratio

`bounds` passed `diag * 1e-3` to `sample_count`, whose absolute floor
of 1e-9 on the tolerance made the count absolute again for a spline
with a diagonal under 1e-6: at a scale of 1e-12 every spline got the
minimum eight segments per degree whatever its shape, and an extreme
between two samples exceeded the 1e-3 margin the box promises. Derive
the count from the ratio of control-polygon length to tolerance in a
helper that `tessellate` (with its floor, unchanged) and `bounds`
(with the ratio to the diagonal) share, so a scaled copy of a spline
samples exactly like the original. The scale test now also runs at
1e-9 and 1e-12.

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

* Sample spline bounds over every knot span

`bounds` sampled one uniform grid over the parameter domain; the
four-per-span floor only raised the count. A narrow non-empty knot
span (a degree-2 peak supported on 0.5003..0.5004, say) fell between
grid points, at any count up to the cap, so the box missed part of
the curve and zoom extents clipped it. Spread the samples over the
non-empty knot spans in proportion to their length with a floor of
four per span, ends included, still folded straight into the box. The
cap holds over all spans: the floor drops to two or three when four
would exceed it, and a spline with more spans than two each allow
falls back to the grid. A typical spline evaluates as many points as
before.

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

* Hold a narrow knot span to eight samples, and say what that buys

The doc said the box falls short of an extreme by far under 1% of the
spline's size, which holds where the budget sets the count but not in a
span held to the floor: four segments across a degree-2 piece can miss
an off-grid peak by about 3% of the control box. Eight segments bring
that under 1%, and the comment now says both figures. A test puts the
peak between the grid's samples and checks the margin; four segments
would miss it by more.

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

* Give a spline with more spans than the cap samples its control box

Past two thousand knot spans the grid of the cap's four thousand
samples can step over a narrow span and leave its extreme out of the
box, which selection uses as a pre-filter. The control box contains the
curve whenever no weight is negative (the convex hull property), so
such a spline gets that box, conservative rather than sampled; only a
negative weight, which voids the property, keeps the grid.

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

* Keep the degree-1 shortcut for positive weights

A degree-1 spline is its control polygon only under the convex hull
property, which a negative weight voids: with control (1, 0), (2, 0)
and weights 1, -0.5 the curve passes (4, 0), outside the control box
that bounds() returned for every degree-1 spline. The shortcut, like
the many-spans one, now needs every weight positive; otherwise the
spline is sampled like any other.

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

* Sample a degenerate spline whose weights are not positive

Coincident control points with zero weights evaluate to the origin,
not to the point they sit on: eval() leaves the unnormalised sum alone
when the weights sum to nothing. The zero-diagonal shortcut returned
that point's box all the same. Like the degree-1 and many-spans
shortcuts, it now needs positive weights; otherwise the curve is
sampled, and the box holds what eval() gives.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:50:14 -04:00
philipp-tchandClaude Opus 5.5 e04056f714 The linetype scale of a block reference scales the linetypes inside the block (#1830)
A block reference's own linetype scale (DXF group 48) was ignored for the objects in the block:
a DASHED line inside a block inserted at linetype scale 2 kept dashes of scale 1. It now
multiplies their linetype scale, as LTSCALE does for the whole drawing (and as a dimension's
does for its block).


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:50:07 -04:00
25faf34d28 Interface Language dialog: list the languages alphabetically by their own name (#1787)
* Interface Language dialog: list the languages alphabetically by their own name

- English (and Auto where offered) stays first as the source language and fallback; the rest sort by native name with accents folded, so Čeština is under C and Magyar under M; Cyrillic and CJK names follow the Latin ones
- The list used the registry order, which is the order the languages were added

* Options dialog lists interface languages in the same order as the picker

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:49:59 -04:00
Books dadd1f227f AutoLISP: entmake defines blocks with BLOCK … ENDBLK (fixes #1662) (#1765)
* AutoLISP: entmake defines blocks with BLOCK ... ENDBLK (fixes #1662)

A BLOCK list wasn't an entity, so entmake returned nil and the entities
after it went into model space. Now a BLOCK list begins a definition
(name, flags, base point, description), the entities made meanwhile go
into it, and ENDBLK adds the block (redefining one of the same name, or
naming an anonymous *U block) and returns its name. An open definition,
an xref, an invalid name or a block that inserts itself is refused with
nil, using BLOCK's own name and self-reference checks.

* AutoLISP entmake blocks: review fixes (anonymous names keep a star, no redefinition during REFEDIT, a failed routine drops its open definition)

An anonymous block (flag 1) named without a * got a name like FO1; it now
gets *U. ENDBLK refuses to redefine a block REFEDIT is editing, as BLOCK
does. A routine that fails drops the definition it began, so the next
routine's entities go into the drawing; one that ends normally still
leaves it open.

* AutoLISP entmake blocks: refuse an anonymous block that inserts itself

The self-reference check ran only for named blocks, so an anonymous *U
block holding an INSERT of the name it was about to get (*U1 in an empty
drawing) was stored. The check now runs after the anonymous name is
given, for both kinds.

* AutoLISP entmake blocks: create a definition's layers only when the block is added

Each pending entity created its layer at once, so a definition ENDBLK
refused (a self-reference, REFEDIT) or a failed routine dropped left an
orphan layer. end_block now creates the layers after its checks pass,
just before it adds the block.
2026-10-24 01:49:54 -04:00
NetterSilverandClaude Haiku 5.5 1428174159 Fall back to decimal for huge architectural/fractional distances (#1762)
Found while testing crates/engine/src/units.rs:27-31 and 44-46

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-24 01:49:49 -04:00
NetterSilverandClaude Haiku 5.5 f686a595e6 Convert px/unitless SVG root size to mm when a viewBox is present (#1749)
Found while testing crates/io/src/svg_read.rs:576-593 (parse_length_mm), used at 605-612 (viewport)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-24 01:49:44 -04:00
NetterSilverandClaude Haiku 5.5 7d2fbd959a Reject repeated leading '#' in Rgb::parse_hex (#1746)
Found while testing crates/color/src/lib.rs:24

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-24 01:49:38 -04:00