Commit Graph
413 Commits
Author SHA1 Message Date
aiongg b90d68ec34 BLOCKREPLACE: a dynamic block or a replacement that references the block is refused when its name is given, and the prompt asks again 2026-10-25 04:17:52 +00:00
aiongg c42763ba0f CUTCLIP and CUTBASE leave the clipboard alone when every object is on a locked layer, and say how many were locked 2026-10-25 04:12:57 +00:00
aiongg 642c3a59a3 COMPAREEXPORT: free block and style names come from a next-index counter per name base instead of a scan from 1 2026-10-25 04:04:14 +00:00
aiongg 53049d82e0 COMPAREEXPORT: keep #2049's *D copies of dimension blocks 2026-10-25 03:58:15 +00:00
aiongg e23a362c54 Merge storytold/cadcraft#2049 (commits after the batch) 2026-10-25 03:57:25 +00:00
aiongg 8171c9be17 DIMTFILL and DIMTFILLCLR are written in DIMSTYLE only when set; COMPAREEXPORT redraws dimensions instead of copying their blocks, and gaps.md notes its groups 2026-10-25 03:43:34 +00:00
dotCooCoo 4dbcbfdaaa COMPAREEXPORT: dimension block copies stay *D blocks, and image paths count in block reuse
A dimension's block took a recolored copy named COMPARE_<tag>_D1, and
the DXF writer redraws a dimension whose block is not a *D block, so
saving the snapshot replaced geometry read from a file. The copy now
gets a free *D name.

A named block both drawings define was reused when its images and
data links had the same relative paths, even when they were relative
to different folders. These paths are now taken from each drawing's
folder before the blocks are compared.
2026-10-24 19:37:44 -07:00
aiongg 429554b389 COMPAREEXPORT: tests move into the engine's it suite (the hook test joins tests/compare.rs); the copy drops constraint display exceptions for objects it doesn't hold 2026-10-25 02:35:48 +00:00
aiongg 1b0cefce9c Merge storytold/cadcraft#2049
# Conflicts:
#	docs/gaps.md
2026-10-25 02:23:03 +00:00
aiongg e1cab10d01 Merge storytold/cadcraft#2089
# Conflicts:
#	crates/engine/src/cmd/utility.rs
2026-10-25 02:22:50 +00:00
aiongg 05cad7db49 Merge storytold/cadcraft#2085
# Conflicts:
#	crates/engine/src/cmd/utility.rs
#	crates/engine/tests/it/inquiry_keywords.rs
2026-10-25 02:22:40 +00:00
aiongg 4e73fb5bb3 Merge storytold/cadcraft#2079
# Conflicts:
#	docs/parity-checklist.md
2026-10-25 02:22:16 +00:00
aiongg 84e3478863 Merge storytold/cadcraft#2071
# Conflicts:
#	docs/parity-checklist.md
2026-10-25 02:22:16 +00:00
aiongg fac2d18df6 Merge storytold/cadcraft#2065
# Conflicts:
#	docs/parity-checklist.md
2026-10-25 02:22:16 +00:00
aiongg 23a0bd4fdb Merge storytold/cadcraft#2054
# Conflicts:
#	docs/parity-checklist.md
2026-10-25 02:22:16 +00:00
aiongg 75c54ce35d Merge storytold/cadcraft#2056 2026-10-25 02:21:58 +00:00
aiongg 313fab8827 Merge storytold/cadcraft#2101 2026-10-25 02:21:58 +00:00
aiongg bda4a9924c Merge storytold/cadcraft#2106 2026-10-25 02:21:58 +00:00
dotCooCoo 20087b4b5f COMPAREEXPORT: multiline attributes take the comparison color too
A multiline attribute is drawn from its own multiline text, so an
inline color code in it overrode the comparison color set on the
attribute: an unchanged attribute could stay red, or a removed one
green. The export now takes the inline colors out of the multiline
text of a reference's attributes and of attribute definitions in the
recolored blocks, as it does for MTEXT and multileader text.
2026-10-24 19:16:04 -07:00
dotCooCoo 5685085add COMPAREEXPORT: a block whose objects are hidden or thicker in the other drawing is copied as PART_2
The comparison of a named block both drawings define counted each
object's geometry, layer, color, linetype, lineweight and linetype
scale. A block whose line was hidden, more transparent, thicker or
extruded another way in the other drawing counted as the same, so the
other drawing's references drew with this drawing's definition. These
properties now count as well.
2026-10-24 18:52:56 -07:00
dotCooCoo 32be8a655e COMPAREEXPORT: a dimension block with another generated name does not make PART differ
A named block such as PART was compared with the other drawing's by the
blocks it draws, including a dimension's block. That block's generated
name (`*D1` here, `*D3` there) was part of its signature, so the same
PART counted as different and was copied as PART_2 with a copy of the
dimension block. The comparison now leaves out an anonymous block's
name and compares its contents.
2026-10-24 18:43:16 -07:00
dotCooCoo 1271b06b02 Refuse negative values for whole-number DIM* variables
DimStyle::apply_fields rounded a value for a whole-number field such as
DIMDEC or DIMTMOVE and clamped it to 0..255, so DIMDEC -1 became 0 and
DIMALTD "300" became 255. DIMSTYLE, DIMOVERRIDE and the SETVAR overrides
that new dimensions take all accepted these values.

