Commit Graph
13 Commits
Author SHA1 Message Date
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
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
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
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
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