A `\t` reaching a center, right, or decimal (`D`) tab stop now aligns the
field that follows it — the run of words up to the next space/tab — instead
of just advancing the pen:
- decimal: the field's first `.` lands on the stop (a column of numbers
aligns on the decimal point; a field with no `.` aligns its end),
- right: the field's end lands on the stop,
- center: its middle.
The stop wins even when the field is wider than the room before it, so the
dot/edge stays put and the field overruns to the left, keeping the column
aligned. Left stops and the default 4-em grid are unchanged. Picks up
acadrust 162f194, which carries the tab-stop kinds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`qj`/`qd` were stubbed to left. Now the layout fills the line to the
content width:
- Distribute (`qd`, and headings that reset with `q*` → justify) shares
the slack across every glyph gap — word gaps widen and letters spread
via tracking (LFF stroke fonts; TTF with zero letter-spacing spread
their word gaps only). Every line, including the paragraph's last.
- Justify spreads only the word gaps and leaves the last line ragged.
Bumps acadrust to 504281a, which parses `\pqd` as Distributed and
`\pq*` as Justified so these alignments reach the layout at all.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An IMAGE whose file can't be opened (a URL, or a missing/renamed file) now
draws its clip/frame outline plus the saved path as centred text in the
image colour — AutoCAD's broken-reference placeholder — instead of an empty
frame. OCS never fetches remote paths. Bumps acadrust so the entity's path
is populated from its ImageDefinition.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the ATTDEF flags/geometry fix so a CONSTANT block attribute
renders instead of being skipped as a template.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A CONSTANT ATTDEF has no per-insert ATTRIB — its value belongs to the block
itself — so block expansion must draw it instead of skipping every ATTDEF.
Non-constant templates are still skipped (the insert supplies their ATTRIB).
Also bumps acadrust to decode the embedded MTEXT of R2018 multiline
attributes, so a value like "A\PB" renders both lines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the ATTRIB flags/generation/field-length/lock fix so an
attribute tagged invisible is no longer drawn (ATTMODE 1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pulls the acadrust fix that decides the R2013 has_ds bit for MULTILEADER by
peeking the class_version, so a MULTILEADER whose writer emits the bit
(budweiser handle 9821) decodes its leader roots / arrowhead / content instead
of an empty context. The leader now renders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Table whose cells are baked into a *T block (block_record_handle) was rendered
by tessellating the block's sub-entities directly. Those are laid out in
block-local space (origin at the table's top-left corner) — the Table entity
carries the world placement in insertion_point + horizontal_direction, exactly
like an INSERT — so the whole table drew at the world origin instead of its
location.
Each baked sub-entity is now cloned and placed (rotated about the local origin
by the horizontal_direction angle, then translated by insertion_point) before
tessellation, so tessellate_entity produces correctly located, relative-to-eye
geometry. Verified on budweiser handle 9378: its baked lines move from (0,0) to
the insertion point (-1200018.7, -800071.4).
Also bumps acadrust to pull the OLE2FRAME corner-decode fix, which places
OLE2FRAME entities (e.g. handle 3088) at their real corners instead of the
(1,1)/(0,0) defaults.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds SatIntCurve (nubs B-spline parse + De Boor sampler, plus sample_range
for an edge's own arc), so intcurve-curve spline edges decode to real
geometry instead of nothing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MTEXT parser turned `\N` into a plain paragraph, identical to `\P`, so
a multi-column MTEXT arrived with nothing left to lay columns out from —
and the writer sent every break back as `\P`, collapsing the columns for
good on round-trip. `MTextParagraph::starts_column` now carries it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pulls acadrust 769741d: the classes reader never derived is_an_entity from
item_class_id, so the flag was false for every class and any class-based entity
without an explicit type mapping was decoded as an object and dropped. Drawings
carrying an application's custom entity silently lost it.
Surfaces such entities as EntityType::Unknown with their raw record and proxy
graphics, so the renderer can fall back to the cached outline the way AutoCAD
does for an object whose enabler is absent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pulls acadrust dd4a4dc: the R2013+ has_ds bit was skipped for any entity
carrying preview graphics, which desynced every preview-bearing IMAGE and
WIPEOUT — an IMAGE's imagedef handle decoded one off, so the IMAGEDEF lookup
missed, the file path resolved to nothing and the linked image silently
disappeared. Regression since v0.8.1 (the has_ds gate landed in 208bb09).
The skip is now narrowed to MULTILEADER, the one type actually observed to
omit the bit; IMAGE, WIPEOUT and MULTILEADER all decode correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Linetypes converted from MicroStation DGN keep their pattern in DGN line-style
objects (empty standard LTYPE), so entities using them drew as a plain line and
lost the symbols (e.g. a pipe's end circles). Walk the line-style tree
(resolved by name to its LSDefinition), collect the symbol components' anonymous
blocks, and draw each at the host polyline's endpoints, scaled by the symbol's
decoded scale (geometry / scale) so it matches the drawing (verified: a 0.75
ellipse renders at the file's intended 0.375 radius).
Bumps acadrust to 8e18c6b (DGN line-style symbol-scale decode). First pass: the
stroke dash pattern and exact per-vertex placement need more of the line-style
leaf decoded; the base line still draws solid.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up acadrust 6508b76: reads DGN line-style objects (AcDbLS*) into the
document's dgn_ls_definitions / dgn_ls_components tables. No OCS code consumes
them yet — that lands with the linetype linkage and renderer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Multilines were drawn with a fixed ±scale/2 two-line guess in the entity
colour, ignoring the MLINESTYLE entirely, so a drawing's custom style (per-
element offsets, colours and linetypes) was lost — a red/yellow multiline
rendered as a white box.
- mline.rs: resolve the MLINESTYLE (by style_handle, then name) and build one
parallel line per element from its offset x scale + justification shift.
- tessellate.rs: special-case MLINE (like Leader) to emit one WireModel per
element in the element's own colour, CPU-dashing every element through
apply_along so the parallel lines stay in phase.
- linetypes.rs: render a drawing's OWN complex linetypes (embedded text/shape),
document definition first and the bundled .lin catalog only as a fallback, so
a file-defined text linetype like PLYN shows its text.
- Run the dash/glyph layout in a local frame (f64 origin subtracted, re-split to
double-single after) so spacing stays precise at UTM coordinates, and pass
tracking = 1.0 so embedded text keeps its letter spacing.
Cargo.lock pins acadrust to the MLINE->MLINESTYLE handle-link reader fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Point the acadrust patch at the OpenAEC-Foundation/acadifc fork (still the
`acadrust` crate). Its main carries the Arc copy-on-write entity storage that
makes the undo document clone O(entities) refcount bumps instead of a full deep
copy — cutting the per-edit snapshot on a 788k-entity drawing from ~1.7s to
~70ms. No source changes here: acadrust`s public API is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the DXF fix that reads DIMENSION group code 2, so dimensions
imported from DXF keep their baked *D block and render (their text no
longer drifts vs the DWG of the same drawing).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the DXF over-annotative MULTILEADER fix (companion to the MTEXT
one): enable_annotation_scale now defaults false and DXF reads code 293.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the DXF over-annotative-MTEXT fix: acadrust now defaults
MTEXT.is_annotative to false, so imported DXF MTEXT is no longer flagged
annotative and over-scaled in annotation-scaled viewports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make objects genuinely annotative — carrying real per-scale
representations that interoperate with other CAD apps — rather than just
a native flag. `create_annotation_context` synthesizes the extension-dict
chain (AcDbContextDataManager -> ACDB_ANNOTATIONSCALES -> per-scale leaf)
from the entity's placement; the editable Annotative toggle now covers
Text and block references as well as MText/MLeader; and a new
"Annotation Object Scale" dialog (OBJECTSCALE) adds/removes an object's
per-scale memberships. Requires the acadrust ObjectContextData encoder
(Cargo.lock bump).
Also fixes a render bug where objects carrying an *empty*
ACDB_ANNOTATIONSCALES (a single-representation marker with no per-scale
reps) were treated as annotative and (mis)scaled by the annotation factor
in scaled paper viewports, ballooning the text: an object is now
annotative-by-context only when its scale collection is non-empty, so
such objects render at their base geometry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Dim line color" row on Leader and Dimension entities was
read-only: an earlier attempt wired it to Leader.override_color, which
acadrust never serialises, so the pick was lost on save. Store it
instead as a standard ACAD_DSTYLE per-object dimension-style override
(DXF code 176, an ACI index) through the existing dim_override codec,
so it round-trips through both DWG and DXF like the other dim
overrides. RGB picks collapse to the nearest ACI, matching the rest of
the dim-colour stack (dimension styles are index-only through the file
layer). The renderer prefers the override over the style's DIMCLRD.
The write branch is guarded to leaders and dimensions so a mixed
selection cannot stamp the override onto other entity types.
Bumps acadrust to c9fe982, which carries parsed XDATA onto dimensions
on DXF read (it was dropping common.extended_data), so the dimension
override persists on DXF save as well as DWG.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pulls the acadrust fix that emits XDATA for every entity on DXF save (not
just hatches), so the leader dimension overrides and hyperlinks now
round-trip through DXF as well as DWG.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the DWG reader reflecting CANNOSCALE from the AcDbVariableDictionary
into the header, so the status-bar annotation-scale pill shows the drawing's
real model-space scale (e.g. 1:70) instead of the "1:1" default.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve, per selected entity, whether it is annotative (from its text /
dimension / multileader style, or its own flag) and — when annotative — the
name(s) of the annotation scale(s) applied to it, shown as a read-only
"Annotative scale" row (Text / MText / Leader / MLeader).
The applied scale is read from the entity's per-scale object-context leaves
via the new acadrust CadDocument.context_scales map (context leaf ->
AcDbScale handle), walked from the entity's extension dictionary
(AcDbContextDataManager -> ACDB_ANNOTATIONSCALES). Bumps acadrust to pick up
that parsing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pulls acadrust 4ac3d68: entities with proxy/preview vector graphics
(MULTILEADER, WIPEOUT) omit the R2013+ has_ds_data bit, so reading it
desynced the record and produced garbage MultiLeader text_location
(~2e39 → NaN glyphs). The labels now parse with finite locations and
render through the existing MText SDF path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First real Windows build surfaced errors that a Linux host couldn't catch:
* Enable the missing `windows` feature gates (Win32_System_LibraryLoader,
Win32_System_SystemServices, Win32_Security — the last brings in the
`Ex` registry APIs) so the imports resolve.
* Depend on `windows-core`: the `#[implement]` macro expands to
`windows_core::` paths, which need the crate nameable in the root.
* `IClassFactory::LockServer` takes `Win32::Foundation::BOOL`, not a
`core` BOOL.
* Name the returned image type via a new `dwg_thumbnailer::RgbaImage`
re-export instead of a direct `image` dependency.
Verified with `cargo check --target x86_64-pc-windows-gnu -p
dwg-thumbnailer-win` (and the native check still passes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round-trip a raster preview through the DWG's embedded preview slot and
surface it to the OS file managers, so drawings show their contents in
Explorer / Finder / Nautilus instead of a generic document icon.
OCS:
* io::thumbnail — rasterize the scene to a BMP DIB on save (embedded via
acadrust's new Preview type) and read it back on open for the Start
page recent list; `--dwg-thumbnail IN OUT SIZE` CLI extracts a badged
PNG for external thumbnailers.
* io::file_association::install_thumbnailer — self-install the OS
integration on startup: Linux writes a .thumbnailer + hicolor mimetype
icons; Windows registers the IThumbnailProvider DLL under HKCU.
Shared core:
* crates/dwg-thumbnailer — lean (image-only) preview extractor + the
`badge_dwg` full-width "DWG" band, used by every platform so the
ribbon is single-sourced. Ships an rlib+staticlib.
* crates/dwg-thumbnailer-win — IThumbnailProvider COM in-proc server
(cfg(windows), CI-built as a workspace member).
* macos/ — QuickLook thumbnail extension (Swift + C-ABI bridge to the
core staticlib), assembled into a .appex.
Icons & packaging (single SVG source -> per-platform assets in CI):
* assets/mimetypes/image-vnd.{dwg,dxf}.svg — themed file icons.
* packaging: WiX ships dwg/dxf.ico + the thumbnail DLL and points the
ProgIds at them; Info.plist gets CFBundleTypeIconFile + the QuickLook
extension in Contents/PlugIns.
* release.yml — generate .ico (Windows) and .icns (macOS) from the SVGs,
build the QuickLook .appex, and bundle everything.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generate a small wire-raster thumbnail when saving a DWG and embed it via
acadrust's preview API, so OCS drawings show a thumbnail in file browsers
and other CAD applications. Read the embedded preview back cheaply (file
header + preview bytes only, no full parse) to show a thumbnail per file
in the Start page's Recent Documents panel.
- io/thumbnail.rs: CPU rasterizer (current-layout wires -> BMP DIB) plus
a preview-only reader/decoder (BMP/PNG -> iced handle) for the Start
page.
- Embedded on save via stamp_thumbnail on both save paths.
- recent_thumbs cache filled on config load / recents change (never from
a view); a thumbnail is drawn beside each recent file.
Requires acadrust at 3111694 (preview read/write), already pinned in
Cargo.lock.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
App preferences were spread across six flat plain-text files (settings.txt,
recent.txt, recent_limit.txt, statusbar.txt, ribbon.txt, plot.txt), each with a
bespoke line parser and no grouping. Replace them with a single grouped JSON,
serialized via serde (enabling the derive feature; serde_json was already a
dependency).
- New app/config.rs: AppConfig { settings, recent, statusbar, ribbon, plot } →
<config>/settings.json (pretty JSON), load()/save().
- Make UserSettings, StatusBarConfig/StatusPill, CollapseMode and
PlotDialogState serde-derived; #[serde(skip)] marks the plot dialog's runtime
fields so only print preferences persist. Their bespoke .txt load/save is
removed.
- App gains current_config/apply_config/save_config: one AppConfig::load at
startup distributes into live state; every settings-change site
(persist_settings_if_changed, recent add/remove/limit, status-bar pill
toggle, ribbon density, plot commit) routes through save_config, which diffs
against the last write to avoid thrashing.
- Clean start: the old flat files are ignored (no migration). ocad.pgp stays a
separate hand-editable file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up acadrust 716d4b4: DWG reader synthesizes the mandatory
*Model_Space/*Paper_Space block records when a file's BLOCK_HEADER for
them never materialised, so model-space geometry is no longer dropped on
save (file shrank + reopened blank).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opening a DXF whose active viewport is stored uppercase (*ACTIVE),
editing and re-saving produced a file with two full-window model-space
viewports — the file's own *ACTIVE plus a fresh *Active we add on save.
The reserved viewport name is case-insensitive, so the two didn't
collapse and other CAD apps drew them one on top of the other, which
read as the copied objects being linked / "reacting as one".
save_model_tiles_to_vports dropped existing active viewports with a
case-sensitive `name != "*Active"` filter, so an uppercase *ACTIVE
survived and sat next to the new entry. Route every *Active comparison
(save filter + camera/tile restore) through a case-insensitive
is_active_vport_name helper so the file's record is recognised and
replaced instead of duplicated.
Also bumps acadrust to #99f75fe (merged PR #1): the ASCII DXF writer now
emits shortest round-trippable floats instead of the full 16-digit
expansion, halving the round-tripped file size.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pulls acadrust f1f4dbf, which makes the DXF reader parse hatch pattern
definition lines (and types group code 79 correctly) so custom/dashed
patterns load identically to the DWG instead of collapsing to flat
horizontal lines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pulls acadrust d32c9cf so DXF import reads RGB layer colours (code 420)
instead of collapsing every true-colour layer to Index(7)/white; ByLayer
entities then resolve to the correct colour.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two related steps for the text pipeline:
- TEXT parsing now goes through acadrust's parse_plain_text (like MTEXT
uses parse_mtext). text::acad_text_encode parses the value's %% codes —
including %%u/%%o underline/overline and %%nnn — and re-encodes to the
stroke tessellator's \L/\O + resolved-Unicode grammar, so OCS no longer
tokenizes TEXT %% codes itself. (ATTRIB/TOLERANCE/DIMENSION still use the
local tokenizer for now.) Bumps the acadrust pin to the commit that
completes the TEXT/MTEXT control-code set.
- SDF text (OCS_TEXT_SDF) now covers MTEXT and every text-producing
entity, not just top-level TEXT. TextStroke carries a GlyphRun (layout
params); the collector walks convert()'s TruckObject::Text runs and
applies annotation scale exactly as tessellate does for strokes.
layout_glyph_quads shares lff::tokenize_run so %% specials resolve and
\L/\O decorations are skipped (decoration LINES in SDF are still TODO).
Verified: stroke rendering unchanged with the flag off; SDF glyphs render
correctly for TEXT and MTEXT with OCS_TEXT_SDF=1. 154 lib tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OCS maintained a second MTEXT inline-code parser that duplicated
acadrust's `mtext_format::parse_mtext`. Replace it with an adapter
(`adapt_mtext_paragraphs`) that maps acadrust's structured MTextDocument
onto the existing layout types (`MTextLine`/`RunState`); the layout
engine (`layout_mtext`, `mtext_line_count`, and their callers) is
unchanged. Removed the hand-rolled `parse_mtext_paragraphs*` and its
helpers.
Validated during migration by a differential test (adapter vs the old
parser matched byte-for-byte over a 20-case corpus) and confirmed
rendering identically in the app; the differential test is replaced with
adapter-only unit tests. Relies on acadrust's new `MTextScalar` to tell
absolute `\H` from relative `\Hx` (bumped the acadrust pin).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pull in acadrust 4d51947, which makes SatDocument::record() an O(1) hit
instead of a linear scan. Tessellating an ACIS-heavy drawing was
O(records²) per solid; this drops the post-load mesh build for Clinic.dwg
(~9900 xref'd solids) from ~2.2s to ~0.85s of parallel tessellation, with
identical geometry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opening a drawing that xrefs several 3D-heavy DWGs (e.g. Clinic.dwg with
5 discipline files) took ~137s, effectively all of it inside the DWG
parser's AcDs SAB blob extraction.
Bump the acadrust pin to ceef045, which bounds the AcDs end-marker search
so a missing marker family no longer scans the whole buffer per record.
Total xref resolve for Clinic.dwg drops from ~97s to ~4s.
Also make the post-xref mesh pass incremental: the host solids were
already tessellated by the background loader and the merge assigns fresh
handles to every imported entity, so `populate_missing_meshes_from_document`
keeps the cached host meshes and tessellates only the merged xref solids
instead of clearing and rebuilding the whole document.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the acadrust change that writes xref blocks as external
references instead of serializing their resolved contents as owned
entities. An xref attached with XATTACH now survives a save + reopen as
a reference instead of being bound/exploded into loose objects.
Closes#268
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A spline stored by fit points (DWG R2013+ scenario 2) has no control points
or knots, so the renderer — which built its curve from control points —
produced nothing and the spline was invisible. When a spline has fewer than
two control points but at least two fit points, interpolate a smooth
Catmull-Rom curve through the fit points and emit it as a dense polyline.
Open ends use reflected phantom points; closed/periodic splines wrap.
Pairs with the acadrust bump (e8e422a) that decodes the R2013+ fit-point
scenario correctly — together they make the fit-point spline in
Annotation-2D-Mesh-Solid-BIM.dwg render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes entity transparency dropping to opaque on DWG read (e.g. the 85%
area-highlight solids in Clinic_Architectural.dwg). The ENC codec read/wrote
the transparency BL before the rgb BL (real order is rgb then transparency)
and stored the on-disk opacity byte without inverting to the internal
transparency-amount representation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>