Commit Graph
45 Commits
Author SHA1 Message Date
Hakan SevenandClaude Opus 4.8 cf817ff9eb fix(io): purge near-collinear arcs that hang load
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]>
2026-06-11 17:10:00 +03:00
Hakan SevenandClaude Opus 4.8 0ca1e58b1f fix(io): purge zero-length point-collapsed splines that hang load
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]>
2026-06-10 20:27:45 +03:00
Hakan SevenandClaude Opus 4.8 e5d0c3950b feat(styles): persist current text/dim/mline style change to DWG
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]>
2026-06-10 19:33:56 +03:00
Hakan SevenandClaude Opus 4.8 b3c1c8b4cb feat(styles): write current table/multileader style back to DWG
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]>
2026-06-10 19:29:17 +03:00
Hakan SevenandClaude Opus 4.8 2044755418 feat(styles): read current table/multileader style from DWG too
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]>
2026-06-10 19:17:07 +03:00
Hakan SevenandClaude Opus 4.8 b3ba3a53d1 feat(styles): green ✓ for current style + resolve it from DWG files
- 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]>
2026-06-10 15:56:13 +03:00
Hakan SevenandClaude Opus 4.8 9cdb5ea5fd fix(io): purge degenerate arcs/splines that OOM on load
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]>
2026-06-10 08:39:50 +03:00
Hakan SevenandClaude Opus 4.8 6520195220 fix(layers): allocate real handles for new layers (#67)
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]>
2026-06-09 19:37:42 +03:00
Hakan SevenandClaude Opus 4.8 5bb6a92235 perf(io): parallelize corrupt-entity detection scan
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]>
2026-06-05 08:58:39 +03:00
Hakan SevenandClaude Opus 4.8 997538a67a feat(io): open-time breakdown log
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]>
2026-06-05 00:41:46 +03:00
Hakan SevenandClaude Opus 4.8 70702d063f perf(hash): swap std HashMap/HashSet for rustc-hash FxHash
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]>
2026-06-05 00:35:53 +03:00
Hakan SevenandClaude Opus 4.8 3098523b8d perf(io,scene): drop redundant purge walk, fuse world-offset scan
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]>
2026-06-05 00:32:39 +03:00
Hakan SevenandClaude Opus 4.8 d8c60c32d4 fix(3d): revolve axis and OBJ import use the Z-up world, not stale Y-up
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]>
2026-06-04 11:03:03 +03:00
Hakan SevenandClaude Opus 4.7 ef1f9a08d6 chore: rename H7CAD to Open CAD Studio
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]>
2026-05-23 17:16:15 +03:00
Hakan SevenandClaude Opus 4.7 30f7ad0918 feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
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]>
2026-05-22 06:52:20 +03:00
Hakan SevenandClaude Opus 4.7 c2f3844cb6 feat(io): export hatch + wipeout fills to PDF
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]>
2026-05-22 06:45:36 +03:00
HakanandClaude Opus 4.7 ace99541d0 fix(io): drop more parser-garbage entities to prevent OOM on open
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]>
2026-05-21 10:39:39 +03:00
Hakan SevenandClaude Opus 4.7 b3e117a969 feat: honour LUNITS / LUPREC unit formatting + INSUNITS scaling on insert
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]>
2026-05-20 05:43:55 +03:00
Hakan SevenandClaude Opus 4.7 412fb857ba chore: drop legacy comments and the last unused dead_code allows
* 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]>
2026-05-16 02:04:41 +03:00
Hakan SevenandClaude Opus 4.7 a1ed87da3d chore: drop dead scene/IO helpers + simplify XATTACH command state
* 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]>
2026-05-16 00:10:20 +03:00
HakanandClaude Opus 4.7 f738a20457 feat(properties): label xref Insert as "External Reference"
Match AutoCAD properties panel — Insert whose block_record carries
is_xref/is_xref_overlay now shows "External Reference" instead of
"Block Reference". Silence dead_code warning on XrefStatus::Unloaded
(intentional, awaiting on-disk Unload-bit detection).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-15 09:19:57 +03:00
Hakan SevenandClaude Opus 4.7 2f0220b1a0 feat(xref+block): bind xref tables, fade rendered geometry, per-defn precision offset
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]>
2026-05-14 21:45:37 +03:00
HakanandClaude Opus 4.7 0996b75bdd Revert: stop honoring BlockRecord.is_loaded for xref skip decision
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]>
2026-05-14 14:14:07 +03:00
HakanandClaude Opus 4.7 e88a946176 feat(xref): honor BlockRecord is_loaded — skip Unloaded xrefs
When the host DWG marks an xref as Unloaded (via XREF→Unload in
AutoCAD), the user explicitly chose NOT to load that reference. We
were ignoring that and pulling the external file in anyway, which
on large drawings could push GPU memory past the device budget.

