Civil3D/AEC objects (and raw MLEADER/Surface records, EED) can only be
written losslessly in their source DWG version. On save, fall back to the
source version when the chosen version is an incompatible encoding family
(acadrust CadDocument::has_version_locked_data), with a command-line
notice, across every save path (Save, Save As, save-before-close/quit)
via a shared dwg_save_version helper. Default the Save-As format dropdown
to the loaded file's version so a round-trip preserves it.
Picks up acadrust DWG-writer conformance fixes (3dbae05) so a Civil3D
R2004 drawing round-trips through OCS and opens in AutoCAD with no errors.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Re-pins acadrust to the dimension DXF round-trip fixes (rotation/text/oblique
degree<->radian units, radial/angular/ordinate group codes, ordinate X/Y bit)
and drops the OCS-side fix_dxf_dimension_rotations dimension arm, which now
lives in the acadrust DXF reader (keeping it here would double-convert).
Completes the dimension audit (Batch 5). Refs #181.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Relocate three root-level modules into io/, where they belong by concern:
- update_check.rs -> io/update_check.rs (its own doc-comment notes it
mirrors how crate::io wraps blocking calls in async).
- patterns.rs, linetypes.rs -> io/ (standard resource catalogs parsed
from asset files and populated into the document — IO-shaped work).
Fix the moved files' include_str! paths (one level deeper), add the
mod declarations to io/mod.rs, drop them from lib.rs/main.rs, and
re-point all crate:: references to crate::io::. No behavior change.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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 <[email protected]>
The internal PDF export set each wire's colour and thickness but never a
dash pattern, so every dashed / centre / dash-dot line printed solid. It
also derived thickness from the on-screen pixel weight, which carries a 2×
screen-legibility boost — so print weights were inconsistent and barely
differentiated.
Emit a PDF dash pattern per wire from its linetype run lengths (mm→pt,
real length = index of the last non-zero element + 1, 1 pt floor so a dot
still marks), resetting to solid where there is no pattern. Convert
lineweight with a factor that undoes both the 96-dpi scaling and the
display boost, so print thickness is the true physical mm.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The web build had bit-rotted: native-only features added since it last
compiled pulled crates that don't build for wasm32-unknown-unknown.
- printpdf (PDF export / printing) → native-only target; the web build gets
stub export_pdf / print_wires. It pulls a wasm-incompatible memchr 1.0.2
via lopdf → nom_locate, and the web has no filesystem.
- getrandom 0.3 (via ahash via acadrust) → enable its wasm_js feature and add
the matching --cfg getrandom_backend rustflag in .cargo/config.toml.
- ocs_plugin_api "host" feature (out-of-process plugin runtime: interprocess,
libloading, memmap2, rkyv) → native-only target; the web gets the
dependency-free manifest/ribbon contract. The host code was already
wasm-gated; only the Cargo wiring pulled it in unconditionally.
`trunk build --release` succeeds again.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Issue #170's fix added a "This PC" volumes root above the drive
letters on Windows. Generalize it so the Save As dialog can switch
volumes on every platform:
- parent_folder() now climbs the filesystem root into the volumes
list on all platforms (`/` -> Computer), not just Windows.
- available_drives() is platform-specific: Windows probes A:-Z:,
macOS lists `/` + /Volumes/*, Linux lists `/` + the removable
mounts under /run/media/$USER, /media/$USER and /mnt. Other Unix
and wasm fall back to `/`.
- drives_root_label() shows "This PC" on Windows, "Computer" elsewhere.
- read_dir_entries() lists the volumes at the drives root on every
platform.
Also gitignore the dist/ web build output so the wasm binary is never
committed by accident.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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 <[email protected]>
3D solid / surface meshes were the last single-f32 path. truck's mesh
tessellation now returns a paired verts_low residual (TruckTessResult::Mesh,
polygon_to_result), MeshModel/MeshLodSet carry it, the model-space offset
and block-instance transforms reconstruct the f64 position and re-split it
after subtracting world_offset, MeshVertex gains position_low (location 3),
and mesh.wgsl reconstructs the eye-relative position. Native SAT / ACIS /
OBJ mesh paths default to a zero residual (no regression — they were never
sub-f32 precise).
With this every world-space pipeline (wire, hatch, image, face3d, mesh)
is full double-single relative-to-eye; large-coordinate jitter is gone.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Gate target-specific code so neither build warns about the other's items:
native-only helpers (view, window icon, file dialogs, file association, plugin
host/guard/registry, self-update) are cfg'd to non-wasm; the wasm-only font
fallback (script_of / cjk_lang / request) is cfg'd to wasm. Drop an unused mut,
allow the few genuinely-conditional dead items (mesh_click_hit, GPU layout
helpers, parity stubs), and gate the embedded PlotSettings-unrelated dead
constants. Both targets now build clean (only the unavoidable nom/quick-xml
future-incompat dependency notice remains).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Remove accidental dead code (unused imports, the superseded paste tool()
helper, a never-read TORIENT handles field, a duplicate let binding, an
unneeded mut) and mark intentional-but-unwired scaffolding with
allow(dead_code): the web-only save_to_bytes, the plugin loadable() probe,
the planned PICK_HIGHLIGHT colours, the DynFieldSpec label, and the layout
ribbon helpers. Native build is now warning-clean apart from the upstream
nom/quick-xml future-incompat notices.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The web save path discarded the chosen format version (`let _ = version`)
and `save_to_bytes` never applied it, so every browser download was
written at the document's default version (AC1032 / DWG 2018) regardless
of the format picked in the Save dialog. (#122)
Thread `version` into `save_to_bytes` and set `doc.version` there, the
byte-buffer counterpart to `save_as_version`. Saving as DWG 2013 now
writes an AC1027 file.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
On the web, Save and Save As now open the in-app Save dialog (filename +
format) like the desktop, and the unsaved-changes prompt's Save routes
through it too — instead of an immediate rfd download. Confirm serializes
to bytes and downloads directly via a Blob + hidden `<a download>` click
(a user gesture), so there is no intermediate "click to download" link.
Replaces the rfd-based download_web / WebSaveResult round-trip with a
synchronous sys::download_bytes. Native save is unchanged.
Native + web build; 75 tests pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Wire up save for the web build:
- `io::save_to_bytes`: serialize a document to a byte buffer (DXF via
write_to_vec, DWG via write_to_writer over a Cursor).
- `io::download_web`: browser save dialog → write bytes (triggers a
download).
- Save and Save As on wasm both go through a new `web_save` helper:
serialize the active tab and offer a download (no persistent path or
in-app save dialog on the web). Format follows the tab name's
extension, defaulting to DWG. Result reported via `WebSaveResult`.
Native build + 75 tests unchanged.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Wire up file open for the web build:
- `io::pick_and_load_web`: browser file picker → read bytes → parse via
`load_bytes` → build caches, returning the same payload as the native
loader so it feeds the shared `Message::FileOpened` handler.
- `Message::OpenFile` cfg-dispatches: native picks a path then loads on a
worker thread; web picks + parses in one async step. The web branch also
sets `opening` (the handler drops results when no open is in progress).
- Disable the cosmic-text shaping / fallback paths on wasm: the web has no
system fonts, so cosmic-text panics with "no default font found". LFF
stroke fonts (embedded) still render; LFF-missing glyphs are skipped.
Native build + 75 tests unchanged. Hatches still don't draw on the web.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
With the wasm bundle building, get it rendering in a browser:
- Enable iced `webgl` so wgpu uses WebGL2 where WebGPU is absent
(e.g. Firefox by default), and `fira-sans` so UI text has an
embedded font (the web has no system fonts) — without it every
label was blank.
- Replace `std::time::Instant` with `iced::time::Instant` (web-safe);
`Instant::now()` panics ("time not implemented") on wasm and crashed
at boot via the command line.
- Skip the batched-hatch pipeline on wasm: its bind group needs
vertex-stage storage buffers, which WebGL2 lacks (validation error
on "New drawing"). Fields become Option; upload/draw guard on them.
Hatches don't draw on the web for now; everything else does.
- Fix the trunk feature flag (`data-cargo-no-default-features`) and
ignore the `dist/` build output.
Native build + 75 tests unchanged.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add a cross-platform `io::load_bytes(name, bytes)` that parses DWG/DXF
from an in-memory buffer via acadrust's stream readers (Cursor), picking
the format from the name's extension. This is the foundation for opening
files on the web, where the browser delivers bytes rather than a path.
Native keeps the streaming `load_file` (lower memory on large drawings);
both share the post-load fixups. Builds on native and wasm.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
wasm32 has no threads, so rayon's global pool would panic at runtime.
Add `crate::par::prelude`: native re-exports `rayon::prelude`, wasm
provides sequential `std`-iterator equivalents (par_iter / into_par_iter
/ flat_map_iter over slices, Vec, HashMap, HashSet). All call sites now
`use crate::par::prelude::*;` instead of rayon directly.
- wire_gpu's rayon `reduce` (2-arg, no std equivalent) is cfg-split to
a sequential flat_map concat on wasm.
- rayon moves to a non-wasm target dependency.
Native still runs in parallel; all 75 tests pass. wasm bin compiles.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
First step toward a web build: the library now compiles for
wasm32-unknown-unknown with --no-default-features.
- Add a `solid3d` feature (default on) gating truck-meshalgo /
truck-shapeops / lzma-sys. These pull vtkio → xz2 → lzma-sys (C),
which cannot cross-compile to wasm. Without the feature the 3-D
solid path (tessellation, booleans, ACIS import) becomes no-ops;
2-D CAD is unaffected. Native builds keep the feature by default.
- Move native-only crates (ureq, open) to non-wasm target deps and
add cfg shims: update-check is a no-op on wasm, and a new `sys`
module provides open_url + handle_path (rfd FileHandle → PathBuf;
synthesizes a name-based path on the web).
Native build + all 75 tests unchanged. Browser runtime wiring
(entry point, trunk, real file I/O) is a follow-up.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Move the 30 flat modules under src/scene/ into six folders by role:
convert (entity→geometry), text (fonts/shaping), model (render models),
pick (hit-test/selection/grips/xclip), view (camera/transform/render),
cache (block + property caches). pipeline/ is unchanged.
Full path migration — all `scene::X` references become
`scene::<group>::X`, no compatibility re-exports. render.rs items that
were `pub(super)` (parent was scene) become `pub(in crate::scene)` to
keep their original scene-wide visibility.
No behavior change; build and tests are green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A RasterImage stores its path on the linked ImageDefinition, usually as the
original author's absolute path (e.g. a Windows/VMware share) that doesn't
exist locally, so the image silently failed to load. On open, resolve each
raster image: use the stored path if it exists, else fall back to the same
file name next to the drawing, and write the resolved path onto the entity.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Both the MSI and the portable build were missing Applications\OpenCADStudio.exe\
DefaultIcon, causing a blank generic icon in the "Open with" context-menu list
and the "Choose another app" picker.
- packaging/windows/main.wxs: add Applications\OpenCADStudio.exe\DefaultIcon
(+ FriendlyAppName, shell\open\command, SupportedTypes) to the
DefaultPrograms component for MSI installs (HKLM).
- src/io/file_association.rs: add DefaultIcon to the Applications\
OpenCADStudio.exe block written at runtime for portable builds (HKCU).
https://claude.ai/code/session_01DFVSKgViFaxEjZD2KsTdtt
The .desktop file used a named icon (Icon=io.github.HakanSeven12.OpenCadStudio)
but the icon file was never written to the XDG icon theme directory, so file
managers and the "Open with" picker fell back to a generic blank document icon.
install_icon() now writes assets/logo.svg (embedded at compile time) to
~/.local/share/icons/hicolor/scalable/apps/{APP_ID}.svg alongside the
.desktop file, and runs gtk-update-icon-cache so the change is picked up
immediately. The write is skipped when the on-disk file is already identical.
https://claude.ai/code/session_01DFVSKgViFaxEjZD2KsTdtt
For MSI installs the installer already writes HKLM Capabilities +
RegisteredApplications, so the "Set as default" dialog works. For
portable .exe builds those registry keys are never created, so the
IApplicationAssociationRegistrationUI dialog silently does nothing.
register_handler() now also writes:
- HKCU\Software\Classes\Applications\OpenCADStudio.exe (Open-with list)
- HKCU\Software\Classes\OpenCADStudio.DWG / .DXF (ProgIDs + icon + open cmd)
- HKCU\Software\Classes\.dwg|.dxf\OpenWithProgids
- HKCU\Software\Open CAD Studio\Capabilities (AssociationToasts shown)
- HKCU\Software\RegisteredApplications (Windows Default Programs)
This makes the "Yes, set as default" button work correctly for portable
builds on all supported Windows versions.
https://claude.ai/code/session_01DFVSKgViFaxEjZD2KsTdtt
Installed builds already register a handler (MSI ProgIDs, the Linux
.desktop MimeType, the macOS Info.plist), but the portable .exe and the
AppImage register nothing, so they never appear in the OS "Open with"
list. Add a best-effort, idempotent runtime registration that runs
silently on a detached thread at startup:
* Windows — writes HKCU\Software\Classes\Applications\OpenCADStudio.exe
(open command + FriendlyAppName + SupportedTypes .dwg/.dxf), which is
what lists the exe in "Open with → Choose another app".
* Linux — writes a user-level .desktop pointing at the running binary
($APPIMAGE when set) and refreshes the desktop database. Skipped when
a system package already provides the desktop entry, so we never
shadow a distro install.
* macOS — no-op; .app bundles auto-register with LaunchServices.
Unlike the default-app prompt this changes no defaults and shows no UI;
it only makes the binary discoverable as a handler.
https://claude.ai/code/session_01DFVSKgViFaxEjZD2KsTdtt
On the first launch that hasn't answered it yet, the app shows a small
dialog offering to become the default handler for .dwg and .dxf files.
The choice is persisted (default_assoc_prompted) so the prompt never
reappears.
The platform plumbing lives in io/file_association.rs:
* Windows — opens the OS's own per-app default-programs dialog via
IApplicationAssociationRegistrationUI::LaunchAdvancedAssociationUI,
passing the "Open CAD Studio" RegisteredApplications name the MSI
registers. Modern Windows guards the actual default behind a
UserChoice hash, so this is the supported consent path.
* Linux — xdg-mime default against the installed .desktop file, whose
MimeType entries already cover image/vnd.dwg and image/vnd.dxf.
* macOS — LSSetDefaultRoleHandlerForContentType binds the DWG/DXF UTIs
declared in Info.plist to the app bundle id.
The work runs on a dedicated thread (the Windows dialog is modal) with
the result delivered back through a oneshot channel and surfaced in the
command line.
https://claude.ai/code/session_01DFVSKgViFaxEjZD2KsTdtt
A 35-unit arc sweeping 6.5e-7 rad has arc length 2.3e-5 — past the
arc-length floor — yet start/mid/end bow off the chord by only ~2e-12.
truck's 3-point circle_arc fit then returns a near-infinite radius and
parameter_division subdivides without bound, hanging the load. Gate on
the sagitta (r·(1−cos(sweep/2))), the true measure of the fit's
conditioning, so long thin arcs survive and sub-precision ones drop.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
sample_layout_03/04.dwg would not open: a periodic 9-control-point spline
with every control point pinned at the origin (zero extent) reaches truck's
parameter_division, which never converges on a zero-length curve and hangs
the first-frame tessellation. The spline passed every existing guard
(finite points, no knots, degree 3). Reject splines whose control-point
extent is sub-precision (< 1e-6 on all axes).
Verified: all five sample_layout DWGs now load + tessellate (03/04 went
from a hang to ~356 ms; 01/02/05 unchanged, no valid spline dropped).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
DWG stores the current text/dim/multiline style as a header handle; the
writer keeps a valid stored handle and otherwise falls back to Standard,
so a Set Current change (which only updates the name) was lost on DWG
save. Treat the current-style names as the source of truth and re-resolve
the header handles from them at save time, alongside the table/multileader
variable-dictionary sync. Now every current-style choice round-trips
through DWG, not just DXF.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
On save, mirror the current table/multileader style from the header into
the variable dictionary (CTABLESTYLE / CMLEADERSTYLE DICTIONARYVAR
entries) so a Set Current choice round-trips through DWG, which stores
them there rather than in the header. Done once in save_as_version (the
shared clone point), so every save path and both formats stay in sync.
No-op when the document has no variable dictionary (fresh files).
Verified: mutate the entry, write+read a real DWG, value survives.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
DWG has no header field for the current table / multileader style — they
live in the variable dictionary as DICTIONARYVAR entries keyed CTABLESTYLE
/ CMLEADERSTYLE. Resolve them on load via the document's dictionaries and
set the header names, so the green ✓ marks the right style for DWG files
as well (DXF already worked via the header vars). A missing entry leaves
the value untouched, so there is no risk to the binary header parse.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- The current-style marker is now a green ✓ in its own fixed-width column
so names stay aligned; rendered as a separate coloured glyph rather than
baked into the label string.
- Resolve the current text / dimension / multiline style from the handle
the DWG header stores into the name the app reads (fix_current_style_names
on load). DXF already provides these as names ($TEXTSTYLE / $DIMSTYLE /
$CMLSTYLE); DWG only stores handles, so the marker previously always fell
back to "Standard". Only overrides when the handle resolves.
Note: table and multileader current styles are not surfaced by the file
reader (no header name/handle exposed), so those markers still default to
the app's active selection until acadrust exposes them.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Large survey drawings (105k entities, multi-million unit extents) could
exhaust all RAM + swap on one core during load and get OOM-killed before
display. Two parser-garbage entities slipped past the corrupt-entity
purge and blew up truck's parameter_division tessellation:
- Near-zero-sweep arcs: the zero-sweep guard only caught sweep < 1e-9,
but a tiny non-zero sweep (e.g. 1.6e-6 rad) still places start/mid/end
within truck's coincidence tolerance and recurses unboundedly. Gate on
arc length (radius x sweep) instead so large-radius small-sweep arcs
(still visible curves) survive while sub-precision arcs drop.
- Desync splines with 100k control points and an invalid knot vector had
no guard at all. Add a Spline arm rejecting the 100k desync signature,
non-finite control points, degree < 1, and bad knot vectors.
Also cap complex-linetype dash expansion (complex_lt::apply_along) at
1e6 pattern repeats, falling back to a solid stroke — a latent OOM of
the same class on map-scale entities carrying a finely-scaled pattern.
Adds regression tests for the arc-length and spline guards.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
New layers were added to the table with a NULL handle, and the first
patch used the non-advancing `next_handle()` getter, so two new layers
(or a layer and the next allocated object) shared one handle. The DWG
format is handle-based, so the collision dropped the layer on save;
DXF is name-based and survived, which is why a rename appeared to fix
it.
Allocate a unique advancing handle via `allocate_handle()` in LAYER NEW
(panel + command line) and on rename when the source handle is invalid.
Rename now also clones the whole record so colour/linetype/lineweight
are kept, not just the flags.
Adds DWG/DXF round-trip tests covering one and multiple new layers.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The purge scan tests every entity with is_entity_corrupt, whose
per-vertex finite/extent checks on large polylines dominate. Detection
is pure and read-only, so gather entity refs in one pass and test across
cores with rayon; removal stays serial (remove_entity needs &mut doc).
Cuts the "purge" span in the open-time breakdown on polyline-heavy files.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase 5.2. Time each open phase — parse, purge, derived-cache build on
the loader thread, plus xref resolve on the UI thread — and emit a
breakdown line on open completion:
parse 1240ms · purge 80ms · caches 340ms · xref 60ms · total 1820ms
Makes open-time regressions (and the wins from 1.1 / 2.4) visible at a
glance. Timings ride back on DerivedCaches; total is wall time from the
Open click to post-xref.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase 4.1. Handle / u64 keys dominate the hot maps (block_defn,
hatch / image / mesh caches, draw-depth map, viewport wire caches);
the default SipHash is wasted work on integer keys. Alias the std
types to rustc_hash::FxHashMap / FxHashSet crate-wide so every map and
set — including the cross-module draw-depth map shared between scene
and the GPU pipelines — uses the faster hasher consistently.
Mechanical: no behavioural change. Constructors moved from ::new() to
::default() (Fx variants carry no RandomState). Build + 13 tests green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase 1.1: resolve_xrefs now purges corrupt entities inline as it
merges each xref and returns the dropped count, removing the second
full-document purge_corrupt_entities walk in the FileOpened handler.
Every xref-bearing open saves one O(N) entities() pass.
Phase 2.4: split compute_world_offset into offset_prep / offset_centroid
/ world_offset_from_centers; centroid accumulation now rides the existing
cache-handle walk in build_derived_caches. Two O(N) entity walks collapse
into one, with identical MSPACE-membership filtering preserved.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The truck B-rep pipeline is Z-up (solids extrude/raise along Z, profile
wires convert pass-through), but two spots still assumed a Y-up world:
- REVOLVE swapped the axis/origin to (x, z, y), so the revolve axis sat
in the XZ plane while the profile sat in XY → wrong solid. Use the
axis/origin straight through.
- OBJ import kept Y-up coordinates as-is on the (now Z-up) viewport, so
imported meshes lay on their side. Rotate +90° about X on import
(positions + normals): (x, y, z) → (x, -z, y).
EXTRUDE (sweep along +Z) and the box/cylinder primitives were already
Z-up and unchanged.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Joining the OpenAEC Foundation (open-aec.com). Crate/struct become
OpenCADStudio, user-facing strings use the spaced "Open CAD Studio"
form, repo URLs point to HakanSeven12/open-cad-studio. Logo (window
icon + assets/logo.svg) redrawn with OCS initials. Drop examples/
binaries — unused.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.
Add a CPU rasteriser as `HatchModel::pattern_segments()`:
- builds an edge list from the boundary, splitting at NaN sentinels and
closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
(`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
and the inclusive k-range bounded by the boundary AABB plus a 4096-
line safety cap;
- intersects each line with every boundary edge, sorts the hit
parameters, walks them in pairs (even-odd) and emits one segment per
in/out interval. Dash patterns are walked along each clipped span with
phase preserved via `t mod period`, matching the GPU shader's
absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
non-zero origins (brick / offset herringbone) line up with the GPU
render.
`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The PDF / print pipeline only walked `WireModel.points` (lines), so every
hatched region disappeared the moment a sheet left H7CAD. Route the same
`paper_canvas_hatches()` / `paper_canvas_wipeouts()` lists that the
on-screen canvas consumes into `pdf_export::export_pdf` and
`print_to_printer::print_wires`, and emit each one as an
`Op::DrawPolygon` with the EvenOdd winding rule so multi-ring boundaries
(islands / holes) render correctly:
- Solid → fill the boundary polygon in the hatch colour.
- Pattern → stroke the outline (PAT line families stay procedural;
follow-up work will rasterise them into segments).
- Gradient → fill with the averaged colour, mirroring paper_canvas.
Wipeouts emit first, then hatches, then wires — same z-order as the
on-screen canvas, so wires draw on top of the fills they belong to.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Adds vertex-coordinate, thickness, and dimension-validity checks to
`is_entity_corrupt`. Sample files with degenerate Arcs (radius ~5e-14
and zero sweep) or 100k-vertex LwPolylines at coords ~1e244 used to
pass the previous check, then crash inside truck's parameter_division
with a 3 GB allocation.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Property-panel coordinate display and block-insert scaling were both
hard-coded, ignoring the document's stored unit configuration.
- LUNITS / LUPREC: edit_prop() now formats values through a thread-local
UnitContext seeded by refresh_properties(). Decimal / Scientific /
Engineering / Architectural / Fractional all render correctly without
threading the document handle through every entity properties builder.
- AUNITS / AUPREC: format_angle() helper available (decimal degrees, DMS,
grad, rad). Callers that already format angular values via radians can
switch over without touching the helper signatures.
- INSUNITS + MEASUREMENT: xref's source INSUNITS is carried onto the
host BlockRecord.units during merge. commit_entity() then scales new
INSERTs so 1 source-unit maps to the host's INSUNITS length. When
either side is unitless (0) MEASUREMENT acts as the fallback
(0=Imperial / inches, 1=Metric / mm), matching AutoCAD's rule.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
* io/xref.rs — replaced the 7-line "BlockRecord::is_loaded turned out to
be unreliable" rationale with a one-liner. The reverted experiment
lives in git history (e88a946, 0996b75).
* refedit::RefEditSession.insert_handle — set at construction but never
read; the "kept for future REFCLOSE DISCARD" TODO can come back via
git history when that feature lands.
* scene::mod — dropped the placeholder MeshModel re-export; nothing
outside `mesh_model` consumes the bare name and MeshLodSet is the
pipeline-facing type now.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
* helpers::wcs_to_ucs — UCS inverse transform with zero call sites.
* scene::Scene::compute_and_set_world_offset — never called; the load
path uses the free function `compute_world_offset` directly.
* tessellate::tessellate_mesh — never called; mesh tessellation goes
through `solid3d_tess`, not the truck Shell/Solid path.
* truck_tess::tess_to_mesh_model — orphaned with tessellate_mesh's
removal.
* MeshModel::WHITE / MeshModel::SELECTED — only used by the removed
tessellate_mesh.
* xattach::Step::FilePath variant — never constructed; XATTACH always
enters through `with_path` after the file picker. Replaced the Step
enum + prefilled_path field with flat `path` / `block_name` fields
and dropped the unreachable text-input flow.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Xref work:
- merge_xref_into_block now copies the xref's layer / linetype / nested
block_record tables into the host under `{xref}|{name}` prefixes and
remaps entity references, so ByLayer/ByBlock resolution lands on the
right entry instead of falling back to WHITE + 1 px.
- expand_insert (and the explode fallback) detect xref BlockRecords by
flag and apply `fade_toward_bg(color, bg, 0.5)` to every emitted final
color (selection bypasses the fade). ExpandCtx propagates `is_xref` +
`bg_color` through nested expansions.
Block precision groundwork:
- BlockDefn gains `local_offset: [f64; 3]` set in build_defn from the
AABB centre of the defn's direct entities. tessellate_sub_local now
passes that offset to tessellate(), so LocalWire.points / fill_tris /
aabb_local / text_obb_local are stored in offset-frame f32 and don't
lose precision for distant content.
- emit_wire / emit_greeked_text / transform_tangent take the defn offset
and re-add it in f64 before composing with accum_xform. defn_aabb_
recursive transforms nested children from their offset frame into the
parent's. transform_offset_aabb_xy replaces transform_aabb_xy.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Empirically the DWG \"loaded\" bit doesn't track AutoCAD's XREF Manager
\"Loaded\"/\"Unloaded\" state reliably:
- File A: manager \"Loaded\", on-disk is_loaded = Some(true)
- File B: manager \"Loaded\", on-disk is_loaded = Some(false)
Same UI state, opposite bit values, so we were incorrectly skipping
xrefs the user wants loaded. Drop the `is_loaded == Some(false)` skip
and auto-resolve every xref again. The OOM problem this was meant to
mitigate is now addressed by the frustum culler, so loading more
xrefs no longer threatens the GPU budget.
The `BlockRecord::is_loaded` field + acadrust patch stay in place so
we can revisit once we identify the actual on-disk representation of
XREF→Unload (likely an XREF dependency record or
ACAD_XREF_PANEL_DATA xrecord).
Co-Authored-By: Claude Opus 4.7 <[email protected]>