Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Koordinat girişi artık göreli/mutlak ayrımını destekliyor:
- `@x,y[,z]` → son girilen noktaya göreli (UCS eksenlerinde döndürülür)
- `#x,y[,z]` → her zaman mutlak (DYN durumundan bağımsız)
- ön ek yok → DYN açıksa göreli, kapalıysa mutlak
İlk noktada referans olmadığından göreli giriş mutlağa düşer. Çözülen
WCS noktası last_point olarak saklanıyor, böylece ardışık göreli
girişler zincirleniyor.
parse_coord artık (Vec3, CoordKind) döndürüyor; relative offset için
ucs_rotate_vec eklendi (origin translation olmadan UCS rotasyonu).
UCS ORIGIN komutu prefix'i yok sayıp mutlak kalıyor.
Ayrıca ViewCube snap refactor'undan kalan kullanılmayan kod temizlendi:
angle_close, active_viewport_yaw_pitch, CubeRegion::snap_angles.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
ViewCube tıklaması artık sabit (yaw, pitch) tablosu yerine bir yön
vektörüne (eye direction) snap ediyor. Up vektörü şöyle seçiliyor:
1. Mevcut up alın.
2. ±X / ±Y / ±Z arasından (yeni gaze ile (anti-)paralel olmayanlar)
mevcut up'a en yüksek dot-product veren cardinal axis'i seç.
3. Bu axis'i yeni gaze yönüne dik plane'e projekte et ve up olarak
kullan.
Sonuç:
- Küçük tilt'ler ilgili canonical axis'e snap olarak temizleniyor.
- TOP yukarıda iken TOP-LEFT edge tıklayınca TOP yukarıda kalıyor;
cube edge'e dönüyor ama kullanıcının "yukarı" hissi korunuyor.
- Upside-down orbit edilmişse -Z up olarak seçilir, snap baş aşağı
yapılandırmayı bozmuyor.
CubeRegion::snap_angles ve Camera::snap_to_angles yerini
snap_direction / snap_to_direction aldı.
snap_active_viewport_to_direction viewport'un view_direction'ını
günceller, twist_angle'a dokunmaz.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Replaces the binary Wireframe/Solid toggle with a 7-mode dropdown
that maps onto acadrust's `ViewportRenderMode` enum: Wireframe 2D /
Wireframe 3D / Hidden Line / Flat Shaded / Gouraud Shaded / Flat
Shaded + Edges / Gouraud Shaded + Edges. Per-tab state lives on
`DocumentTab::render_mode`; the old `SetWireframe` message forwards
to `SetRenderMode` so the ribbon + WIREFRAME / SOLID command line
keep working.
`scene::render::render_mode_flags()` translates the mode into three
pipeline-facing booleans (`face3d_fill`, `mesh_fill`,
`show_3d_edges`); `mesh_fill` / `show_3d_edges` are stubs that the
upcoming phases will consume. `Primitive` carries the derived
`view_wireframe` plus the two new fields through to `prepare()`.
`Pipeline::upload_face3d` takes a `wireframe_only` flag that drops
3DFACE quad fills (built from `key_vertices`) while keeping the
`all_wires.fill_tris` channel — that channel carries text-LOD greek
rectangles and PolyfaceMesh face triangles which must stay visible
in wireframe mode. 3DFACE edges stay on regardless of mode.
Solid3D mesh wireframe rendering, Hidden Line z-prepass, and Flat /
Gouraud shader uniforms come in phases 1–4.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Type-as-you-go suggestion popup above the input that filters known
command names by substring (so `LEADER` surfaces both LEADER and
MLEADER). Up / Down navigates the list, Enter dispatches the
highlighted entry, clicking a row dispatches directly. Esc on a
non-empty input clears the buffer instead of escalating to
command-cancel.
The name corpus comes from `inventory::submit!` blocks placed next
to each `CadCommand` impl rather than a hand-maintained static list.
Adding a new interactive command means dropping one `inventory::submit!`
line in its own module file — the autocomplete picks it up at the
next compile. 98 submissions seeded across 60 module files;
`command::all_registered_command_names()` flattens them at runtime.
One-shot dispatch arms in `app/commands.rs` (NEW, OPEN, SAVE, UNDO,
…) don't have a `CadCommand` impl and are therefore absent from
autocomplete by design.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Add a ▸/▾ toggle button on the right edge of the input row. Tapping
it pops up the full backlog of everything the command line has pushed
since startup — useful once the on-overlay rows have faded out.
The dropdown is a fixed-height scrollable panel rendered above the
input pill. `anchor_bottom` keeps the newest entry in view on open
and `opaque` blocks wheel events from bubbling through to the
viewport shader behind it (without it, scrolling the history zoomed
the drawing). Submitting a command closes the dropdown so the new
prompt is immediately visible.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Two follow-ups to the always-on keyboard work:
1. **Repeat last command on empty Submit.** Pressing Space (or Enter)
after a command ended did nothing — the trailing-space submit path
reached `CommandSubmit`, found no active command and no buffered
text, and bailed. `CommandFinalize` already had the repeat-last
shortcut via `tab.last_cmd`; mirror it inside `CommandSubmit` so
the space-as-submit path picks it up too.
2. **Drop the per-event focus_cmd_input calls** added in the previous
commit for `ViewportLeftPress`, `ViewportClick`, `TabNew`,
`TabSwitch`, `RibbonSelectTab` and `RibbonToolClick`. The global
keyboard subscription routes unhandled character / backspace
events into the buffer regardless of focus, so the only places
that still need an explicit refocus are `cmd_result.rs` (command
lifecycle), `boot()` (initial caret), and the per-char append
handlers (caret feedback while typing).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Two behaviour changes that bring the command line in line with
established CAD workflow:
1. **Always-on keyboard capture.** The input was blurred whenever no
command was running, so typed letters went nowhere until the user
clicked into the field. Replace the blur with a re-focus on every
command lifecycle transition (cmd_result.rs) and on viewport / tab
/ ribbon clicks (update.rs), focus the input at startup, and add
a global key subscription that catches any unhandled character key
or backspace and routes it into the buffer via two new messages
(CommandAppendChar / CommandBackspace) — so the command line keeps
accumulating keystrokes even if focus parks somewhere not
focusable. Removes the unused blur_cmd_input helper.
2. **Space submits.** Typing a space at the end of the input now
submits the buffer the same way Enter does. Only exception: an
active command that's currently collecting free-form prose
(TEXT / MTEXT / DDEDIT body, ATTDEF prompt / default value,
INSERT attribute value, ATTEDIT cell). Those advertise the new
`CadCommand::wants_text_with_spaces()` flag so the Space character
reaches the buffer as a literal.
Co-Authored-By: Claude Opus 4.7 <[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]>
LWDISPLAY (Show Lineweight) flipping the toggle no longer triggers a full
retessellate. `render_style_for` always bakes the entity's resolved (layer-
inherited) lineweight, and the wire shader reads `Uniforms.lwdisplay_enable`
to either honour the per-vertex half_width or collapse the quad to a 1 px line.
Side effect: PDF export now uses the real lineweight regardless of LWDISPLAY,
matching AutoCAD's plot semantics (LWDISPLAY only affects screen render).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Wide LWPolyline/Polyline2D fill bands now sit on the centerline:
- polyline_segment_fill arc center matched explode.rs / acadrust convention
(signed d_perp = r·(1-b²)/(1+b²), handles major arcs correctly)
- per-vertex hw0/hw1 taper applied along arc; tess density 24/circle
- synced_hatch_models subtracts hatch_offset from band boundary so it lives
in the same offset-rel space as the wire tessellation
LWDISPLAY:
- new command `LWDISPLAY [ON|OFF]` toggles header.lineweight_display
- LWT status-bar pill (between ORTHO and POLAR) drives the same flag
- bump_geometry() on change so the viewport re-tess immediately
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The update notice dialog now mirrors what GitHub publishes: side-by-side
Installed → Latest version cards (the new one accent-tinted to draw the
eye) and a scrollable release-notes panel under them. Light markdown
rendering covers `##` headings, bullet lists, and inline bold/code
markers — enough for the GitHub release body format we ship.
- update_check::check_for_update() returns UpdateInfo { version, body };
body is parsed from the releases API JSON with a tiny custom
string-field extractor (\n / \" / \u handling) so we keep the
serde_json-free dependency footprint.
- Window grows from 480x240 to 560x460 and becomes resizable to make
room for the notes scroller.
- Layout tightens: header bottom padding drops 14 → 6, notes heading
10, and the notes block is wrapped in a Fill-height container so it
claims every leftover pixel — no more dead space between heading and
footer.
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]>
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The Home-ribbon Color / Linetype / Lineweight chips live on the
global `self.ribbon` and were never re-seeded from the active tab's
header, so opening a fresh file, creating a new tab, or switching
between tabs kept showing whatever the prior tab had picked. Layer
worked already because `self.tabs[i].active_layer` is per-tab.
Two halves of the fix:
1. When the user picks a new value from the ribbon with no entity
selection, persist it into that tab's `document.header`
(CLAYER + handle, CECOLOR, CELTYPE + handle, CELWEIGHT) and
mark the tab dirty. Switching back later reads the saved value
back out.
2. After every event that changes the active tab — `FileOpened`,
`TabNew`, `TabSwitch`, `TabClose` (including the
close-last-and-reset path) and `UnsavedDialogDiscard` — call
`sync_ribbon_from_selection()` so the chips track the new
tab's defaults (or its current selection).
Closes#21
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Picking a layer / color / linetype / lineweight from the Home ribbon was
only updating the creation default — selected entities stayed unchanged.
DWG editors uniformly treat these dropdowns as edit controls when an
entity is selected.
Route the ribbon Layer / Color / Linetype / Lineweight handlers through
the same property_target_handles + apply_common_prop / apply_color /
apply_line_weight path the Properties panel uses. With no selection the
old behaviour (change creation default) is preserved.
Follow-up to #17.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Adds a one-shot update check that runs on app boot and pops up a small
"Update Available" window when the GitHub releases API reports a newer
tag than this build's `CARGO_PKG_VERSION`.
- `src/update_check.rs` — async wrapper around a blocking `ureq` GET to
`api.github.com/repos/HakanSeven12/H7CAD/releases/latest`, returns
`Some(version)` when a newer release exists, `None` otherwise (silent
on network / parse failure so a missing connection never blocks
launch).
- `H7CAD::boot` chains a `Task::perform` next to the main-window open
so the check runs in parallel with first-paint.
- New window plumbed like the other floating panels (page setup, about,
etc.): `update_notice_window`/`update_notice_version` state, title
entry, `OsWindowClosed` cleanup, and a `ui::update_notice::view_window`
modal that shows installed vs. latest version plus "Later" /
"Open Release Page" buttons. Open routes through `open::that` to the
releases URL and dismisses the modal.
Dependency: adds `ureq = { version = "2", default-features = false,
features = ["tls"] }` — the smallest blocking HTTP client + TLS combo
that doesn't drag in an async runtime.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Two related click bugs:
1. `point_in_polygon` treated NaN points (the path separators used by
multi-path hatches with islands / holes) as regular vertices,
producing spurious closing edges between sub-paths. The cursor
inside a real hatch failed the test → the click iterator moved on
to the NEXT hatch in HashMap order and silently returned the wrong
one. Rewrite the ray-cast to reset the previous-vertex / path-start
state at every NaN, so each sub-path closes against its own first
vertex and contributes its parity flip correctly.
2. Clicking on a hatch *inside* a block was either missing entirely
(after the visible_hatches_for_click filter) or returning the
block-defn source hatch handle (misleading — that handle isn't the
thing the user sees). AutoCAD selects the parent Insert when a
sub-entity of a block is clicked. Add `Scene::insert_hatches_for_click`
which walks the same explode path `synced_hatch_models` uses and
tags every exploded sub-hatch with its parent Insert handle, plus
`hit_test::click_hit_insert_hatch` which iterates that list. The
click resolver now tries wire → MSPACE hatch → insert-internal hatch
in order, so clicking on a block-internal hatch selects the Insert.
Box / lasso select paths are unchanged — wire hit-test already picks
up Inserts, and dragging a box over block-internal hatches is rare
enough not to need its own resolution path yet.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
ACIS SAT entities (Solid3D / Region / Body) were tessellated at one
fixed sampling density (CIRC_SEGS=48, GRID_U=32, GRID_V=16) regardless
of how far they were from the camera. Distant solids paid the full
triangle bill for fragments that mostly resolved to sub-pixel size.
Generate three pre-built LODs per entity at load time:
* LOD 0 (HIGH) — 48/32/16 segments. Use above 200 px projected diag.
* LOD 1 (MID) — 24/16/8. Use 50–200 px.
* LOD 2 (LOW) — 12/8/4. Use below 50 px.
Tessellation cost is paid 3× on load (cheap — typical files have few
solids). Render picks per-frame via `compute_mesh_lod` from each mesh's
`world_aabb` projected diagonal. When a level is missing (e.g. a solid
entity that only emits LOD 0), the selector walks down to the nearest
available slot.
API ripples:
* New `LodConfig` (HIGH/MID/LOW) drives `tess_sat`, `tess_cone_face`,
`tess_sphere_face`, `tess_torus_face`.
* `tessellate_solid3d / _region / _body` now return `MeshLodSet`.
* `MeshLodSet { lods: Vec<MeshModel>, world_aabb }` + `from_single`
constructor so interactive truck-based commands (BOX/CYLINDER/etc.)
that only generate one tessellation stay one-line.
* GPU side gets `MeshLodGpu { lods: Vec<MeshGpu>, world_aabb }` and
a `mesh_lod_levels` per-frame index vector.
* STL/STEP export pulls slot 0 explicitly so the on-disk geometry
isn't downgraded by the view-dependent ladder.
Background-thread lazy generation (per the roadmap) is deferred — the
synchronous eager build is fine for typical CAD files. Switch to a
queue + background tessellator when files with hundreds of solids
become a load-time bottleneck.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
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]>
A real DWG with an xref containing ~74k sub-entities froze on the first
render: Insert tessellation exploded the block, style-resolved every sub
serially, and one bad sub could hang the loop forever.
- Insert short-circuits to an insertion marker when the referenced block
has more than INSERT_SUB_LIMIT (5_000) sub-entities. Documented as
temporary in RENDERING_OPTIMIZATION.md.
- Re-run purge_corrupt_entities after resolve_xrefs so parser-garbage
inside xref content is caught (initial purge runs before xref load).
- build_derived_caches: single pass over entities collecting handles
per kind; tessellation reads via doc.get_entity instead of cloning
every EntityType. Image decode now parallel too.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
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]>
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]>
PolyfaceMesh and PolygonMesh were wireframe-only. They now render with
the same flat-color solid fill as Face3D entities.
- Add fill_tris: Vec<[f32;3]> to TruckEntity and WireModel
- PolyfaceMesh: fan-triangulate each face into fill_tris
- PolygonMesh: triangulate each grid quad into fill_tris
- tessellate.rs Lines branch: apply world_offset to fill_tris
- Face3DGpu::from_wires: collect fill_tris from all wires in addition
to Face3D key_vertices quads
- upload_face3d: pass all_wires so fill_tris are batched into one draw call
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
- Scale popup dropdown replaces cycle behavior; paper-space viewport scale
editable when a viewport is active/selected
- CANNOSCALE persisted via DXF $USERR1 (read on open, write on save)
- PSLTSCALE: linetype dash lengths scaled by viewport anno_scale so dashes
appear uniform physical size in paper space
- MultiLeader: arrow size, dogleg length, and text height scale with
anno_scale; leader line paths (world coords) stay fixed
- Leader: arrow size and hookline length scale with anno_scale; also
corrects world_offset subtraction that was previously deferred to the
TruckObject::Lines path
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Viewport-projected wires now clip at pixel level to their viewport boundary.
- WireModel.vp_scissor: Option<[f32; 4]> — paper-space bbox set on projection
- WireGpu.vp_scissor carries it through to the draw loop
- Pipeline.compute_wire_scissors() converts paper→NDC→pixel each frame
- render() calls set_scissor_rect() per wire, reset after viewport group
- INTEGRATION_GAPS.md: Viewport ⚠️ → ✅, all 51 gaps resolved
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
LwPolyline PLINEGEN=0 (DXF default): emit SegmentedLines with NaN
separators between vertex-to-vertex segments; wire_gpu resets
arc-length to 0 after each NaN so linetype pattern restarts per vertex.
PLINEGEN=1: existing Contour path unchanged (continuous pattern).
WireModel gains plinegen field propagated through tessellate pipeline.
Solid3D/Region/Body: call SabReader::read() when parse_sat() returns
None due to binary SAB format (DWG files / newer AutoCAD exports).
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
GripApply::Absolute adds world_offset to convert local-space coordinates
back to DXF space for model-space entities. Paper-space entities (viewports,
etc.) use sheet mm coordinates with no world_offset, but the grip drag code
was unconditionally adding scene.world_offset — causing grips to jump by the
full model-space centroid offset (potentially hundreds of km for UTM drawings).
Now world_offset is only applied when current_layout == "Model".
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
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]>
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]>
- Grip points in paper space now use the 2-D linear to_px transform
(tx/ty/ortho_size) instead of the 3-D view_proj matrix, matching the
coordinate system used by PaperCanvas
- world_offset is no longer subtracted from paper-space entity grip
positions — paper-space coordinates are already unoffset (same rule
as wire tessellation in wires_for_block)
- MTEXT v_offset corrected for all attachment points: CXF glyphs sit
on the baseline and extend upward, so Top attachment needs v_offset=-h,
Bottom needs (n_lines-1)*line_h, Middle is the midpoint of the two
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Opens a floating OS window when the user tries to close a dirty tab or
quit with unsaved changes. The main window stays alive until the dialog
is resolved — Cancel aborts the close entirely. For quit with multiple
dirty tabs the dialog loops through each one individually.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
paper_limits() can return a non-zero origin (e.g. when min_limits has
negative margin values). PageSetup was using x1/y1 directly instead of
(x1-x0)/(y1-y0), giving wrong dimensions in those cases.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Direct camera.orbit/pan/zoom calls in update.rs bypassed camera_generation
so the Tick handler never detected a change and never wrote the camera back
to the document. Add camera_generation += 1 after each navigation call.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- On file open and layout tab switch, restore the camera from the
document's saved view (*Active VPort for model space, sheet viewport
for paper space) instead of fitting to wire bounds.
- After any zoom/pan/rotate, write the new camera position back to the
document and mark the file dirty so it is saved with the file.
- Uses last_synced_camera_gen to avoid marking the file dirty on the
initial camera restore.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
The acadrust DWG reader never assigns a viewport id (always 0).
Some DXF exporters use id=-1 for all viewports. In both cases the
previous id > 1 check excluded every viewport from content rendering.
Replace is_user_viewport_id(id) with is_content_viewport(vp) that
uses a geometry heuristic for unknown ids (0 / negative):
- id=1 → always sheet viewport (excluded)
- id≥2 → always user viewport (included)
- id=0 or id<0 → sheet if center≈(0,0) AND scale≈1.0, else user
This allows DWG files and non-standard DXF files to display model
content inside their paper-space viewports and be entered via
double-click.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
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]>
Entity grips() return world-space coordinates (raw DXF values). After
the world_offset change the camera view_proj is in local space, so
projecting world-space grips through it gave wrong screen positions and
hit-test failures.
- refresh_selected_grips: subtract world_offset from every grip.world
so selected_grips are stored in local (camera) space
- Grip drag: cursor/snap positions are already local space; for
GripApply::Absolute add world_offset back before passing to entity
so entity coordinates stay in world space. Translate deltas are
identical in both spaces so no conversion is needed there.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Subtracts the DXF header extents centroid (world_offset) from all coordinates
in f64 BEFORE casting to f32. This eliminates the ~0.5 m ULP error that occurs
at 4,000,000 m UTM coordinates and caused geometry to disappear on zoom-out.
- Scene: adds world_offset field + compute_and_set_world_offset() method
- tessellate.rs: threads world_offset through all f64→f32 conversions
(legacy_geometry, dimension_geometry, solid_wire_fallback, multileader)
- truck_tess.rs: to_local() helper already in place from prior session
- mod.rs: tessellate_entity() and entity_aabb() apply world_offset
- update.rs: compute_and_set_world_offset() called after doc load
- cmd_result.rs / solid3d_cmds.rs: pass correct offset to truck tessellators
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Add `aabb: [f32; 4]` field to WireModel (world-space 2-D bounding box).
In tessellate_entity() set each wire's AABB from e.as_entity().bounding_box()
via new entity_aabb() helper; Insert sub-entities each get their own AABB.
Preview/interim wires and any entity returning a zero-extent default box get
UNBOUNDED_AABB so they are never pre-rejected.
Replace the chord-sphere heuristic in Snapper::wire_in_range() with a proper
AABB vs snap-circle overlap test. The new check is four scalar comparisons and
correctly handles closed curves (circles, arcs) which the chord-sphere approach
had to special-case.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Option I: add per-viewport Arc wire cache so paper-space viewport rendering
no longer re-tessellates model content every frame.
Option J: hit_test_wires() returns Arc<Vec<WireModel>> — O(1) pointer copy
in model space instead of a full Vec clone on every mouse move during commands.
paper_canvas_hatches/wipeouts() now route through the epoch caches added in F.
Option K: add world_snap_r + wire_in_range() pre-check to Snapper::snap().
All Endpoint/Midpoint/Nearest/Perpendicular/Intersection loops skip wires
whose chord sphere is outside the snap circle using O(1) scalar comparisons,
eliminating O(entities × vertices) matrix multiply cost when zoomed in.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Add two complementary render caches that make mouse navigation essentially
free regardless of file size:
Option A – Wire tessellation cache: entity_wires_arc() caches the
tessellated Vec<WireModel> keyed by geometry_epoch and returns an Arc,
so navigation frames (no preview wires) incur zero Vec clones and zero
tessellation work. Primitive::wires changed to Arc<Vec<WireModel>>.
Option B – GPU buffer cache: Pipeline stores cached_epoch; prepare()
skips all upload_wires/hatches/images/meshes calls when the epoch is
unchanged, writing only the 192-byte camera uniform instead.
geometry_epoch is a process-wide AtomicU64 incremented by every Scene
mutation (add/erase entity, selection, layer visibility, layout switch,
populate_*, grip, transform, copy, clear, preview wires). Using a global
counter rather than a per-scene counter prevents two tabs that happen to
have the same local bump count from sharing a cached_epoch value, which
was causing the first file's geometry to remain on screen after opening a
second file.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Integrate NAVVCUBE, NAVBAR, PROPERTIES, FILETAB, LAYOUTTAB, TOOLPALETTES
and SHEETSET commands to toggle UI widgets at runtime. Add 15 new SVG icons
for view module tools previously using glyph fallbacks. Restyle all SVG
icons to use the established two-color scheme: #e0e0e0 for passive geometry,
#4cc9f0 for active/accent elements.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- paper_entity_extents() computes the bounding box of actual DXF entities
so the white fill always matches the drawn title-block / frame borders,
regardless of whether Layout min/max_limits match entity positions
- Falls back to paper_limits() (→ A4 297×210) when layout has no entities
- Removed paper_boundary_wire: the white fill now acts as the visual paper
edge so the near-white wire (invisible on white) was redundant
- LayoutCreate sets limits to A4 landscape immediately after add_layout()
to override acadrust's imperial default (12×9)
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Add orbit_active_viewport(), snap_active_viewport_to_angles(),
active_view_rotation_mat(), active_viewport_yaw_pitch() helpers so
navigation gestures modify the DXF viewport entity's view_direction
instead of the paper-space camera.
- Fix "already there → flip opposite" check: the previous code used
rot * Vec4::W (always (0,0,0,1) for a rotation matrix); now reads
(yaw, pitch) directly from camera_for_viewport() via the new helper.
- MSPACE right-drag orbit early-returns after writing the viewport
entity, preventing the paper-space camera from also being modified.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- REPORT opens GitHub new-issue page
- ABOUT opens a dialog window with version, OS and arch info + Copy Info button
- CHANGELOG opens GitHub releases page
- Fix Windows build: add windows-sys as target-specific dependency for PRINT command
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>