Bump the acadrust dep to the branch that exposes
`BlockRecord::is_loaded`, then short-circuit `resolve_xrefs` for
any entry with `is_loaded == Some(false)`. New `XrefStatus::Unloaded`
surfaces this in the command-line log so the user can see which
references were skipped (and can re-load them via the XREF dialog).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-14 11:20:11 +03:00
HakanandClaude Opus 4.7 b2588e1790 feat(io): drop parser-garbage entities at load time
acadrust's DWG reader occasionally desynchronises on certain files
(observed on render.dwg: 1519 of 1763 LwPolylines arrived with non-unit
normals in the 1e200+ range and 100k phantom vertices). Tessellating
those entities allocates gigabytes and produces NaN-laden wires,
freezing the UI.

Add a pre-tessellation guard that scans freshly loaded documents and
drops any entity failing a cheap sanity check: non-unit normal,
non-finite or absurd coordinate, or vertex count above 100k. Count of
dropped entities is threaded through DerivedCaches and surfaced as a
warning in the command line so the user knows the file was patched up
on the way in.

Same file in DXF form parses cleanly (0 corrupt), so this is strictly
a DWG-path workaround until the upstream parser bug is fixed.

The new examples/inspect_polyline.rs is a one-off diagnostic that dumps
LwPolyline/Polyline2D/Polyline3D fields plus a corrupt-entity count;
useful for narrowing down the acadrust bug.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-13 10:00:16 +03:00
HakanandClaude Opus 4.7 586df77b2c feat(io): show loading overlay while opening CAD files
Big DWG/DXF files block the UI for seconds during parse + cache build,
leaving the user wondering whether the app froze. Split the file-open
flow into pick → load and show a modal overlay during the load: file
name, size, phase label (Reading / Parsing / Building caches /
Finalizing), animated indeterminate bar, and Cancel button.

Phase is tracked via a shared AtomicU8 written by the loader thread
and read by the overlay each frame. Cancel hides the overlay and
discards the result; the parser thread runs to completion in the
background since acadrust has no interrupt hook.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-13 09:59:07 +03:00
Hakan ca07268c7c cargo fmt 2026-05-12 10:36:15 +03:00
Hakan Seven 2da3640b45 perf: build hatch/image/mesh caches off the UI thread
Parsing + DerivedCaches construction now runs entirely in a dedicated
OS thread inside open_path(). FileOpened handler only assigns the
pre-built caches, keeping the UI thread free during file load.

- Add build_derived_caches(): parallel hatch + mesh tessellation via rayon
- Extend Message::FileOpened / open_path return type with DerivedCaches
- Drop populate_*_from_document() calls from FileOpened handler
2026-05-06 23:04:53 +03:00
Hakan SevenandClaude Sonnet 4.6 f000a8acfb feat: add OLE2Frame interactivity + fix Shape rotation from DXF
OLE2Frame: full TruckConvertible/Grippable/PropertyEditable/Transformable
impls in src/entities/ole2frame.rs — grips on corners + center, properties
panel, translate/scale transforms.

Shape: rotation (DXF code 50) now converted degrees→radians on load in
fix_dxf_dimension_rotations(), matching Dimension and Attribute behaviour.