A value below 0 or one that rounds above 255 is now refused, as an
out-of-range value is. DIMADEC still takes -1. A lineweight given as
-1, -2 or -3 (ByLayer, ByBlock, Default) is no longer read as 0 when
the style's lineweight is positive, and a lineweight above 211 given as
text is refused instead of read as 211.
2026-10-24 16:34:50 -07:00
dotCooCoo 6dc336160d SETVAR refuses DIM* values the dimension style refuses
SETVAR wrote a DIM* variable to the drawing header without checking it,
so values such as DIMTMOVE 3, DIMATFIT 7 or DIMLWD 500 were accepted.
New dimensions then ignored them, because the style override goes
through DimStyle::apply_fields, which refuses them.

SETVAR now runs the value through apply_fields on a copy of the current
dimension style, as DIMSTYLE and DIMOVERRIDE do, and reports an invalid
value instead of writing it.
2026-10-24 15:22:26 -07:00
dotCooCoo 8a64e42100 DIMTFILL: dimension text gets a background in the drawing's background color or in DIMTFILLCLR
DIMTFILL and DIMTFILLCLR were not modeled, so a dimension's text never had a
background and lines under it stayed visible. Dimension styles now carry both
variables. DIMTFILL 1 hides what is drawn under the text in the drawing's
background color, and DIMTFILL 2 fills the box in DIMTFILLCLR. The box is the
text box widened by DIMGAP on every side. The arc length symbol over the text
and the DIMINSPECT frame are drawn over the background.

The variables work with SETVAR, DIMOVERRIDE and the style fields. DXF stores
them as DIMSTYLE groups 69 and 70 from R2007 and in the style's CADCRAFT
xdata in older files; the reader skips the record's flags, which also use 70.
For DIMTFILL 2, the dimension's *D block and EXPLODE add a SOLID in
DIMTFILLCLR under the text. A dimension drawn from the *D block it was read
with gets a background around each of the block's texts, before the block's
first text or first segment of the text's marks, except for a text that a
SOLID of the block already covers. The background stays after the drawing is
saved and reopened.
2026-10-24 14:11:01 -07:00
aiongg fcfdf33001 Merge pull request #1556 from silaghis/fix/stretch-ortho-window
Keep rectangle opposite corners free of ortho (STRETCH window and friends)
2026-10-24 16:15:54 -04:00
dotCooCoo 898b539630 COMPAREEXPORT: a block left out at the block limit is reported when this drawing's differs
Past the limit on blocks copied, a block of the other drawing that this
drawing also defines was skipped without a note, and the red group drew
this drawing's definition even when the two differed. Such a block now
counts as left out unless both drawings' blocks draw the same thing, so
the export says that some blocks were too many to copy.
2026-10-24 11:50:33 -07:00
dotCooCoo bfa30e2192 MEASUREGEOM: Tools > Inquiry > Radius asks for the object, and Radius measures polyline arcs
Tools > Inquiry > Radius had no prompts, so with nothing selected it
stopped with "`handle` is required". It now asks "Select arc or circle"
and reports the radius and diameter of the arc or circle picked. A
selected arc or circle is still measured at once.

