723 Commits
Author SHA1 Message Date
Valentin KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 ThomasandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 TruittandClaude Opus 5.5 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
STE1200andClaude Fable 5.1 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 TruittandClaude Opus 5.5 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
STE1200andClaude Fable 5.1 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 ThomasandClaude Opus 5.5 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 ThomasandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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 KleinandClaude Opus 5.5 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