All ❌ gaps resolved. Remaining: Viewport GPU scissor, LWPolyline plinegen.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-05 20:12:44 +03:00
Hakan SevenandClaude Sonnet 4.6 7e8fc8ef88 feat: replace rfd Save As with native iced file chooser
Implements a full in-app file browser window for Save As, replacing the
rfd dialog that could not expose the selected filter index on Linux.
The new chooser includes directory navigation, a scrollable file list,
filename input, and a format drop-down (DWG/DXF × 2018/2013/2010/2007/2004/2000/R14).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-01 16:20:38 +03:00
Hakan SevenandClaude Sonnet 4.6 9cd8eff77f fix: replace broken version filters with custom Save As dialog
rfd does not expose the selected filter index, so the old DWG/DXF version
filters had no effect — every file was always saved as AC1032 (R2018).

Replace with a single custom iced Save As window that shows:
- File name text input
- Save-in folder input + Browse button (rfd folder picker)
- Format dropdown: DWG/DXF × 2018/2013/2010/2007/2004/2000/R14
- Save / Cancel buttons

The selected format is passed to save_as_version() which temporarily sets
doc.version before writing, so DwgWriter/DxfWriter emit the correct file
format. Also works in the unsaved-changes Save flow.

Closes #9

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-01 16:12:32 +03:00
Hakan SevenandClaude Sonnet 4.6 66a73b00e1 fix: make viewports from external DXF/DWG files visible and enterable
Two root causes prevented paper-space viewports from loading correctly:

1. acadrust's ViewportStatusFlags::from_bits() maps bit 0 → is_on, but
   the real DXF/DWG spec uses bit 15 (0x8000) for "viewport on". Post-load
   fixup in io/mod.rs now corrects this for both DXF and DWG files: when
   bit 0 is clear but bit 15 is set, is_on is forced true and locked is
   derived from bit 14 (0x4000).

2. Some DXF exporters write id=-1 for all viewport entities instead of the
   standard id=1 (sheet) / id≥2 (user) convention. The is_user_viewport_id()
   helper now treats any id ≠ 0 and ≠ 1 as a user viewport, covering both
   the standard id≥2 and the non-standard id=-1 cases.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-04-30 21:57:02 +03:00
Hakan SevenandClaude Sonnet 4.6 c7a7022293 fix: Polyline3D spline vertex flags and AttributeEntity/Definition DXF rotation
Polyline3D tessellation now separates spline-fit curve vertices (flag 8)
from frame control points (flag 16): the curve vertices form the rendered
wire while control points are exposed as key_vertices for snap. Polylines
without spline flags are unchanged.

DXF post-load fixup now also converts AttributeEntity and AttributeDefinition
rotation from degrees to radians (group code 50 bug), matching the existing
LinearDimension fix. Attribute text rendered from DXF files now has correct
orientation.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-04-26 22:12:56 +03:00
Hakan SevenandClaude Sonnet 4.6 19bb846c50 fix: dimension line geometry and DXF rotation unit mismatch
append_linear_dimension() applied the same perp offset to both d1 and d2,
but only d2 is guaranteed to be at the definition_point level; d1 must be
projected independently onto the dimension line, otherwise non-collinear
first/second points produce a parallelogram instead of a rectangle.

Fix: compute dim_line_pos = def.dot(perp) and project each endpoint
separately so the dimension line is always parallel to axis.

Additionally, the acadrust DXF reader stores LinearDimension.rotation from
group code 50 (degrees) without converting to radians, while DWG and our
own annotation commands store it in radians.  Add a post-load fixup in
load_file() that converts the rotation to radians for DXF files so
tessellation can call cos/sin uniformly.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-04-26 16:57:23 +03:00
Hakan SevenandClaude Sonnet 4.6 b24e0c5da9 feat: STEPOUT command — export 3D meshes to STEP AP203
Adds STEPOUT / STPOUT / EXPORTSTEP command that exports all tessellated
Solid3D / Region / Body meshes to an ISO 10303-21 (STEP AP203) file.