MEASUREGEOM's Radius option and Tools > Inquiry > Radius also measure
the arc segment of a polyline under the pick. radius.measure takes an
optional `point`, and for a polyline it measures the arc segment nearest
that point.
2026-10-24 11:34:08 -07:00
dotCooCoo 432cbfb106 COMPAREEXPORT: a named block whose dimension draws another block is copied as PART_2
A named block that holds a dimension was kept as this drawing's
definition when only the block the dimension draws differed between the
drawings, so the red group drew this drawing's dimension graphics. The
export's block comparison now covers a dimension's block, as it covers a
multileader's.
2026-10-24 11:24:50 -07:00
dotCooCoo 596ea36465 MEASUREGEOM: malformed points and volumes past f64's range are errors
AREA and Volume measured the selected object when `points` was given but
was not a list of points; they now report an error, and only an absent
or null `points` measures the selection. A volume too large for f64
(an area times a height) is an error in the JSON form. At the typed
prompt, a volume or an Add/Subtract volume running total too large for
f64 makes the prompt say "The volume is too large." and ask for the
height again, where it reported "Volume = 0" or "Total volume = 0".
2026-10-24 11:11:51 -07:00
dotCooCoo dc081c6f7f MEASUREGEOM: add an Angle option, and Tools > Inquiry > Angle asks for the objects
MEASUREGEOM typed at the command line offered Distance, Radius and ARea,
and Tools > Inquiry > Angle ran without a prompt, so it failed asking for
{vertex,p1,p2} or {h1,h2}. MEASUREGEOM now has an Angle option, and it
and Tools > Inquiry > Angle ask "Select arc, circle, line <specify
vertex>". An arc reports its included angle. A circle takes the pick as
the first endpoint, uses the center as the vertex, and asks for the
second endpoint. Two lines or polyline line segments report the angle
where the lines meet, extended. Enter asks for a vertex and two
endpoints. The JSON form of angle.measure is unchanged.
2026-10-24 09:30:38 -07:00
dotCooCoo 746942613c CUTBASE: a base point is required, and a handle listed twice is cut once
cutbase and copybase now fail with "`base` ([x, y]) is required" when
`base` is missing or not a point, as their parameters say, instead of
taking the lower-left corner of the objects. cutbase checks it before
erasing anything.

Copy, Copy with Base Point, Cut and Cut with Base Point put an object on
the clipboard once however often `handles` lists it, so the counts they
return match the objects copied or cut.
2026-10-24 09:00:24 -07:00
dotCooCoo 1b8be0b77d COMPAREEXPORT: a named block the other drawing defines differently is copied as PART_2
A block reference of the other drawing whose named block exists in this
drawing with other contents drew this drawing's definition in the red
group. The export now keeps this drawing's block only when both
drawings' blocks draw the same thing: the same objects, base point,
units, annotative and overlay flags and xref file, in the block and in
the blocks its references and multileaders use. Otherwise the other
drawing's block is copied under a free name (PART_2, PART_3, ...) and
its references point at the copy.
2026-10-24 08:26:46 -07:00
dotCooCoo 7fe832b284 MEASUREGEOM: a Volume option, and Tools > Inquiry > Volume
MEASUREGEOM offered Distance, Radius and ARea, and Tools > Inquiry had
no Volume item. The Volume option measures a polygon of three or more
points, or a closed 2D object as AREA measures it, and asks for a
height. It reports the area times the absolute value of the height.
Add volume and Subtract volume keep a running total, as AREA's Add
area and Subtract area do. Tools > Inquiry > Volume (volume.measure)
starts the same prompts, and the JSON form takes points or a handle
and a height.

Engineering and architectural units show the volume as a decimal
number. The de, fr and ru catalogs get a "Volume" row, and
docs/parity-checklist.md is regenerated with cargo xtask parity.
2026-10-24 08:09:26 -07:00
dotCooCoo 1fcdfc87b4 ATTDISP: View > Attribute Display > Normal, On and Off, and 'ATTDISP inside another command
ATTDISP had no menu entry. View > Attribute Display now has Normal, On
and Off, which set ATTMODE to 1, 2 and 0 as ATTDISP's options do. ATTDISP
can also be typed as 'ATTDISP while another command runs, and that
command resumes afterward.

The eight catalogs without a "Normal" row get one, and
docs/parity-checklist.md is regenerated with cargo xtask parity.
2026-10-24 07:04:43 -07:00
dotCooCoo 5a82f090df BLOCKREPLACE: Tools > Express Tools > Replace Block replaces every reference of one block with another
BLOCKREPLACE (and -BLOCKREPLACE) turns every reference of one block, in
model space, the layouts and other blocks, into a reference of another
block at the same insertion point, scale and rotation. Each new
reference carries the replacement's attributes, with the values of the
tags both blocks define, and is annotative when the replacement is.
The command then asks whether to purge the replaced definition, and
removes it only when no dimension style names it and no block
reference, dimension or multileader in the drawing or in another block
definition does.

