The annotative feature (1b037a6) added a 15th `anno_scale: f32` parameter to `scene::cache::block_cache::expand_insert` and updated the production call sites, but the text_font_rendering integration test was left at 14 arguments, so the test binary fails to compile with E0061 once rebuilt.
Pass `1.0` (no annotative scaling), matching the `BlockCache::build(&doc, 1.0, ...)` call the test already uses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Since text became SDF-only (the legacy stroke-text path was removed), all
text — standalone TEXT/MTEXT and dimension text — rendered on-screen only as
GPU glyph quads carried on `WireModel::text_verts`. The CPU PDF/print exporter
draws only wire stroke `points` and hatch fills, so it silently dropped every
glyph: dimensions and text were missing from exported PDFs and prints
(present in v0.7.6).
Reconstruct vector text in the exporter instead of resurrecting the stroke
path, so the SDF render path and its caching are untouched:
- sdf_atlas: `GlyphAtlas::export_table()` snapshots each baked glyph's vector
geometry (outline strokes + fill triangles, glyph space) keyed by its tile
`uv_min`, re-resolved through `Face` so it honours the current TEXTFILL.
- pdf_export: a new `emit_text` pass walks each wire's `text_verts` (one
6-vertex quad per glyph), recovers the glyph geometry by `uv_min`, and
affine-maps it into the quad — `DrawLine` polylines for stroke (LFF/SHX)
fonts, filled triangles for TrueType, plus solid decoration bars. Runs under
the same rotation/scale/clip CTM as the wires, so it covers model space,
paper space and windowed plots, and every SDF-text kind (text, dimension,
multileader, table, tolerance).
Text embedded in complex linetypes is SDF-only with no stroke fallback and
stays out of scope.
Tests: a unit test asserts a stroke glyph emits vector ops within the glyph's
world bbox; an integration test drives a real TEXT + linear DIMENSION through
scene.entity_wires() -> export_pdf and checks text reaches the file. Verified
bug-first: with emit_text disabled the end-to-end test fails with the reported
symptom (text adds no content).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Installing a newer version of a plugin whose previous version is currently
loaded failed on Windows. The marketplace writes the native library to the
same filename, but a resident cdylib is memory-mapped by its runner process,
so the overwrite is rejected with "being used by another process" (os error 32).
Users hit this upgrading e.g. Land Survey 0.4.0 -> 0.4.4 and assumed a reboot
was required.
A loaded DLL can't be truncated or removed, but it *can* be renamed (the loader
opens it with FILE_SHARE_DELETE). install() now moves every resident library in
the package aside to a `.old` stash and writes the new one under its own name;
the stash keeps serving the running session and the loader picks up the fresh
library on the next start. Stale stashes are swept on the following upgrade, and
this subsumes the previous differently-named-library cleanup.
Adds regression tests, including one that reproduces the exact lock on Windows
(FILE_SHARE_READ | FILE_SHARE_DELETE, no write share) and confirms an in-place
overwrite fails while replace_library upgrades successfully.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MATCHPROP only copied the generic properties (layer, color, linetype,
linetype scale, lineweight), so matching text to text left the
destination's style and height unchanged. Copy the source's text style
and height to TEXT and MTEXT destinations, in both directions, like
AutoCAD's "Text" special property.
Adds an in-process regression test that drives the real interactive
command path (source pick -> destination selection) and fails on the
old code with the reported symptom.
Fixes#361
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Export to PDF could end in complete silence: the flow hands off to an
async rfd save dialog, and when that future resolves to None the app
mapped it straight to Task::none(). None does not only mean the user
cancelled — on Linux the XDG portal SaveFile call can fail outright
(rfd then tries a zenity fallback, which may not be installed) and the
future still just resolves to None. rfd reports those failures only
through the `log` crate, and logging is opt-in (--log / RUST_LOG), so
the reporter saw exactly what #369 describes: no dialog, no error, no
console output, no file.
- EXPORT / EXPORTPDF now accept an optional path argument that exports
straight to the file with no dialog — both the workaround for systems
where the native save dialog cannot open and a headless-testable
entry point. A relative path lands next to the drawing; a missing
extension gets .pdf.
- The dialog-None arms (PlotExportPath / PlotWindowExportPath) now push
a command-line message pointing at EXPORTPDF <path> instead of doing
nothing.
- The success message prints the full output path, not just the file
name, so users can find the PDF.
Verified end to end on the real binary via --serve: LINE + CIRCLE then
EXPORTPDF <path> writes a valid PDF with the expected geometry. Both
new tests fail on the previous code.
Fixes#369
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BATTMAN matched the ATTMAN command arm but the argument was extracted
with `cmd.trim_start_matches("ATTMAN")`. "BATTMAN" has no leading
"ATTMAN" to strip, so the whole word survived as a bogus block-name
argument — BATTMAN always reported "ATTMAN: no matching block" instead
of listing attributes. `BATTMAN <name>` never matched at all.
Strip whichever keyword was actually typed (BATTMAN first, since ATTMAN
is a suffix of it) with `strip_prefix`, and match the `BATTMAN ` prefix
form too. BATTMAN now behaves identically to ATTMAN.
Adds headless regression tests driving the real dispatcher; they fail on
the previous code with the reported symptom and pass with the fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
XLine/Ray tessellate as +/-1e6 display segments (entities/ray.rs), and
both fit_all outlier defenses miss them: the IQR reject passes because a
construction line through the drawing has its centroid at its base
point, inside the consensus cluster; and the per-point lim filter passes
because local_extent_max is computed once at document load and stays at
the 1e9 default for drawings created fresh in-app. The far endpoints
then poison the bounds and the view fits +/-1e6, shrinking real
geometry to a dot.
Exclude XLine/Ray wires from the extents up front (AutoCAD likewise
ignores infinite lines in ZOOM Extents), falling back to fitting their
base points when the drawing holds nothing else.
Regression tests verified to fail without the fix (camera distance
3,000,000) and pass with it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keyboard accelerators (copy/paste/save/undo/new/open) were gated on
`Modifiers::control()`, which on macOS matches only the physical Control
key. Mac users pressing Cmd+C/V/S/Z got nothing, contrary to platform
convention and every other Mac app.
Switch the gate to `Modifiers::command()`, which iced resolves to the
Logo (Cmd) key on macOS and Ctrl on Windows/Linux. On non-mac targets
this expands to exactly `control()`, so behavior there is unchanged.
Also make the Keyboard Shortcuts reference window display the
platform-correct modifier name (Cmd vs Ctrl) so the help matches the
keys the user actually presses.
Closes#202
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Builds on the background-persistence change: let BACKGROUND take a single
named-colour token in addition to `<r> <g> <b>`, so a common dark/light
background is one word instead of a remembered triplet:
BACKGROUND WHITE | BLACK | GRAY|GREY | DARKGRAY|DARKGREY | LTGRAY
Colour parsing moves into `parse_background_color`; the persisted-default
and recolor paths are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The BACKGROUND command set a per-tab colour that was lost on restart and
didn't apply to new drawings, so there was no way to make a non-black
background stick. Persist the chosen model/paper background in UserSettings
(settings.txt) and re-apply it on launch and to every drawing tab created
afterward.
- settings.rs: persist `bg_color` / `paper_bg_color` as 0–255 RGB triplets.
- App gains `default_bg_color` / `default_paper_bg_color`; current_settings /
apply_settings round-trip them, and apply_settings pushes the restored
colour onto existing tabs. New/opened/replaced tabs call `apply_bg_default`.
- BACKGROUND updates the persisted default, saved centrally via
persist_settings_if_changed; RESET clears it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DistCommand stored the first point as Vec3 and ran on_point through
`let pt = pt.as_vec3();`, downcasting the f64 picked point to f32 before
computing distance/deltas. At survey-scale coordinates (eastings ~5e5)
f32's 24-bit mantissa gives ~0.03–0.06 unit resolution, so a snapped
endpoint measured slightly off — matching the report in #193.
Store `first` as DVec3 and keep the math in f64; cast to f32 only for
the visual preview wire vertices.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A DWG DIMENSION references an anonymous *D<n> block that holds its drawn
geometry; AutoCAD-family readers render that block rather than re-deriving it
from the definition points. OCS re-tessellates dimensions for display and never
materialises the block, so on save a created dimension goes out referencing a
block that doesn't exist and the writer emits a null block handle. Strict
readers (DWG TrueView, QCAD) then drop the dimension or demand a recovery, and
lenient ones (BricsCAD) regenerate it at a different position.
Add bake_dimension_blocks(), called on the cloned document in
io::save_as_version / save_to_bytes, which builds a *D<n> anonymous block from
each block-less dimension's exploded geometry (extension lines + dimension line
+ measurement text) and points its block_name at it. Dimensions that already
own a block (imported, or copied via the *D-cloning copy path) are left
untouched.
Verified end-to-end: before, AutoCAD/Civil 3D 2026 reads 0 dimensions from an
OCS-saved file; after baking it reads all of them.
Fixes#178, #181, #182.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The custom Save As dialog navigated up the tree solely via
`Path::parent()`, which returns `None` at a drive root (`C:\`).
Navigation therefore dead-ended on the starting volume, so other
drives and USB sticks were unreachable.
Add a synthetic Windows "This PC" drives root above the drive
letters. `parent_folder()` now climbs `…\sub → C:\ → This PC`, and
`read_dir_entries()` lists the available volumes (probed A:–Z: by
stat, no new dependency) when at that root. The dialog's up-button
and path bar route through the same helper and show "This PC".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>