Commit Graph

438 Commits

Author SHA1 Message Date
Valentin Klein 7ecac5fb66
Tiled model viewports: VPORTS, saved viewport configurations, DXF VPORT table (part of #463) (#504)
Squash of PR #504 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:43:11 -04:00
Valentin Klein c9c3031de9
Named views (VIEW), Zoom Realtime and Dynamic, ZOOMFACTOR (part of #463) (#503)
Squash of PR #503 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:42:56 -04:00
Valentin Klein b4b62e4cf0
FILLET and CHAMFER with polyline segments (fixes #466) (#502)
Squash of PR #502 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:42:41 -04:00
Valentin Klein 8e07de0f4e
Annotative scaling: DXF and status bar annotation scale list (fixes #468) (#506)
Squash of PR #506 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:42:27 -04:00
Valentin Klein e7bb447d58
Annotative scaling: scale list, CANNOSCALE, OBJECTSCALE, ANNOALLVISIBLE (part of #468) (#486)
Squash of PR #486 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:42:13 -04:00
Valentin Klein c0a19dfb9e
FILLET with elliptical arcs and splines (part of #466) (#470)
Squash of PR #470 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:41:59 -04:00
Valentin Klein ea0f866c7c
EXTEND, JOIN and LENGTHEN on elliptical arcs and splines (part of #466) (#469)
Squash of PR #469 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:41:45 -04:00
Valentin Klein 10f7a9b2a3
SETVAR/GETVAR: header variables the commands keep, TRANSPARENCYDISPLAY, dimension variables in SETVAR ? (fixes #461) (#467)
Squash of PR #467 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:36:59 -04:00
Valentin Klein 0371c448f6
DXF: native MULTILEADER read and write instead of LEADER + MTEXT (fixes #459) (#465)
Squash of PR #465 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:36:45 -04:00
Valentin Klein f3553783a0
UNDO Mark/Back, BEgin/End, Auto and Control; MREDO; UNDOCTL (part of #461) (#464)
Squash of PR #464 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:36:30 -04:00
Valentin Klein 6aa7c03fad
Status bar: viewport scale list and lock toggle (fixes #453) (#462)
Squash of PR #462 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:36:16 -04:00
Valentin Klein d42313cde2
ARRAYEDIT and ARRAYCLOSE for associative arrays (fixes #448) (#460)
Squash of PR #460 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:36:02 -04:00
Valentin Klein 61a8a5f984
Viewports: polygonal, object and clipped viewports, VPCLIP, MVIEW ON/OFF/Lock/Restore (part of #453) (#458)
Squash of PR #458 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:35:46 -04:00
Valentin Klein d6aa802c75
Hatch: HPGAPTOL, pattern origin, separate hatches, island style; HATCHEDIT options and -HATCHEDIT (fixes #451) (#457)
Squash of PR #457 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:29:05 -04:00
Valentin Klein bd2a7597c5
Associative arrays: ASsociative, Levels and polar ROWs (part of #448) (#455)
Squash of PR #455 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:28:48 -04:00
Valentin Klein e02d1ebd08
Hatch: associative hatches follow their boundary objects; DXF 97/330 source objects (part of #451) (#454)
Squash of PR #454 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:28:33 -04:00
Valentin Klein 61812b25de
OVERKILL: tolerance, ignored properties, overlapping and end-to-end lines and arcs, polyline segments (fixes #439) (#452)
Squash of PR #452 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:28:18 -04:00
Valentin Klein 6f1ca71e98
PolarSnap, relative polar angles, Extension on ellipse and spline ends, tracking options in Drafting Settings (fixes #441) (#450)
Squash of PR #450 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:28:02 -04:00
Valentin Klein a652c57537
MATCHPROP: source/destination picker and Settings (part of #439) (#449)
Squash of PR #449 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:27:48 -04:00
Valentin Klein 151722feb0
DATAEXTRACTION and EATTEXT: block and attribute data to rows, CSV and a table (fixes #433) (#447)
Squash of PR #447 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:24:14 -04:00
Valentin Klein 9b136d6c19
Draw order: DRAWORDER Above/Under objects, TEXTTOFRONT, HATCHTOBACK, SORTENTSTABLE read (part of #439) (#446)
Squash of PR #446 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:23:59 -04:00
Valentin Klein 9427b77209
Temporary tracking point TT and tracking TK at every point prompt (part of #441) (#445)
Squash of PR #445 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:23:45 -04:00
Valentin Klein 77de6af455
BATTMAN: edit, remove and reorder attribute definitions; references follow (part of #433) (#444)
Squash of PR #444 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:23:32 -04:00
Valentin Klein 7429e7dcbe
Grips: multifunctional grip menus for polylines, lines and arcs (fixes #425) (#440)
Squash of PR #440 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:23:18 -04:00
Valentin Klein 25c9658908
Select objects: BOX, AUto, SIngle, Multiple, Group and Undo keywords (fixes #428) (#438)
Squash of PR #438 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:23:03 -04:00
Valentin Klein 608aa225a7
Grips: hot-grip options (Base point/Copy/Undo/Reference/eXit) and multiple hot grips (part of #425) (#435)
Squash of PR #435 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:22:48 -04:00
Valentin Klein 65d5ee48d3
SELECTSIMILAR and object isolation: ISOLATEOBJECTS, HIDEOBJECTS, UNISOLATEOBJECTS (part of #428) (#434)
Squash of PR #434 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:20:02 -04:00
Valentin Klein b3a73626b2
SAVEAS version and binary DXF, default save version (fixes #421) (#431)
Squash of PR #431 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:19:47 -04:00
Valentin Klein 9e3379882f
TOLERANCE: GD&T feature control frames (fixes #420) (#427)
Squash of PR #427 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:19:32 -04:00
Valentin Klein 139d98bf6f
DXF/DWG at a chosen version 2000–2018 and binary DXF (part of #421) (#426)
Squash of PR #426 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:19:18 -04:00
Valentin Klein 475a70e573
Groups: PICKSTYLE group selection, Ctrl+Shift+A and -GROUP options (fixes #417) (#424)
Squash of PR #424 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:19:04 -04:00
Valentin Klein 64eed455e5
Autosave (SAVETIME, SAVEFILEPATH) and Drawing Recovery after a crash (fixes #406) (#416)
Squash of PR #416 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:18:49 -04:00
Valentin Klein b4777fa8b3
fonts: embed craft-fonts when built with CRAFT_FONTS_DIR (#321)
Squash of PR #321 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:13:43 -04:00
Valentin Klein d74f7a4108
Right-to-left drawing text: bidi and Arabic-script shaping (Persian, Arabic, Urdu) (#315)
Squash of PR #315 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:13:29 -04:00
Valentin Klein becb109cc1
Scripts and the command line read a trailing or double space as Enter (fixes #227) (#229)
Squash of PR #229 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:13:15 -04:00
Valentin Klein 6900110291
Select objects: W, C, F, WP and CP selection modes (fixes #224) (#226)
Squash of PR #226 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:13:01 -04:00
Valentin Klein 4e505a6adb
Transparent 'ZOOM/'PAN run nested and resume the interrupted command (fixes #220) (#221)
Squash of PR #221 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:12:47 -04:00
Valentin Klein c35a952271
Print opens a Plot dialog (#160) (#200)
Squash of PR #200 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:12:34 -04:00
Valentin Klein 699dd1aeca
IMPORT, EXPORT, Plot Preview and Batch Publish; ATTACH, SHARE, 3DORBIT say not available yet (part of #9) (#186)
Squash of PR #186 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:26:39 -04:00
skylar taylor-barrick 58a8494f98 UI: retain shared workspace while merging current main 2026-10-10 18:31:07 -06:00
Valentin Klein fc4220e9b7
Plot styles: named styles (STB), PLOTSTYLE, STYLESMANAGER and table files (part of #410) (#418)
Layers and objects get a PlotStyle property (ByLayer/ByBlock/name) applied
through named tables (built-in default.stb and monochrome.stb, our own
definitions). DXF keeps the names in the ACAD_PLOTSTYLENAME dictionary of
ACDBPLACEHOLDER objects with group 390 on layers and objects; tables kept in
the drawing travel in a CADCRAFT_PLOTSTYLES XRECORD. PLOTSTYLE sets an object's
or the current (CPLOTSTYLE) style; STYLESMANAGER lists, creates, edits, deletes,
loads and saves tables in our own JSON format (docs/plot-styles.md). A Plot
Style Manager dialog edits a table's styles and the layout's table; the
Properties palette shows the current plot style and table.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:21:24 -04:00
Valentin Klein 58a349772e
Groups: GROUP, UNGROUP and GROUPEDIT commands; erased members leave their groups (part of #417) (#422)
GROUP makes named or unnamed (*A1, *A2…) groups from the selection, typed
("Select objects or [Name/Description]") or as JSON. UNGROUP removes a group
picked by one of its objects or by name. GROUPEDIT adds and removes objects and
renames a group; removing the last object deletes it. groups.list reports them.
After every command, members that no longer exist leave their groups and empty
groups are deleted (undo restores both). The drawing model and the ACAD_GROUP
DXF round trip already existed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:20:06 -04:00
Valentin Klein 87d1e28345
MLEADER: spline and no-leader types, block content (fixes #408) (#419)
MLEADER's Options answered "not available yet" for Leader type sPline and
None and for Content type Block. Spline leaders now draw as smooth curves
through their points (DXF LEADER path type 72 = 1); None places only the
content at one picked point; Block asks for a block name and how it
attaches (Center extents or Insertion point) and inserts it at the end of
the landing. MLeader gains `spline` and `block` (serde defaults); DXF stays
LEADER + MTEXT/INSERT. PURGE, RENAME, copy/paste, FLATTEN, grips and
extents follow the block content. The choices are remembered like the
other MLEADER options. docs/gaps.md: top-10 row 5, F1 and F10 narrowed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:20:02 -04:00
Valentin Klein a395ec3d2d
SPLINE: Knots, start/end Tangency, toLerance and Object (part of #408) (#413)
SPLINE's Knots, Object, start/end Tangency and toLerance options printed
"not available yet". Fit-point splines now take a knot parametrisation
(chord, square root, uniform), end tangents (clamped cubic with derivative
constraints) and a fit tolerance (least-squares approximation with the
fewest control points that stay within it). The spline keeps these options
so grip and SPLINEDIT edits refit the same way; DXF writes the tolerance
(44) and tangents (12/13) and recovers the parametrisation from the knots.
Object turns 2D/3D polylines with straight segments (PEDIT Spline output)
into splines through their vertices.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:19:57 -04:00
Valentin Klein 3ffe1ca2af
MTEXT: Height, Justify, Line spacing, Rotation, Style, Width, Columns (part of #408) (#415)
The options at MTEXT's "Specify opposite corner" prompt printed "not
available yet". Each now asks its question and returns to the corner
prompt: Height (also sets TEXTSIZE), Justify (TL..BR, the insertion point
is the box's attachment point), Line spacing (At least/Exactly, then a
factor such as 1.5x or a distance), Rotation (picked, or typed through the
drawing's angle settings; the box turns with it), Style (? lists them),
Width (replaces the opposite corner) and Columns (No; Dynamic and Static
say columns are not available yet). MText stores the line spacing style
(DXF group 73) so Exactly round-trips.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:19:53 -04:00
Valentin Klein ee34f423cc
Plot styles: colour-dependent tables apply to PDF plots and layouts (part of #410) (#412)
A PlotStyleTable model in doc with our own built-in tables (default.ctb keeps
object properties, monochrome.ctb prints black, grayscale.ctb prints the grey of
each colour's luminance). The render builder resolves each object's style by its
index colour (true colours by the nearest index) and applies colour, screening,
linetype and lineweight. PLOT/EXPORTPDF use the page setup's table (or the
plotStyleTable option); layouts with Display plot styles (PAGESETUP
displayPlotStyles, DXF plot flag 2) show it on screen and in PNG/SVG exports.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:19:48 -04:00
Valentin Klein f537377355
AUDIT and RECOVER: find and fix database errors, open damaged drawings repaired (part of #406) (#411)
AUDIT checks for missing layers, linetypes, styles and block definitions,
current-style variables naming missing entries, invalid geometry (NaN or
infinite numbers, zero-length lines, zero-radius circles and arcs),
block definitions that contain themselves, duplicate handles, and
references to missing objects (dimension associations, groups,
constraints, viewport layer lists). It asks "Fix any errors detected?
[Yes/No] <N>:"; the JSON form `audit {fix?}` returns a structured
report. RECOVER opens a file through the read hook and applies AUDIT's
fixes. A question prompt now shows its answers without "or".

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:19:44 -04:00
Brandon Thomas 0ef61cdff5 cargo fmt after the community PR batch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:44:34 -04:00
Valentin Klein 242a00923d
Object snap overrides at point prompts, Shift/Ctrl+right-click snap menu, Tab cycling (fixes #393) (#409)
Typed END, MID, CEN, GCEN, NOD, QUA, INT, APP, EXT, INS, PER, TAN, NEA,
PAR (any start of the word from three letters, with `_`/`'` prefixes)
and NON push a one-pick snap frame on the point-modifier layer: the
prompt reads "Endpoint of:", a typed point is snapped with that mode
alone within the aperture (or rejected with "No Endpoint found for
specified point."), and the canvas snaps the cursor with it
(Session::snap_override). A command's own keyword still wins (ARC/ZOOM
`cen`); `_cen` always snaps. Overrides nest with FROM and the filters.

Shift+right-click or Ctrl+right-click on the canvas opens the object
snap menu (From, Mid Between 2 Points, Point Filters, the snap modes,
None, Osnap Settings...), whose items type the same overrides. Tab over
a snap marker with nothing typed cycles the candidate snaps
(pointmod::snap_candidates: the running snap first, then the closest of
each mode).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:44:21 -04:00
Valentin Klein 2f9ebc6f63
PLINE arc options and Length, XLINE polyline segments (fixes #397) (#407)
PLINE arc mode answered "not available yet" for Angle, CEnter, Direction,
Radius and Second pt, and line mode for Length. Each now asks its questions
(included angles and directions read the drawing's angle settings) and adds
the segment. XLINE's options were already in draw2's XlineM2; the dead
keyword stub in draw.rs's RAY machine goes, and Offset/Reference use the
polyline segment picked rather than the first one.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:44:08 -04:00
Valentin Klein 7965f8728b
Object snap tracking, Extension, Parallel and Apparent Intersection snaps (fixes #379) (#405)
Resting the cursor on a snap point acquires it for tracking (seven at
most; resting again releases it). Acquired points send horizontal and
vertical alignment paths, or every polar angle with POLARMODE 2; the
cursor snaps where two paths cross, where a path crosses an object, or
onto the nearest path. EXT runs a line or arc on from an acquired end,
PAR gives a path parallel to a rested-on line through the base point,
APP snaps where objects would meet if extended. Direct distance entry
follows the path the cursor is on. POLARMODE is now AutoCAD's bitcode;
POLARADDANG and AUTOSNAP are new.

The engine owns all of it (Session::snap_cursor, snap/tracking.rs); the
canvas draws the + markers, dotted paths and tooltip.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:43:33 -04:00
Valentin Klein 1961a3186a
TRIM/EXTEND: cutting edges, Fence, Crossing, mOde, Project, Edge, eRase, Undo (part of #397) (#403)
The options TRIM and EXTEND list at their prompt all printed "not available
yet". Quick and Standard modes follow TRIMEXTENDMODE, Edge extends lines and
arcs virtually (EDGEMODE), Project is kept in PROJMODE. The JSON forms take
fence, crossing, edgeMode and (TRIM) mode.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:43:28 -04:00
Valentin Klein 65db0c2f6a
Point prompts: FROM, M2P/MTP, point filters, * prefix and <angle override (fixes #392) (#402)
A point modifier typed at any point prompt pushes a frame on the session
(Session::point_mods) that asks for what it needs ("Base point:",
"<Offset>:", "First point of mid:", ".X of", "(need YZ):") and hands one
point to the command, so every command gets them from the command line
and in scripts, and they nest (FROM then M2P). `*x,y`, `*@` and `@*`
parse as world coordinates (the same as the plain forms: no UCS yet).
`<a` locks the next point to that angle from the base point: a typed
distance follows it, a pick is projected onto it, typed coordinates win.
TK is left to object snap tracking (#379).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:43:24 -04:00
Valentin Klein 156bd0304e
Dimension text: DIMPOST's suffix also goes on tolerances (fixes #390) (#394)
With DIMTOL on, the DIMPOST suffix was added to the measurement only.
As in AutoCAD, the suffix (what follows `<>`, or the whole value when
there is no `<>`) now also follows each tolerance value: symmetric
(10.0mm±0.1mm) and stacked deviations alike. The prefix stays on the
measurement; limits are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:43:20 -04:00
Valentin Klein 9e19a10b29
Layouts: a viewport on a frozen layer hides its contents (fixes #395) (#396)
Freezing a viewport's own layer hid only its border; the model space
seen through it still showed on screen and in plots. As in AutoCAD, a
frozen viewport layer now hides the contents too, while turning the
layer off still hides only the border. The check is in the shared
layout path, so display, PDF/SVG/PNG export and plotting agree.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:43:15 -04:00
Nigel Ihza Firdausy 95dc3f22a7
fix(geom): keep finite-scale tolerance and midpoint math stable (#400) 2026-10-10 18:43:10 -04:00
Nigel Ihza Firdausy bc2c80289c
fix(select): include solid interiors in crossing-window selection (#401)
* fix(select): include solid interiors in crossing-window selection

* test(select): keep solid crossing regressions in an isolated module
2026-10-10 18:43:06 -04:00
Brandon Thomas 964eb0fb40 cargo fmt after the community PR batch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:11:18 -04:00
Valentin Klein d461b95872
ZOOM: Center, Scale, Dynamic and a plain n; All/Extents fit the sheet in paper space (fixes #388) (#391)
Center, Scale and Dynamic all zoomed to the extents, so the point or
factor typed after them ran as a command. Center now asks for the centre
point and a magnification or height (a height, nX or nXP), Scale for a
factor, and Dynamic, which needs a drag, asks for a window. A plain n
(at the first prompt and at Scale) is relative to the limits, as in
AutoCAD: 1 fits them. ZOOM All and Extents with nothing drawn used the
model limits in paper space too; Session::zoom_limits gives the sheet
there.

Builds on #372 (ZOOM inside a viewport).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:11:05 -04:00
Valentin Klein 304e56ba84
Attributes keep their own layer, colour, linetype and lineweight (fixes #358) (#359)
`Attrib` gains optional per-attribute properties (`props`), each overriding
the attribute definition's. DXF reading takes them from the ATTRIB's groups
8/6/62/420/370; writing gives each ATTRIB the properties it is drawn with.
The renderer prefers them over the definition's. -ATTEDIT's Layer and Color
options set them; ATTSYNC resets them to the definitions, as in AutoCAD.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:10:40 -04:00
Valentin Klein 19f7bc39ed
VPLAYER: option prompts at the command line (fixes #381) (#387)
Typed VPLAYER ran its JSON form with no parameters and ended, so its
options ran as commands (F started FILLET). It now loops on
?/Color/Freeze/Thaw/Reset/Newfrz/Vpvisdflt until Enter, asks for layer
names (wildcards, comma lists) and then which viewports to change:
All/Select/Current/Except current (Current by default inside a viewport,
Select by point on the sheet in paper space). Changes go through
viewport.set, layer.new and layer.set; the session is one undo step.
The Model tab refuses it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:10:35 -04:00
Valentin Klein 157ea6290d
LAYOUT / -LAYOUT: option prompts at the command line (fixes #378) (#380)
Typed LAYOUT ran its JSON default and ended, so the option and the
layout names typed after it ran as commands. It now asks for an option
(Copy/Delete/New/Template/Rename/SAveas/Set/?, Enter = Set) and the
layout name(s) the option needs, with the current layout as the default.
The options run the layout.* command bodies, so a refused name re-prompts
and the command is one undo step. Template and SAveas report that they
are not available yet and end.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:10:30 -04:00
Valentin Klein 1496d704b0
ZOOM inside a layout viewport acts on the viewport; nXP sets its scale (fixes #370) (#372)
In MSPACE, Extents/All/Window/Object fitted the drawing window's aspect and
paper-view height, In/Out/nX zoomed about the screen centre, and nXP was read
as nX. Now ZOOM works on the active (unlocked) viewport's model view: fits use
the viewport's aspect, zooms keep its centre, and nXP sets the viewport scale
to n paper units per model unit. Locked viewports and the Model tab are
unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:09:31 -04:00
Valentin Klein 54d75a1623
Control channel: cmdline.key replies like cmdline.input (fixes #360) (#361)
When Enter was refused (Enter repeats a command that isn't available),
headless failed the cmdline.key call while the app dropped the error,
and pressing Enter in the app's command line echoes it. Both now echo
the error to the history and reply with the command-line state, the
`output` lines the key added and `error` only when Enter was refused,
the shape #351 gives cmdline.input.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:09:26 -04:00
Valentin Klein de8ed87a6e
MLEADER remembers the placement order and Options (fixes #362) (#365)
The next MLEADER started again with "arrowhead first", Maxpoints 2 and
a landing line. As in AutoCAD, the order (arrowhead, landing or content
first) and the Options values (Maxpoints, leader landing, content type,
first and second angle) are now kept in Session::last_used for the
rest of the session and are where the next MLEADER starts. JSON calls
keep their documented defaults.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:09:20 -04:00
Valentin Klein 8db51c2766
Exploded dimension text and DXF dimension blocks match the drawn text (fixes #367) (#369)
EXPLODE and the DXF *D block took the text height from the bare style's
DIMTXT × DIMSCALE, ignoring the dimension's overrides (SETVAR DIM*, DIMOVERRIDE),
a fixed-height text style and the DIMSCALE 0 fallback (text ~1e-10 high), and
EXPLODE always wrote rotation 0, so aligned text turned horizontal. Both now
use the geometry the renderer draws (dimension_in): its text height, angle and
the overridden text style; EXPLODE adds no MTEXT for suppressed text.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:09:15 -04:00
Valentin Klein 409968ecbb
DIMTEDIT: command-line prompts (fixes #382) (#383)
Typed at the command line or run from a script, DIMTEDIT ran the JSON
form at once and failed with "select dimensions". It now asks "Select
dimension", then "Specify new location for dimension text or
[Left/Right/Center/Home/Angle]": a point moves the text there (the text
follows the cursor in the preview), Left/Right/Center justify it,
Home restores the default position and Angle asks for the text angle.
A single preselected dimension is taken at once. Each answer runs the
existing JSON form on the picked dimension. DIMTED is its alias.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:09:10 -04:00
Valentin Klein 1418130d45
DIMRADIUS/DIMDIAMETER: a location inside the circle puts the text inside (fixes #385) (#389)
Only the direction of the picked dimension line location was used, so
the text always went outside beyond the arrowhead. A location inside
the circle (typed, picked or JSON `at`) now places the text there: the
dimension stores it as its text position, and the dimension line runs
from the text out to the arrowhead; a diameter's spans the circle,
broken around the text. DIMTIH (not DIMTOH) decides whether inside
text is horizontal. Outside locations keep the default placement.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:09:05 -04:00
Valentin Klein 2f4ceea7a9
Dimension text: DIMRND rounds only the measurement; no DIMPOST on angles (fixes #373) (#376)
Tolerances and alternate units copied the primary number format, so they were
rounded by DIMRND and took DIMZIN's zero suppression: DIMRND 0.5 turned a
±0.1 tolerance into ±0.0000, and 10.3 in showed as [261.50] mm. Tolerances are
no longer rounded and use DIMTZIN. Alternate units use the new DIMALTRND
(rounding), DIMALTU (unit format) and DIMALTZ (zero suppression). The new
variables can be set with SETVAR, DIMSTYLE and overrides. A DXF keeps them as
DIMSTYLE group codes 148, 273, 284 and 285.

DIMPOST is a prefix/suffix for linear dimensions, so angular dimensions no
longer get it ("90°mm"). A text override still wraps the angle.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:08:59 -04:00
Valentin Klein a119873e20
Angular dimensions keep each leg's extension line; centre mark only outside (fixes #374) (#377)
When the arc location lies outside the sweep from the first leg to the second,
the arc runs from leg 2 to leg 1. The extension lines, DIMSE1/DIMSE2 and
DIMBLK1/DIMBLK2 still took the arc start as leg 1, so each extension line was
sized from the other leg. A short leg got no extension line, a leg already
crossing the arc got one drawn along it, and DIMSE1/DIMSE2 suppressed the
wrong line. The arc's ends are now paired with their legs.

Radius and diameter dimensions drew the DIMCEN mark even when the dimension
line is inside the circle. As in AutoCAD, the mark is now drawn only when the
line is outside: a radius led out to its text. A diameter line runs through
the centre, so it gets no mark.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:08:55 -04:00
Valentin Klein 179ecc1f0b
DIMBASELINE spacing follows DIM* overrides and DIMSCALE 0 (fixes #364) (#366)
Baseline dimensions were spaced by the bare style's DIMDLI × DIMSCALE, so
SETVAR DIMSCALE/DIMDLI (stored as overrides since #129) had no effect and a
style DIMSCALE of 0 collapsed the spacing to ~0. Use the continued
dimension's style with its overrides and the effective scale (the drawing's
DIMSCALE when the style's is 0), as drawing does; DIMSPACE's default spacing
uses the same scale.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:08:50 -04:00
Valentin Klein 7f5b39fb25
Undo/redo of LAYOUT Rename/New keeps the session on a layout that exists (fixes #371) (#375)
Undo and redo swapped the drawing but left the session on the current
layout's name. After undoing a rename or the creation of the current layout,
that layout no longer existed: nothing was drawn and commands failed with
"no such space". Now the session follows a renamed layout to its other name
(same tab order), keeping its paper view, and returns to the Model tab when
the layout is gone.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:08:45 -04:00
MarekSpodBiedry 6d8af2e5a4
Keep egui on the System theme preference so the OS appearance is followed live (#301) (#363) 2026-10-10 18:08:41 -04:00
Nigel Ihza Firdausy 35a8617855
fix(geom): use scale-aware circle-circle intersections (#384) 2026-10-10 18:08:37 -04:00
Nigel Ihza Firdausy 7bb64e5fce
fix(geom): preserve boundary vertices far from the origin (#386) 2026-10-10 18:08:32 -04:00
Brandon Thomas 1ff426dbe7 PURGE test: add the zero-length line directly
#344 made JSON LINE refuse coincident points; files can still hold them, so
the test that checks PURGE Zero-length adds one to the drawing directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:45:43 -04:00
Valentin Klein 9b7d7bd090
STRETCH: repeat Select objects until Enter, C/W windows, Displacement (fixes #228) (#231)
STRETCH gets its own prompt machine instead of a special case inside the
shared MOVE/ROTATE/SCALE one. "Select objects" now repeats until Enter:
implied windows (crossing right to left), `C` and `W` windows, single
picks and All/Last/Previous (moved whole), Remove/Add. Vertices inside any
of the windows move. Enter or `Displacement` at the base point asks for a
displacement, and Enter at the second point uses the base point as the
displacement, as in AutoCAD. Polygon and fence selection say they are not
available yet instead of being misread.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:39 -04:00
Valentin Klein 93152afa8e
Copy and paste between drawings; PASTEORIG, PASTEBLOCK; right-click menu (part of #325) (#355)
Copying in one drawing and pasting in another lost what the objects
refer to: a line on a layer the target lacked landed on a new default
layer (colour and linetype gone), and a block reference pointed at a
block the target didn't have. Cmd/Ctrl+C, X and V did nothing on the
desktop, because egui delivers them as clipboard events, not key
presses. PASTEORIG had no checks, and the canvas had no right-click
menu.

- Engine (cmd/clipboard.rs): the session clipboard keeps, with the
  copied objects, the layers, linetypes, text/dimension/multileader/
  table styles and blocks (nested too) they use. Pasting adds the ones
  the target lacks; names it has keep the target's definitions, as in
  AutoCAD; generated (*U, *D) blocks whose name is taken get a fresh
  one; objects get new handles.
- PASTEORIG pastes at the original coordinates, only into a drawing
  other than the one copied from (greyed and refused there, as in
  AutoCAD). PASTECLIP and PASTEBLOCK are greyed with an empty clipboard.
- PASTEBLOCK (Edit > Paste as Block, Cmd+Shift+V): the clipboard
  becomes an A$C… block inserted at a point. AI_DESELECT.
- UI: Cut/Copy/Paste events run the clipboard commands (Shift: Copy
  with Base Point / Paste as Block). Each copy is also put on the system
  clipboard as DXF text, and pasted DXF text that isn't our own last
  copy becomes the clipboard, so another CADCraft window can paste it.
- Right-click on the canvas with no command running opens a shortcut
  menu: Repeat <last>, Clipboard (Cut, Copy, Copy with Base Point,
  Paste, Paste as Block, Paste to Original Coordinates), the
  selection's editing commands and Deselect All, Undo/Redo, Pan/Zoom,
  Quick Select, Find. During a command it is still Enter.
- Ukrainian labels for the new items.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:34 -04:00
Valentin Klein 4010b94919
INSERT inserts exploded (*NAME) and drawing files (fixes #340) (#341)
At the block name prompt, *NAME inserts the block's objects: after the
insertion point INSERT asks one scale factor and the rotation, and no
attribute values. A path (C:\parts\bolt.dxf), NAME=path or NAME= reads
the drawing through the io hooks and defines a block named after the
file (or NAME) from its model space, with the file's $INSBASE as the
base point, adding the layers, linetypes, styles and blocks it needs and
keeping this drawing's definitions. A plain path whose block already
exists asks whether to redefine it; NAME=path redefines without asking.
A file that can't be read is reported at the name prompt. The JSON form
takes `file` (optional `name`) and "*NAME".

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:29 -04:00
Valentin Klein dacd8ece49
Control channel: headless and app reply alike to cmdline.input, state and app.open (fixes #351) (#352)
Both backends now return the same command-line state (prompt, running,
keywords, accept, buffer, history, historyExpanded). cmdline.input echoes
a refused line to the history and `output` and also reports it in `error`
on both sides. Headless app.open without a path answers "missing path"
like the app instead of reaching OPEN. Documented in control-protocol.md.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:24 -04:00
Valentin Klein cdd25840cc
Control channel: a timed-out request is cancelled, and the reply says whether it ran (fixes #345) (#348)
The control server gave up after 60 s with "timeout" but left the request
queued, so the app ran it later and a client retry applied it twice.
Requests now carry a shared state: on timeout the server cancels a request
the app has not started (reply state "not-run", safe to retry) and the app
skips it; one already started replies state "may-have-run". The MCP bridge
passes that on in its error.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:19 -04:00
Valentin Klein 45755697b6
Angle prompts read AUNITS, ANGBASE and ANGDIR (fixes #353) (#354)
ROTATE's angle and the hatch pattern angle are rotations (AUNITS, clockwise
when ANGDIR is 1). The dimension line (Rotated) and dimension text angles are
directions from ANGBASE. CHAMFER's angle and the MLEADER angle constraints are
amounts in AUNITS. The ROTATE and HATCH defaults are offered as they would be
typed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:13 -04:00
Valentin Klein 937dca6590
DWG and DXF keep ANGBASE/ANGDIR, fillet/chamfer, elevation and the snap grid (fixes #349) (#350)
acadrust's DXF writer leaves ANGBASE, ANGDIR, FILLETRAD, CHAMFERA/B,
ELEVATION and THICKNESS out of the HEADER although its DWG reader fills
them in; the DWG bridge now adds them to the DXF it hands our reader.
SNAPBASE/SNAPANG are written to and read from the *ACTIVE VPORT record
(groups 13/23 and 50), where R13+ files keep them.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:07 -04:00
Valentin Klein 7bed82838d
JSON draw/annotation commands refuse invalid sizes and coincident points (fixes #343) (#344)
CIRCLE/ARC no longer make a negative radius positive; POLYGON refuses a
non-integer sides count and a zero/negative radius or zero-length edge;
DONUT, RECTANG, PLINE, HATCH and TABLE refuse negative or zero sizes
instead of clamping them; LINE drops repeated points like the interactive
command; SPLINE needs two different fit points; DIMRADIUS/DIMDIAMETER/
DIMANGULAR refuse coincident points; DDPTYPE refuses an invalid PDMODE;
DIMSTYLE/DIMOVERRIDE list a non-positive DIMTXT/DIMTFAC or a negative
DIMASZ/DIMSCALE/DIMTSZ as ignored.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:32:01 -04:00
Valentin Klein 71706f8e6a
TL/ML/BL text is placed by its alignment point (fixes #342) (#346)
For every justification but baseline-left (and aligned/fit) the DXF
Reference makes group 11 the point the text is placed by; group 10 is a
computed point on the baseline. Left-justified text with a vertical
alignment was drawn, measured and picked from group 10 and then shifted
by the alignment again: TL and ML too low, BL too high. The renderer
(fonts::place) and text extents (doc text_box, used for picking) now
place it from the alignment point; a mirrored (extrusion 0,0,-1) TL/ML/BL
text keeps its alignment point when it turns right-justified; and the
Properties position moves justified text by its alignment point.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:31:56 -04:00
Valentin Klein 2f7a1e60d7
QDIM: one snap index per call, cap the input (fixes #356) (#357)
With DIMASSOC on, every dimension QDIM added looked up the objects under
its definition points by scanning the whole space, and each dimension
adds an entity, so QDIM over many objects took quadratic time (20,000
lines: 28 s). QDIM now builds the SnapIndex DIMCONTINUE/DIMBASELINE use
(#281) once per call; dimensions are no snap targets, so it stays valid
while QDIM adds them. A call naming more than 100,000 objects, or whose
objects give more than 100,000 distinct points, is refused before
anything is created.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:31:50 -04:00
Valentin Klein 776d99a1e0
FILLET/CHAMFER: Trim, Multiple, Undo; CHAMFER Angle, mEthod, Polyline (fixes #248) (#251)
* Pick box at object-selection prompts; prompts remember the last option

Part of #34.

The crosshair showed the pick box only at "Select objects" prompts. The
commands that pick one object themselves (FILLET, CHAMFER, OFFSET, TRIM,
EXTEND, BREAK, the dimension and constraint picks...) ask with a plain
point prompt, so the cursor stayed a bare crosshair there and hitting an
object was guesswork. The idle crosshair also showed the box with
PICKFIRST off.

- Engine: Prompt::picks_objects() (a selection prompt, or a point prompt
  that starts with "Select") and Session::picking_objects() (that, or no
  command with PICKFIRST on). The canvas uses it for the pick box and the
  rollover highlight.
- The box is sized from PICKBOX as the pick aperture the click uses
  (PICKBOX x 1.5 px half side), hidden at PICKBOX 0, and drawn with a new
  `crosshair` theme token.
- HATCH/GRADIENT: "Select objects" mode is a real mode now ("Select
  objects or [picK internal point/...]", Enter creates the hatch) and the
  next HATCH starts in the mode used last. Pattern, scale and angle
  sub-prompts show the current value as <default>. At select prompts P, L
  and ALL stay selection modes even when a keyword shares the letter.
- POLYGON offers the last Inscribed/Circumscribed choice, ROTATE the last
  angle, SCALE the last factor (Session::last_used, session only).
- FILLET shows the radius as <default>; CHAMFER asks for the second
  distance too, defaulting to the first (FILLETRAD, CHAMFERA/B).

JSON calls are unchanged: explicit params and documented defaults win.

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

* FILLET/CHAMFER Trim, Multiple, Undo; CHAMFER Angle, mEthod, Polyline (fixes #248)

Trim asks for the trim mode (TRIMMODE); with No trim the fillet arc or
chamfer line is added and both objects are kept. Multiple repeats until
Enter and Undo takes back the last fillet/chamfer of the command. CHAMFER
Angle asks for a length on the first line and an angle from it (CHAMFERC,
CHAMFERD, CHAMMODE = 1), mEthod picks Distance or Angle, and Polyline
chamfers every straight corner of a 2D polyline. The JSON forms take
`trim`, and CHAMFER `length`/`angle` and `polyline`.

Keyword matching now prefers a whole keyword or its capital-letter
shortcut over a prefix of another keyword, so `M` is Multiple rather than
the start of mEthod.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:56:01 -04:00
Valentin Klein c26150ec07
Dimension and multileader options take effect (fixes #243) (#247)
DIMORDINATE Xdatum/Ydatum/Mtext/Text/Angle, DIMRADIUS/DIMDIAMETER
Mtext/Text/Angle, DIMANGULAR Mtext/Text/Angle/Quadrant and DIMARC
Mtext/Text/Angle/Partial were listed but ignored (any DIMORDINATE
keyword other than Xdatum forced a Y datum). DIMLINEAR/DIMALIGNED
Angle rotated the dimension line instead of the text.

A shared DimText handles Mtext/Text (override text, <> = measurement)
and Angle (text rotation) for every placement prompt. Quadrant locks
the measured angle to a picked point's side; Partial dimensions the
part of the arc between two picked points. After Horizontal/Vertical/
Rotated only the text options remain listed.

MLEADER: leader Landing first / Content first switch the placement
order, Enter or Options opens the options loop (Leader type, leader
lAnding, Content type, Maxpoints, First/Second angle). Spline leaders,
leaderless and block content say "not available yet". Angle prompts
take numbers so scripts keep splitting the following tokens.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:55 -04:00
Valentin Klein d433112f1e
Draw hatch island styles, backgrounds, two-colour gradients and file pattern definitions (fixes #289, fixes #290) (#302)
- Island detection style (group 75) picks the loops that take part: outer keeps the
  outermost loops and their first islands, ignore only the outermost loops.
- A pattern hatch's background colour is filled under its pattern lines.
- Gradients blend colour 1 to colour 2 (LINEAR, CYLINDER, CURVED, SPHERICAL,
  HEMISPHERICAL and the INV forms) along their angle, shifted when not centred, drawn as
  bounded colour steps; ByLayer / ByBlock gradient colours resolve to the hatch's layer /
  block colour instead of colour 7.
- DXF HATCH pattern definition lines (78, 53, 43-46, 79, 49) of a pattern the library
  doesn't define are kept on the hatch (new `patternLines` field), drawn and written back;
  hostile counts are capped.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:49 -04:00
Valentin Klein 3e4eb7a0e9
-LAYER: command-line option loop for layers (fixes #254) (#260)
Typed -LAYER (and LAYER in scripts and headless sessions) now starts the
option loop instead of listing the layers and ending: ?, Make, Set, New,
Rename, ON, OFF, Color (with Truecolor), Ltype, LWeight, TRansparency,
Plot, Freeze, Thaw, LOck, Unlock and Description, until Enter. MATerial,
stAte and rEconcile report "not available yet" without taking the next
token. Every option runs the layer.* command bodies, so the session is
one undo step and refusals match the JSON form. Turning the current
layer off asks first; freezing it is refused.

layer.new and layer.set now reject a linetype that is not loaded (or
ByLayer/ByBlock), and layer.set refuses to rename a layer onto the name
of another existing layer.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:44 -04:00
Valentin Klein 2483676ba6
-PURGE: type, names and verify prompts; purge by type (fixes #262) (#263)
Typed -PURGE (PURGE has no dialog yet) now asks for the type of unused
objects, the names to purge <*> and whether to verify each name, instead
of purging everything at once and leaving the rest of the line to run as
commands. Verify Yes confirms each name; No purges every match. Types
CADCraft does not have read their answers and report nothing to purge;
Zero-length geometry and Empty text objects purge at once.

The JSON form takes {type, names}: LA purges only layers. All covers
every named type, so unused text, dimension, multileader and table
styles and empty groups are purged as the params doc promised. Layers and
text styles go through layer.delete and style.delete. Linetypes used
only inside block definitions, or set as CELTYPE, are kept.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:39 -04:00
Valentin Klein 7a876c74c9
ATTEDIT prompts at the command line; -ATTEDIT; tags in any case (fixes #273, #274) (#280)
ATTEDIT / EATTEDIT typed at the command line ran the JSON form and failed
with "`handle` is required". It now asks "Select block reference:" and then
"Enter value for <TAG> <current>:" for each attribute (Enter keeps the
value); the answers are applied together as one undo step.

-ATTEDIT is a command of its own (Modify > Object > Attribute > Global):
one at a time (Value: Change/Replace, Next) or global string replacement,
over the attributes chosen by block name, tag and value specifications.
Session::start now prefers an id that keeps its leading dash, so -ATTEDIT
is not taken for ATTEDIT; dashed aliases (-INSERT...) resolve as before.

INSERT `attribs` and ATTEDIT `values` match attribute tags in any case and
report the keys no attribute has in `unknownTags`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:33 -04:00
Valentin Klein 5b286119d2
ATTDISP and ATTSYNC; the display follows ATTMODE (fixes #275, #276) (#282)
ATTDISP asks "Enter attribute visibility setting [Normal/ON/OFF]" (default:
the current setting) and sets ATTMODE to 1 / 2 / 0. The renderer now reads
ATTMODE: OFF hides every attribute, ON also draws invisible ones, Normal
draws the visible ones as before.

ATTSYNC ("Enter an option [?/Name/Select] <Select>") brings every reference
of a block, in model space, layouts and other block definitions, in line
with the block's attribute definitions: missing attributes are added with
their defaults, ones whose definition is gone are removed, values are kept
for tags still defined (any case), and position, height, style and
visibility come from the definition.

Both live in the new cmd/blocks/attribs.rs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:27 -04:00
Valentin Klein cf9fbb8bf5
LAYMRG and LAYDEL: merge layers, delete a layer with its objects (fixes #286) (#296)
LAYMRG moves every object on the source layers (model space, layouts
and block definitions) to the target layer, drops the sources from
viewport freeze lists and colour overrides, and deletes the source
layers. LAYDEL deletes the layers and every object on them the same
way. Typed, both ask "Select object on layer to merge/delete or
[Name]:" until Enter (Undo drops the last one), LAYMRG then asks for
the target layer, and both warn and ask [Yes/No] <No> before changing
anything. Scripts answer Y. The JSON forms laymrg {from, to} and
laydel {names} never prompt.

Layer 0, Defpoints and xref-dependent layers are refused, and so is
merging a layer into itself. LAYDEL refuses the current layer; LAYMRG
merges it and makes the target current. Each run is one undo step.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:21 -04:00
Valentin Klein e9ec19b138
Midpoint, Perpendicular and Tangent snaps on elliptical arcs and splines; Quadrant stays on the arc (fixes #332) (#333)
Midpoint snaps halfway along the length of an elliptical arc or open spline.
Perpendicular and Tangent from a base point find every foot point and
tangent point on ellipses, elliptical arcs and splines (the one nearest the
cursor wins, as for circles): tangents to an ellipse exactly through the unit
circle, the rest by sampling the parameter range and bisecting each sign
change, with fixed iteration bounds. Quadrant on an elliptical arc offers only
the quadrant points the arc passes through.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:16 -04:00
Valentin Klein 6a15ba2a7d
Dimensions snap; justified text snaps and grips at its alignment point; grid snap follows SNAPBASE/SNAPANG; OSNAPHATCH (fixes #334) (#337)
- A dimension snaps to what it draws: the objects of its block (read from a
  file), expanded like block references, or else the lines generated from it
  (Endpoint on their ends, Midpoint, Intersection, Nearest); Node on its
  definition points.
- TEXT justified other than left-baseline (as read from DXF, group 11)
  snaps (Insertion) and grips at its alignment point; attributes too.
- The snap grid starts at SNAPBASE and turns by SNAPANG (drawing variables,
  defaults 0,0 and 0 degrees). SETVAR takes `x,y` for point variables.
- Hatches are left out of object snaps unless OSNAPHATCH is 1 (new session
  variable, default 0 as in AutoCAD).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:12 -04:00
Valentin Klein b1a78fbb8f
Typed angles follow ANGBASE, ANGDIR and AUNITS (fixes #326) (#328)
Polar points and angle prompts read bare numbers in AUNITS (explicit
d/r/g, deg-min-sec and bearings still work) and measure directions from
ANGBASE in the ANGDIR sense; `d<<a` (degrees) and `d<<<a` (AUNITS)
measure from +X counterclockwise. XLINE Reference turns per ANGDIR;
included angles and sweeps take the units only. ANGBASE is held in
radians like $ANGBASE in DXF; SETVAR reads and reports it in degrees.
JSON params keep their meaning.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:07 -04:00
Valentin Klein fe0eb45537
Dimensions take viewport layer colours, leader arrows their style's DIMSCALE, attributes their own properties (fixes #313) (#319)
- A dimension on a layer with a viewport colour override now takes the
  override inside that viewport (dimension and extension lines, arrows,
  text), like every other object.
- A LEADER's arrowhead is DIMASZ x DIMSCALE of the leader's dimension
  style; the drawing's DIMSCALE only applies when the style's is 0 (as
  for dimensions), and both 0 no longer gives a zero-size arrow.
- Attributes are drawn with the properties of their attribute
  definition, which AutoCAD copies to the attribute on insertion:
  ByLayer = the definition's layer (layer 0 = the block reference's
  layer), ByBlock = the block reference's colour and lineweight,
  explicit colours and lineweights as set. An attribute on an off or
  frozen layer is hidden. Attributes without a definition keep the block
  reference's colour and now its lineweight too, instead of 0.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:55:02 -04:00
Valentin Klein 282487fbc5
MCP: reject tool arguments that don't fit the schema; query_entities counts all matches and windows by geometry (fixes #335) (#336)
tools/call checks the arguments against the tool's input schema: an
unknown tool, non-object arguments, a missing required argument or one
of the wrong type is a JSON-RPC -32602 error, as the MCP spec says, so
`command_line {}` no longer presses Enter and `script {}` no longer
succeeds. An empty batch `[]` gets one Invalid Request (-32600).

The `entities` command behind query_entities rejects a malformed window,
non-string type/layer and bad limit/offset instead of ignoring them.
`count` is every match, `returned` the page (`offset` is now in the tool
schema), and a window matches objects whose geometry is inside or
crosses it (the window/crossing selection test), with `crossing: false`
for entirely inside.

list_commands' filter matches command ids, labels and aliases, not any
text in the command's JSON.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:54:58 -04:00
Valentin Klein 2e3da66380
Refuse edits whose result has infinite or undefined coordinates (fixes #338) (#339)
Inputs to MOVE, SCALE, ROTATE, ARRAY, OFFSET and the rest are checked for
being finite, but finite inputs can still overflow: moving by 1e308 twice
put a circle at infinity, after which inspect showed null coordinates,
the extents were null and a DXF save wrote it at 0,0.

After a command changes the drawing (JSON `execute`, grip edits, and an
interactive command when it ends), the objects it changed are checked:
every number in their fields and their extents must be finite. If not,
the drawing and selection are put back, no undo step is recorded and the
command fails with "the result is too large: object … would get infinite
or undefined coordinates. Nothing was changed".

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:54:53 -04:00
Valentin Klein 0fef879509
Grip edits act on the whole selection (fixes #320) (#322)
Stretching a grip now drags the grips of the other selected objects that sit
on the same point, so connected lines stay connected, and the Move, Rotate,
Scale and Mirror grip modes transform every selected object about the hot
grip, as in AutoCAD. grip.move/grip.stretch take the other objects in
`handles`; with only `handle` they edit that object alone, as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:54:48 -04:00
Valentin Klein 049504a81e
Nearest snap picks the closest object in any drawing order (fixes #323) (#324)
A circle in the aperture replaced the best Nearest candidate without
comparing distances, so a closer line drawn earlier lost to it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:54:42 -04:00
Valentin Klein ba026c484a
Save As to an image or PDF exports without renaming the drawing (fixes #327) (#329)
SAVEAS / QSAVE with a .svg, .png or .pdf path wrote the export and then
marked the drawing saved under that file: the title became the image name,
the unsaved changes were cleared (no "Save changes?" on close) and a later
Save wrote the image again. Exports now only store the file; DXF/DWG saves
are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:54:37 -04:00
Valentin Klein eee9de96f5
JSON handles: reject a malformed handle instead of using the selection (fixes #330) (#331)
targets() fell back to the current selection when `handle` wasn't a hex
handle or `handles` wasn't an array, and dropped unreadable entries from
`handles`. A typo such as {"handle": "#101"} made ERASE, MOVE and the other
~70 commands that take objects act on whatever was selected (in a bridged
MCP session: the user's selection in the window). Such values are now an
error naming the bad value; the selection is used only when neither key is
given (or it is null).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:54:33 -04:00
Brandon Thomas 55531fe230 Snaps in block references: spend budget per array cell; skip empty blocks
Follow-up to #318: a huge array of an empty or tiny block could spin up to
1e8 cells in the snap path without touching the entity budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:29:32 -04:00
Valentin Klein 9d2ae09a79
OFFSET Erase/Layer/Multiple/Undo and MOVE/COPY Displacement take effect (fixes #244) (#246)
OFFSET's Erase and Layer keywords open their Yes/No and Current/Source
prompts (remembered in OFFSETERASE/OFFSETLAYER and shown in the current
settings line); Multiple keeps offsetting the newest copy until Enter and
Undo takes back the last offset. The JSON form takes `erase` and `layer`.
MOVE and COPY ask for a displacement after the Displacement keyword or
Enter at the base point prompt.

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

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

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

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

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:11:58 -04:00
Valentin Klein 4617613595
Draw the text of complex linetypes along the line (fixes #312) (#316)
Linetype text elements (FENCELINE1, GAS-style linetypes from DXF files)
were drawn as part of the gap. Each text is now placed where the element
that carries it ends, at every repeat of the pattern: in its text style's
font, at the element's scale times the style's fixed height and the
linetype scale, shifted by its X/Y offsets along and across the line, and
rotated relative to the line direction or absolutely. Runs drawn
continuous (pattern smaller than a pixel) draw no text, and coarse
previews without text skip it. Shape elements need SHX shape files,
which CADCraft doesn't read, so they still draw nothing.

FENCELINE1 carries its circle on the first gap, centred between the two
gaps, so it no longer sits on the start of the long dash.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:11:52 -04:00
by Truitt c87a38546c
Default save format for new drawings (DXF or DWG) (#298)
Save As suggested `.dxf` for every untitled drawing, so saving new work as
DWG meant typing the extension each time. AutoCAD keeps this choice in
Options > Open and Save > "Save as".

- File > Default Save Format > DXF / DWG sets the format Save As suggests
  for drawings that have no file yet. Drawings opened from a file keep that
  file's format, as before. DXF stays the default.
- The choice is saved across restarts with the other preferences
  (`saveFormat`), survives Reset Palettes, and is scriptable as
  `ui.saveformat {format}` or the `saveFormat` UiState field.
- Ukrainian labels for the new menu entries.

On macOS the suggested `.dwg` name needs DWG in the Save As filter list
(#198) to survive the dialog.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:11:46 -04:00
STE1200 253f0c8497
Extents: ignore what draws nothing (rays, missing or empty blocks, hidden block geometry), as ZOOM Extents does (#288)
* Extents: ignore rays and references to missing or empty blocks, as ZOOM Extents does

Opening real drawings in CADCraft often showed a tiny plan in a huge empty
canvas, because the extents included things that draw nothing:

- the base point of a RAY (Autodesk's own reference says infinite
  construction lines and rays do not affect ZOOM Extents; an office plan in
  our corpus carries two rays 1.98 million units from the building);
- the insertion point of an INSERT whose block is missing (a purged
  definition, a bound xref that never came along) or empty (an unloaded
  xref): a floor plan had such references 125 000 and 3.4 million units away.

XLINEs were already ignored; rays now are too, and an insert contributes only
what its block draws (attributes still count). Two regression tests use the
measured coordinates.

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

* Extents: skip invisible block entities, attdefs and blank texts

- Entities in a block definition count for an insert's (and a dimension
  block's) extents only if they are not AttDefs, have common.visible set,
  and sit on layer "0" or on a layer that is on and thawed. Dynamic blocks
  keep hidden constraint geometry far from the visible part.
- Text/MText that is empty, whitespace-only or only "\~" has empty bounds.
- Tests cover each rule separately.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 16:11:41 -04:00
Valentin Klein 7336a3f597
Wipeouts mask what is drawn before them; WIPEOUTFRAME controls the frame (fixes #291) (#308)
A wipeout was drawn only as its outline, so objects behind it stayed
visible on the canvas and in PNG, SVG and PDF output. The display list
gets a new primitive kind, Kind::Mask: the wipeout polygon as triangles
that every backend fills with its background colour (paper white for
SVG/PDF and plots, the raster background for PNG, the canvas colour or
the white sheet on screen), in draw order, so it hides what came before
it (DRAWORDER decides). The frame follows WIPEOUTFRAME: 0 none, 1 shown
and plotted (default), 2 shown but not plotted.

The GPU canvas drew all fills before all lines; it now splits the mesh
into draw-order groups at each mask (MeshData::breaks) and draws each
group's fills, lines and quads in turn. Infinite lines stay on top as
before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:11:35 -04:00
Valentin Klein 91c2471e2b
Wide polylines are drawn with their linetype (fixes #293) (#309)
A polyline with width was always filled as one continuous band. Now,
with a linetype, each dash is a filled piece of the band (tapers and arc
segments included) and the gaps stay empty; dots are drawn as a line
across the band. As for thin polylines, PLINEGEN runs the pattern along
the whole polyline, otherwise it restarts at every vertex, and a pattern
too small to see (min_dash) fills the whole band.

linetype::ranges lays the pattern out along a length; wide::dashed cuts
the band at those distances.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:11:30 -04:00
Valentin Klein f1743df2dd
Only colour 7 flips with the background (fixes #272) (#285)
display_rgb flipped every near-white colour to black on paper and every
near-black colour to white on the dark canvas, so ACI 51/255 and
true-colour white printed black and dark index colours showed white.
Display-list primitives now carry an aci7 flag set when the colour
resolves to 7 (directly, by layer or by block); only those flip.

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

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

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

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

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:11:02 -04:00
Valentin Klein 5f0390eb20
Default lineweight follows LWDEFAULT (fixes #292) (#295)
Objects whose lineweight resolves to Default were always drawn and
plotted at 0.25 mm. They now use LWDEFAULT (hundredths of a mm, 0..211),
falling back to 0.25 mm when it is unset or out of range. All backends
(canvas, PNG, SVG, PDF) read the width from the display list.

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:34:24 -04:00
by Truitt afe8b93255
Save As: offer DWG, with the drawing's format first (#198)
The Save As dialog listed DXF, SVG, PNG and PDF, though CADCraft writes
DWG. On macOS the dialog accepts only listed extensions, so a name typed as
`part.dwg` was changed to .dxf and the drawing was saved as DXF.

Save As now lists "Drawing (DWG)" too. The existing suggested-type-first
ordering does the rest: a drawing opened from DWG defaults to DWG, while new
drawings and DXF files keep DXF first. The list moves into
`filters_for_save` with a unit test, and the label gets its Ukrainian
translation following "Drawing (DXF)".

Related to #14 (the Open half was fixed in #21).


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:34:18 -04:00
STE1200 be8ae81c40
DXF read: map mirrored OCS (extrusion 0,0,-1) of 2D entities to WCS (#277)
AutoCAD and BricsCAD store a mirrored block reference, and many 2D
entities drawn in a mirrored UCS, with extrusion (0,0,-1). Their points
and angles are then in an OCS whose x axis runs the other way. The reader
only honoured that for CIRCLE, ARC, LWPOLYLINE and SOLID/TRACE, so
mirrored blocks, hatches and texts landed on the wrong side of the
y axis (e.g. profile blocks at x -57618 instead of +57618).

- INSERT: insertion point through the OCS, negative x scale, opposite
  rotation and negative column spacing. Only for extrusions parallel to
  -Z (x/y noise up to 1e-3); a tilted block plane needs a 3D transform
  that Insert cannot hold and is left as read.
- HATCH and R12 2D POLYLINE: boundary x and bulges mirrored like
  LWPOLYLINE; the hatch pattern angle mirrors too.
- TEXT/ATTDEF/ATTRIB and MTEXT: points through the OCS; drawn readable
  (MIRRTEXT = 0) with the mirrored footprint, so the direction turns and
  left/right justification or attachment swap.
- ELLIPSE: centre and major axis are WCS, but the minor axis is
  extrusion x major, so the parameter range is negated.
- DIMENSION: the text midpoint (11) is the one OCS point.

Checked against an independent DXF library on private drawings: every
INSERT transform (plain, mirrored, tilted) matches, as do the mirrored
arcs, ellipses, polylines and hatches. The remaining extents outliers in
those drawings come from references to empty blocks, which still add
their insertion point to the extents.

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

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:34:01 -04:00
Valentin Klein 1bd1c5cfa2
io: only the first CADCraft xdata string marks the PAPERVIEW viewport (#253)
dxf_ext::is_paper_view (#225) matched any 1000 string in the CADCraft xdata, but later strings
are layer names (VPFROZEN, VPCOLORS). A real viewport with a frozen or recoloured layer named
PAPERVIEW was therefore dropped on read. Only the first string, where the writer puts the
marker, counts now. Regression test in tests/paperview_marker.rs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:33:56 -04:00
Valentin Klein 3ef0edaed9
Layout viewports scale model-space linetypes by PSLTSCALE (fixes #257) (#259)
With PSLTSCALE on (the default), dashes of model-space objects seen through a
viewport keep their paper-space length whatever the viewport scale; before,
a 1:100 viewport drew them 100 times too short, so dashed lines looked solid
in layouts and PDF plots. PSLTSCALE 0 keeps the old behaviour.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:33:50 -04:00
Valentin Klein 4c056c54bd
Points draw with the point style (PDMODE/PDSIZE) (fixes #261) (#265)
POINT objects were always a one-pixel dot. They now draw the figure PDMODE
picks (nothing, plus, cross, tick, with an optional circle and/or square)
at the size PDSIZE gives: drawing units when positive, a percentage of the
view height when negative, 5 % when 0. The canvas passes its view height;
exports and plots use the plotted window or the extents.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:33:43 -04:00
Valentin Klein 575e690cc5
Bound block expansion in drawing and extents: nested, self-referencing or arrayed blocks no longer hang (fixes #255) (#267)
* Bound block expansion in drawing and extents (fixes #255)

MAX_BLOCK_DEPTH bounds only the depth of nested block references, so a
small DXF with a self-referencing block, blocks that each insert the next
several times, or one large MINSERT expanded to billions of copies and
hung the app (drawing, PNG/SVG/PDF export, extents, saving).

- render: a per-display-list budget (MAX_BLOCK_EXPANSION, 2 million
  block copies and entities drawn inside blocks); past it the remaining
  block contents are not drawn.
- doc: entity_bounds computes each block's content bounds once per call
  and reuses them for every further reference.

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

* Move Builder::expand below the drawing primitives

Pure code motion so this branch merges cleanly with fix/272-aci7-only-flips
(#272), which changes the Builder::polyline signature next to it.

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

---------

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:33:31 -04:00
Valentin Klein 160b0fd644
Draw raster points as a filled path so debug builds don't panic (#271)
tiny-skia's anti-aliased fill_rect hits a debug_assert when a rect under
two pixels wide has both sides mid-pixel, so PNG export panicked in debug
builds depending on where a point landed. Fill the same 1.5 px square
through the path filler instead.

Fixes #270.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:33:25 -04:00
skylar taylor-barrick e09fb50627 UI.9: integrate shared workspace docking and preserve CAD controls 2026-10-10 13:30:09 -06:00
Brandon Thomas 17a41184ca Fix two tests after the community PR batch
- entity_props_roundtrip: look for the Standard text style inside the STYLE
  table; #216 now also writes Standard multileader and table styles.
- uk.tsv: translate the Accessibility menu and Show System Cursor (#187).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:02:46 -04:00
Brandon Thomas 06f742c787 Clippy (as_chunks) and rustfmt after the community PR batch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:50:54 -04:00
Valentin Klein 27d6bf2548
DWG: keep true colours, transparency, table styles, layouts and viewport ids (fixes #223) (#225)
DWG saves convert our R2000 DXF with acadrust, which wrote an R2000 DWG:
no true colours, no entity transparency, no table styles, and its R2000
reader puts every layout's paper-space entities into the first layout.
The bridge now writes at least AutoCAD 2004 (AC1018) DWG; the DXF itself
stays R2000. acadrust goes to 0.6.3 (explicit transparency encoding).

DWG doesn't store viewport ids: readers number a layout's viewports in
order, and id 1 is the paper-space viewport (the sheet). CADCraft keeps
that one implicit, so its first viewport came back as id 1 and wasn't
drawn. The DXF writer now puts a paper-space viewport (id 1, marked with
CADCraft xdata) ahead of a layout's viewports when it has none, and the
reader drops it again. Viewports also get status 68 and the always-set
bit 32768 in group 90, so other readers don't take them as "off".

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:39:58 -04:00
MarekSpodBiedry 3d125493f9
Layer rename re-points paper-space and block-definition entities (#75) (#191) 2026-10-10 14:39:43 -04:00
MarekSpodBiedry e13de2c2f1
Layer delete and purge keep layers used by block definitions (#72) (#189) 2026-10-10 14:39:28 -04:00
Nigel Ihza Firdausy 9441dad29f
fix(geom): avoid false line-circle tangencies on long construction lines (#214)
* fix(geom): avoid false line-circle tangencies on long lines

* fix(geom): construct tangent points relative to the projection foot
2026-10-10 14:39:13 -04:00
Nigel Ihza Firdausy c52f4d190e
fix(geom): measure bulged polylines and distant polygons accurately (#212)
* fix(geom): compute polyline bulge areas without tessellation error

* fix(geom): preserve polygon area far from the coordinate origin
2026-10-10 14:38:57 -04:00
Nigel Ihza Firdausy f38b3a6447
fix(geom): calculate tight ellipse and arc bounds analytically (#211) 2026-10-10 14:38:42 -04:00
Valentin Klein a560048dd0
Web: File > Open… shows the browser's file picker (fixes #55) (#199)
The web app ran CadApp with Services::default(), so OPEN found no file
picker and returned without doing anything: no picker, no message.

The web app now installs an open picker (apps/cadcraft-web/src/upload.rs,
web-sys only): a hidden <input type="file" accept=".dxf,.dwg"> is
clicked, the chosen file is read in the background (Blob.arrayBuffer)
and handed to the app on its next frame, which runs OPEN with
{data, name} through the new CadApp::open_bytes. Cancelling the picker
does nothing. Files dropped on the page, which eframe hands over unread
on the web and the UI ignored there, take the same route.

Files over 256 MB (CadApp::MAX_OPEN_BYTES; the browser holds the drawing
several times while decoding it) are refused before they are read.
Unreadable or rejected files report on the command line and in the
status bar; nothing panics.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:38:20 -04:00
Valentin Klein 238162f332
Option to keep the system cursor over the drawing area for magnifiers (#39) (#187)
The canvas hides the OS cursor (CursorIcon::None, which winit turns into
ShowCursor(FALSE) on Windows) and draws its own crosshair, so Windows
Magnifier's "follow the mouse pointer", screen readers, recorders and
remote desktops lose the pointer there. View > Accessibility > Show System
Cursor (ui.toggle.systemcursor, ui.set {"systemCursor": true}, or
CADCRAFT_SYSTEM_CURSOR=1 at startup) keeps a system crosshair visible at
the centre of the drawn one.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:38:13 -04:00
Valentin Klein d4e1345974
macOS: the window title lines up with the traffic lights (fixes #161) (#202)
AppKit draws the traffic lights centred in its standard 28 pt title-bar
band, and the integrated title row is that band, but the title text sat
1 pt below the centre line. It is now centred on the lights. Neither
winit nor eframe can move the lights without AppKit calls (unsafe), so
the row fits them instead, as in WordCraft (storytold/wordcraft#255).

The geometry lives in chrome.rs. A long drawing name in a narrow window
no longer slides under the lights: the title starts after them, with the
same gap the lights keep from the top of the window.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:38:04 -04:00
Valentin Klein 6d7502abf9
UNDO asks for the number of operations; U undoes one (fixes #241) (#242)
UNDO now prompts "Enter the number of operations to undo or
[Auto/Control/BEgin/End/Mark/Back] <1>" and undoes that many; Enter undoes
one. Its other options say they are not available yet (Auto and Control
still take their value), so nothing that follows is run as a command.
The JSON form (`undo {count?}`) is unchanged.

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

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

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

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

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

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

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

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:37:26 -04:00
Valentin Klein 6b43587279
DXF: keep linetype text, multileader styles, page setups, views, UCSs and groups (fixes #209) (#216)
Saving to DXF and reopening lost several table and object-section items.
They are now written and read as the DXF Reference describes:

- LTYPE: embedded text and shape elements (74 type flags, 75 shape number,
  340 STYLE, 46 scale, 50 rotation, 44/45 offset, 9 text). Shapes from a
  shape file get an unnamed shape STYLE record (70 bit 1). DashElement
  gains an `absolute` flag for absolute rotations (74 bit 1). The built-in
  FENCELINE1 names its text style (Standard).
- MLEADERSTYLE objects in ACAD_MLEADERSTYLE with arrow size, text height,
  landing gap, dogleg length and text style, plus $CMLEADERSTYLE, so the
  current multileader style survives reopening.
- LAYOUT plot settings: device (2), plot style table (7), plot area (74),
  custom scale (142/143, 147), scale to fit (70 bit 16 with 75 = 0),
  centred plot and lineweights (70 bits 4 and 128). The layout's saved
  paper-space view goes in CADCraft xdata on the LAYOUT object.
- VIEW and UCS table records, and GROUP objects in ACAD_GROUP (members
  that were written to the file). A named view's layer state is CADCraft
  xdata on the VIEW record.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:37:20 -04:00
Valentin Klein ec80db936e
DXF: keep thickness, point angle, hatch origin/background/elevation, xref paths and the built-in font name (#213) (#215)
Saving to DXF and reopening lost several entity, block and style
properties:

- Thickness (group 39) is written for LINE, POINT, CIRCLE, ARC, TEXT,
  SOLID, TRACE, LWPOLYLINE and POLYLINE when non-zero.
- POINT writes its X-axis angle (group 50).
- HATCH: the origin goes into the pattern lines' base points (43/44), so
  other programs see the shift, and travels with the background colour as
  CADCRAFT xdata (1000 HATCH, 1011 origin, 1000 colour name); the DXF
  Reference has no groups for either. It is appended to the gradient
  xdata when there is one. The reader takes the elevation from the z of
  the first group 10.
- Xref blocks get BLOCK 70 bit 4 and their path in group 1, and the
  reader reads the path.
- The built-in "CADCraft Stroke" font is still written as "txt" for other
  readers; its name travels as CADCRAFT xdata (1000 FONT) and is used
  while group 3 is still "txt".
- Polyface (70 bit 64) and polygon mesh (bit 16) POLYLINEs from other
  programs are read as 3D faces instead of a 3D polyline through their
  vertices; face records no longer become vertices at the origin. Bad
  indices and counts are skipped.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:37:14 -04:00
Valentin Klein b4985c5d56
DXF: keep block descriptions and constant attribute definitions (fixes #204) (#207)
The ATTDEF writer put only the invisible bit into group 70, so constant
attribute definitions came back as variable ones: INSERT then created an
attribute for them, and other programs stopped drawing their text. The
constant bit (2) is now written too.

BLOCK's description was never written (BLOCK group 4) nor read, and the
insertion units (BLOCK_RECORD 70) and explodability (280) of blocks were
neither read nor written. All three now round-trip.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:37:07 -04:00
Valentin Klein b265cb1772
DXF: keep saved layer states through save and reopen (fixes #201) (#203)
LAYERSTATE.SAVE stored its states in Drawing::layer_states, but the DXF
writer never wrote them and the reader never read any, so every saved
layer state was gone after saving and reopening (DWG too, as it is
written from the same DXF).

They are now written like the parametric constraints: as JSON in a
CADCRAFT_LAYERSTATES XRECORD of the named object dictionary, read back
with the same payload and size limits. The XRECORD chunking and reading
are shared by both records.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:37:01 -04:00
MarekSpodBiedry e5a91f2eee
Keep viewport layer colour overrides through DXF save and reopen (#99) (#197) 2026-10-10 14:36:53 -04:00
MarekSpodBiedry 932ba8cc58
Respect locked layers in Clear, Cut, Stretch, Trim, Extend, Fillet, Explode and SplineEdit (#87) (#196) 2026-10-10 14:36:48 -04:00
MarekSpodBiedry 6957ef8756
Keep the active drawing when closing an earlier inactive one (#76) (#192) 2026-10-10 14:36:37 -04:00
MarekSpodBiedry da4aa413c6
Reject two-line angular constraints over 180 degrees that can never be satisfied (#73) (#190) 2026-10-10 14:36:27 -04:00
MarekSpodBiedry 5a214a0cac
Reject layer.set atomically: a refused call no longer applies other changes (#71) (#188) 2026-10-10 14:36:18 -04:00
Nigel Ihza Firdausy 2e3dbe029f
fix(geom): preserve three-point circle precision at large coordinates (#210) 2026-10-10 14:36:05 -04:00
Dmytro 659d0fa125
i18n: add Ukrainian interface translation (#36)
Rebase onto main: the interface language is saved with main's other
preferences (cadcraft.prefs, beside the theme) instead of its own storage
key, on desktop and web; strings main added or changed in the touched
places (Dynamic Input settings, Properties header and current-property
rows, block picker, theme and command-history menu items, open/save
filters) are wrapped and translated; main's native-menu poll order is kept.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:48:44 -04:00
LloydNicholson 1a4a477f91
Ask to save unsaved changes before closing the window (#34) (#47)
Closing the window (title bar, File > Exit, Cmd+Q) with unsaved drawings now
cancels the close and asks Save / Don't Save / Cancel for each one, through
the same in-app prompt as File > Close All (#172). The app quits once every
drawing has been saved or discarded; Cancel keeps it open. The control
channel's app.quit never asks.

Rebuilt on top of #172 from LloydNicholson's original PR.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:23:37 -04:00
by Truitt 3e0adadf94
Dynamic Input: pointer input boxes with Tab, comma and relative entry (#60)
* Dynamic Input: pointer input boxes, Tab separator, relative entry

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

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

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

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

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

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

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

* Dynamic Input tooltip: border colour from theme tokens

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

---------

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

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:16:16 -04:00
MarekSpodBiedry d648e4d1f1
Show Ctrl/Shift shortcuts on Windows and Linux, symbols only on macOS (#53) (#54) 2026-10-10 13:16:01 -04:00
Books 5c2a18638e
DIMANGULAR: accept a circle (#20)
* DIMANGULAR: accept a circle

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

* cargo fmt

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

---------

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

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

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

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

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

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

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


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:15:20 -04:00
MarekSpodBiedry 1dfa3660a7
Write Wipeout entities to DXF so saving keeps them (#79) (#147)
* Write Wipeout entities to DXF so saving keeps them (#79)

* Wipeout: clip vertices are in image pixel space (origin top-left, y down)

Write and read the boundary with y flipped, so other programs show the same
shape and wipeouts from other programs are no longer mirrored vertically.
Tested with an asymmetric boundary.

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

* Wipeout: collapse nested if (clippy)

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:15:07 -04:00
Valentin Klein f0d6f248f7
DXF/DWG: keep hatch gradients through save and reopen (fixes #80) (#162)
* DXF/DWG: keep hatch gradients through save and reopen (fixes #80)

The HATCH writer never emitted a gradient and the reader always set
`gradient: None`, so a GRADIENT fill reopened as a plain SOLID hatch after
any DXF or DWG save.

- Writer: native HATCH groups per the DXF Reference (450 gradient flag,
  451, 452 two-colour, 453 colour count, 460 angle in radians, 461 shift
  (0 = centered), 462 tint, 463 per-colour records with 63 ACI and 421 true
  colour, 470 name), plus a `CADCRAFT` `GRADIENT` xdata copy (name cut to
  255 bytes, angle, centered, colour names).
- Reader: native groups win when present (another program may have edited
  them); otherwise the xdata copy is used. The xdata is what survives DWG
  saves: the file is R2000 (AC1015), whose DWG HATCH has no gradient fields,
  so the DWG bridge drops the native groups.
- Hostile values (NaN angles, out-of-range colours, extra colour records,
  missing groups) fall back to the GRADIENT command's defaults.

Tests: gradient_hatch_roundtrips_through_dxf,
gradient_hatch_roundtrips_through_dwg,
gradient_falls_back_to_xdata_and_native_groups_win,
long_gradient_name_is_cut_only_in_xdata, hostile_gradient_groups_never_panic.

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

* contributors: add vangel76

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

* Gradient test: expect \U+ escapes now that non-ASCII text is escaped (#169)

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

---------

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

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

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

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

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

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

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

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

* SCALE: Reference option (#17)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Add Tru10 to contributors/people.toml

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

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

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

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

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

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

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

Fixes #83. Fixes #104.

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

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

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

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

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

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

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

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

---------

Co-authored-by: SBaksa <sabaksa11@gmail.com>
Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
Co-authored-by: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com>
Co-authored-by: lloyd <34131158+LloydNicholson@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Tru10 <133776756+Tru10@users.noreply.github.com>
Co-authored-by: Gert Pellin <gert@pellin.be>
2026-10-10 12:58:26 -04:00
MarekSpodBiedry 3886add967
ui.render: fit the drawing and return pngBase64 like the headless render (#49, #50) (#52) 2026-10-10 12:57:53 -04:00
LloydNicholson 5cbc4f078e
Layer Properties Manager: stop the window growing every frame (fixes #29) (#46)
The vertical separator between the filter tree and the table takes all the
height it is offered, and a resizable window offers its previous height, so
the window grew 30px per frame until it filled the screen. Cap the row at the
table's fixed height. Regression test runs the dialog headless for 30 frames
and checks the height stays put.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:57:48 -04:00
MarekSpodBiedry 415da86d39
Scripts: split a line at keyword prompts so POLYGON 6 200,20 I 15 works (#43) (#45) 2026-10-10 12:57:42 -04:00
MarekSpodBiedry 8995fb9174
Blocks window: click a block to insert it (#8) (#28)
Each block name in the Blocks window is now a button. Clicking it starts INSERT with that block already chosen, so the command line asks for the insertion point.
2026-10-10 12:57:36 -04:00
MarekSpodBiedry a1df2f2c8c
SCALE: Reference option (#17) (#27)
SCALE > Reference printed "not available yet". It now asks for the reference length (typed, or two points) and the new length (typed, or a point measured from the base point), and scales by new / reference. Zero, negative or non-finite lengths are rejected and asked again.
2026-10-10 12:57:31 -04:00
MarekSpodBiedry 986307f764
Print and Ctrl+P ask where to save the PDF (#5) (#26)
PLOT/EXPORTPDF started from the menu, the toolbar, Ctrl+P or the command line had no path,
so the PDF was built in memory and dropped. They now open the save dialog (PDF filter first)
and write the file there. JSON calls from scripts, the control channel and MCP still never
open a dialog. Ctrl+P was shown in the menu but not bound; it is now.
2026-10-10 12:57:25 -04:00
Books 093c87d08a
DIMANGULAR: accept polyline segments (fixes #10) (#15)
Picking a side of a rectangle or a PLINE segment was rejected with
"Select a line or an arc." because only LINE and ARC entities were
accepted. The picked object is now resolved to the segment under the
cursor: straight polyline segments work like lines and arc segments
like arcs.
2026-10-10 12:57:13 -04:00
Books 698f2c8c36
Redraw edits made while a command is still running (fixes #4) (#22)
LINE and PLINE add geometry on every point, but the canvas only rebuilt
its display list when the drawing revision or the document pointer
changed. The revision was bumped when a command finished, and the
pointer changed only on the first copy-on-write edit, so only the first
segment appeared until the command ended. Session::feed now bumps the
revision after each input once the running command has changed the
drawing.
2026-10-10 12:57:07 -04:00
by Truitt f3c1e121d2
macOS: the delete key erases the selection, guarded against typing (#64)
The key labelled "delete" on a Mac sends Backspace, and only forward Delete
(fn+delete) erased the selection, so the obvious key did nothing.

On macOS (native, and the web build via egui's user-agent detection),
Backspace now erases selected objects, but only when it can't be meant for
the command line:
- a fresh press, never an auto-repeat, so holding delete to clear typing
  can't run on into the drawing;
- a plain press: Option/Cmd+delete delete a word or line of text on macOS;
- no command running and the command line empty;
- nothing typed at the command line since the selection was made (typing,
  then clearing it, leaves delete editing text until the selection changes).

Windows and Linux are unchanged: Backspace only edits the command line
there. The decision is a small pure function with unit tests.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:26:52 -04:00
Books 348d3b6724
Extents: a wide polyline's bounds follow its drawn band (fixes #90) (#127)
Polyline extents were the centre line padded by const_width / 2, so
per-segment start/end widths never counted: a band built from vertex
widths, a taper, or segments of different widths reported the
zero-height bounds of the centre line. The bounds now enclose the
filled band of every segment as it is drawn: the exact corners of
straight segments (tapers included), and both edges of arc segments,
sampled along the arc with the width interpolated and widened by the
sampling tolerance so they are never too small. The constant width
still wins over vertex widths, the end vertex of an open polyline
starts no segment, and non-finite or negative widths count as zero.
Thin polylines take the same path as before.

Fixes #90.
2026-10-10 12:26:30 -04:00
Books 3c577968f6
ARC: refuse zero-radius arcs instead of adding them (fixes #85) (#123)
The `arc` command checked its three-point form for collinear points but
added the arcs built by its start/center/end and center/radius forms
unchecked, so radius 0 (or a start point on the center) added a
degenerate Arc, dirtied the drawing and recorded an undo step. Every
form now goes through the existing finite_arc check and fails with the
same "these values do not define an arc" error the arc.* variants and
the interactive ARC already use, leaving the drawing unchanged.

Fixes #85.
2026-10-10 12:26:20 -04:00
MarekSpodBiedry 882c4d67a6
Keep dimension text rotation when saving DXF (#78) (#153) 2026-10-10 12:26:11 -04:00
MarekSpodBiedry 3e0de7e4eb
Keep a free measured origin in place when its source moves (#109) (#152) 2026-10-10 12:26:01 -04:00
kaffeochknivar 8fe9baddb5
Menu commands open their dialogs; JSON calls never open file pickers (fixes #65, fixes #101) (#125)
* Render tests: drop the always-true assert in degenerate_input

`assert!(... || true)` trips clippy's overly_complex_bool_expr on current
stable (1.96), which fails `cargo xtask ci` at the clippy step. The check
only proves that NaN input does not panic, so call the function and
discard the result instead.

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

* Menu commands open their dialogs; JSON calls never open pickers (#65, #101)

Tools > Drafting Settings... (DSETTINGS, DS, SE) ran the JSON form with no
parameters, which changed nothing, so the menu item looked dead; only the
status-bar gear opened the dialog. Typed or menu-invoked DSETTINGS now
opens the Drafting Settings dialog, and PROPERTIES (PR, CH) shows the
Properties palette.

The engine's OPEN, SAVEAS and QSAVE opened a native file picker whenever
their parameters had no path, so `engine.execute` and the bridged MCP
`execute` tool could block on a desktop dialog. They now ask for a file
only when typed or chosen from a menu; JSON calls get the command's own
missing-path error. The ui.open / ui.saveas commands (Cmd+O,
Cmd+Shift+S) still open the pickers.

Typing the `cmd {json}` form at the command line (e.g. `layer {}`) no
longer opens the command's dialog or picker; the engine runs it with
those parameters.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:16:48 -04:00
Valentin Klein 1fdee1a1a4
Ask Save / Don't Save / Cancel before closing a dirty drawing (#124) (#172)
File > Close, a file tab's x and File > Close All ran the engine's
close/closeall directly, which drop the drawing. Nothing checked for
unsaved changes on any platform; the web report just made it visible.

- New ui-egui `closing` module: close requests from the UI go through a
  queue of drawings (by DocState uid). A dirty drawing is shown and asked
  about in an in-app egui modal ("Save changes to X?" Save / Don't Save /
  Cancel, Escape = Cancel), the same on desktop and web.
- Save routes to the existing qsave path (picker for untitled drawings)
  and closes only if the drawing is really clean afterwards; otherwise it
  stays open with its edits and a status message.
- Close All asks per dirty drawing in tab order; Cancel stops it.
- Menu, typed CLOSE/CLOSEALL (null params) and the tab x use the prompt;
  JSON calls (engine.execute, control, MCP, scripts) keep discarding
  without asking.
- Shortcuts are paused while the prompt is open; ui.inspect reports
  `closePrompt`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:16:41 -04:00
Valentin Klein 3dd97cad56
Linux: present without a vsync queue so the crosshair follows the mouse (#25) (#183)
* Linux: present without a vsync queue so the crosshair follows the mouse (#25)

The canvas hides the OS pointer and draws its own crosshair, so every frame
the swapchain holds back shows as cursor lag. Under vsync (FIFO) wgpu asks for
one queued frame, but Vulkan clamps it to the driver's minimum swapchain size:
three images on X11 (Linux Mint's Cinnamon session), i.e. two frames queued
behind the one on screen. Windows DX12 honours one. The canvas also requested
a repaint on every frame while hovered, which kept that queue full.

- gpu::surface_config: AutoNoVsync (Immediate, then Mailbox, then FIFO; valid
  on every surface) on Linux/BSD, frame latency 1. CADCRAFT_VSYNC=1 restores
  vsync, CADCRAFT_VSYNC=0 selects low latency elsewhere.
- gpu::FrameCap (240 fps) when presenting without vsync: nothing else paces
  frames on X11, and a 1000 Hz input stream rendered ~900 frames per second.
- No continuous repaint while hovering the canvas; input repaints by itself.
  The status-bar message schedules its own repaint to clear on time.
- Hover/selection highlight looks up primitives in a per-list sorted index
  instead of scanning the whole display list every frame (~2.5 ms per frame
  on a 270k-entity drawing).

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

* Move frame-cap and status-repaint hunks away from other PRs' to avoid merge conflicts

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:16:34 -04:00
Valentin Klein a349e4925f
Command line, ViewCube and UCS icon take the pointer; history window (F2) (#181)
Clicks on the command line's history, the UCS icon and the ViewCube ring fell
through to the drawing (picking objects or starting a selection window, or
entering a point during a command), the chevron did nothing, and the wheel over
the overlays zoomed the drawing. The ViewCube's Top face was a click-only
widget, so the canvas still counted as hovered underneath it (crosshair, wheel).

- Every overlay now registers a widget that senses clicks and drags, so egui
  hands the pointer to the overlay instead of the canvas.
- Command line: a click on the history lines or the chevron (now pointing up)
  opens a scrollable history window with the whole log; F2 and TEXTSCR open it
  too (`ui.toggle.history`, Window > Command History). Dragging the history's
  top edge sets how many lines show (`ui.cmdline.lines`, 0-12).
- ViewCube (new viewcube.rs): Top face and a Home button run `zoom.extents`;
  hover highlights; right-click menu (Home, Hide ViewCube). The compass ring
  says that the 2D plan view can't rotate (no view twist yet) instead of
  doing nothing.
- UCS icon and the WCS pill open the coordinate-system menu (World, Hide UCS
  Icon).
- Control channel: `ui.drag` (real press, move, release) and
  `cmdline.state` -> `historyExpanded`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:16:27 -04:00
Valentin Klein 24a3954b39
Light and System interface themes (System follows the OS appearance live) (#179)
* Light and System interface themes (System follows the OS appearance live)

CADCraft had one hard-coded dark interface: Tokens::get() always returned
Tokens::DARK and Visuals::dark() was installed once at startup.

- theme.rs: Tokens::LIGHT (light grey chrome, dark text) beside the unchanged
  Tokens::DARK; Tokens::get() returns the palette of the theme in use (a
  UI-thread thread-local set once per frame). Both egui styles are built from
  the tokens and egui is switched to the shown theme, so standard widgets,
  menus and popups follow too. The drawing area (canvas, grid, axes,
  selection, grips, ViewCube, UCS icon) keeps its model-space colours in both
  themes: drawing colours are not part of the interface theme.
- ThemePref { System, Light, Dark } in UiState (`theme`), resolved every
  frame against ctx.system_theme(), so System follows the OS (winit, or the
  browser's prefers-color-scheme) live without touching the saved choice.
  No reported appearance: System shows Dark (SYSTEM_FALLBACK). Default stays
  Dark (today's look).
- View > Interface Theme > Use System Setting / Light / Dark (ui.theme.system,
  ui.theme.light, ui.theme.dark) and ui.theme {theme} for scripts and the
  control channel; ui.inspect reports the theme shown.
- Hard-coded colours on the command line (history, border, keywords,
  AutoComplete list), the status-bar message and the layer status icon go
  through tokens; overlays drawn on the canvas use a new canvas_ink token.
- Desktop app: the choice persists via eframe storage (`persistence`
  feature, key cadcraft.prefs); window geometry and egui memory are not
  persisted, as before.

Fixes #59.

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

* Document the theme field away from other PRs' edits to avoid merge conflicts

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:16:20 -04:00
Valentin Klein 14cffbcbe6
MCP plate test: finish TEXT before undo and check the centre line survives (#171)
The scripted TEXT ended with a single newline, so TEXT was still asking for
the next line when the test ran undo. Undo then reverted to before the centre
line (taking the uncommitted text with it), and the only post-undo check,
"no Text", passed anyway.

The script now ends TEXT with a blank line (Enter), as an AutoCAD script
would, and the test asserts no command is running afterwards. After undo it
checks that exactly the text was undone and that the four circles, the
outline and the Center-layer line are still there.

Fixes #51.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:16:05 -04:00
Valentin Klein 396b098d9a
CLI: image exports honour the current view and take --window/--view (fixes #165) (#170)
`convert` and `run --export` always fitted the model-space extents, so
`zoom`, `pan` or `view.set` in a run had no effect on the image and there
was no way to export a region.

- io: `write_framed(d, name, window)` frames PNG, SVG and PDF on a
  model-space rectangle (fitted without margin, centred, no distortion);
  `write` is unchanged and still fits the extents. `check_window` rejects
  non-finite, inverted, zero-size and out-of-range (±1e12) windows.
- CLI: `run --export` shows the current view when the commands changed
  it, otherwise the extents as before. `--window x0,y0,x1,y1` and
  `--view extents|current` on `convert` and `run` choose the framing
  explicitly. The usage text and README say how images are framed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:15:58 -04:00
Valentin Klein 6c145ed733
Menu items open their missing dialogs; COUNT shows its result; New Layout asks for a name (#126, part 1) (#180)
* Menu items open their missing dialogs; COUNT shows its result; New Layout asks for a name (#126)

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

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

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

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

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

---------

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:15:09 -04:00
Valentin Klein 61875b1f0c
DWG open: stop runaway conversions early and name the limit (#57) (#175)
* DWG open: stop runaway conversions early, name the limit (#57)

"file too large" was the DXF reader's 50,000,000 group-code cap (MAX_TAGS),
not a file-size limit. For a DWG it was only checked at the very end: after
acadrust had read the whole file, written all of its DXF text into one Vec,
the text had been copied again for decoding, and 50 million tags had been
parsed. A real 10 MB DWG is nowhere near the cap (a synthetic 10 MB AC1024
file is 3.7 million group codes and opens in about 2 s). A damaged file, or
objects the DWG reader misreads, can be: acadrust caps every count at 100,000
and reads zeros past the end of the data, so one desynchronised object becomes
a polyline with 100,000 vertices. A fuzzed 0.25 MB AC1024 file made it write
11 GB of DXF.

- cadcraft-dwg: Limits { max_dwg_bytes: 512 MB, max_dxf_bytes: 1 GB }. The DWG
  size is checked before the reader runs; the DXF is streamed into a writer
  that fails as soon as it passes the limit, so the conversion stops there
  instead of filling memory. Both errors give actual and allowed sizes. The
  input is no longer copied, and the DXF writer also runs under catch_unwind.
- cadcraft-dxf: TooLarge { tags, limit } says "drawing too large: N DXF group
  codes, more than the 50000000 CADCraft reads". For ASCII the count comes
  from the line count before any decoding or tag allocation. Valid UTF-8 text
  is borrowed instead of copied. MAX_TAGS is public and unchanged.
- cadcraft-io: errors from the DXF converted from a DWG say so.

On the fuzzed file: before, 76 s and 13.4 GB, then the process aborted copying
the 11 GB text (under a 16 GB memory limit). After, 15 s and 5.3 GB, with
"DWG: converting this 0.2 MB file produced more than 1024 MB of drawing data
(the limit); ...". What's left is acadrust's reader itself (8 s, 5.3 GB on that
file), which this bridge can't bound without patching the crate.

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

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

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

---------

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

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

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

Regression test: programmatic_undo_during_a_command_ends_it_first.

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

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:14:53 -04:00
Valentin Klein bacb951059
DXF: write non-ASCII text as \U+ escapes so R2000 files read correctly elsewhere (fixes #164) (#169)
* DXF: write non-ASCII text as \U+ escapes so AC1015 files stay valid (#164)

The writer declares $ACADVER AC1015 (R2000) and $DWGCODEPAGE ANSI_1252, where
text is in the code page, but wrote strings as raw UTF-8. Every other reader
decoded those bytes as CP1252 and turned non-ASCII text into mojibake; only
CADCraft (which tries UTF-8 first) read its own files back correctly.

- cadcraft_dxf::write_ascii now writes pure ASCII: every non-ASCII character
  in a string group becomes the pre-R2007 \U+XXXX escape (UTF-16 code units,
  so characters above U+FFFF are a surrogate pair of escapes). This covers
  TEXT, MTEXT, ATTRIB/ATTDEF, table, block and layer names, xdata and
  XRECORD strings alike. The header and the R2000 structure are unchanged,
  so the DWG bridge (acadrust, which decodes the escapes) behaves as before.
- The reader combines surrogate-pair escapes, requires four hex digits, keeps
  malformed escapes and unpaired surrogates as literal text, and now also
  unescapes strings in binary DXF.
- Tests: R2000 save/reopen of CJK, Latin-1 and astral text in TEXT, MTEXT, a
  layer name, a block name and ATTRIB/ATTDEF; pure-ASCII output with the
  AC1015/ANSI_1252 header; UTF-8 (AC1032) and code-page (AC1015) input still
  read; DWG round trip; hostile escapes.

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

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

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

---------

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:14:37 -04:00
kaffeochknivar 0cdde077ef
DXF: keep layer transparency and description through save and reopen (fixes #96, fixes #97) (#131)
* Render tests: drop the always-true assert in degenerate_input

`assert!(... || true)` trips clippy's overly_complex_bool_expr on current
stable (1.96), which fails `cargo xtask ci` at the clippy step. The check
only proves that NaN input does not panic, so call the function and
discard the result instead.

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

* DXF: keep layer transparency and description through save and reopen (#96, #97)

The LAYER writer omitted both properties and the reader left them at
their defaults, so a drawing reopened with every layer opaque and every
description empty. Both are xdata on the LAYER record, as AutoCAD writes
them:

- transparency: AcCmTransparency, 1071 = 0x02000000 | alpha, with
  alpha = (100 - percent) * 255 / 100 (40 % is alpha 153); values that
  aren't "by alpha" read as opaque
- description: AcAecLayerStandard, two 1000 strings (the layer
  standard's name, unused, then the description), cut to the 255 bytes
  a 1000 group may hold

Both applications are registered in the APPID table.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:14:29 -04:00
MarekSpodBiedry b7f83a6687
Keep viewport frozen-layer lists through DXF save and reopen (#98) (#150) 2026-10-10 12:14:21 -04:00
MarekSpodBiedry 576e6f9020
Write LWPOLYLINE elevation (group 38) to DXF (#77) (#142) 2026-10-10 12:14:13 -04:00
MarekSpodBiedry 56c39f1c17
Keep ATTDEF prompt when saving DXF (#81) (#137) 2026-10-10 12:14:06 -04:00
MarekSpodBiedry df1c074fff
Keep an ordinate dimension's datum fixed when its feature moves (#105) (#157) 2026-10-10 12:14:00 -04:00
MarekSpodBiedry 947535158a
Store canonical autoTypes names so accepted spellings take effect (#95) (#156) 2026-10-10 12:13:53 -04:00
MarekSpodBiedry 0af1528f4c
AREA of a normal hatch no longer counts unfilled islands (#91) (#155) 2026-10-10 12:13:47 -04:00
MarekSpodBiedry 9882b85a41
Reject nonpositive MTEXT height on creation (#84) (#154) 2026-10-10 12:13:39 -04:00
MarekSpodBiedry bd46065c27
HATCHEDIT: clear the gradient when a named pattern is chosen (#107) (#151) 2026-10-10 12:13:31 -04:00
MarekSpodBiedry 52f05f1514
Fence selection now includes crossed XLINE and RAY objects (#92) (#149) 2026-10-10 12:13:24 -04:00
MarekSpodBiedry 74cbdf7e2f
Keep selected objects outside the joined chain when using JOIN (#86) (#148) 2026-10-10 12:13:16 -04:00
MarekSpodBiedry a983cf1ed0
MCP bridge: do not resend a mutation whose reply was lost (#102) (#146) 2026-10-10 12:13:06 -04:00
MarekSpodBiedry 24f074a9ad
Reject deleting a dimension that a remaining dimension refers to (#94) (#145) 2026-10-10 12:12:58 -04:00
MarekSpodBiedry fabc5bacd9
Fix justified TEXT extents and picking (#89) (#144) 2026-10-10 12:12:50 -04:00
MarekSpodBiedry 279c1bed6a
Convert typed ellipse arc angles to ellipse parameters (#82) (#143) 2026-10-10 12:12:41 -04:00
MarekSpodBiedry 3ef52e31d8
Apply DIMOVERRIDE text together with dimension variables (#108) (#141) 2026-10-10 12:12:33 -04:00
MarekSpodBiedry 50b9a23a98
Reject out-of-domain values in PARAMETERS instead of dropping the constraint (#93) (#139) 2026-10-10 12:12:23 -04:00
MarekSpodBiedry fadbea5dfe
Keep elevations when EXPLODE splits a 3D polyline (#88) (#138) 2026-10-10 12:12:16 -04:00
kaffeochknivar da8894dd08
Render tests: drop the always-true assert in degenerate_input (#37)
`assert!(... || true)` trips clippy's overly_complex_bool_expr on current
stable (1.96), which fails `cargo xtask ci` at the clippy step. The check
only proves that NaN input does not panic, so call the function and
discard the result instead.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:12:08 -04:00
Brandon Thomas ef1b55dca6
About: contributor and model credits compiled in (grab bag + table, self-submitted names) (#3)
* About: contributors compiled in from contributors.json (grab bag + table, sortable, name toggle)

Replaces the TSV files and `cargo xtask contributors` with the craftrules standard
(standards/contributors.md): contributors/contributors.json is generated by
craftrules/scripts/contributors.py and baked into the binary by build.rs as static tables.
Each contributor line: @username, opt-in display/real name, PRs, commits, lines +/−/Δ,
binary assets +/−, first/last commit. Sortable by every column; alphabetical ignores
case and the leading @.

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

* Contributors docs: craftrules is two levels up (../../craftrules)

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

* Contributors: self-submitted names in contributors/people.toml, read by build.rs

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

* Contributors: regenerate (people_entry verified)

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

* AGENTS.md: contributor credits section (agents offer to credit their own human only); docs

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:16:30 -04:00
Brandon Thomas 1c86787459 Merge branch 'worktree-agent-a2f28e507557a5b11' 2026-10-06 02:12:34 -04:00
Brandon Thomas a599137356 README screenshots, alpha/parity assessment in ROADMAP, MLEADER written to DXF as LEADER + MTEXT
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 02:12:34 -04:00
Brandon Thomas 9646f20b73 Layer Properties Manager, QSELECT + Quick Properties, constraint display, Parameters Manager
- Layer Properties Manager: filter tree (All / All Used Layers) with search, sortable table
  (Status, Name, On, Freeze, Lock, Plot, Color, Linetype, Lineweight, Transparency,
  New VP Freeze, Description; VP Freeze / VP Color inside an MSPACE viewport), inline
  rename and description, ACI + true-colour picker, linetype picker with Load from the
  library, lineweight picker, layer states save/restore/delete.
- Engine: qselect rewritten ({type, property, operator =,!=,>,<,*, value, mode new|append|
  exclude, applyTo}) + qselect.info; layerstate.list/delete/rename; layer list reports
  used layers and states; Layer.vp_freeze_new (DXF flag 2, applied by MVIEW) and
  layer.set newVpFreeze; per-viewport layer colours (Viewport.layer_colors, vplayer
  colors, honoured by the renderer); vplayer defaults to the active viewport; QPMODE
  setting/sysvar/toggle; properties.set length/angle for lines.
- Canvas: constraint bars with code-drawn glyphs and coincident markers, dynamic
  dimensional constraints (aligned/horizontal/vertical/radius/diameter/angular) with
  name=value labels and lock icon, honouring CONSTRAINTBAR/DCDISPLAY; cached per
  drawing revision; double-click a label to edit its expression (parameters).
- Quick Properties palette near the selection (status-bar toggle), Quick Select dialog,
  Parameters Manager (edit, add user parameter, delete).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 02:11:43 -04:00
Brandon Thomas 12d5f5d967 Merge branch 'worktree-agent-a8983b474d3226b17' 2026-10-06 02:06:13 -04:00
Brandon Thomas d2ce7e6fef DXF round trip: DIMSTYLE, DSTYLE overrides, DIMASSOC, constraints, tables, text style flags
- DIMSTYLE: every DimStyle field with its standard group code (DIMBLK/1/2 as
  BLOCK_RECORD handles 342/343/344 with DIMSAH, DIMTXSTY 340, DIMDSEP, DIMCLRx, ...);
  arrowhead blocks generated from our own geometry when no user block exists, and
  not re-imported as user blocks on read.
- Per-dimension overrides as ACAD "DSTYLE" xdata (1070 code + typed value).
- Associativity: standard ACAD_DIMASSOC extension dictionary + DIMASSOC objects
  (AcDbOsnapPointRef) for linear/aligned/ordinate extension origins, plus exact
  links in CADCRAFT xdata; reader infers links from DIMASSOC for foreign files.
- Constraints/parameters: JSON in an XRECORD under the named-object-dictionary
  key CADCRAFT_CONSTRAINTS (250-char chunks, backslashes escaped so no \U+).
- ACAD_TABLE write/read (sizes, cell text, merges) with *T display block,
  TABLESTYLE objects and CADCRAFT xdata for title/header/text height/style.
- STYLE: vertical (70), backwards/upside-down (71), AcadAnnotative xdata.
- Reader: owner 330 taken outside 102 groups; hostile-input caps and tests.
- Validated: ezdxf strict readfile + audit (0 errors/0 fixes) and resave;
  DXF->DWG->DXF via acadrust keeps all of it except TABLESTYLE objects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 02:05:03 -04:00
Brandon Thomas 2287134dbd Incremental R-tree: patch only changed store chunks after an edit
A pick after one edit at 200k entities drops from ~110 ms (full rebuild) to ~4 ms;
steady-state picks stay at ~1 us. Adds a parity test against fresh builds and a
perf row for the incremental path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 01:36:17 -04:00
Brandon Thomas f9aa1b279a M4 annotation depth + M9 parametric constraints
Annotation (M4):
- TrueType text from system fonts (filled/outlined), shared placement for all text
- MTEXT formatting codes: fonts, heights, colours, under/overline, stacks, wrapping
- Full DIMSTYLE variables, per-dimension overrides, all arrowheads and ticks,
  unit formats, tolerances, limits, alternate units, colours by role
- Associative dimensions that follow geometry; DIMREASSOCIATE/DIMDISASSOCIATE
- STYLE/DIMSTYLE management commands, DIMTEDIT, DIMSPACE, TABLE create/edit/merge

Parametric (M9):
- New cadcraft-constraints crate: weighted damped Gauss-Newton solver, conflict
  and redundancy detection, expressions, AutoConstrain inference
- All GC*/DC* commands, GEOMCONSTRAINT/DIMCONSTRAINT, PARAMETERS, DELCONSTRAINT,
  constraint bar / display settings; re-solve after every edit

Also: MODEL/PAPER status bar toggle, cargo xtask alias, roadmap update
(233/491 menu items, 290 commands).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 01:27:38 -04:00
Brandon Thomas c35fa82373 MSPACE/PSPACE: work in model space through layout viewports
Double-click a viewport (or MS) to edit model space inside it; the screen view maps
consistently so picks, snaps, grips and previews work in model units, and zoom/pan
change the viewport's view (or the sheet when the viewport is locked). PS or a
double-click outside returns to paper space.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 01:23:57 -04:00
Brandon Thomas 8188c7ed0a UI: hot grips (click-to-edit, Space cycles stretch/move/rotate/scale/mirror), paper-space sheet display, new-layout buttons
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 01:02:56 -04:00
Brandon Thomas 3da0219ac9 M1/M2/M6/M10: draw/modify depth, grips API, layouts/viewports/PLOT to PDF, GPU canvas, R-tree index
- All 2D Draw-menu items: ARC variants, CIRCLE TTR/TTT, DIVIDE/MEASURE, REVCLOUD, WIPEOUT, 3DPOLY,
  MLINE, HELIX, SPLINE CV, CENTERMARK/CENTERLINE, XLINE and RECTANG options.
- PEDIT, SPLINEDIT, LENGTHEN, ALIGN, BLEND, REVERSE, NCOPY, CHSPACE, FLATTEN, ARRAYPATH, TEXTEDIT;
  FILLET with arcs and polylines; TRIM ellipses and splines; grip editing commands.
- Layouts, MVIEW/VPORTS, viewport scale/lock/layers, PAGESETUP, PLOT/EXPORTPDF with our own PDF
  writer; paper-space rendering through viewports.
- GPU canvas (wgpu batches, CPU fallback); rstar spatial index for pick/select/osnap;
  cadcraft-cli perf harness.
- CAL, FIND/replace, MASSPROP, MEASUREGEOM; TrueType outlines in cadcraft-fonts; contributors
  standard (xtask contributors, About window credits); docs/mcp.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:40:03 -04:00
Brandon Thomas 4c74c5591c Release pipeline: signed macOS universal, Windows x64/x86, Linux, FreeBSD and web builds; MTEXT editor
Workflows and packaging adapted from the craftrules playbook (release environment with the twelve
signing secrets, new WiX upgrade code, .dxf/.dwg file associations), app icon set (hicolor, icns,
ico), docs/releasing.md. MTEXT now opens a text editor window for its contents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:50:51 -04:00
Brandon Thomas 3e2155e587 M7: DWG read/write through the acadrust bridge (crates/dwg)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:43:22 -04:00
Brandon Thomas cb68bed225 M4/M5: dimension commands, multileader, hatch boundary detection, blocks
DIMLINEAR/ALIGNED/RADIUS/DIAMETER/ANGULAR/ARC/ORDINATE/CONTINUE/BASELINE, QDIM, MLEADER, LEADER;
HATCH/GRADIENT/BOUNDARY/REGION with pick-point boundary tracing (planar face walk with islands);
BLOCK/INSERT/ATTDEF/ATTEDIT/WBLOCK/PURGE/BATTMAN; dimension text gaps and horizontal text;
DXF header writer whitelist (correct group codes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:31:44 -04:00
Brandon Thomas d9ab00c53c M0: CADCraft skeleton and vertical slice
Workspace (geom, dxf, color, doc, fonts, render, io, engine, ui-egui, mcp; apps cadcraft,
cadcraft-cli, cadcraft-web; xtask), AutoCAD-for-Mac style UI with code-drawn icons, command line
with prompts/keywords/AutoComplete, draw and modify commands, object snaps, polar/ortho,
selection, undo, layers and properties palettes, DXF read/write, SVG/PNG export, MCP server,
JSON control channel, CLI, wasm build, README, ROADMAP, AGENTS.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:22:25 -04:00