The command refuses a replacement that references the block it
replaces, a dynamic block as the block to replace, and blocks that
come from an xref. It does not run while the Block Editor or REFEDIT
is open. The ATTSYNC helpers in blocks/attribs.rs walk the references
and build the new attributes.

The self-reference check that BLOCK, INSERT of a file, REFCLOSE and
Lisp block creation share now also follows dimension blocks and
multileader block content, so BLOCK no longer accepts a redefinition
of A from a multileader whose content is A.

docs/parity-checklist.md is regenerated.
2026-10-24 05:57:38 -07:00
dotCooCoo 6fe715e11c THICKNESS: Format > Thickness, and new 2D objects take the current thickness
New lines, arcs, circles, points and lightweight polylines (for example
PLINE, POLYGON, DONUT and BOUNDARY) now take the THICKNESS system
variable as their 3D thickness. THICKNESS could already be set with
SETVAR and with the Properties palette when nothing is selected, but new
objects were always created with a thickness of 0, apart from RECTANG's
own Thickness option. RECTANG keeps that option, and single-line text
does not take THICKNESS, as in AutoCAD.

The new THICKNESS command is on Format > Thickness. Typed or picked from
the menu, it asks "Enter new value for THICKNESS <0.0000>:"; Enter keeps
the current value, and a negative value is accepted.
docs/parity-checklist.md is regenerated.
2026-10-24 03:24:13 -07:00
dotCooCoo 028b98d71c CUTBASE: cut objects to the clipboard with a base point
Edit > Cut with Base Point asks for a base point, then for the objects,
copies them to the clipboard with that base point and erases them, as
COPYBASE does without the erase. A pickfirst selection skips the object
prompt. Objects on locked layers stay, as with CUTCLIP, which now shares
the cut helper. The JSON form takes {handles?, base: [x,y]}.

Catalog rows for the new menu label are added in all 15 languages, and
docs/parity-checklist.md is regenerated with cargo xtask parity.
2026-10-24 02:29:26 -07:00
Dylan M. TaylorandClaude Sonnet 5.5 b9f96d3dd3 refactor: replace wildcard imports in two test modules (S2208) (#2036)
* refactor: replace wildcard imports in two test modules (S2208)

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

* style: fmt

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 04:49:51 -04:00
dotCooCoo 4aed7a2267 COMPAREEXPORT: export a drawing comparison to a new drawing
COMPAREEXPORT compares the two drawings again and opens the result as a
new untitled drawing, compare_<this> vs <other>.dwg. Model space holds
the other drawing's removed and changed objects as group
COMPARE_COMPARED and the compared tab's objects as group
COMPARE_CURRENT, in the overlay's colors: green for objects only in
this drawing or changed in it, red for the other drawing's, gray for
objects in both.

Block references, dimensions and multileaders refer to recolored copies
of their blocks, tables and tolerances use recolored copies of their
styles, and inline MTEXT and cell colors, dimension style colors,
multiline element colors and gradients follow the object. The new
drawing takes the layers and styles of the other drawing that this
drawing lacks, and the blocks its exported objects refer to; an
anonymous block whose name is taken is copied under a free name. The
comparison bar gets an Export button.
2026-10-24 01:36:29 -07:00
63394e872d chore: resolve SonarCloud maintainability issues (#2025)
* chore: resolve SonarCloud maintainability issues

* style: format with rustfmt

* fix(engine): reference DocState instead of DrawingState

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

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

* test: fix assertions flagged in review and apply rustfmt

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

* test: initialise drift filter in struct literal for clippy

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

---------

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

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

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

Fixes #1439

* docs: document the functions touched by #1439

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

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

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

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

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

---------

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

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

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

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

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

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

Reviewer follow-ups on #1820:

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

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

---------

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

* docs: document the functions touched by #1440

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

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

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

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

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

---------

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

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

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

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

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

* docs: document the functions touched by #1651

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

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:56:17 -04:00
fde62a39ab MEASUREINIT: choose the units of new drawings, kept in the profile (#1792)
* MEASUREINIT: choose the units of new drawings, kept in the profile

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

Part of #1182.

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

* contributors: add Richard Sauer (aussieboer61)

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

---------

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

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

---------

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

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

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

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

---------

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

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

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

* Match the active layout by name without regard to case

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

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

* Test case-insensitive active layout matching

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: ffishman <20211677+ffishman@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 01:53:49 -04:00