Each triangle is encoded as a minimal ADVANCED_FACE with a PLANE surface
and CLOSED_SHELL topology — sufficient for import into all major CAD systems.
A file-save dialog picks the output path; the command line reports success or
errors. Requires at least one tessellated solid in the drawing.

Closes ROADMAP 14.10.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-04-08 22:23:58 +03:00
Hakan SevenandClaude Sonnet 4.6 f515b556c7 feat: PRINT command — send layout to system printer via lp/lpr
The PRINT command renders the current layout to a temporary PDF (reusing
the existing PDF export pipeline) and dispatches it to the OS default
printer:
  - Linux/macOS: `lp` (CUPS) with `lpr` as fallback.
  - Windows: ShellExecute "print" verb (compiled-in, unused on Linux).

PLOT and EXPORT still open the save-file dialog as before.
Status messages are shown in the command line during the async job.

Closes ROADMAP 1.8.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-04-08 22:21:50 +03:00
Hakan SevenandClaude Sonnet 4.6 6fa3157e72 feat: OBJ mesh import (IMPORTOBJ / OBJIMPORT command)
Parses Wavefront OBJ files (v/vn/f, fan-triangulation, face normals
fallback) and inserts the result as a Solid3D placeholder + MeshModel
visible in the 3D viewport.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-04-08 12:59:27 +03:00
Hakan SevenandClaude Sonnet 4.6 25b91bc09e feat: STL export (STLOUT / EXPORTSTL command)
Exports all tessellated 3D mesh models in the current drawing to a
binary STL file via a save-file dialog. Normals are taken from the
MeshModel where available, otherwise computed from triangle vertices.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-04-08 00:46:05 +03:00
Hakan SevenandClaude Sonnet 4.6 64837e9aed feat: XREF management — auto-resolve, XATTACH, XREF, XRELOAD
- src/io/xref.rs: resolve_xrefs() scans block_records for is_xref flags,
  resolves paths relative to the opened file's directory, loads the
  referenced DWG/DXF and merges model-space entities into the xref block
- src/modules/insert/xattach.rs: XATTACH command with file-picker flow
  (XAttachPick/XAttachPickResult messages), prepare_xref_block() creates
  BlockRecord + Block entities and immediately resolves content
- XREF command: lists all external references in the drawing
- XRELOAD command: re-resolves all xref paths and rebuilds scene caches
- Auto-resolve on FileOpened: reports Loaded/NotFound per xref block
- CadCommand trait: added optional xattach_path() method
- ROADMAP.md: 1.9 XREF management marked ✅

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-04-08 00:02:38 +03:00
Hakan SevenandClaude Sonnet 4.6 4312b6c3a5 feat: raster image render pipeline (IMAGE command + GPU textured quad)
- Add IMAGE / IMAGEATTACH / IM command with async file-picker (rfd)
- src/scene/image_model.rs: ImageModel with RGBA pixel data + quad corners
- src/scene/pipeline/image_gpu.rs: ImageGpu with wgpu texture, sampler, opacity uniform
- src/scene/pipeline/mod.rs: image render pipeline (pass 2, between hatches and meshes)
- src/scene/render.rs: include images in Primitive, call upload_images in prepare
- src/scene/mod.rs: images HashMap, populate_images_from_document, cache on add_entity
- src/modules/home/draw/raster_image.rs: ImageCommand (two-click: origin + width, aspect-correct preview)
- src/io/mod.rs: pick_image_file() async helper (file dialog + dimension decode)
- app: ImagePick / ImagePickResult messages, populate on file open and undo/redo

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-04-06 23:19:08 +03:00
Hakan SevenandClaude Sonnet 4.6 1db3ad6a33 Feat: implement CTB/STB plot style table with PDF export integration
- src/io/plot_style.rs: PlotStyleTable and PlotStyleEntry data structures;
  CTB reader (deflate/zlib decompressed text format) with ACI→RGB and
  ACI→lineweight lookup; CTB writer (zlib compressed); LW_TABLE constant
