52 Commits
Author SHA1 Message Date
a2b6c9ac4a Animation effects follow the operating system's setting; View ▸ Accessibility ▸ Animation Effects (#759)
With Windows set to show no animations (Settings ▸ Accessibility ▸ Visual
effects ▸ Animation effects), dialogs and menus still faded in and wheel
input glided. All of that motion is egui's default behaviour, and nothing
asked the operating system.

- crates/ui-egui/src/animation.rs: AnimationPref (System, On, Off). Off sets
  egui's animation_time to 0 and scroll_animation to none (window, menu,
  tooltip and popup fades, collapsing sections, scroll bars, scroll-to
  glides), hands each wheel notch to scroll areas whole, and lets
  Cmd/Ctrl+wheel zoom reach its zoom in one step.
- System, the default, follows the OS live: UISettings::AnimationsEnabled on
  Windows, NSWorkspace's Reduce motion on macOS, the XDG Desktop Portal's
  reduced-motion (then GNOME's enable-animations) on Linux and the BSDs,
  prefers-reduced-motion on the web.
- View ▸ Accessibility ▸ Animation Effects ▸ Use System Setting / On / Off,
  the same choice in Options ▸ Display, and `ui.animations`, `options` and
  `ui.set` {"animations"}; `ui.inspect` reports whether animations show.

Drafting behaviour is untouched: tracking's acquisition pause, the
time-sensitive right-click, previews, the caret blink, realtime pan and
zoom. Plain wheel zoom already moved a whole notch per frame (#692).

macOS and Linux are compile-checked (clippy -D warnings for
aarch64-apple-darwin and x86_64-unknown-linux-gnu), not run.

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-13 05:34:56 -04:00
8166e4a809 AutoLISP: load and run .lsp routines (fixes #758) (#855)
* 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 <[email protected]>

---------

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-13 05:33:57 -04:00
dc92f38772 DXF: count the records the reader can't place; info reports them, convert warns (part of #865) (#872)
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-13 05:31:58 -04:00
25e4900fcd Recent files: the Start page and File ▸ Open Recent list the drawings opened last (fixes #858) (#864)
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-13 04:44:44 -04:00
Marco Arnulfo 3f709bed2e Desktop: a bare --control keeps the CADCRAFT_CONTROL_PORT port (fixes #844) (#848)
`--control` as the last argument read `args.next()` as None and reset the
port taken from CADCRAFT_CONTROL_PORT, without logging anything, so
`CADCRAFT_CONTROL_PORT=47321 cadcraft --control` started no control
channel. A bare `--control` now keeps the variable's port and logs a
warning either way; a port given on the command line still wins. The
precedence is documented in docs/control-protocol.md.
2026-10-13 04:37:52 -04:00
Books 7fcdc6e3bc Active viewport highlight: selection on the GPU, hover cached apart, kept inside a clipped outline (#770)
* Active viewport: redraw while panning and zooming inside it, without rebuilding the layout (#688)

Inside an active MSPACE viewport, middle-drag, trackpad pan and wheel or
pinch zoom change the viewport's view through set_view_quiet, which makes
no new revision. The layout's display list, which draws model space
through each viewport, was cached on (drawing, revision, zoom band), so it
kept showing the old view until a command such as ZOOM rebuilt it: the
viewport froze, or appeared to ignore the zoom.

The active viewport's contents are now drawn apart from the layout's list,
the way model space itself is drawn. They are built once for an area
around the viewport (half its larger side of margin all round), then moved
and scaled to the current view on the GPU and drawn under the layout's own
objects. They are rebuilt when the drawing changes, when the zoom has
moved 2x from the view they were built for, when the view leaves the built
area, and once a zoom comes to rest, so dash lengths and curve detail
follow the viewport's scale again. A pan step takes about 1 ms with 70,000
polylines, where rebuilding the layout takes about 140 ms per frame.

Rectangular viewports are clipped with the scissor rectangle. A clipped
viewport's outline is drawn into a stencil buffer as a triangle fan that
flips a bit (even-odd, as the outline clips on the CPU), and its contents
draw only where the bit is set; the native window now asks eframe for an
8-bit stencil buffer, and every canvas pipeline names its format. Where
there is no stencil buffer (the CPU canvas, the web runner), a clipped
viewport's contents stay in the layout's list, whose cache now follows the
viewport's view.

Render: Options::skip_viewport leaves a viewport's contents out of a
layout, and viewport_content builds them for any area. Viewport contents
are built on all cores, like model space (130 ms to 37 ms for 70,000
polylines).

* Active viewport highlight: selection on the GPU, hover cached apart, kept inside a clipped outline

Inside an active MSPACE viewport the selection highlight was drawn as
dashed egui shapes on the CPU every frame, about 8-13 ms per frame with
7,000 polylines selected, and its geometry was cached together with the
hover, so moving the cursor over objects tessellated the whole selection
again. The selection and the hover are now cached apart, and the
selection goes through the GPU highlight path model space already uses:
about 0.1 ms per frame. In a clipped viewport it is kept inside the
outline with the stencil mask its contents use; before, it was only cut
to the outline's bounding rectangle.
2026-10-13 04:34:40 -04:00
aldozarandClaude Opus 5.5 483a9304ca cadcraft-cli mcp: --lazy and --launch for clients that start before the app (#756)
MCP clients start their servers when they start, usually before CADCraft is
open, and `--connect` refuses to start then. `--lazy` starts the bridge
anyway and reaches the app at the first tool call; until the app answers,
each call fails with how to open it. `--launch` (implies --lazy) opens
CADCraft on macOS with that control port at a call that finds nothing, and
waits up to 30 s. Neither falls back to a headless session (#103).

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-13 04:34:14 -04:00
SteamSmithandClaude Opus 5.5 61c018845a control server: bound request lines and concurrent connections (#678)
Squash of PR #678 by @SteamSmith.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 03:12:34 -04:00
PedroGuilleandClaude Opus 5.5 ac9483c9c1 Add raw-HID SpaceMouse 2D navigation (#650)
Squash of PR #650 by @PedroGuille.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 20:29:48 -04:00
Daniel LošákandClaude Opus 5.5 6d7c652b65 i18n: add Czech (cs) interface language (#639)
Squash of PR #639 by @Losminek.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 20:11:51 -04:00
Gert Pellin - GPEandClaude Opus 5.5 f7c6293d09 feat: SVG import — read SVG and SVGZ into native entities (#636)
Squash of PR #636 by @switch87.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 20:11:22 -04:00
aldozarandClaude Opus 5.5 1e1f68bf40 Fix fatal wgpu buffer overflow; draw MOVE/COPY/ROTATE/SCALE previews and the selection highlight on the GPU (#642)
Squash of PR #642 by @aldozar.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 20:10:51 -04:00
STE1200andClaude Opus 5.5 244feb6e89 Render: level of detail and view clipping for pattern hatches on screen (fixes #544) (#638)
Squash of PR #638 by @STE1200.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 20:10:37 -04:00
Fabio LussiandClaude Opus 5.5 370150186b Linux: Theme > System follows the desktop's light/dark setting (#641)
Squash of PR #641 by @itrycodinganditsbad.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 19:53:48 -04:00
PedroGuilleandClaude Opus 5.5 f967dc7aca Persist SNAPUNIT and GRIDUNIT in DXF (fix #607) (#614)
Squash of PR #614 by @PedroGuille.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 14:28:56 -04:00
MarekSpodBiedryandClaude Opus 5.5 5e71a75383 Web: ?cpu selects the CPU canvas instead of installing the GPU canvas (#500) (#592)
Squash of PR #592 by @MarekSpodBiedry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 14:27:46 -04:00
PedroGuilleandClaude Opus 5.5 ce1cc631d5 Windows: use FXC by default for DirectX 12 startup (#613)
Squash of PR #613 by @PedroGuille.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 13:39:24 -04:00
Valentin KleinandClaude Opus 5.5 74e72c93f8 Plotters: device configurations with paper sizes and printable areas (fixes #564) (#570)
Squash of PR #570 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 13:25:00 -04:00
Gert Pellin - GPEandClaude Opus 5.5 18b4a8d0dd CLI: cadcraft-cli exits quietly when stdout is closed early (#568)
Squash of PR #568 by @switch87.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 13:24:30 -04:00
Valentin KleinandClaude Opus 5.5 d0dd5cfe16 Support files: DWT templates for NEW/QNEW, SAVEAS .dwt, user alias files (fixes #552) (#562)
Squash of PR #562 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 13:12:32 -04:00
Valentin KleinandClaude Opus 5.5 a4504cc098 DXF: keep what we don't model on save — unknown entities and objects, foreign xdata, extension dictionaries, reactors, colour books (fixes #528) (#545)
Squash of PR #545 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 12:46:58 -04:00
Valentin KleinandClaude Opus 5.5 b13f1e6fa4 OPTIONS: preferences dialog, JSON form and a user profile that survives restarts (fixes #23) (#531)
Squash of PR #531 by @vangel76.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 12:34:34 -04:00
CyberFox-81andClaude Opus 5.5 7dead193e4 Web: opt-in embedding bridge for host pages (?host=parent) (#509)
Squash of PR #509 by @CyberFox-81.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 11:47:27 -04:00
Valentin KleinandClaude Opus 5.5 b3a73626b2 SAVEAS version and binary DXF, default save version (fixes #421) (#431)
Squash of PR #431 by @vangel76.

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 11:18:49 -04:00
skylar taylor-barrick a3f43a700f UI: recover persisted docking without resetting workspace preferences 2026-10-10 18:49:16 -06:00
skylar taylor-barrick 58a8494f98 UI: retain shared workspace while merging current main 2026-10-10 18:31:07 -06:00
Valentin KleinandClaude Opus 5.5 cdd25840cc Control channel: a timed-out request is cancelled, and the reply says whether it ran (fixes #345) (#348)
The control server gave up after 60 s with "timeout" but left the request
queued, so the app ran it later and a client retry applied it twice.
Requests now carry a shared state: on timeout the server cancels a request
the app has not started (reply state "not-run", safe to retry) and the app
skips it; one already started replies state "may-have-run". The MCP bridge
passes that on in its error.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-10 17:32:19 -04:00
by TruittandClaude Opus 5.5 afe8b93255 Save As: offer DWG, with the drawing's format first (#198)
The Save As dialog listed DXF, SVG, PNG and PDF, though CADCraft writes
DWG. On macOS the dialog accepts only listed extensions, so a name typed as
`part.dwg` was changed to .dxf and the drawing was saved as DXF.

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

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


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

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-10 15:34:18 -04:00
skylar taylor-barrick e09fb50627 UI.9: integrate shared workspace docking and preserve CAD controls 2026-10-10 13:30:09 -06:00
Valentin KleinandClaude Opus 5.5 3b73884abd Deliver browser saves as downloads; keep unsaved state when they fail (#133) (#193)
On wasm, saving skipped the file write but still marked the drawing saved,
renamed its tab and reported success, so nothing reached the user. Saving
now hands the bytes to a host delivery hook (`file::set_deliver`); the web
app installs a small web-sys download helper (Blob + object URL + temporary
<a download>), reusable for other files such as plots. Without the hook, or
when the download can't be started, save errors and the drawing stays dirty.
A started download counts as saved and keeps the name for later QSAVEs.
The web app's Save/Save As (menu, close prompt) download under the drawing's
name, as there is no file picker in the browser.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-10 14:39:58 -04:00
Valentin KleinandClaude Opus 5.5 a560048dd0 Web: File > Open… shows the browser's file picker (fixes #55) (#199)
The web app ran CadApp with Services::default(), so OPEN found no file
picker and returned without doing anything: no picker, no message.

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

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

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

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-10 14:37:26 -04:00
DmytroandClaude Opus 5.5 659d0fa125 i18n: add Ukrainian interface translation (#36)
Rebase onto main: the interface language is saved with main's other
preferences (cadcraft.prefs, beside the theme) instead of its own storage
key, on desktop and web; strings main added or changed in the touched
places (Dynamic Input settings, Properties header and current-property
rows, block picker, theme and command-history menu items, open/save
filters) are wrapped and translated; main's native-menu poll order is kept.

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

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

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-10 13:23:37 -04:00
3e0adadf94 Dynamic Input: pointer input boxes with Tab, comma and relative entry (#60)
* Dynamic Input: pointer input boxes, Tab separator, relative entry

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr

* Dynamic Input tooltip: border colour from theme tokens

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 13:04:59 -04:00
3e7e0bdf2a Linux: default to XWayland for file drag-and-drop on Wayland desktops (#159)
* DIMANGULAR: accept polyline segments (fixes #10)

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

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

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

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

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

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

* SCALE: Reference option (#17)

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr

* Add Tru10 to contributors/people.toml

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>

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

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

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

Fixes #83. Fixes #104.

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>

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

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

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

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

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

---------

Co-authored-by: SBaksa <[email protected]>
Co-authored-by: DWAA1660 <[email protected]>
Co-authored-by: MarekSpodBiedry <[email protected]>
Co-authored-by: lloyd <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: vangel76 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Tru10 <[email protected]>
Co-authored-by: Gert Pellin <[email protected]>
2026-10-10 12:58:26 -04:00
MarekSpodBiedry 986307f764 Print and Ctrl+P ask where to save the PDF (#5) (#26)
PLOT/EXPORTPDF started from the menu, the toolbar, Ctrl+P or the command line had no path,
so the PDF was built in memory and dropped. They now open the save dialog (PDF filter first)
and write the file there. JSON calls from scripts, the control channel and MCP still never
open a dialog. Ctrl+P was shown in the menu but not bound; it is now.
2026-10-10 12:57:25 -04:00
DWAA1660 3278c58942 Fix #14: include DWG in the Open drawing filter (#21) 2026-10-10 12:57:20 -04:00
DWAA1660andClaude Opus 5.5 8be4b9b01c Default Windows startup to DirectX 12 (#31)
eframe's default wgpu instance enables PRIMARY | GL on Windows. Creating
the OpenGL backend can crash inside AMD's atio6axx.dll before the window
appears, so the app exits silently at launch. When WGPU_BACKEND is not
set, the Windows instance now enables only DirectX 12; an explicit
WGPU_BACKEND is still honoured. macOS, Linux and web are unchanged.

Same fix as storytold/designcraft#168 and storytold/filmcraft#179.


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

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-10 12:26:59 -04:00
cyfung1031 8b13fa3b17 fix(cadcraft-cli): reject unknown options (#120) 2026-10-10 12:26:40 -04:00
cyfung1031 083a3f095d Use macOS-margin runtime app icon on macOS (#128) 2026-10-10 12:16:55 -04:00
Valentin KleinandClaude Opus 5.5 3dd97cad56 Linux: present without a vsync queue so the crosshair follows the mouse (#25) (#183)
* Linux: present without a vsync queue so the crosshair follows the mouse (#25)

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-10 12:16:34 -04:00
Valentin KleinandClaude Opus 5.5 24a3954b39 Light and System interface themes (System follows the OS appearance live) (#179)
* Light and System interface themes (System follows the OS appearance live)

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

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

Fixes #59.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

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

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-10 12:15:58 -04:00
Valentin KleinandClaude Opus 5.5 bcbbc5e8a4 cadcraft-cli mcp: --connect without HOST:PORT is an error, not a headless session (fixes #103) (#167)
* cadcraft-cli mcp: --connect without HOST:PORT is an error, not a headless session (#103)

`mcp --connect` with no address (or an empty or flag-like one) fell through
to the headless backend, so a client meant for the app got successful edits
in a separate drawing. The mcp arguments now go through parse_mcp_args:
--connect must be followed by a non-empty value that doesn't start with `-`,
unknown arguments and a repeated --connect are rejected, and every error
exits non-zero before any MCP request is served. Plain `mcp` is still
headless; a real address still connects, and an unreachable one still fails.

- Unit tests for the parser; integration tests (tests/mcp_connect.rs) run the
  binary: missing/empty/flag-like address, headless control, loopback
  endpoint routing, closed port.
- docs/mcp.md says what happens without an address.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

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

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

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-10 12:15:18 -04:00
Brandon ThomasandClaude Opus 5.5 7c8404794a cadcraft-cli sample subcommand; apartment example drawing
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 01:39:02 -04:00
Brandon ThomasandClaude Opus 5.5 2287134dbd Incremental R-tree: patch only changed store chunks after an edit
A pick after one edit at 200k entities drops from ~110 ms (full rebuild) to ~4 ms;
steady-state picks stay at ~1 us. Adds a parity test against fresh builds and a
perf row for the incremental path.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 01:36:17 -04:00