The plot dialog asks CUPS printers for their sheets and margins; on Windows
the query was a stub, so every printer showed the paper catalogue with
unverified margins. The driver is now asked the way the plot dialog expects:
- windows_media (compiled and tested everywhere): the three parallel
DeviceCapabilities tables (ids, sizes in tenths of a millimetre, 64-WCHAR
names) become one PrinterMedia per distinct size — a rotated or
borderless twin folds onto its sheet, catalogue sheets keep their name,
unknown sizes carry the driver's — with the device's margins on every
entry and the driver's default sheet found by id or by user size.
- print_to_printer (Windows): DeviceCapabilitiesW for the tables, the
default DEVMODE for the default sheet, one portrait printer DC for the
unprintable margins (physical offsets and extents, per device — a
documented approximation). The DEVMODE fetch is shared with the print
path (with_printer_devmode).
- A direct print now asks the driver for the plot's own sheet (by id, or
as a user-defined size), so an A3 plot prints on A3 rather than shrunk
into the driver's default; a driver that refuses the sheet is asked for
the orientation alone.
- The capability cache remembers printers that could not be asked, so a
slow failing query runs once per session, not on every dialog open.
- PRINTERS <name> reports a printer's sheets, margins and default sheet.
Tests: names (NUL trim, full slot, lossy), media folding and dropping,
default by id / user size / none, DC margins, paper id lookup.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On the web build, {"op":"open","path":…} resolves the name against the
browser's recent-files cache, so a file that is not already there cannot be
opened by the control channel. Accept {"op":"open","name":…,
"data_base64":…} instead: the bytes go through the same parse, caching
and recovery path as a file chosen in the browser picker
(io::open_bytes_web, now shared with pick_and_load_web).
Native builds keep path-based open and reject data_base64 with
invalid_request.
Refs #1349
load_file, used by headless export, xrefs and block import, finalizes a
drawing without purging, so the spline degree repair never ran there and
`--export drawing.dxf out.dwg` still hit the knot underflow. Run the repair
when any load is finalized.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Retaining a line's length added a P2PDistance constraint, whose gradient
divides by the current distance. For a zero-length line the solver got a NaN
Jacobian and its SVD fallback never returned, freezing the editor (e.g.
ECONSTRAINT on a line drawn by clicking one point twice). Hold coincident
endpoints together with per-axis differences instead.
Control-point splines read without knots are given a degree their control
points can carry (at most n - 1) and a clamped knot vector when a drawing
loads. Generating knots for fewer points than the degree underflowed during
DWG write: a panic in debug builds, an unbounded allocation in release, and
reachable from autosave. A lone control point with no knots or fit points is
purged as corrupt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A page setup names its plotter, and AutoCAD looks that name up in its own
configurations: `DWG To PDF.pc3` for PDF output, the spooler name for a
system printer, `None` when nothing is assigned. Drawings saved here used
to carry the dialog's label ("Save to PDF file…"), so AutoCAD reported a
missing device on every layout plotted to PDF from Open CAD Studio.
- New `io::plot_device`: `PlotDevice { None, Pdf, Printer }` with AutoCAD's
spellings, a reader for stored names that keeps any plotter
configuration AutoCAD wrote (an HP `.pc3`, `DWF6 ePlot.pc3` — plotted to
PDF here but preserved for AutoCAD) and upgrades the legacy label.
- Printable areas: the PDF driver's margins per sheet variant (full bleed
edge to edge; standard / expand insets provisional until read off
AutoCAD's plotter configuration), and for CUPS printers the media and
margins the queue reports over IPP (`ipptool` `media-col-database`),
parsed once per printer and cached. Windows enumeration follows with
native GDI printing.
- Page setups now store the device in AutoCAD's spelling and the margins
of the chosen device/sheet, keeping the drawing's own values while
neither changed (AutoCAD's `PAPERUPDATE` behaviour). The paper-space
printable-area rectangle follows.
- Plot dialog: the default entry names the printer it resolves to; a
printer that answered lists its own sheets with a note; the media query
runs in the background and answers for a since-deselected printer are
dropped.
Tests: device names, stored-name resolution, PDF margin table, IPP media
parsing (sizes, margins, borderless folding, default sheet), PWG names,
plus dialog ↔ layout round-trips for PDF, legacy label, preserved plotter
configuration, sheet change → margins, and printer selection.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AutoCAD stores a layout's sheet as the plotter driver's canonical media
name (`ISO_A4_(210.00_x_297.00_MM)`, `ANSI_full_bleed_B_(11.00_x_17.00_Inches)`),
so a drawing saved here has to carry the same spelling for AutoCAD to find
the sheet in its DWG To PDF list, and a name that arrives from AutoCAD must
resolve to its dimensions and go back out untouched.
- New `io::paper_catalog` replaces the eight-entry `paper_sizes`: ISO A/B,
ANSI, ARCH (incl. E1) and JIS series with full-bleed / expand variants,
Letter / Legal / Tabloid, millimetre and inch units, a parser that
reconstructs any well-formed AutoCAD name (custom or driver-specific)
from its embedded dimensions, driver / legacy-config aliases (`A4`,
`Letter`, `Ledger`), and dimension matching that prefers the plain sheet.
- The Plot dialog stores the canonical name and shows the sheet as
`ISO A4 (210 × 297 mm)`; a sheet that only exists in the drawing is
listed first so it stays selectable.
- A sheet the user did not change keeps the drawing's own media name
(`paper_name_for_settings`), so a Windows printer's `A4` is not rewritten
to `ISO_A4_(…)` just by passing through the dialog.
- Drops the unused `PlotFormat` / `PlotOrientation` messages,
`paper_label_from_dims`, and three duplicated `match "A4" | …` tables.
Tests: catalogue spelling, parser round-trip, aliases, dimension matching,
display formatting, plus dialog ↔ layout round-trips for driver-named,
catalogue and unknown sheets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- IMAGEEMBED: pack raster images into OLE2FRAME entities so a .dwg is
self-contained; 'Embed Image' entry in the XREF attach menu; control/MCP
op embed_image for external tools; dxf_name for OLE2FRAME queries.
- Scene: populate_images_from_document now rebuilds OLE2FRAME and PDF
underlay models too (they used to vanish after any XREF op).
- Printing: add PRINTER_ENUM_NETWORK pass when listing printers; GDI
spooler fallback (hayro raster at 300 DPI + StretchDIBits) for hosts
whose PDF handler registers no print verb; DEVMODE orientation with a
90-degree raster rotation fallback for v4 drivers; fix the fit-scale
precedence bug that printed blank pages.
- PDF export dialogs on Windows use the async file dialog so the native
overwrite confirmation no longer deadlocks the UI thread.
- Plot dialog: persist the chosen printer (saved on selection) and the
picked plot window across sessions; page setups with no device no
longer reset the output device.
- Community fonts: detect unresolvable .shx references on open and offer
a download from the repository's fonts/ folder (contribution guide in
fonts/README.md); custom font source URL setting for intranet font
servers; downloaded fonts resolve from the per-user fonts directory.
Path normalization with stable keys, relative/absolute/none pathtypes, wildcard matching, and reference entries with loaded, stale, missing, and unreferenced states.
Problem
The open guard rejects circles and arcs with a radius below 1e-10. A normal
open fails when a drawing contains one. The user gets the recovery prompt.
AutoCAD and nanoCAD open these drawings.
The check came from ace99541. Its comment says the tessellator cannot sample
a degenerate curve. That was true of truck's parameter_division. Commit
8c7ebdc9 removed truck. Commit 96be8214 made the kernel sample circles and
arcs by sweep angle with a floored segment count. The reason for the check
no longer exists.
Change
- Remove the near-zero radius check for circles and arcs.
- Keep the checks for non-finite values and radii above 1e10.
- Update the guard comments. The purge rejects parser desync. It does not
reject degenerate geometry. Be generous in what you accept and strict in
what you produce.
- Add tests: a zero radius passes the guard, an absurd radius does not.
- Change the automation purge test sample from radius 0 to radius 1e11.
Behavior
- Zero-radius circles and arcs load. They render as dots.
- Tessellation, hit testing, snapping, and hatch boundaries accept a zero
radius.
- The DXF writer does not change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Resolve curve intersections through cadkernel, retain empty exact results, gather analytic candidates independently of tessellation, and bound material uploads. Preserve large MINSERT data while limiting rendering work; remove obsolete review narratives.
Automation's "open" op called io::load_bytes directly and installed
the result as-is, skipping everything the canonical path-based loader
(load_file_for_open / finalize_loaded_outcome) runs after parsing:
block-origin normalization for legacy non-zero-origin blocks,
relative raster/material path resolution, source_path assignment,
and the corrupt-entity purge the UI open path always applies. The
same file opened two ways behaved differently, and headless/automation
use was more exposed to malformed geometry with no filter at all.
Added io::load_bytes_finalized(path, bytes), which runs load_bytes
plus that same normalization and purge using the real path (not just
its basename, which the old code passed to load_bytes as the "name"
parameter — losing the directory needed for raster-path resolution
and source_path). automation.rs's "open" handler now uses it and
reports the purge count as a "purged" field in the response.
load_bytes itself is unchanged, since the byte round-trip tests
(sketch_persist.rs, named_parameters_persist.rs) deliberately want
the simpler byte-only path with no real filesystem path behind it.
New test constructs a document with one normal and one corrupt
(zero-radius) entity, saves it, opens it through automation, and
checks the corrupt entity was purged and source_path was set —
proving the automation path now matches the UI path.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two independent MINSERT (array INSERT) bugs found while auditing
render_graph.rs:
1. array_offsets() reserved and filled one Vec element per array
instance, with row_count/column_count parsed as unchecked u16s.
A corrupt or adversarial 65535x65535 pair requests 4,294,836,225
instances (~96 GiB) before any GPU-side chunking limit can help,
and before the render graph even reaches the GPU. Fixed with a
documented instance budget (20,000) enforced at both the load-time
corrupt-entity filter (is_entity_corrupt, matching the existing
polyline/spline vertex-count guards) and a failsafe clamp in
array_offsets itself, for an insert built or edited some other way.
2. insert_instance_transform composed the per-instance row/column
offset translation *before* the INSERT's own transform, so the
block's x/y scale multiplied the spacing along with the block
contents — an INSERT with x-scale 2 and column spacing 10 rendered
20 units between columns instead of 10. The pinned CAD model's own
Insert::array_points() confirms the correct behavior: spacing
rotates with the insert but is never scaled. Fixed by orienting the
offset with rotation + OCS only, then translating the already-
composed (scaled) instance transform by that world-space vector.
7 new tests cover both: pathological/reasonable instance counts at
the load-time filter and the render-graph failsafe, and instance
transforms for scaled and rotated arrays.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The 52 s silent stretch in a large-DWG open could not be attributed:
OpenTimings covered parse / purge / xref / caches, but the FINALIZING
phase -- prepare_open_geometry, which does the first full Model
tessellation and builds the interaction index -- was unmeasured and
reports only two progress ticks, so it was indistinguishable from parse
time.
Add finalize_ms to OpenTimings and a per-stage PERF breakdown:
open-phases parse / purge / xref / caches / finalize
open-finalize tessellation vs interaction index
open-index index build vs prepare_screen
wires-build sort cache, visible filter, block cache, classify,
memo hits, parallel tessellate, materialize, sort
resident-post scene markers, refedit fade, resident layout
wire-upload build_wire_buffers, which runs on the render thread
The wires-build line separates the parallel tessellate from the serial
materialize loop that deep-clones every WireModel while the memo retains
a second copy -- the leading hypothesis for both the stall and the
6.45 GB RSS.
No behaviour change. Every timer is gated on crate::perf::enabled(), so
nothing runs without PERF=1 or the in-app PERF panel. The localized
open-summary format string is left alone; changing it would rekey the
string across every locale, so the full breakdown goes to PERF instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Saving to a path that already holds a drawing fails on Windows with
"could not verify <path> before saving: ... (os error 33)". Save As over any
existing drawing is affected; saving to a new path is not.
EditLease::acquire() opens the destination and takes the platform drawing
lock on it, then the save fingerprints the same path to check nothing changed
underneath. LockFileEx is mandatory and scoped to the handle that took it, so
that second open is refused against our own lock, lands in the non-NotFound
arm and aborts the save. flock on Unix is advisory and the second open
succeeds, so neither CI nor a Linux desktop sees it.
Read through the handle the lease already holds instead. A duplicate shares
the lock ownership, so the pre-replace re-check on the save worker can use
one too rather than re-opening the path.
The same collision reached the headless open op, which read a path this
session may hold leased: automation's save_then_open_round_trips fails on
main for that reason, one line after the save it no longer reaches.
The last four truck crates are gone from the manifest. What they were doing
falls into three parts, and each now has one answer instead of two.
NURBS evaluation. truck was carrying B-spline curves and surfaces for the
SPLINE entity, hatch boundaries, BLEND's endpoint frames, the spline preview
and ACIS spline-surface faces. The kernel now has both, over one de Boor
written across however many coordinates a control point holds — so a plane
curve and a space curve cannot drift apart, and a surface is the same
algorithm applied twice. The rational and the polynomial cases stop being
separate types: weights absent means polynomial.
The entity conversion. Every LINE, ARC, ELLIPSE, SPLINE and polyline was
built into truck topology purely so it could be sampled back into points.
They are sampled through `entities::curve` now, which is where each one's
geometry is already defined once and what EXTRUDE and REVOLVE read — so a
circle drawn on screen and a circle handed to the Model tab come from the
same definition. That retires four of TruckObject's variants and the module
is renamed for what it does.
SWEEP and LOFT. Both only ever produced a mesh, so both are built from point
lists: a band of quads per span, and a lid where a profile closes. Lofting
profiles of different densities resamples them by distance rather than by
index, so a circle lofted to a square no longer twists.
Two things worth noting for anyone reading the old comments. The tolerance
globals that lived in the tessellation module were never about truck at all —
they are the per-frame chord height, and they move to `curve_tol`. And the
rule that a profile handed over as `Lines` silently broke EXTRUDE and REVOLVE
no longer holds: those read the curve directly and never look at this channel.
The two `automation` test failures are unchanged from before this and are not
caused by it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reading a drawing by path is gated off on wasm, and two features added
since 0.9.3 call it unconditionally: importing a drawing as a block, and
taking layer standards from one. Both compile into the web build, so the
web bundle stopped building and the v0.9.4 Pages deploy failed.
Neither feature can work in the browser — a file arrives through the page,
not from a filesystem the app can open — so the fix is not to make them
work but to let them say so. `load_file` now exists on wasm and returns
that as its error, which both callers already surface. Keeping the
signature means neither has to know that the web has no files, and the
next feature that reads a second drawing will not break the bundle
either.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drawings arrive from elsewhere carrying someone else's layer names, and
folding them into a set of house layers meant renaming and merging by
hand, one layer at a time.
LAYTRANS reads a layer set out of another drawing and moves this one onto
it. Given a file it maps every name the two already share and translates
at once, which is all most incoming drawings need. Bare, it opens a
translator: the drawing's layers on one side, the loaded set on the
other, mappings built up between them and applied only on Translate, so a
mapping can be reconsidered and the whole translation is a single undo
step. Mappings can be saved and read back, objects can be forced to
ByLayer so the target layer's properties are what actually shows, and the
result can be written out as a log.
The move itself is the one LAYMRG already made for a single pair, so
LAYMRG now goes through it too rather than keeping its own copy.
Three things fell out for free or nearly so. A standards file is a
drawing, so .dws and .dwt need nothing but a place in the file filter.
The entity list is flat and holds block definitions, so objects inside
blocks travel with everything else instead of being stranded on a layer
that no longer exists. And a target layer that this drawing already has
is left alone: the drawing has an opinion about it, and restyling it
would reach past what was asked for -- only newly created targets take
the standard's properties.
Mappings are saved beside the drawing rather than inside a DWS. That is
where AutoCAD keeps them, but putting them there would mean inventing a
place in the drawing that nothing else reads back.
Closes#624
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- unify Model and Paper Space plot area behavior
- add CTB discovery, read/write, and bundled defaults
- apply scale, render mode, and output options consistently
Hold edit leases and compare disk fingerprints before replacement so another editor cannot be silently overwritten.
Offer retry, reload, Save As, or explicit overwrite recovery.
Closes#498
SHAPE entities only drew a diamond placeholder — .SHX files were never
parsed. A new reader (scene/text/shx.rs) decodes the classic
`AutoCAD-86 shapes 1.0/1.1` container and interprets the full shape
bytecode — pen up/down, scale divide/multiply, position push/pop,
subshapes, signed XY runs, octant/fractional/bulge arcs, the 16
direction vectors and the vertical-only skip — into unit-space
polylines, cached per file and per shape number.
The entity resolves its STYLE by handle (name fallback), the style's
shape file with the shared path fallbacks (as stored → relative to the
drawing → basename next to it; traded files carry foreign absolute
paths), then places the glyph with size × relative X scale, oblique
shear and rotation through the OCS mapping. load_file now records
doc.source_path so render-time resolution knows the drawing's folder.
A missing or unreadable shape file keeps the diamond placeholder.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PDF underlays only drew their clip outline / insertion cross; the page
itself never appeared. The chain now runs: the definition's file path
resolves with the raster-image fallbacks (as stored, relative to the
drawing, basename next to the drawing — attach-time relative paths go
stale), the page rasterises through the system pdftoppm (poppler) at
150 dpi — no new crate; missing tool degrades to the outline — cached
per (path, page) including negative results, and the pixels feed the
existing image pipeline as an ImageModel: 1 underlay unit = 1 PDF inch
(px/dpi × entity scale), rotated about the insertion, fade as opacity,
the clip polygon (non-inverted) as the textured region. Draw-order,
layer visibility and per-viewport freezes ride the generic image path;
in-place edits rebuild through the shared image seed.
When the page renders, the wire path draws its frame with an interior
pick surface instead of the lone cross, so selection covers the visible
page. wasm keeps the outline placeholder (no external process).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every rfd dialog result funnels through sys::handle_path, which now
records the picked file's folder (persisted to the config dir), and all
pickers are built via sys::file_dialog(), seeded with that folder.
Save-As keeps the current tab's own directory as the first choice and
falls back to the remembered one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Section marks and drawing-view borders are now real acadrust entity
types instead of Unknown-with-extras, so the renderer dispatches on the
variant directly, the arrow-direction resolver reads the active viewport
off the ViewBorder entity, and selection / properties show "Section
Symbol" and "View Border" instead of "Unknown". View borders are
non-plotting and tessellate to nothing; both count toward the lossy
Save-As warning since they round-trip via their raw records.
Bumps acadrust for the entity promotion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Double-clicking a drawing started a whole second copy of the app. Now it
lands as a tab in the editor already open, and that window comes forward.
The election is the bind itself: every GUI launch tries a deterministic
per-user loopback port, and the OS grants it to exactly one process and
releases it on any death, SIGKILL included. Nothing to reap, no PID to
probe, no stale lock after a crash. The winner serves the port from an
iced subscription; the loser checks it is talking to a matching editor,
hands the paths over and exits. Every surprise — no answer, a stranger on
the port, a timeout — boots a normal window, i.e. the old behaviour: the
feature can degrade, but it cannot lose the file.
The gate in main is positional, after every headless mode has returned.
The plugin runner is this same binary re-spawning itself, so a flag list
would rot the first time a mode is added; a position cannot.
Handing a drawing over that is already open switches to its tab instead
of loading a second copy, comparing resolved paths so a symlink or a
`..` hop is still recognised as the same file.
`opening` is a single slot that a second open would overwrite, and
on_file_opened drops any result arriving once it is clear — so drawings
handed over at once would silently vanish. They queue in pending_opens,
drained from every path that clears the slot, error and cancel included.
Exec becomes %F so one launch takes the whole selection; the CLI takes a
list and boot fans it out through the same door. --new-instance forces a
separate process; --read-only / --script / --new always get their own.
Not reused: the automation server. It dispatches arbitrary commands and
writes arbitrary paths, and its open replaces the active tab's document
in place — publishing that on a port every local process can reach would
be unforced privilege escalation. Only its JSON-lines framing is shared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>