* OFFSET uses a single preselected object (#1610)
With exactly one offsettable object selected before OFFSET, take it and go
to the side prompt instead of asking to select an object again; a multiple
selection keeps the select-object loop. AutoCAD ignores preselection here,
but this is the requested behaviour for users coming from other tools.
* docs: document the functions touched by #1610
Autosave files are unsaved drawings. The autosave folder was made with
create_dir_all, by the first write or for the lock file, so on Unix it
got the default mode and other accounts on the machine could read the
files. A folder the autosaver creates is now 0o700. A folder that already
exists keeps its mode, since SAVEFILEPATH can be any folder the user
picked.
The same change as storytold/pdfcraft#3320 and storytold/vectorcraft#3406.
* AutoLISP: entmake defines blocks with BLOCK ... ENDBLK (fixes#1662)
A BLOCK list wasn't an entity, so entmake returned nil and the entities
after it went into model space. Now a BLOCK list begins a definition
(name, flags, base point, description), the entities made meanwhile go
into it, and ENDBLK adds the block (redefining one of the same name, or
naming an anonymous *U block) and returns its name. An open definition,
an xref, an invalid name or a block that inserts itself is refused with
nil, using BLOCK's own name and self-reference checks.
* AutoLISP entmake blocks: review fixes (anonymous names keep a star, no redefinition during REFEDIT, a failed routine drops its open definition)
An anonymous block (flag 1) named without a * got a name like FO1; it now
gets *U. ENDBLK refuses to redefine a block REFEDIT is editing, as BLOCK
does. A routine that fails drops the definition it began, so the next
routine's entities go into the drawing; one that ends normally still
leaves it open.
* AutoLISP entmake blocks: refuse an anonymous block that inserts itself
The self-reference check ran only for named blocks, so an anonymous *U
block holding an INSERT of the name it was about to get (*U1 in an empty
drawing) was stored. The check now runs after the anonymous name is
given, for both kinds.
* AutoLISP entmake blocks: create a definition's layers only when the block is added
Each pending entity created its layer at once, so a definition ENDBLK
refused (a self-reference, REFEDIT) or a failed routine dropped left an
orphan layer. end_block now creates the layers after its checks pass,
just before it adds the block.
* Make properties.set apply thickness and properties report it (#1722)
* test: CIRCLE returns its new object as `handle`
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>
* Report DWGPREFIX, DWGTITLED, FILEDIA, CMDECHO and more from GETVAR (#1715)
* Keep CMDECHO and FILEDIA in session settings so they survive drawing switches (#1715)
* Leave LOGINNAME out of GETVAR so scripts and agents can't read the user's login name
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>
* fix(snap): keep an arc's sweep for TAN, PER and deferred snaps
An arc was snapped as its full circle: TAN and PER solutions on the circle
that fall outside the arc's sweep were offered as if they touched the arc,
and a deferred tangent lost the sweep when it was stored.
- SnapCurve gains an Arc variant carrying start and end, so a deferred snap
keeps the sweep and resolves onto the arc.
- TAN and PER filter their solutions with Arc::contains_angle; PER clamps to
the arc with Arc::closest instead of the circle's closest point.
- Tangents and perpendiculars between two deferred snaps are filtered on
both ends.
- The viewport projection maps the new variant.
- Tests: a tangent in the gap is not offered, the perpendicular clamps to an
end, and a deferred tangent keeps its sweep and solves onto the arc.
Fixes#1697
* fix(snap): correct direct PER, support Arc in deferred pairs
Addresses the review on #1698:
- Direct PER on an arc used `arc.closest(bp)`, which clamps a gap solution to an
end and misses the valid opposite antipode; both antipodes are now offered,
filtered to the arc's sweep.
- `deferred_pairs` matched only circles and lines, so any deferred pair with an
arc operand fell through to None. An arc is now matched as its carrier circle
for the math, with the sweep filter still applied to the solutions.
- Tests: the direct PER case now expects the antipode on the arc, and a
TAN-TAN pair between an arc and a circle only keeps the tangent on the arc.
* test(snap): use a margin over the boundary in the arc PER test
Addresses the review on #1698: the cursor is 5 units from the antipode, so the
aperture is 5.5 rather than 5.0 to keep the distance comparison off the
boundary; the comment now describes the antipode rather than the nearest end.
* 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>
* DXF/DWG: keep a file's own arrowhead blocks with standard names
Reading dropped every block with a standard arrowhead name (`_Dot`,
`_DotSmall`, ...) that no INSERT used, since CADCraft regenerates its own on
save. A block only a dimension style names (DIMBLK) was dropped too, so
AutoCAD's `_Dot` (a polyline with width) or a user's redefinition came back
as CADCraft's disc of 24 SOLIDs. Now only blocks whose content is exactly what
CADCraft writes are dropped; any other content is kept and saved as it was,
through DXF and DWG. PURGE keeps arrowhead blocks that dimension styles or
dimension overrides name.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Arrowheads: compare all properties, trim names in PURGE, test every standard arrow
is_generated_arrow now requires the whole Common to match, so only blocks
exactly as CADCraft writes them are regenerated. PURGE trims arrowhead names
from dimension styles and overrides. A new test writes every standard
arrowhead to DXF and DWG and checks none reads back as a user block. Review
of #1694.
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>
Inserting a DWG/DXF as a block merged its layers, linetypes and text,
dimension, multileader and table styles, but not its multiline styles.
A drawing that brings its wall styles in by inserting a library file
(AutoLISP add-ons do this: INSERT walls.dwg, ERASE the reference, then
MLINE with one of its styles) got "Cannot find multiline style". The
styles now merge like the other tables: a name the drawing already has
keeps the drawing's definition.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Xrefs and data links: use resolve_absolute for stored paths
* CI: check that git dependencies come from storytold
* CI: release only with git dependencies from storytold
* Parse Cargo.lock as TOML when checking git sources
* Use the shared git-sources check in CI
* Parse cargo config, legacy .cargo/config included, in the git-sources check
* Build: line tables only in dev, no debug info for dependencies
* Tests: one integration-test binary per crate
Each crate's tests/*.rs becomes a module of tests/it/main.rs. Files that
install process-wide state (the file io hooks, user aliases, SAVETIME and
SAVEFILEPATH, user linetype and pattern files, registered fonts) stay their
own test binaries.
* xtask ci: nextest, tests without examples, one wasm check call
* security: bound Lisp callbacks, native comparisons, and local file reads (CWE-400, CWE-407)
The command and scripting engine had several unbounded paths a supplied LISP
routine or a selected path could exploit to monopolise the thread or exhaust
memory:
- `call_sync` ran synchronous callbacks (vl-sort comparators, vl-remove-if
predicates, …) with `u64::MAX` fuel, so a constant-memory infinite loop never
yielded and bypassed the blocking execution cap. It now drives the callback in
bounded slices and aborts with an error past a hard step cap (row 2).
- `wcmatch`/`glob` explored `*` placements with only a recursion-depth guard,
and `equal` re-traversed shared cons subtrees. Both now charge every step
against a shared work budget and stop when it is exhausted (row 33).
- `setvar` converted a LISP value to JSON unboundedly, so a shared cons DAG
expanded into gigabytes of owned arrays. Conversion is now depth- and
element-bounded and rejects over-budget values with a LISP error (row 39).
- OPEN/LOAD/APPLOAD (`read_text_file`), SCRIPT, and native open (`read_file`,
`run_open`) buffered a path with `std::fs::read`, so an endless device
exhausted memory before any size check. Reads now go through a new
`capped_read` helper that rejects non-regular files and caps bytes
(rows 21, 34).
New tests cover the callback loop, the oversized setvar structure, and the
capped reader. Addresses findings rows 2, 21, 33, 34, 39.
* fix: bound native Lisp scans and nonblocking file reads
* fix: enforce callback fuel before each execution slice
---------
Co-authored-by: skylartaylor <26588250+skylartaylor@users.noreply.github.com>
* security: write files atomically through an exclusive temp file (CWE-59, CWE-377)
Save, export, plot, autosave and the audit log each wrote their output by
putting bytes in a predictable sibling temporary path (`<path>.cadcraft-tmp`,
`<recovery>.tmp`, or the companion `.adt`) and renaming it over the real file.
Opening that pre-existing temporary path for a truncating write follows a
symlink planted there by another local user, letting them redirect the write
onto any file the victim can write before the rename.
Add a shared `atomic_write` helper that creates a fresh temporary file with
`create_new` (O_CREAT|O_EXCL) in the destination directory, writes through the
retained handle, flushes/syncs, and atomically renames it into place. An
existing entry (symlink or not) makes the exclusive create fail instead of
being followed. Route autosave, save, export, plot and the audit log through it.
The macOS CI cert importer likewise wrote the decoded PKCS#12 container to a
fixed name in a possibly shared temp directory; it now creates a private
per-run directory with `mktemp -d` and creates the file with noclobber.
Addresses findings rows 1, 25, 40 (CWE-59) and 19 (CWE-377).
* fix: preserve permissions during atomic replacement
* fix: restrict temporary permissions during exclusive creation
---------
Co-authored-by: skylartaylor <26588250+skylartaylor@users.noreply.github.com>
The pick-box heuristic read the message for a leading "Select", and
this branch had widened it past leading whitespace so that a routine's
own message would be read the same way. That makes a plain point prompt
look like a pick: (getpoint "\nSelect a point: ") showed the pick box
and lost its point modifiers, since pointmod refuses them wherever
picks_objects holds.
A routine writes all of its own message (raw), and says through
picks_one when it picks an object, so the wording needs reading only
where the engine wrote it. Covered for both spellings a routine might
use, with and without the leading newline.
Whether the cursor shows the pick box came from `Prompt::picks_objects()`,
which read the prompt's wording: a selection-set prompt, or a point prompt whose
message starts with "Select". An `entsel`/`nentsel` prompt takes a point and is
not a selection set, so it fell to the wording -- and an AutoLISP routine writes
its own message, conventionally starting with a newline:
(nentsel) "Select object: " pick box
(nentsel "\nSelect second line or polyline: ") starts with "\n" none
(entsel "\nPick a line: ") says no such word none
A prompt now says that it picks one object rather than being read for it, and
`GetRequest::prompt()` sets that for `GetKind::EntSel`, so all three show the
pick box. The wording test is read past leading whitespace as well, so a command
machine's prompt beginning with a newline is not caught by the same thing.
Point modifiers (`.x`, `.y`…) are offered at a point prompt that is not a pick,
so they no longer appear at an `entsel` prompt with a message -- as they never
did at one without.
Fix: #1598
`(= (type x) 'LIST)` is how an AutoLISP routine tests a type, because `type`
returns a symbol. `=` coerced both of its arguments to numbers, so the symbol
failed the coercion and the routine stopped at its first type test:
_$ (= (type 1) 'INT)
bad argument type: numberp: INT
`=` and `/=` now compare numbers and strings as before, and any other atom -- a
symbol, `nil`, an entity name, a selection set, a file handle -- by what it is,
the way `eq` does. Two consequences, both closer to AutoLISP than raising was:
a mismatch of kinds answers `nil` rather than failing, so `(= 1 "1")` and
`(= '(1) '(1))` answer (comparing list structure is `equal`'s job), and
`(= nil nil)` is `T`.
The ordering comparisons still take numbers and strings only, as AutoLISP has
them: `(< 'a 'b)` remains `bad argument type: numberp: A`.
Fix: #1597
Found while testing crates/engine/src/cmd/utility/calc.rs:124-140 (Parser::unary recursion, MAX_DEPTH only counted in expr() and '[')
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
AREA's Object option reported a full ellipse's perimeter with
Ramanujan's closed-form estimate, which falls short as the ellipse
flattens: 46.0256 for a 10 x 4 ellipse (exactly 46.0262), 39.8741 for a
10 x 0.1 one (exactly 40.0110, 0.34% short). It now integrates the
perimeter with the geometry crate's ellipse arc length, the one LENGTHEN
measures with, so both report the same length for the same ellipse.
Ellipse::arc_length_from halves a panel where it needs it (the same
adaptive Gauss-Legendre as spline arc lengths): with fixed panels a
100:1 ellipse came out 4e-8 long, as it turns sharply at its ends.
Tests check four ellipses (round to 100:1) against the
arithmetic-geometric mean to 1e-9, and the printed AREA line.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/engine/src/units.rs:13
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Tools > Express Tools > Break-line Symbol draws one polyline from the first point to the second with a zigzag break symbol at a picked location (the midpoint on Enter), as AutoCAD's Express Tool does. Size scales the symbol (times DIMSCALE), Extension runs the line past both points, and Block takes a block of the drawing whose two points on the Defpoints layer mark where the line meets the symbol. The built-in symbol is our own geometry. One undo step; the options are remembered for the session.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/engine/src/cmd/modify.rs:1457 (break_entity; helpers `line()`/`arc()`/`lwpoly()` in crates/engine/src/cmd/helpers.rs:10-23)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
## 변경 내용
- entities 명령에 selected 불리언 필터를 추가해 현재 공간의 선택 객체만 조회
- 기존 종류·레이어·영역 필터와 결합하고 필터 적용 후 페이지 처리
- MCP query_entities 스키마와 명령·제어 프로토콜 문서에 옵션 공개
- 빈 선택, 중복·오래된 핸들, 다른 공간 객체, 잘못된 입력 및 조회 전후 상태 보존 회귀 테스트 추가
## 검증
- cargo xtask ci 6단계 통과: fmt, clippy, 테스트 2,012개, assets, layers, wasm
- 별도 .NET 클라이언트와 실제 헤드리스 엔진의 localhost 통합 검증 통과
- 실제 데스크톱 UI 조작 및 고객 DWG 왕복 검증은 수행하지 않음
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
At a point prompt with a base point, holding Shift turns ortho on when
ORTHOMODE is off and off when it is on; releasing it restores the setting.
ORTHOMODE itself is never changed. While the override turns ortho on, polar
tracking is off, as with ortho on.
The UI already passed the Shift state to Session::snap_cursor (for POLARMODE
bit 8's Shift to acquire), so the change is in the engine only: snap_cursor
applies the override to its copy of the settings. Window corners (ZOOM
Window) and prompts without a base point are unaffected.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Selection cycling: a click over overlapping objects lists them, Shift+Space steps through them (fixes#1143, fixes#1145)
* canvas: keep viewport_content's doc comment on it
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>
* Dynamic blocks: a grip drag previews the reference in its own colours
The preview of a dynamic grip drag is the reference re-evaluated at the
cursor. It was drawn in the preview colour, fills left out, so on a filled
label the text was white on white. Commands can now give preview objects
with their own properties (`Interactive::preview_objects`, drawn under the
rubber band); the grip drag gives the block's objects with the properties
they draw with in the reference (layer 0 on its layer, ByBlock its
colour, linetype, lineweight and transparency) and its attributes with their
definitions', fills included in the block's order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Preview: the rubber band is grip blue over objects previewed in their own colours
Over a block previewed in its own colours the rubber band and tick marks,
in the current layer's colour, could vanish on its fill (white on white).
They are drawn in the grip blue there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Cody Vogt <18035288+codyvogt@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Inquiry: SHAPES reports closed-shape geometry facts
`shapes` is a read-only command that reports, for every closed shape in the
current space (closed polylines, circles, full ellipses, closed splines),
its perimeter, area, centroid, winding, first self-intersection and
coincident vertices, exact with arcs and bulges. It filters and pages like
`entities` and counts other matches under `skipped` with the reason.
`Polyline::centroid` and `polygon_centroid` give the area centroid.
Fixes#1436.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* shapes: cap the quadratic self-intersection test at 4000 edges
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>
* feat(engine): support AUDITCTL system variable and .adt log generation (gaps F4)
* audit: write the .adt log only beside a saved drawing
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>
* Properties: a dimension's text height, arrow size, overall scale and precision (#1259)
The Properties palette showed only a dimension's text override and style,
so the only way to change its text height was a new dimension style.
It now shows Text height (DIMTXT), Arrow size (DIMASZ), Dim scale overall
(DIMSCALE) and Precision (DIMDEC) with the values the dimension draws
with. Editing one sets a per-dimension override (DIMOVERRIDE) on the
selected dimensions, as AutoCAD's Properties palette does; the style is
left alone. properties.set takes textHeight, arrowSize, dimScale and
precision for dimensions and refuses bad values before changing anything.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* props: keep DIMOVERRIDE's doc comment on dim_override
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>
* Wrap format_angle to zero when rounding reaches a full turn
Found while testing crates/engine/src/units.rs:63-66,87-88,102
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* units: wrap a full turn by the formatted value, so ties round as {:.*} does
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
ID always printed Z = 0, DIST's Delta Z and angle from the XY plane were
0, and LIST printed Z = 0 for lines, circles, arcs and polyline vertices,
though the drawing kept every Z: a topographic drawing's levels could not
be read in CADCraft.
Points reach these commands in 2D, so the Z is that of what is drawn at
the point, as AutoCAD reports a snapped point's: the object a pick finds
there (a point, a 3D line or 3D polyline interpolated, a polyline's
elevation, down Z for a mirrored one, a circle, arc, text, block or the
plane of a 3D face), else the current elevation (ELEVATION). A Z given
with a JSON point wins. DIST gives the 3D distance, Delta Z and the angle
from the XY plane; LIST the Z of lines, circles, arcs and text, a line's
3D length and deltas, and a polyline's elevation.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/engine/src/cmd/modify.rs:584-597 (offset_kind, EntityKind::Ellipse branch)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Found while testing crates/engine/src/cmd/table/breaks.rs:167 (parse_csv), fed by read_link at :236 (String::from_utf8_lossy, no BOM removal)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>