- src/io/pdf_export.rs: accept optional PlotStyleTable; apply ACI color
  and lineweight overrides during PDF rendering
- src/scene/wire_model.rs: add `aci: u8` field to carry ACI index
- src/scene/render.rs: render_style() now returns ACI alongside RGBA
- src/scene/mod.rs + tessellate.rs: propagate aci through wire building
- app: PlotStyleLoad/Loaded/Clear messages and handlers;
  PLOTSTYLE LOAD | CLEAR | STATUS command; active_plot_style state on H7CAD
- flate2 added as dependency

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-04-06 14:37:23 +03:00
Hakan SevenandClaude Sonnet 4.6 617d7ba56a Feat: Phase 2 — expanded PageSetup panel and PlotSettings integration
PageSetup panel:
- Paper size presets (A4/A3/A2/A1/A0 portrait+landscape, Letter)
- Plot area selector: Layout vs Extents
- Center-on-page toggle
- Plot offset X/Y fields
- Rotation picker: 0° / 90° / 180° / 270°

PlotSettings (acadrust object):
- PageSetupCommit finds or creates a PlotSettings object in the document
- Writes paper size, plot type, centering, offset, rotation to it
- DXF round-trip: PlotSettings persisted to file

PDF export:
- Reads PlotSettings for the active layout
- Applies centering (auto-computes offset from wire extents)
- Applies offset X/Y from PlotSettings
- Applies rotation via PDF CTM transformation matrix
- Swaps paper dimensions for 90°/270° rotation

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-04-01 00:12:07 +03:00
Hakan SevenandClaude Sonnet 4.6 845770938c Fix: paper limits, Cohen-Sutherland clipping, frozen layers UI, PDF improvements
Paper limits:
- Change A4 fallback from 12×9 to 297×210 mm in paper_limits()

Context menu:
- Translate layout context menu labels to English (Rename / Delete)

Viewport clipping:
- Replace AABB-only culling with proper Cohen-Sutherland line clipping
  in viewport_content_wires(); each projected segment is now clipped at
  the exact viewport boundary instead of being included or discarded whole
- Add cs_clip() and clip_polyline_to_rect() free functions in scene/mod.rs

Frozen layers per-viewport UI:
- Inject a "Frozen Layers" EditText property in the Properties panel for
  viewport entities (layer names resolved from handles at display time)
- Handle "frozen_layers" in PropGeomCommit: parse comma-separated names,
  resolve to layer handles, update vp.frozen_layers in-place

PDF export improvements:
- Add offset_x/offset_y params to export_pdf() for model-space normalization:
  coordinates are shifted so the drawing origin maps to the page origin
- Model space export computes extents via model_space_extents() (now public)
  and adds a 5 % margin; falls back to A4 landscape for empty drawings
- Skip __paper_boundary__ wire in PDF (white background covers it)
- Map yellow (active viewport border) → black and cyan (vp border) → dark
  blue for print-friendly output

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-31 08:18:29 +03:00
Hakan SevenandClaude Sonnet 4.6 e1d296da10 Phase 7: Plot settings and PDF export
- Add printpdf 0.9.1 dependency for native PDF generation.
- New src/io/pdf_export.rs: converts paper-space WireModels to a PDF
  page (per-wire stroke color, line weight in Pt, NaN-split segments).
  White/near-white colors are inverted to black for print output.
- Add PlotExport / PlotExportPath messages: opens a save-file dialog
  and writes a .pdf next to the drawing file.
- Add PageSetupOpen/Close/WidthEdit/HeightEdit/Commit messages: a
  centered modal panel lets the user enter paper width and height (mm);
  on commit the Layout object's min/max_limits are updated in-place.
- Register PRINT / PLOT / EXPORT and PAGESETUP (PS) commands in
  dispatch_command; fix MVIEW error message to English.
- Layout ribbon gains a "Plot" group with Page Setup and Export PDF
  ribbon buttons.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-31 08:05:17 +03:00
Hakan Seven d2fdac7f1e first commit 2026-03-22 14:56:27 +03:00