The folders are made by a new writer only user-named targets use, so autosave
and a dangling symlink's target keep the strict write_atomic. Save, Save As,
Export, Plot, WBLOCK, layout templates, render and the CLI report the topmost
new folder at the command line and in createdFolder.
The interactive save only knew about DWG, so overwriting a 2013+ DXF lost SAT-text
solids on a command-line note. loss_on_save now lists them, and EXPORT reports them
with the DXF wording.
* DWG save keeps OLE objects (OLE2FRAME): stop reporting them as not saved
acadrust reads OLE2FRAME from our DXF into its own entity and writes it to
DWG of every version we save, frame and embedded data unchanged; only the
"Not saved in the DWG file" warning (and the QSAVE loss prompt) listed it.
not_saved_as now skips the types the DWG bridge writes (DWG_WRITES).
Tests: a synthetic OLE2FRAME through DWG (resaved, and as 2000 and 2010
DWG) keeps every byte; OLE objects are not reported at any version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* DWG_WRITES matches the type as stored, not case-insensitively
The DXF given to the DWG bridge carries an unknown entity's type as stored, and
acadrust reads only "OLE2FRAME" into its OLE entity: any other spelling becomes
its Unknown and is dropped, so it must stay in the "Not saved" report. The test
adds a lower-case "ole2frame" that is still listed. (Review comment on #2582.)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* SAVEAS portableFonts: Windows fonts for Linux text style fonts
A text style using a Linux TrueType font (Liberation, DejaVu, Open Sans)
shows in AutoCAD on Windows with a substitute it picks. A save now names
those styles on the command line (and as `nonPortableFonts` in the
result), and SAVEAS {"portableFonts": true} writes the Windows equivalent
in the file (Liberation -> Arial, Times New Roman, Courier New; DejaVu
Sans -> Verdana; Open Sans -> Arial) while the drawing keeps its own
fonts. Narrow and condensed families have no Windows stand-in (Arial
Narrow comes with Office) and are kept and named. QSAVE takes no SAVEAS
options.
Tests: stand_in_tests in cmd/file.rs, tests/save_portable_fonts.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Portable fonts kept in the file, applied by the writer
Reworked after review. SAVEAS `portableFonts` writes a copy of the drawing
with `Drawing::portable_fonts` set, and the DXF/DWG writer turns each
style's Linux font into its Windows font, keeping the style's own font in
the CADCraft FONT xdata already used for the built-in font. Reading the
file gives the Linux fonts back (unless the font was changed since, in
AutoCAD); the drawing itself is never changed.
It applies to that save: a later QSAVE writes the drawing's fonts and
says so, naming the styles and SAVEAS portableFonts. The family table
moves to `cadcraft_fonts::windows`, used by the writer and the note.
Styles from an xref aren't saved and aren't named. The parity docs are
left to the maintainers (CLAUDE.md).
Tests: crates/io/tests/it/portable_fonts.rs (DXF, DWG, a font changed in
AutoCAD), crates/engine/tests/save_portable_fonts.rs, windows.rs tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Portable fonts: match the Windows font by file name
A STYLE whose Windows font carries a path (C:\Windows\Fonts\…) still
gets its own font back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Portable fonts: say the font list again only when it changes
A save still returns nonPortableFonts, but the command line note isn't
repeated on every QSAVE of the same drawing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
plan.dwg and plan.dxf in one folder shared plan.bak. A DWG keeps <name>.bak, as AutoCAD names it, and any other drawing appends .bak. The naming comes from #1006.
Co-authored-by: Filippo Reimer <49290121+filforce95@users.noreply.github.com>
A real DWG write and read-back keeps a SAT-text REGION's groups up to 2010 and
returns it without them at 2013 and 2018, so the SAT-text skip in not_saved_as
applies before 2013 only. The DWG note now says solids are kept in a DXF only
when saved as 2010 or earlier.
The writer keeps an ACDSRECORD by its first 320 only, so data_store_entities now
credits that handle alone (one shared rule, data_store_owner). The DWG writer
converts a solid's SAT text itself at any version, so not_saved_as no longer
lists solids that hold SAT text as left out of a DWG.
QSAVE takes no version and writes the drawing's own, but the SAT-text note read a `version`
passed to it, so `qsave {version: 2010}` on a 2018 drawing skipped the note while still
leaving the solids out. Only SAVEAS's requested version now picks the reporting branch.
Test: saving_sat_text_solids_as_dxf_2013_reports_them (qsave case fails without the fix).
The DWG bridge writes R2000 text for the DWG writer, which keeps SAT-text
solids: the skip is now keyed on the DXF's declared version, not the DWG
target. The save note reads the version and count from the host drawing while
the Block Editor is open, and leaves out xref blocks (path or xref| name), which
the DXF writer empties anyway; the xref name rule is shared with the writer.
* Tests remove the temp files they write
Every test run left about 22 folders and files in the system temp
directory: xref, alias, template, insert, linetype, script and
recent-file fixtures, plus the pathless screenshot folder. The PDFBOOK
test also wrote its second book to `$TMPDIR/b.pdf`, because `a^b.pdf`
splits into `<prefix>-a.pdf` and `b.pdf`, and then removed a file that
was never written.
Tests that write files now use a `TempDir` guard
(`tests/support/temp_dir.rs`, shared by the engine's unit and
integration tests and copied for the UI crate). It gives each test its
own folder and removes it on drop, even when an assertion fails. The
PDFBOOK test also checks that both books land in its folder. The five
tests that write pathless screenshots hold a test-only lock, so the
shared screenshot folder can be removed once empty without racing
another test's write.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012FFPi6hJVyaEKV4PyE2fKq
* Pathless screenshot cleanup survives failed tests and writes
The test lock's guard now empties and removes the per-process
screenshot directory when dropped, so a test that panics no longer
leaves files behind, and the per-test remove calls are gone. A failed
PNG write or flush removes the partial file, since the reply carries
no path for anyone else to clean up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: bmh129 <11478682+bmh129@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
REGION, 3DSOLID and BODY read from a DXF 2000 hold SAT text; DXF 2013 and later
keep ACIS geometry in the data store section, so writing the text there made a
file other readers reject. Those entities are now left out of 2013+ DXF output
and counted in the save note; 2010 and earlier, and DWG, are unchanged.
Test: sat_text_solids_are_left_out_of_dxf_2013_and_later.
* test(cli): pin today's command-line behaviour
The later commits change these tests on purpose, so each behaviour change shows in their diff.
* refactor(cli): split main.rs into modules, move only
- output: outln!, stdout state, atomic write, export helpers, view/window helpers
- args: USAGE, option validation, framing options, MCP argument parsing and its tests
- info, convert, sample, mcp: one subcommand each
- perf: synthetic drawing, Rng, perf subcommands
- run: run subcommand and {last} handle chaining
- main: crate docs, mod lines, install_io, open, main, cli, unit tests
No behaviour change.
* refactor(cli): typed errors and run(args, out, err)
cadcraft-cli is now a library plus a thin binary. `cadcraft_cli::run(args, out, err)
-> i32` is the whole program; main.rs only passes `args_os()` and the process
streams. Handlers write through a `Streams` wrapper (output.rs) instead of the
process streams; it keeps the closed-stdout semantics (drop later output, carry
on, a closed pipe is not an error, other write errors are reported once and make
the exit code 1). `mcp` still speaks MCP on the real stdin/stdout.
Errors are typed (thiserror): CliError (error.rs) wraps EngineError, DocError,
IoError, io::Error and the per-module WindowError, ArgsError, RunError,
OutputError and PerfError. `run` prints `error: {e}` and exits 1.
The usage text and the "unknown option/argument" messages stay as one
transitional CliError::Usage(String), printed without the `error: ` prefix.
USAGE stays a const; step 4 (clap) removes both. The engine's IoHooks take
`Result<_, String>`, so the three `.map_err(|e| e.to_string())` in install_io
remain. The hooks are set-once (OnceLock), so installing them on every `run`
call is harmless.
tests/it/usage.rs calls `run` in-process with Vec<u8> writers; the headless mcp
case runs through the binary (it reads the real stdin) and a non-Unicode
argument test is new.
Behaviour changes (old -> new):
- Every error line other than the usage-class ones gains the prefix `error: `
(e.g. `--view needs ...` -> `error: --view needs ...`, `unknown command
`x`` -> `error: unknown command `x``, `invalid parameters for `open`: ...`
-> `error: invalid parameters ...`, `unsupported file type` likewise).
- `cadcraft-cli: cannot connect to HOST:PORT: e` -> `error: cannot connect to
HOST:PORT: e` (the old prefix is replaced).
- A non-Unicode argument: panic, exit 101 -> `error: argument "..." is not
valid Unicode`, exit 1.
- Unchanged: usage text, `X: unknown option --y`, `unknown option --y`,
`mcp: unknown argument ...` (no prefix); warnings and `wrote ...` lines;
the `cadcraft-cli: can't write to stdout: e` report; exit codes.
* refactor(cli): parse arguments with clap
Replace the hand-written parser (USAGE, validate_options, OptionSpec,
framing_option, parse_mcp_args, CliError::Usage) with clap derive types
in args.rs: a Cli parser and one Args struct per subcommand, with the
framing flags (--window, --view) in a FramingArgs struct flattened into
convert and run. USAGE's framing and --cmd chaining text moves into the
subcommands' and flags' doc comments and run's long help, so --help
carries it. `commands` moves from an inline closure to commands.rs.
`command()` exposes the clap definition for man pages and completions.
clap reads the arguments as OsStrings. Help and --version go to stdout
with exit 0; usage errors are clap's, on stderr with exit 2. A bare
--help or -h before `--` shows the subcommand's help even after a
mistake on the line (`run --script --help`). run opens its drawings
(FILEs, --sample, --metric) and runs its steps (--script, --script-file,
--cmd) in command-line order, from ArgMatches::indices_of. Values stay
strings checked by the handlers, with their messages and exit 1
(--window, --view, --cmd JSON, sample names, perf counts). The last of a
repeated flag wins (args_override_self on every subcommand but mcp).
Behaviour changes:
Help and version
- --help, -h, help: the USAGE page -> clap's help, first line
"cadcraft-cli: headless CADCraft".
- bare cadcraft-cli: USAGE on stderr, exit 1 -> clap's help on stderr,
exit 2.
- <subcommand> --help / -h, help <subcommand>: "unknown option --help"
(exit 1), the USAGE page (convert, help <subcommand>) or a file named
-h (info -h, run -h) -> that subcommand's help, stdout, exit 0.
- <subcommand> --version: "unknown option --version", exit 1 -> clap's
unexpected argument, exit 2.
Usage errors (exit 1 -> exit 2, clap's messages)
- Unknown subcommand, empty subcommand, unknown top-level flag: USAGE ->
"error: unrecognized subcommand 'foo'" / "error: unexpected argument
'--bogus' found".
- Unknown flag on any subcommand: "info: unknown option --bogus",
"unknown option --bogus", "mcp: unknown argument --bogus" or USAGE
(convert) -> "error: unexpected argument '--bogus' found".
- A short flag where a file is expected (info -x): opened as a file ->
unexpected argument; `--` before it keeps it a file name.
- A value flag at the end or followed by a flag (--window, --view,
--script, --script-file, --cmd, --save, --export, --connect): "error:
--script needs text" and the like, or the next flag taken as the
value (--window --view, --script --sample, --cmd --sample,
--script-file --sample) -> "error: a value is required for '--script
<TEXT>' but none was supplied". --connect followed by an unknown flag
(--connect --foo, --connect -v) names that flag instead (clap reports
the unknown argument, not the missing value).
- mcp --connect with an empty value: "error: --connect needs HOST:PORT
..." -> "error: invalid value ' ' for '--connect <HOST:PORT>':
expected HOST:PORT (...)"; --connect=-v likewise.
- mcp --connect twice: "error: --connect given more than once" ->
"error: the argument '--connect <HOST:PORT>' cannot be used multiple
times".
- mcp --lazy / --launch without --connect: "error: --lazy and --launch
need --connect HOST:PORT" -> "error: the following required arguments
were not provided: --connect <HOST:PORT>".
- Missing positionals (info, convert with 0 or 1 files, sample with 0
or 1 arguments): USAGE -> "error: the following required arguments
were not provided".
- Extra positionals: convert with three files (USAGE), and info,
commands, perf, sample, which ignored the extra argument -> "error:
unexpected argument 'extra' found".
- perf -5: opened as a file -> unexpected argument '-5'.
- A non-Unicode argument where text is expected: "error: argument ... is
not valid Unicode", exit 1 -> "error: invalid UTF-8 was detected in
one or more arguments", exit 2.
Accepted where they were errors
- --x=v is a spelling of --x v for every value flag (--window, --view,
--script, --script-file, --cmd, --save, --export, --connect); before
it was an unknown option or USAGE. A value starting with - needs the
= form (--cmd=-x).
- commands --: "unknown option --" -> `--` ends the options (all
commands listed).
- run --view extents --view current: "use only one of --window and
--view" -> the last wins, like every other repeat.
Other
- run --sample --sample opens one sample drawing (was two): repeated
flags are last-wins.
- run checks --window/--view values before opening any drawing, so a
bad framing value is reported ahead of a missing file.
- `-h` after a mistake shows the long help (`--help`): the retry asks
for --help.
Unchanged: --window with --view, framing without an image output,
--view's case-insensitivity, all value messages (--window numbers,
--view values, --cmd JSON, {last}, sample names, --script-file read
errors), multiple FILEs, --sample with --metric or a FILE, last-wins
for --save, --export, --window, --script/--cmd order, and `\n` in
--script.
clap forced: exit 2 for usage errors, its message wording, and the
unknown-flag-over-missing-value precedence above.
* refactor(cli): reject repeated flags, long version (photocraft#7071 decisions)
Behaviour changes, old -> new:
- --save, --export, --window, --view, --sample, --metric, --lazy, --launch given twice:
last one won -> usage error, exit 2 ("the argument ... cannot be used multiple times").
--connect was already rejected. --script, --script-file, --cmd and run's FILEs still repeat.
- --version / -V: "cadcraft-cli <version>" -> "cadcraft-cli <version> (<sha>)", sha from
CADCRAFT_BUILD_SHA or "dev", as the desktop app prints it.
- "cadcraft-cli version" is an unrecognized-subcommand usage error (as before; now tested).
- A value starting with "-" needs the = form for every value flag (as before, tested).
--window -10,-10,10,10 stays a usage error ("unexpected argument -1"); write
--window=-10,-10,10,10. clap's allow_negative_numbers does not accept a comma list, and
allow_hyphen_values would let --window swallow the next flag, so neither is set. The
--window help text names the = form.
* refactor(cli): typed values and flag relations
Step 6 of #2228: values are parsed into their types and flag relations
are checked at parse time, so a bad value or combination is a usage
error (exit 2) naming the flag, before any work starts. The handlers
receive typed values only. Runtime failures (files, the engine, a
`{last}` with no earlier handle) stay exit 1.
- --window parses to a Bounds2 (parse_window plus cadcraft_io's
check_window); its WindowError is the value parser's error.
- --view is a ValueEnum (extents, current), case-insensitive; the long
help lists what each value does.
- --window conflicts_with --view. In run, --window/--view require
--export (an ArgGroup "framing" with requires). A framing flag with
an output that isn't an image (convert's OUT, run's --export) is a
UsageError turned into a clap error by into_clap (ArgumentConflict).
- sample's name is a ValueEnum (bracket, floorplan); `floor` stays a
hidden alias.
- perf's N|FILE is one typed positional: a value that parses as a
count is one, clamped to 1..=20000000 as before (so `perf 0` still
times one entity); anything else is a drawing.
- --cmd 'id {json}' parses to an id and a serde_json::Value (default
{}); `{last}` is still filled in when the command runs.
- Paths are PathBufs (info FILE, convert IN/OUT, run FILEs,
--script-file, --save, --export, sample OUT, perf FILE). The engine
and io crates take file names as text, so a path that isn't valid
Unicode is refused by the handler, naming the file (exit 1).
- Deleted: check_framed_output, the --view match, ArgsError,
CliError::Window/Args/UnknownSample, RunError::CmdJson,
PerfError::BadCount.
- run's FILEs, --sample and --metric still combine (each opens another
drawing, the last is current): neither photocraft#7071 nor RFC-0012 requires
otherwise.
Behaviour changes (old -> new):
- --window with a bad value (wrong count, not a number, NaN/inf,
reversed corners, beyond 1e12): exit 1 `error: --window ...` ->
exit 2 `error: invalid value 'V' for '--window <x0,y0,x1,y1>':
<reason>`. The reasons no longer repeat the flag or the value.
- --view with a bad value: exit 1 `error: --view needs `extents` or
`current`` -> exit 2 `error: invalid value 'V' for '--view
<extents|current>'` plus clap's possible-values line.
- --window with --view: exit 1 `error: use only one of --window and
--view` -> exit 2 `error: the argument '--window <x0,y0,x1,y1>'
cannot be used with '--view <extents|current>'`.
- run --window/--view without --export: exit 1 `error: --window/--view
need an image output ...` -> exit 2 `error: the following required
arguments were not provided: --export <OUT.png>`.
- A framing flag with a non-image output: exit 1 `error:
--window/--view only apply to image output (png, svg, pdf), not
`OUT`` -> exit 2 `error: --window only applies to image output (png,
svg, pdf), not `OUT`` (or --view: the message names the flag given).
- sample with an unknown name: exit 1 `error: unknown sample `X`
(bracket, floorplan)` -> exit 2 `error: invalid value 'X' for
'<bracket|floorplan>'` plus the possible values. sample's help lists
the possible values.
- --cmd with JSON that doesn't parse: exit 1 `error: --cmd ID:
<serde error>`, after the earlier steps ran -> exit 2 `error: invalid
value 'V' for '--cmd <id {json}>': <serde error>`, before anything
runs.
- --cmd with an empty id (`--cmd=` or `--cmd ' {}'`): exit 1 `error:
unknown command `` ` -> exit 2 `... expected a command id, then
optionally its JSON parameters`.
- An empty path argument (info "", sample bracket "", perf "", ...):
exit 1 from the engine or io -> exit 2 `error: a value is required
for '<FILE>' but none was supplied` (clap's PathBuf parser).
- A path that isn't valid Unicode: exit 2 `error: invalid UTF-8 was
detected ...` -> exit 1 `error: PATH: the file name is not valid
Unicode` (text values that aren't Unicode stay exit 2). A non-Unicode
--save or --export is refused before any drawing opens.
- perf's help gives the count range.
Tests: usage.rs rows above moved to exit 2 with clap's messages; new
rows for --view with a non-image output, run's framing with a dxf
--export, an empty --cmd id, bad --cmd JSON after a good step, a
`{last}` with no earlier handle, --window beyond 1e12, case-sensitive
sample names and empty paths. The non-Unicode test covers both a path
(exit 1) and text (exit 2). Unit tests: the --window parser tests moved
to args.rs; new ones for --cmd values and perf targets; framing_options
checks the clap error kinds.
* refactor(cadcraft): move launch-argument parsing into launch_args
Moves the hand-written parser (the argument loop, control_port_from, control_flag and their tests) from main.rs into launch_args.rs, which returns LaunchArgs. No behaviour change.
* refactor(cadcraft): parse launch flags with clap (RFC-0012)
Replaces the hand-written loop in launch_args.rs with clap derive types, as photocraft's launch_args.rs does. --help is generated from the doc comments and lists the other CADCRAFT_* variables.
Behaviour changes (old -> new):
- An unknown flag (--sampel) was opened as a file; it is now a usage error on stderr, exit 2, before the window opens. A file starting with "-" goes after "--".
- A repeated --control or --sample was accepted (last wins); it is now exit 2.
- A bad port (--control abc, 70000, -1) was logged and the control channel left off; it is now exit 2. The same for CADCRAFT_CONTROL_PORT, and the error names the variable when the value came from it.
- A bare --control (no value) kept CADCRAFT_CONTROL_PORT's port with a warning; it is now exit 2 ("a value is required"). A following flag is never taken as the value.
- An empty or blank CADCRAFT_CONTROL_PORT was logged as a bad port; it is now unset.
- --control=7979 is accepted (the equals form was opened as a file before).
- --version / -V and --help stop parsing at once and exit 0; version text is unchanged ("cadcraft <version> (<sha>)").
- -psn_* (Finder process serial number) is dropped instead of opened as a file.
- A file name that is not valid Unicode used to panic in std::env::args(); it is now read from args_os and reported with a log warning, not opened, because CadApp::open_path takes text.
Copied from photocraft's crates/automation/src/args.rs: blank_env_is_unset and name_env_vars unchanged (name_env_vars gets a local use for ContextKind/ContextValue); control_port and NotAPort unchanged. allow_negative_numbers is set on --control as in photocraft, so -1 is a bad value rather than a missing one.
* feat(cli): shell completions and man pages
Adds examples/man-and-completions.rs, which writes cadcraft-cli's bash, zsh, fish, elvish and
PowerShell completions (clap_complete) and man pages (clap_mangen: cadcraft-cli.1 and one per
subcommand) into a directory. clap_complete and clap_mangen are dev-dependencies, so the binary
is unchanged.
packaging/linux/package.sh builds the example in its own cargo run, runs it (through $EMULATOR
for cross builds; warns and leaves the files out without one) and installs the man pages and the
bash, zsh and fish completions into the staged tree. nfpm.yaml already ships the whole
usr/share tree, so it needs no change.
CADCraft has no FreeBSD packaging, so there is no FreeBSD script to update. macOS and Windows
packaging are untouched.
tests/man_and_completions.rs checks the generated file names.
* ci(cli): check the command-line code with ast-grep (RFC-0012)
Adds ast-grep rules from RFC-0012, run by craft-common-actions'
reusable ast-grep workflow (Linux, no Rust build, path-filtered):
- clap-error-message: a string literal, format!, .to_string() or
SCREAMING_CASE const passed to Command::error / Error::raw
- clap-error-message-variable: the same message built into a variable
first, in the same function
- string-error: Result<_, String>, Err(format!(..)), Err(".."),
Err(x.to_string())
- args-os: std::env::args(), which panics on non-Unicode arguments
Scan paths: apps/cadcraft-cli/src, apps/cadcraft/src/launch_args.rs,
apps/cadcraft/src/main.rs. Rule tests run first in CI.
Source: photocraft#9750 at d16686cd. Only the scan paths and the
workflow path filters differ from it.
Fixes: none needed. The rule tests pass (4 passed) and the scan finds
nothing in the scanned paths.
* docs(cli): cadcraft-cli page, following RFC-0012
docs/cli.md: --help is the reference; exit codes, streams, usage
errors, values starting with -, paths, completions and man pages; the
environment variables (none for cadcraft-cli, CADCRAFT_CONTROL_PORT for
the desktop app's --control); and the rules not followed yet, with the
reason. The README links it.
* feat(cli): refuse an output that replaces an input
RFC-0012 rule 1.6: nothing destructive by default.
Behaviour change: `convert a.dxf a.dxf`, `run a.dxf … --save a.dxf` and
`run a.dxf … --export a.dxf` used to overwrite the input. They are now a
usage error (exit 2, `error: …` on stderr, nothing opened or written)
unless `--in-place` is given. "Same file" is file identity (same-file
crate), so `dir/./a.dxf` and a symlink to the input count. An output
that does not exist, or a failed identity check, is not refused; the
open or write reports the real error. Replacing an existing file that
is not an input is still allowed.
Scope: `--save` and `--export` only. A save that a --cmd or script runs
(for example `saveas`) is the engine's and is not checked.
Every write the CLI makes (convert OUT, --save, --export, sample OUT) resolves
an existing output first, so writing through a symlink updates the file it
points to and the link stays a link; the atomic rename used to replace the
link with a regular file, leaving the target (with --in-place, the input)
unchanged.
* feat(cli): --compact for JSON output
RFC-0012 rule 1.3: info and commands pretty-print their JSON by
default; --compact prints it on one line. run prints one JSON line per
--cmd result (JSON lines, one result per step), as its help and the
README document, so it takes no --compact. mcp's stdout is the MCP
protocol and is unchanged.
Behaviour change: info and commands accept --compact (it was an
unknown flag, exit 2). Their default output is unchanged.
docs/cli.md says which subcommand prints which form, and lists perf's
text table (no --json) and saves a --cmd or script runs (not checked
by --in-place) under "Not followed yet".
* fix(cadcraft): one Switch parser for boolean environment switches
CADCRAFT_VSYNC, CADCRAFT_SYSTEM_CURSOR, CADCRAFT_WAYLAND and
CADCRAFT_NO_NATIVE_MENU each had their own idea of "on". They now share
cadcraft_ui_egui::switch::Switch: 1/on/true/yes is On, 0/off/false/no is
Off (case- and space-insensitive), unset or blank is Default, and any
other value is Default and logged with the variable name. The ENVIRONMENT
help text says the same for all four.
Behaviour changes (old -> new):
- CADCRAFT_WAYLAND=0 counted as on (any value did); now only On keeps
native Wayland.
- CADCRAFT_NO_NATIVE_MENU=0 counted as on (any value did); now only On
keeps the menus inside the window.
- CADCRAFT_SYSTEM_CURSOR treated every value except ""/0/false/off as on;
now only the On spellings do, and an unknown value is logged.
- CADCRAFT_VSYNC is unchanged except that an unknown value is logged.
* fix(cli): keep paths as paths until a file is opened
A path is now text only where text is needed.
- cadcraft: FileNames.unreadable is Vec<PathBuf>, logged with {:?}
(escaped bytes) instead of a lossy rendering.
- cadcraft-cli: unicode() is called only inside open(), the one place
that sends a path to the engine as JSON. convert, sample and run
--save/--export pass the output as &Path. write_atomic, export,
export_framed and OutputError::Write take or hold a Path/PathBuf;
errors use .display(), and the `wrote ...` line uses .display().
- Dropped the dead find_subcommand lookup in launch_args (LaunchArgs has
no subcommands).
- docs/cli.md: non-Unicode names are refused only where a drawing is
opened.
Behaviour change: an output name that is not valid Unicode used to be
refused (exit 1); it is now written under that exact name. Opening such
a drawing is still refused.
Lossy strings that remain (all feed APIs that take text and only look at
the extension, never at the path the bytes are written to):
- cadcraft_io::write / write_framed (name picks the format) in sample.rs
and output.rs::export_framed;
- cadcraft_io::is_image in convert.rs, run.rs and args.rs (framing flag
check);
- cadcraft_engine::cmd::file::drops_unknown in export_framed.
These go away when cadcraft_io and the engine take impl AsRef<Path>
(the A1 follow-up). A non-Unicode extension is not a known format either
way, so it is rejected as an unsupported type, as before.
* refactor(cadcraft): read the environment once into Env
The desktop app reads its CADCRAFT_* variables once at startup into
apps/cadcraft/src/env.rs `Env` and passes the values on; no library crate
reads a CADCRAFT_* variable at run time any more.
- One `VARS` table (name, help line) feeds both the reader and the
Environment section of `cadcraft --help`. A test checks that every
variable the reader asks for is in the table, that every table entry
sets a field, and that the help lists every entry.
- vsync goes to `gpu::surface_config`, the system cursor to
`CadApp::set_system_cursor_default` (Options reset and Reset Palettes go
back to it), FONTALT/FONTFALLBACK to the `cadcraft_fonts::ttf` setters
(re-exported as `cadcraft_engine::fonts`), the settings directory to the
new `cadcraft_engine::settings_dir::set`, and to `Workspace::load`.
- The three copies of the settings-directory rule (main.rs, workspace.rs,
the engine's plotter folder) are one: `settings_dir::platform`.
- `Switch::from_env` is gone; `Switch::parse` stays.
- The child-process tests for CADCRAFT_CONTROL_PORT are replaced by tests
that inject the variable lookup (`blank_env_is_unset`) or point
`--control`'s fallback at PATH.
cadcraft-cli: plotter configurations matter to its output (`run --cmd plot`
or a script's PLOT with a user device, and the same through `mcp`), and the
font variables change how convert/run/sample draw text. So, following
RFC-0012 rule 1.7, the variables become fallbacks of flags declared with
`env =`: --config-dir (CADCRAFT_CONFIG_DIR), --fontalt (CADCRAFT_FONTALT)
and --fontfallback (CADCRAFT_FONTFALLBACK) on convert, run, mcp and sample.
Behaviour changes:
- CADCRAFT_CONFIG_DIR set but empty counts as unset everywhere: the
workspace file and the plotter folder use the platform folder (was:
workspace persistence and the plotter folder off). Logs and autosave
already treated it so.
- The workspace file and the plotter folder ignore a relative
XDG_CONFIG_HOME, as logs and autosave did (was: used it).
- A blank CADCRAFT_LOCALE means the system's languages (was: [""]).
- cadcraft-cli: the three variables apply only to convert, run, mcp and
sample, as flag fallbacks (was: read by the engine and fonts in every
subcommand, perf included). A font variable that isn't valid Unicode is
a usage error naming the flag (was: ignored).
* refactor(io): one Format enum for file extensions
`cadcraft_doc::Format` (re-exported as `cadcraft_io::Format`) is the one
place that maps a file name's extension to a format: Dxf, Dwg, Dwt, Svg,
Svgz, Png, Pdf. `from_path` is ASCII case-insensitive; no extension (or a
trailing dot) is Dxf; an unknown extension is None. `READ` and `WRITE`
list the formats `read` and `write` accept. It lives in cadcraft-doc
because the engine depends on cadcraft-io only for tests.
Each place used to match on its own lower-cased extension string and now
matches on Format, with the same results:
- io::read: svg/svgz -> SVG reader, dxf/none -> DXF reader, other ->
try DXF, else Unsupported(ext).
- io::write_with: dxf/none -> DXF, dwg -> DWG bridge, other with binary
-> error, rest -> write_framed.
- io::write_framed: dwt -> DWG; dxf/none/dwg -> write_with; svg, png,
pdf -> image writers; other -> Unsupported(ext).
- io::is_image: svg, png, pdf (not svgz, which is read only).
- engine file.rs is_dxf, drops_unknown (dwg, dwt), is_drawing_file
(dxf, dwg, dwt, none).
Behaviour change (help text only): the CLI help lists the formats from
Format::READ / Format::WRITE instead of hand-written lists. `info` and
`convert IN` now name svg and svgz as readable, and `convert`, `sample`,
`run --save` and `run --export` name dwt (and `sample` pdf) as writable.
A test checks each file argument's help names every format of its list.
* refactor: one version string for the app and the CLI
cadcraft_engine::build_version() builds "<version> (<commit>)" once; the
desktop app's --version, its start-up log line and cadcraft-cli --version
all use it. No change in output.
* refactor(cli): share the environment helpers with the CLI
Move blank_env_is_unset and name_env_vars from the desktop app into
cadcraft_mcp::args and apply them in the CLI parser.
Behaviour changes (old -> new):
- Empty or blank CADCRAFT_FONTALT / CADCRAFT_FONTFALLBACK: reached clap as
Some("") -> counts as unset.
- A bad value from an environment variable: the error named the flag ->
names the variable.
- CADCRAFT_CONFIG_DIR is unchanged: empty still means no settings folder.
- Desktop app: unchanged; it uses the shared helpers.
* refactor: pass paths, not text, through io, engine and open_path
cadcraft_io::{read, write, write_with, write_framed, is_image}, the engine's
drops_unknown/is_drawing_file/read_text_file and CadApp::open_path take
paths (impl AsRef<Path> / &Path). The desktop launch arguments and the CLI
no longer convert paths to text to call them.
Behaviour changes (old -> new):
- A dropped file whose name is not valid Unicode opened with a "can't
read" error naming a mangled path; it now opens natively. (Its path is
not remembered for QSAVE, which then asks where to save.)
- A launch argument that is not valid Unicode was logged and skipped; it
now opens like any other.
- A .dwt export builds the .dwg name as a path, not formatted text.
- On the web, a non-Unicode path passed to open_path reports "the path
is not valid Unicode" instead of a lossy name.
- Opening errors print path.display().
FileNames and LaunchArgs::file_names are removed; the launch tests assert
the parsed PathBufs. New io test: non-Unicode names pick the format by
extension.
* docs(cli): CADCRAFT_CONFIG_DIR is the one flag fallback where empty isn't unset
* fix(engine): refuse a drawing path that isn't valid Unicode
read_file opened such a path with no saved path, so QSAVE could not save back to it.
It now returns an open error naming the file before reading it. The ui-egui opener
already reports read_file errors like any other failed open.
* fix(cli): report a failed write of help or version
A write error on help or version output (other than a closed pipe) now exits 1 and
tries to say so on stderr; it was ignored and exited with the clap code. A closed pipe
keeps the clap exit code.
* fix(cli): --connect needs HOST:PORT
--connect example.com was accepted and failed later. It is now a usage error (exit 2)
unless the value is a non-empty host (or a bracketed IPv6 address) and a u16 port.
The host is not resolved.
* docs(cli): run prints no line for a command with no result
* refactor(cli): explicit imports and smaller functions for the stricter lints
Lints: clippy::wildcard_imports, too_many_arguments, cognitive_complexity (30).
Replaced the non-test `use super::*;` in args, commands, convert, info,
output, perf and run with explicit imports; lib.rs keeps only the imports
its non-test code uses, and its test module imports the rest itself. The
`super::*` in the test modules stays.
A clippy run with all three lints and threshold 30 reports nothing in
apps/cadcraft-cli or in code this branch added, so no function needed
splitting.
* refactor: replace wildcard imports with explicit ones (S2208, batch_02)
* Fix unused and missing imports the batch's CI gate reported
Remove two unused imports and restore the json macro and resolve_alias
in test modules that the explicit import lists had dropped.
* fix: keep the imports the explicit lists still need after the rebase
* fix(draw2): import machines, distinct_points and ok from cmd
* 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>
* 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>
* Block Editor: BEDIT, BSAVE, BSAVEAS and BCLOSE for static blocks
BEDIT opens a block definition in the drawing's own tab with its base point
at the origin, and the block's objects are edited with the ordinary commands.
BSAVE writes them back into the definition, so every reference in the drawing
updates (one undo step in the drawing); BSAVEAS saves under a new name;
BCLOSE goes back to the drawing, asking whether to save unsaved changes.
- The drawing waits untouched while the editor is open (blockedit.rs): its
undo history, selection and views come back on BCLOSE. Objects left
unchanged are saved back exactly, so no coordinate drifts.
- Files save the drawing outside the editor; autosave keeps the editor's
unsaved changes in the block; the close/quit prompt's Save puts them into
the block first.
- Right-click a selected block reference: Block Editor. In the editor the
canvas is tinted, the base point is marked, and a bar offers Save Block,
Save Block As and Close Block Editor (also in the right-click menu). The
Block tool set's Block Editor button is now live. BLOCKEDITOR sysvar.
- Layout and paper-space commands are refused while the editor is open; a
block can't be saved to contain a reference to itself.
- Dynamic blocks: their data is kept; the editor says it can't edit their
parameters yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* BEDIT: find dynamic block data on the block's BLOCK_RECORD
A dynamic block's ACAD_ENHANCEDBLOCK dictionary hangs off its BLOCK_RECORD
table entry, not the BLOCK entity, so the "parameters are kept" note never
showed for AutoCAD files. Regression test with the data laid out as an
AutoCAD DXF has it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* BEDIT: a dynamic block reference opens its dynamic block
A dynamic block reference with values of its own shows an anonymous block
(`*U12`), so the right-click Block Editor (and BEDIT on a picked or selected
reference) refused it and asked for a name. The anonymous block's record
names its dynamic block in AcDbBlockRepBTag xdata; BEDIT now opens that, as
AutoCAD does. Regression test.
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>
* AutoLISP: load and run .lsp routines (#758)
CADCraft runs AutoLISP routines. `.lsp` files load with APPLOAD, (load)
or by dropping them on the window, and at startup from the support folder
(acad.lsp once per session or per drawing with ACADLSPASDOC, acaddoc.lsp
per drawing, then S::STARTUP; never from a drawing's own folder).
(defun c:NAME …) defines commands, (autoload) defers loading to first
use, and (expression) and !variable work at the command line and at
prompts. docs/autolisp.md lists what is supported.
The interpreter (crates/engine/src/lisp) reads source with its own
stack and evaluates on an explicit-stack machine, so a routine can stop
for input (getpoint, pause in command) and carry on later on one thread,
deep recursion is an error rather than a crash, and long loops run a
slice per frame (Esc stops them). It follows AutoLISP: 32-bit wrapping
integers, dynamic scope, *error*, vl-catch-all-apply, AutoLISP's error
wording. About 200 built-ins cover the language, command (with pause and
commands left open), entity lists (entget/entmake/entmod/entdel/entlast/
entnext/handent, through the DXF reader and writer's groups, exposed by
cadcraft-io and handed to the engine through a hook), ssget with modes
and filters, tables, system variables, get… input with initget, and
units. A routine is one undo step; each drawing has its own symbols.
Also: keywords typed as _KEYWORD match at prompts; TEXT run by a
routine's command takes one line; DIMENSION records carry their
measurement (42); Prompt::raw shows a get… prompt as written; the
interface asks Session::prompting() (a command's or a routine's prompt)
where it asked whether a command runs, so clicks, right-click and Delete
work at a routine's prompts; getcorner and based get… prompts drag a
rubber band.
Tests: the language, errors, startup files, interactive input, hostile
input (lisp/tests.rs); the #648 corpus, 18/18, when CADCRAFT_LISP_CORPUS
points at it (tests/lisp_corpus.rs); drop, frames and canvas clicks in
the app (ui-egui/tests/lisp_files.rs).
* AutoLISP: bound memory, drop nested values iteratively, guard the per-frame tick
- Cons cells drop with a work list over both car and cdr (and function
bodies), so a list nested a million deep in its car no longer overflows
the stack when it goes away.
- Memory caps so a runaway routine fails with a LISP error instead of an
uncatchable out-of-memory abort: at most 5,000,000 live cons cells
(counted in Value::cons / Drop, checked every step; cells the drawing's
variables already held at the run's start don't count against it), no
string a built-in makes longer than 8 MiB (strcat checks before joining,
printed text is cut), and a 2 GiB per-run budget for new strings,
printed text and files read.
- Session::lisp_tick runs under catch_unwind, and so does every slice of
the LISP machine: a panic ends the routine with "internal error", puts
the drawing back and leaves the session usable.
- Regression tests for each (deep car nesting, nested functions, the cell
cap, string length and budget, a panicking built-in in a blocking run
and in a ticked one).
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>
Bump acadrust to 0.6.4 and read DWG in its failsafe mode: an object
record the reader can't decode is skipped instead of failing the whole
file. The drawing's new read_notes (never saved) say how many records
were left out and of which types; OPEN echoes them, returns them as
`warnings`, and the app shows the first in the status bar. A file whose
objects stream can't be read at all is still an error.
acadrust 0.6.4 writes a merged table cell's anchor with 173=0 and the
span, so the DXF reader now takes a merge from the span alone. Unknown
records the DWG writer can't write back are dropped before writing.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* REFEDIT, REFSET and REFCLOSE: edit block references in place (#813)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Move REFEDIT translation lines into the middle of the catalogs (avoids PR merge conflicts)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
From AutoCAD 2013 on, a 3DSOLID, REGION or BODY doesn't carry its ACIS
geometry: the binary SAB lives in the file's data store section (ACDSDATA),
one ACDSRECORD per entity bound by a 320 handle. CADCraft kept those
entities (as unknown entities, with their handles) but dropped the section,
so a drawing saved as DWG or DXF had empty solids, and saving reported them
as not saved.
- doc: Foreign::data_store keeps the section's groups as read;
data_store_entities() lists the entities it has records for.
- io: the reader keeps the section; the writer puts it back for files of
2013 and later (and in the DXF the DWG bridge converts, which acadrust
reads into the DWG), without the records of entities no longer written.
Nothing is written for older files or when no record's entity remains.
- engine: not_saved_as(d, version): from 2013 a solid, region or body with a
data store record isn't reported; saves and document.bytes pass the version
being written.
On a 39,600-object permit set with 625 3D solids and 2 regions, DWG ->
CADCraft -> DWG now keeps all 627 bodies with byte-identical SAB (before:
none), and the save no longer reports them.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>