2026-03-22 22:44:32 +03:00
|
|
|
use crate::command::CadCommand;
|
2026-06-26 23:06:40 +03:00
|
|
|
use crate::io::linetypes;
|
2026-07-14 13:03:04 +03:00
|
|
|
use crate::modules::draw::modify::block_edit::BlockEditSession;
|
2026-06-16 11:49:14 +03:00
|
|
|
use crate::modules::draw::modify::refedit::RefEditSession;
|
2026-06-16 11:33:46 +03:00
|
|
|
use crate::scene::pick::grip::GripEdit;
|
2026-03-22 22:44:32 +03:00
|
|
|
use crate::scene::GripDef;
|
2026-07-27 23:36:02 +03:00
|
|
|
use crate::scene::{ObjectIsolationState, Scene};
|
2026-05-12 10:36:15 +03:00
|
|
|
use crate::snap::SnapResult;
|
|
|
|
|
use crate::ui::{LayerPanel, PropertiesPanel};
|
2026-08-15 19:56:35 +03:00
|
|
|
use acadrust::tables::{normalize_name, Ucs};
|
perf(undo): record entity deltas instead of cloning the whole document
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:11:17 +03:00
|
|
|
use acadrust::{CadDocument, EntityType, Handle};
|
2026-04-08 00:12:09 +03:00
|
|
|
use iced;
|
2026-08-02 22:09:25 +03:00
|
|
|
use crate::t;
|
2026-06-17 15:50:52 +03:00
|
|
|
use std::any::Any;
|
2026-06-09 13:41:21 -04:00
|
|
|
use std::collections::HashMap;
|
2026-05-12 10:36:15 +03:00
|
|
|
use std::path::PathBuf;
|
2026-07-25 02:58:46 +03:00
|
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
2026-07-24 23:25:33 +03:00
|
|
|
use std::sync::Arc;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
2026-07-25 02:58:46 +03:00
|
|
|
static NEXT_DOCUMENT_TAB_ID: AtomicU64 = AtomicU64::new(1);
|
|
|
|
|
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
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 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
// ── Dynamic input ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// One quantity shown in the dynamic-input overlay near the cursor.
|
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
|
|
|
pub(super) enum DynComponent {
|
|
|
|
|
/// Absolute X ordinate.
|
|
|
|
|
X,
|
|
|
|
|
/// Absolute Y ordinate.
|
|
|
|
|
Y,
|
fix(dyn-input): support cartesian coords via "," (#35)
Typing \`,\` while DYN was on appended the character to the focused
field's buffer (treated as a European decimal point), so users
couldn't switch to cartesian coordinates mid-command. The dynamic
input was stuck in polar \`[Distance, Angle]\` whenever a base point
existed.
\`,\` now acts as the coordinate separator (matching the AutoCAD
convention): pressing it locks the current field's buffer, then
either advances within the existing cartesian set or reshapes a
polar configuration into cartesian:
- \`[Distance, Angle]\` + \`,\` on the first field → \`[X(buf), Y]\`
- \`[X, Y]\` + \`,\` on Y → \`[X, Y, Z]\`
- otherwise advances to the next field (Tab-equivalent)
\`sync_dyn_fields\` accepts both polar and cartesian shapes as valid
\`Point\` configurations so a mouse-move after the reshape doesn't
revert to polar. \`dyn_resolve_point\` and the overlay's live-value
formatter treat cartesian inputs as RELATIVE to the base point when
one exists — matching the DYN-on relative-coords convention from
#26. \`DynComponent::Z\` plus its label / value paths fill out the
3-D variant.
Closes #35
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:46:18 +03:00
|
|
|
/// Absolute Z ordinate (only visible after the user types a second
|
|
|
|
|
/// `,` separator from a cartesian X/Y configuration).
|
|
|
|
|
Z,
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
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 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
/// Linear distance from the last point.
|
|
|
|
|
Distance,
|
|
|
|
|
/// Angle from the last point, in degrees.
|
|
|
|
|
Angle,
|
2026-06-09 18:32:03 +03:00
|
|
|
/// A scalar the command reads from the command line (a count, a radius,
|
|
|
|
|
/// a delta). Typed-only — it has no geometric live value derived from
|
|
|
|
|
/// the cursor unless the command supplies one via `dyn_live_value`.
|
|
|
|
|
Scalar,
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
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 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A single editable dynamic-input field. `buffer == None` means the box
|
|
|
|
|
/// tracks the cursor live; once the user types, the typed text is held in
|
|
|
|
|
/// `buffer` and the box stops following the cursor (it is "locked").
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub(super) struct DynFieldEntry {
|
|
|
|
|
pub(super) component: DynComponent,
|
2026-06-15 17:12:08 +03:00
|
|
|
/// Semantic role — drives the label and value scaling (e.g. diameter).
|
|
|
|
|
/// Defaults to the role matching `component` on the legacy path.
|
|
|
|
|
pub(super) role: crate::command::DynRole,
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
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 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
pub(super) buffer: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DynFieldEntry {
|
|
|
|
|
pub(super) fn new(component: DynComponent) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
component,
|
2026-06-15 17:12:08 +03:00
|
|
|
role: default_role_for(component),
|
|
|
|
|
buffer: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/// Build from an explicit role (spec-driven path); the resolution
|
|
|
|
|
/// component is derived from the role.
|
|
|
|
|
pub(super) fn from_role(role: crate::command::DynRole) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
component: component_for_role(role),
|
|
|
|
|
role,
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
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 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
buffer: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
pub(super) fn locked(&self) -> bool {
|
|
|
|
|
self.buffer.is_some()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 17:12:08 +03:00
|
|
|
/// Map a [`DynRole`](crate::command::DynRole) to the ordinate/distance/angle
|
|
|
|
|
/// component used by point resolution.
|
|
|
|
|
pub(super) fn component_for_role(role: crate::command::DynRole) -> DynComponent {
|
|
|
|
|
use crate::command::DynRole;
|
|
|
|
|
match role {
|
|
|
|
|
DynRole::X | DynRole::Width => DynComponent::X,
|
|
|
|
|
DynRole::Y | DynRole::Height => DynComponent::Y,
|
|
|
|
|
DynRole::Z => DynComponent::Z,
|
|
|
|
|
DynRole::Distance | DynRole::Radius | DynRole::Diameter => DynComponent::Distance,
|
|
|
|
|
DynRole::Angle => DynComponent::Angle,
|
|
|
|
|
DynRole::Factor | DynRole::Count => DynComponent::Scalar,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_role_for(component: DynComponent) -> crate::command::DynRole {
|
|
|
|
|
use crate::command::DynRole;
|
|
|
|
|
match component {
|
|
|
|
|
DynComponent::X => DynRole::X,
|
|
|
|
|
DynComponent::Y => DynRole::Y,
|
|
|
|
|
DynComponent::Z => DynRole::Z,
|
|
|
|
|
DynComponent::Distance => DynRole::Distance,
|
|
|
|
|
DynComponent::Angle => DynRole::Angle,
|
|
|
|
|
DynComponent::Scalar => DynRole::Factor,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
// ── Per-document tab state ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
pub(super) struct DocumentTab {
|
2026-07-25 02:58:46 +03:00
|
|
|
/// Stable identity across tab insert/remove operations. Background work
|
|
|
|
|
/// must never target a tab by its transient vector index.
|
|
|
|
|
pub(super) id: u64,
|
2026-03-22 22:44:32 +03:00
|
|
|
pub(super) scene: Scene,
|
|
|
|
|
pub(super) current_path: Option<PathBuf>,
|
2026-07-27 13:13:58 +03:00
|
|
|
/// Persistent native edit lease for the drawing currently at
|
|
|
|
|
/// `current_path`. The sidecar portion survives atomic file replacement.
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
pub(super) edit_lease: Option<crate::io::edit_lock::EditLease>,
|
|
|
|
|
/// True when another editor owns the drawing lease. Direct Save stays
|
|
|
|
|
/// blocked until Retry acquires it or the user chooses Save As.
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
pub(super) edit_lock_conflict: bool,
|
|
|
|
|
/// Disk state observed after open or the latest successful save.
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
pub(super) disk_fingerprint: Option<crate::io::edit_lock::FileFingerprint>,
|
2026-03-22 22:44:32 +03:00
|
|
|
pub(super) dirty: bool,
|
2026-08-03 12:47:48 +03:00
|
|
|
/// Direct Save is redirected to Save As after open-time repairs so the
|
|
|
|
|
/// source drawing cannot be overwritten accidentally.
|
|
|
|
|
pub(super) recovery_save_as_required: bool,
|
2026-07-25 02:58:46 +03:00
|
|
|
/// Monotonic committed-edit/undo/redo revision. Background save completion
|
|
|
|
|
/// uses it with scene epochs so an older snapshot never clears newer work.
|
|
|
|
|
pub(super) edit_revision: u64,
|
2026-03-22 22:44:32 +03:00
|
|
|
pub(super) tab_title: String,
|
|
|
|
|
pub(super) properties: PropertiesPanel,
|
|
|
|
|
pub(super) layers: LayerPanel,
|
|
|
|
|
pub(super) active_cmd: Option<Box<dyn CadCommand>>,
|
2026-07-21 11:01:51 +03:00
|
|
|
/// The selection set the most recent command worked on, captured when a
|
|
|
|
|
/// finishing command drops the live selection — re-selectable with the
|
|
|
|
|
/// "Previous" keyword at any Select objects prompt (#426).
|
|
|
|
|
pub(super) prev_selection: Vec<acadrust::Handle>,
|
2026-03-22 22:44:32 +03:00
|
|
|
pub(super) last_cmd: Option<String>,
|
2026-08-01 19:09:19 +03:00
|
|
|
/// Most recently created path drawable. A fresh LINE/PLINE can accept its
|
|
|
|
|
/// current final endpoint with Enter before the first click.
|
|
|
|
|
pub(super) last_draw_anchor: Option<Handle>,
|
2026-03-22 22:44:32 +03:00
|
|
|
pub(super) snap_result: Option<SnapResult>,
|
|
|
|
|
pub(super) active_grip: Option<GripEdit>,
|
|
|
|
|
pub(super) selected_grips: Vec<GripDef>,
|
2026-07-31 22:05:30 +03:00
|
|
|
/// Entity handle for each entry in `selected_grips`.
|
|
|
|
|
pub(super) selected_grip_handles: Vec<Handle>,
|
|
|
|
|
/// Shift-selected grips, keyed by entity and object-local grip id.
|
|
|
|
|
pub(super) hot_grips: rustc_hash::FxHashSet<(Handle, usize)>,
|
2026-03-22 22:44:32 +03:00
|
|
|
pub(super) selected_handle: Option<Handle>,
|
2026-06-11 20:11:19 +03:00
|
|
|
/// Dynamic-block visibility grip for the current single selection.
|
|
|
|
|
pub(super) visibility_grip: Option<super::visibility::VisibilityGrip>,
|
2026-03-22 22:44:32 +03:00
|
|
|
pub(super) wireframe: bool,
|
2026-05-27 00:08:37 +03:00
|
|
|
pub(super) render_mode: acadrust::entities::ViewportRenderMode,
|
2026-03-22 22:44:32 +03:00
|
|
|
pub(super) visual_style: String,
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
pub(super) last_cursor_world: glam::DVec3,
|
2026-04-08 00:12:09 +03:00
|
|
|
pub(super) last_cursor_screen: iced::Point,
|
2026-06-15 17:12:08 +03:00
|
|
|
/// Base point (`App::last_point`) projected to viewport pixels, refreshed
|
|
|
|
|
/// on cursor move. Lets the dynamic-input overlay place the distance label
|
|
|
|
|
/// along the rubber-band line and the angle label at its end.
|
|
|
|
|
pub(super) last_point_screen: Option<iced::Point>,
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
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 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
/// Dynamic-input fields shown near the cursor while a command waits
|
|
|
|
|
/// for a point/distance/angle. Rebuilt whenever the active command's
|
|
|
|
|
/// `dyn_field()` or the presence of a base point changes. Empty when
|
|
|
|
|
/// dynamic input is not active.
|
|
|
|
|
pub(super) dyn_fields: Vec<DynFieldEntry>,
|
2026-06-15 17:12:08 +03:00
|
|
|
/// Guide geometry the overlay draws for the current step (set alongside
|
|
|
|
|
/// `dyn_fields`). Polar arc, radius line, axis-delta projections, etc.
|
|
|
|
|
pub(super) dyn_guide: crate::command::DynGuide,
|
|
|
|
|
/// World-space anchor the current step's values are measured from. `None`
|
|
|
|
|
/// falls back to `App::last_point`.
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
pub(super) dyn_anchor: Option<glam::DVec3>,
|
2026-06-15 18:46:34 +03:00
|
|
|
/// Far end of a reference line through `dyn_anchor` (for the `Perp` guide:
|
|
|
|
|
/// the base edge / major axis the offset is measured square to).
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
pub(super) dyn_ref: Option<glam::DVec3>,
|
2026-06-15 18:46:34 +03:00
|
|
|
/// `dyn_ref` projected to viewport pixels.
|
|
|
|
|
pub(super) dyn_ref_screen: Option<iced::Point>,
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
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 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
/// Index of the field that TAB has focused (the one keystrokes edit).
|
|
|
|
|
pub(super) dyn_active: usize,
|
2026-03-22 22:44:32 +03:00
|
|
|
pub(super) history: HistoryState,
|
|
|
|
|
pub(super) active_layer: String,
|
2026-04-06 14:16:06 +03:00
|
|
|
/// Currently active UCS. `None` means WCS (identity transform).
|
|
|
|
|
pub(super) active_ucs: Option<Ucs>,
|
2026-04-06 23:26:26 +03:00
|
|
|
/// Custom model-space background color. `None` = default dark grey.
|
|
|
|
|
pub(super) bg_color: Option<[f32; 4]>,
|
|
|
|
|
/// Custom paper-space background color. `None` = default off-white grey.
|
|
|
|
|
pub(super) paper_bg_color: Option<[f32; 4]>,
|
2026-04-08 12:40:19 +03:00
|
|
|
/// Active REFEDIT session, if any.
|
|
|
|
|
pub(super) refedit_session: Option<RefEditSession>,
|
2026-07-28 19:13:27 +03:00
|
|
|
/// Open BEDIT block tabs. Definitions are edited live; each tab owns its
|
|
|
|
|
/// entry snapshot and camera so nested blocks can remain open independently.
|
|
|
|
|
pub(super) block_edits: Vec<BlockEditSession>,
|
|
|
|
|
/// Index of the block tab currently shown, or `None` for Model/Paper space.
|
|
|
|
|
pub(super) active_block_edit: Option<usize>,
|
2026-04-08 18:13:21 +03:00
|
|
|
/// Currently active MLeader style name.
|
|
|
|
|
pub(super) active_mleader_style: String,
|
2026-05-01 07:07:19 +03:00
|
|
|
/// Last camera_generation value written back to the document.
|
|
|
|
|
pub(super) last_synced_camera_gen: u64,
|
feat(app): add fixed Start tab with welcome page and recent documents
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 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
/// Sentinel "Welcome / Start" tab. Always at index 0 when present.
|
|
|
|
|
/// Cannot be closed; the viewport area renders a welcome page instead
|
|
|
|
|
/// of the model-space shader. The scene is still constructed so the
|
|
|
|
|
/// rest of the code can treat it as a normal tab when reading.
|
|
|
|
|
pub(super) is_start: bool,
|
2026-06-25 10:03:54 +03:00
|
|
|
/// Interactive PAN mode (the PAN command / tool). While active, a left-
|
|
|
|
|
/// button drag pans the view instead of selecting — the only pan path on a
|
|
|
|
|
/// device with no middle mouse button (a trackpad / web client). Exited
|
|
|
|
|
/// with Esc or by starting another command.
|
|
|
|
|
pub(super) pan_mode: bool,
|
2026-08-04 18:21:52 +03:00
|
|
|
/// Interactive 3-D orbit mode. While active, a left-button drag follows
|
|
|
|
|
/// the same camera-orbit path as Shift + middle-button drag.
|
|
|
|
|
pub(super) orbit_mode: bool,
|
2026-08-08 20:37:53 +03:00
|
|
|
/// Interactive ZOOM Dynamic mode. A left drag pans horizontally and zooms
|
|
|
|
|
/// vertically until Escape or another command ends the mode.
|
|
|
|
|
pub(super) zoom_dynamic_mode: bool,
|
2026-06-09 13:41:21 -04:00
|
|
|
/// Per-plugin document state (`plugin::BuiltinPlugin` manifest id → state).
|
2026-06-22 02:03:34 +03:00
|
|
|
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
2026-06-09 13:41:21 -04:00
|
|
|
pub(super) plugin_state: HashMap<&'static str, Box<dyn Any + Send + Sync>>,
|
2026-06-18 14:55:08 +01:00
|
|
|
pub(super) suspended_cmd: Option<Box<dyn CadCommand>>,
|
2026-03-22 22:44:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DocumentTab {
|
2026-08-15 19:56:35 +03:00
|
|
|
pub(super) fn rename_layer(&mut self, old_name: &str, new_name: &str) -> bool {
|
|
|
|
|
let active = normalize_name(&self.active_layer) == normalize_name(old_name);
|
|
|
|
|
if !self.scene.rename_layer(old_name, new_name) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if active {
|
|
|
|
|
self.active_layer = new_name.to_string();
|
|
|
|
|
}
|
|
|
|
|
self.dirty = true;
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 19:13:27 +03:00
|
|
|
pub(super) fn active_block_edit_session(&self) -> Option<&BlockEditSession> {
|
|
|
|
|
self.active_block_edit
|
|
|
|
|
.and_then(|index| self.block_edits.get(index))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn active_block_edit_session_mut(&mut self) -> Option<&mut BlockEditSession> {
|
|
|
|
|
self.active_block_edit
|
|
|
|
|
.and_then(|index| self.block_edits.get_mut(index))
|
|
|
|
|
}
|
|
|
|
|
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
/// The active WCS↔UCS converter for this tab — identity when no UCS is set.
|
|
|
|
|
/// Every consumer that needs UCS-relative coordinates goes through this.
|
|
|
|
|
pub(super) fn ucs_xform(&self) -> super::helpers::UcsXform {
|
|
|
|
|
super::helpers::UcsXform::from_active(self.active_ucs.as_ref())
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-27 01:12:45 +03:00
|
|
|
/// World-space UCS origin in full f64 precision (stored f64 in the header /
|
|
|
|
|
/// viewport). Used to anchor the UCS icon and as the cursor-pick plane point
|
|
|
|
|
/// — `UcsXform`'s origin is f32 and would quantize the anchor at UTM scale.
|
|
|
|
|
pub(super) fn ucs_origin_world(&self) -> glam::DVec3 {
|
|
|
|
|
self.active_ucs
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|u| glam::dvec3(u.origin.x, u.origin.y, u.origin.z))
|
|
|
|
|
.unwrap_or(glam::DVec3::ZERO)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 17:23:29 +03:00
|
|
|
/// True when the active pane edits **model-space** geometry: the Model tab,
|
|
|
|
|
/// or inside a floating viewport (MSPACE). The UCS applies in both; plain
|
|
|
|
|
/// paper space (no active viewport) is excluded. Single predicate so every
|
|
|
|
|
/// UCS-aware system shares one rule. [[feedback_shared_infra]]
|
|
|
|
|
pub(super) fn editing_model_space(&self) -> bool {
|
|
|
|
|
self.scene.current_layout == "Model" || self.scene.active_viewport.is_some()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The document's saved model-space UCS (header), as a `Ucs`. `None` when it
|
|
|
|
|
/// is identity (plain WCS).
|
2026-07-30 19:50:33 +03:00
|
|
|
pub(super) fn model_ucs_from_header(&self) -> Option<Ucs> {
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
let h = &self.scene.document.header;
|
2026-08-04 23:17:54 +03:00
|
|
|
let mut u = if h.model_space_ucs_name.is_empty() {
|
|
|
|
|
Ucs::new("*ACTIVE*")
|
|
|
|
|
} else {
|
|
|
|
|
self.scene
|
|
|
|
|
.document
|
|
|
|
|
.ucss
|
|
|
|
|
.get(&h.model_space_ucs_name)
|
|
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or_else(|| Ucs::new(h.model_space_ucs_name.clone()))
|
|
|
|
|
};
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
u.origin = h.model_space_ucs_origin;
|
|
|
|
|
u.x_axis = h.model_space_ucs_x_axis;
|
|
|
|
|
u.y_axis = h.model_space_ucs_y_axis;
|
2026-08-04 23:17:54 +03:00
|
|
|
if h.model_space_ucs_name.is_empty()
|
|
|
|
|
&& super::helpers::UcsXform::from_ucs(&u).is_identity()
|
|
|
|
|
{
|
2026-06-24 17:23:29 +03:00
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(u)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A floating viewport's own per-viewport UCS, if it has one set. `None`
|
|
|
|
|
/// when the viewport uses world coordinates or the handle is not a viewport.
|
|
|
|
|
pub(super) fn ucs_from_viewport(&self, h: Handle) -> Option<Ucs> {
|
|
|
|
|
let vp = match self.scene.document.get_entity(h) {
|
|
|
|
|
Some(acadrust::EntityType::Viewport(vp)) => vp,
|
|
|
|
|
_ => return None,
|
|
|
|
|
};
|
|
|
|
|
if !vp.ucs_per_viewport {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
2026-08-04 23:17:54 +03:00
|
|
|
let mut u = if vp.ucs_handle.is_null() {
|
|
|
|
|
Ucs::new("*VPUCS*")
|
|
|
|
|
} else {
|
|
|
|
|
self.scene
|
|
|
|
|
.document
|
|
|
|
|
.ucss
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|ucs| ucs.handle == vp.ucs_handle)
|
|
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or_else(|| Ucs::new("*VPUCS*"))
|
|
|
|
|
};
|
2026-06-24 17:23:29 +03:00
|
|
|
u.origin = vp.ucs_origin;
|
|
|
|
|
u.x_axis = vp.ucs_x_axis;
|
|
|
|
|
u.y_axis = vp.ucs_y_axis;
|
2026-08-04 23:17:54 +03:00
|
|
|
if vp.ucs_handle.is_null() && super::helpers::UcsXform::from_ucs(&u).is_identity() {
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(u)
|
2026-06-24 17:23:29 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Set `active_ucs` to the UCS of the *current pane*: the entered viewport's
|
|
|
|
|
/// own per-viewport UCS, the model header UCS in the Model tab, or none in
|
|
|
|
|
/// plain paper space. Keeps the ViewCube in lock-step. Call on every pane
|
|
|
|
|
/// change (enter/exit viewport, layout / tab switch, load) so one field
|
|
|
|
|
/// drives all UCS-aware systems regardless of where editing happens.
|
|
|
|
|
pub(super) fn refresh_active_ucs(&mut self) {
|
2026-07-30 19:50:33 +03:00
|
|
|
self.active_ucs = if let Some(session) = self.active_block_edit_session() {
|
|
|
|
|
session.editor_ucs.clone()
|
|
|
|
|
} else if let Some(h) = self.scene.active_viewport {
|
2026-06-24 17:23:29 +03:00
|
|
|
self.ucs_from_viewport(h)
|
|
|
|
|
} else if self.scene.current_layout == "Model" {
|
|
|
|
|
self.model_ucs_from_header()
|
|
|
|
|
} else {
|
|
|
|
|
None
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
};
|
2026-06-22 10:10:47 +03:00
|
|
|
self.sync_ucs_to_scene();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 17:23:29 +03:00
|
|
|
/// Persist `active_ucs` back to its pane's storage so it round-trips: the
|
|
|
|
|
/// entered viewport's per-viewport UCS fields, or the document header's
|
|
|
|
|
/// model-space UCS in the Model tab. No-op in plain paper space. Call after
|
|
|
|
|
/// any UCS change.
|
|
|
|
|
pub(super) fn persist_active_ucs(&mut self) {
|
|
|
|
|
use acadrust::types::Vector3;
|
2026-07-30 19:50:33 +03:00
|
|
|
if let Some(index) = self.active_block_edit {
|
|
|
|
|
let active_ucs = self.active_ucs.clone();
|
|
|
|
|
if let Some(session) = self.block_edits.get_mut(index) {
|
|
|
|
|
session.editor_ucs = active_ucs;
|
|
|
|
|
}
|
|
|
|
|
} else if let Some(h) = self.scene.active_viewport {
|
|
|
|
|
let (o, x, y, handle, per) = match &self.active_ucs {
|
|
|
|
|
Some(u) => (u.origin, u.x_axis, u.y_axis, u.handle, true),
|
|
|
|
|
None => (
|
|
|
|
|
Vector3::ZERO,
|
|
|
|
|
Vector3::UNIT_X,
|
|
|
|
|
Vector3::UNIT_Y,
|
|
|
|
|
Handle::NULL,
|
|
|
|
|
false,
|
|
|
|
|
),
|
2026-06-24 17:23:29 +03:00
|
|
|
};
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
if let Some(acadrust::EntityType::Viewport(vp)) = self.scene.document.get_entity_mut(h)
|
2026-06-24 17:23:29 +03:00
|
|
|
{
|
|
|
|
|
vp.ucs_origin = o;
|
|
|
|
|
vp.ucs_x_axis = x;
|
|
|
|
|
vp.ucs_y_axis = y;
|
2026-07-30 19:50:33 +03:00
|
|
|
vp.ucs_handle = handle;
|
2026-06-24 17:23:29 +03:00
|
|
|
vp.ucs_per_viewport = per;
|
|
|
|
|
}
|
|
|
|
|
} else if self.scene.current_layout == "Model" {
|
|
|
|
|
let h = &mut self.scene.document.header;
|
|
|
|
|
match &self.active_ucs {
|
|
|
|
|
Some(u) => {
|
|
|
|
|
h.model_space_ucs_origin = u.origin;
|
|
|
|
|
h.model_space_ucs_x_axis = u.x_axis;
|
|
|
|
|
h.model_space_ucs_y_axis = u.y_axis;
|
2026-07-30 19:50:33 +03:00
|
|
|
h.model_space_ucs_name = u.name.clone();
|
2026-06-24 17:23:29 +03:00
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
h.model_space_ucs_origin = Vector3::ZERO;
|
|
|
|
|
h.model_space_ucs_x_axis = Vector3::UNIT_X;
|
|
|
|
|
h.model_space_ucs_y_axis = Vector3::UNIT_Y;
|
2026-07-30 19:50:33 +03:00
|
|
|
h.model_space_ucs_name.clear();
|
2026-06-24 17:23:29 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Adopt the active pane's UCS on load (the file's saved model-space UCS in
|
|
|
|
|
/// the Model tab). Thin wrapper over [`refresh_active_ucs`] kept for the
|
|
|
|
|
/// load call sites.
|
|
|
|
|
pub(super) fn adopt_active_ucs_from_header(&mut self) {
|
|
|
|
|
self.refresh_active_ucs();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 10:10:47 +03:00
|
|
|
/// Push the active UCS rotation into the scene so the ViewCube composes with
|
|
|
|
|
/// it. Call after any change to `active_ucs`.
|
|
|
|
|
pub(super) fn sync_ucs_to_scene(&mut self) {
|
2026-06-22 11:17:55 +03:00
|
|
|
self.scene.viewcube_ucs = self.ucs_xform().rotation_mat();
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
}
|
|
|
|
|
|
2026-06-22 11:27:34 +03:00
|
|
|
/// Grid origin (render/wire space) and UCS→world rotation for grid snap and
|
|
|
|
|
/// the grid overlay. Identity / origin-at-zero outside model space.
|
|
|
|
|
pub(super) fn ucs_grid_basis(&self) -> (glam::Vec3, glam::Mat4) {
|
2026-06-24 17:23:29 +03:00
|
|
|
if !self.editing_model_space() {
|
2026-06-22 11:27:34 +03:00
|
|
|
return (glam::Vec3::ZERO, glam::Mat4::IDENTITY);
|
|
|
|
|
}
|
|
|
|
|
let xf = self.ucs_xform();
|
|
|
|
|
let (o, ..) = xf.axes();
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
let origin = glam::Vec3::new(o.x as f32, o.y as f32, o.z as f32);
|
2026-06-22 11:27:34 +03:00
|
|
|
(origin, xf.rotation_mat())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
pub(super) fn new_drawing(n: usize) -> Self {
|
|
|
|
|
let mut scene = Scene::new();
|
|
|
|
|
linetypes::populate_document(&mut scene.document);
|
2026-07-29 17:21:57 +03:00
|
|
|
// Paper layouts start as A4 landscape.
|
2026-04-23 14:23:38 +03:00
|
|
|
for obj in scene.document.objects.values_mut() {
|
|
|
|
|
if let acadrust::objects::ObjectType::Layout(l) = obj {
|
|
|
|
|
if l.name != "Model" {
|
|
|
|
|
l.min_limits = (0.0, 0.0);
|
|
|
|
|
l.max_limits = (297.0, 210.0);
|
|
|
|
|
l.min_extents = (0.0, 0.0, 0.0);
|
|
|
|
|
l.max_extents = (297.0, 210.0, 0.0);
|
2026-07-31 23:08:41 +03:00
|
|
|
l.paper_width = 297.0;
|
|
|
|
|
l.paper_height = 210.0;
|
|
|
|
|
l.plot_paper_units = 1;
|
|
|
|
|
l.plot_scale_numerator = 1.0;
|
|
|
|
|
l.plot_scale_denominator = 1.0;
|
|
|
|
|
l.paper_size = "ISO_A4_(297.00_x_210.00_MM)".into();
|
2026-04-23 14:23:38 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
Self {
|
2026-07-25 02:58:46 +03:00
|
|
|
id: NEXT_DOCUMENT_TAB_ID.fetch_add(1, Ordering::Relaxed),
|
2026-03-22 22:44:32 +03:00
|
|
|
scene,
|
|
|
|
|
current_path: None,
|
2026-07-27 13:13:58 +03:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
edit_lease: None,
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
edit_lock_conflict: false,
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
disk_fingerprint: None,
|
2026-03-22 22:44:32 +03:00
|
|
|
dirty: false,
|
2026-08-03 12:47:48 +03:00
|
|
|
recovery_save_as_required: false,
|
2026-07-25 02:58:46 +03:00
|
|
|
edit_revision: 0,
|
2026-07-21 11:01:51 +03:00
|
|
|
prev_selection: Vec::new(),
|
2026-03-22 22:44:32 +03:00
|
|
|
tab_title: format!("Drawing{}", n),
|
|
|
|
|
properties: PropertiesPanel::empty(),
|
|
|
|
|
layers: LayerPanel::default(),
|
|
|
|
|
active_cmd: None,
|
|
|
|
|
last_cmd: None,
|
2026-08-01 19:09:19 +03:00
|
|
|
last_draw_anchor: None,
|
2026-03-22 22:44:32 +03:00
|
|
|
snap_result: None,
|
|
|
|
|
active_grip: None,
|
|
|
|
|
selected_grips: vec![],
|
2026-07-31 22:05:30 +03:00
|
|
|
selected_grip_handles: vec![],
|
|
|
|
|
hot_grips: rustc_hash::FxHashSet::default(),
|
2026-03-22 22:44:32 +03:00
|
|
|
selected_handle: None,
|
2026-06-11 20:11:19 +03:00
|
|
|
visibility_grip: None,
|
2026-03-22 22:44:32 +03:00
|
|
|
wireframe: false,
|
2026-05-27 00:08:37 +03:00
|
|
|
render_mode: acadrust::entities::ViewportRenderMode::Wireframe2D,
|
|
|
|
|
visual_style: "Wireframe 2D".into(),
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
last_cursor_world: glam::DVec3::ZERO,
|
2026-04-08 00:12:09 +03:00
|
|
|
last_cursor_screen: iced::Point::ORIGIN,
|
2026-06-15 17:12:08 +03:00
|
|
|
last_point_screen: None,
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
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 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
dyn_fields: Vec::new(),
|
2026-06-15 17:12:08 +03:00
|
|
|
dyn_guide: crate::command::DynGuide::Polar,
|
|
|
|
|
dyn_anchor: None,
|
2026-06-15 18:46:34 +03:00
|
|
|
dyn_ref: None,
|
|
|
|
|
dyn_ref_screen: None,
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
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 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
dyn_active: 0,
|
2026-03-22 22:44:32 +03:00
|
|
|
history: HistoryState::default(),
|
|
|
|
|
active_layer: "0".to_string(),
|
2026-04-06 14:16:06 +03:00
|
|
|
active_ucs: None,
|
2026-04-06 23:26:26 +03:00
|
|
|
bg_color: None,
|
|
|
|
|
paper_bg_color: None,
|
2026-04-08 12:40:19 +03:00
|
|
|
refedit_session: None,
|
2026-07-28 19:13:27 +03:00
|
|
|
block_edits: Vec::new(),
|
|
|
|
|
active_block_edit: None,
|
2026-04-08 18:13:21 +03:00
|
|
|
active_mleader_style: "Standard".to_string(),
|
2026-05-01 07:07:19 +03:00
|
|
|
last_synced_camera_gen: 0,
|
feat(app): add fixed Start tab with welcome page and recent documents
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 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
is_start: false,
|
2026-06-25 10:03:54 +03:00
|
|
|
pan_mode: false,
|
2026-08-04 18:21:52 +03:00
|
|
|
orbit_mode: false,
|
2026-08-08 20:37:53 +03:00
|
|
|
zoom_dynamic_mode: false,
|
2026-06-09 13:41:21 -04:00
|
|
|
plugin_state: HashMap::new(),
|
2026-06-18 14:55:08 +01:00
|
|
|
suspended_cmd: None,
|
2026-03-22 22:44:32 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(app): add fixed Start tab with welcome page and recent documents
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 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
/// Welcome / Start tab. Carries a dummy Scene so the rest of the app
|
|
|
|
|
/// can read tab state uniformly; the viewport renderer detects
|
|
|
|
|
/// `is_start` and shows a welcome page instead.
|
|
|
|
|
pub(super) fn new_start() -> Self {
|
|
|
|
|
let mut t = Self::new_drawing(0);
|
|
|
|
|
t.tab_title = "Start".to_string();
|
|
|
|
|
t.is_start = true;
|
|
|
|
|
t
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
pub(super) fn tab_display_name(&self) -> String {
|
|
|
|
|
match &self.current_path {
|
|
|
|
|
Some(p) => p
|
|
|
|
|
.file_name()
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.to_string_lossy()
|
|
|
|
|
.to_string(),
|
2026-08-02 22:09:25 +03:00
|
|
|
None => {
|
|
|
|
|
if self.is_start {
|
|
|
|
|
t!("Start").into_owned()
|
|
|
|
|
} else {
|
|
|
|
|
self.tab_title.clone()
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-24 23:25:33 +03:00
|
|
|
/// One undo/redo entry. Every edit is represented as a first-touch entity delta
|
|
|
|
|
/// plus an optional structure-only document image, so history never clones the
|
|
|
|
|
/// full entity store.
|
2026-03-22 22:44:32 +03:00
|
|
|
#[derive(Clone)]
|
perf(undo): record entity deltas instead of cloning the whole document
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:11:17 +03:00
|
|
|
pub(super) enum HistorySnapshot {
|
|
|
|
|
Delta(DeltaSnapshot),
|
2026-07-27 23:36:02 +03:00
|
|
|
ObjectVisibility(ObjectVisibilitySnapshot),
|
perf(undo): record entity deltas instead of cloning the whole document
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:11:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl HistorySnapshot {
|
|
|
|
|
pub(super) fn label(&self) -> &str {
|
|
|
|
|
match self {
|
|
|
|
|
HistorySnapshot::Delta(d) => &d.label,
|
2026-07-27 23:36:02 +03:00
|
|
|
HistorySnapshot::ObjectVisibility(v) => &v.label,
|
perf(undo): record entity deltas instead of cloning the whole document
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:11:17 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-24 23:25:33 +03:00
|
|
|
/// Approximate retained history memory. Entity images are shared through
|
|
|
|
|
/// `Arc`; the estimate conservatively budgets their payloads and optional
|
|
|
|
|
/// structure state to keep pathological histories bounded.
|
|
|
|
|
pub(super) fn estimated_bytes(&self) -> usize {
|
|
|
|
|
match self {
|
|
|
|
|
HistorySnapshot::Delta(d) => d
|
|
|
|
|
.entities
|
|
|
|
|
.len()
|
|
|
|
|
.saturating_mul(512)
|
|
|
|
|
.saturating_add(
|
|
|
|
|
d.structure
|
|
|
|
|
.as_ref()
|
2026-07-25 18:49:18 +03:00
|
|
|
.map_or(0, StructureSnapshot::estimated_bytes),
|
2026-07-24 23:25:33 +03:00
|
|
|
)
|
|
|
|
|
.saturating_add(d.selected_before.len().saturating_mul(16))
|
|
|
|
|
.saturating_add(d.selected_after.len().saturating_mul(16))
|
2026-08-15 19:56:35 +03:00
|
|
|
.saturating_add(
|
|
|
|
|
d.active_layer
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map_or(0, |(before, after)| before.len().saturating_add(after.len())),
|
|
|
|
|
)
|
2026-07-24 23:25:33 +03:00
|
|
|
.saturating_add(d.label.len()),
|
2026-07-27 23:36:02 +03:00
|
|
|
HistorySnapshot::ObjectVisibility(v) => v
|
|
|
|
|
.before
|
|
|
|
|
.hidden
|
|
|
|
|
.len()
|
|
|
|
|
.saturating_add(
|
|
|
|
|
v.before
|
|
|
|
|
.keep
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map_or(0, rustc_hash::FxHashSet::len),
|
|
|
|
|
)
|
|
|
|
|
.saturating_add(v.after.hidden.len())
|
|
|
|
|
.saturating_add(
|
|
|
|
|
v.after
|
|
|
|
|
.keep
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map_or(0, rustc_hash::FxHashSet::len),
|
|
|
|
|
)
|
|
|
|
|
.saturating_add(v.selected_before.len())
|
|
|
|
|
.saturating_add(v.selected_after.len())
|
|
|
|
|
.saturating_mul(16)
|
|
|
|
|
.saturating_add(v.label.len()),
|
2026-07-24 23:25:33 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
}
|
|
|
|
|
|
2026-07-27 23:36:02 +03:00
|
|
|
/// Symmetric undo/redo image for session-only object visibility. It contains
|
|
|
|
|
/// no document entity data and therefore can never make isolation serializable.
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub(super) struct ObjectVisibilitySnapshot {
|
|
|
|
|
pub(super) before: ObjectIsolationState,
|
|
|
|
|
pub(super) after: ObjectIsolationState,
|
|
|
|
|
pub(super) selected_before: Vec<Handle>,
|
|
|
|
|
pub(super) selected_after: Vec<Handle>,
|
|
|
|
|
pub(super) label: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-24 23:25:33 +03:00
|
|
|
/// A transactional undo entry: for each touched handle, its before-image and
|
perf(undo): record entity deltas instead of cloning the whole document
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:11:17 +03:00
|
|
|
/// after-image (`None` = the entity was absent on that side, i.e. an add or an
|
|
|
|
|
/// erase). Symmetric — undo applies the before side, redo the after side — so
|
|
|
|
|
/// one delta moves between the undo and redo stacks without re-capturing. The
|
2026-07-24 23:25:33 +03:00
|
|
|
/// Optional structure state covers layers, objects, blocks and tables without
|
|
|
|
|
/// cloning the flat entity store.
|
perf(undo): record entity deltas instead of cloning the whole document
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:11:17 +03:00
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub(super) struct DeltaSnapshot {
|
2026-07-24 23:25:33 +03:00
|
|
|
pub(super) entities: Vec<(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)>,
|
|
|
|
|
pub(super) current_layout_before: String,
|
|
|
|
|
pub(super) current_layout_after: String,
|
perf(undo): record entity deltas instead of cloning the whole document
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:11:17 +03:00
|
|
|
pub(super) selected_before: Vec<Handle>,
|
|
|
|
|
pub(super) selected_after: Vec<Handle>,
|
|
|
|
|
pub(super) dirty_before: bool,
|
|
|
|
|
pub(super) dirty_after: bool,
|
2026-08-15 19:56:35 +03:00
|
|
|
pub(super) active_layer: Option<(String, String)>,
|
2026-07-24 23:25:33 +03:00
|
|
|
/// Opposite non-entity document state. `apply_delta_state` swaps this with
|
|
|
|
|
/// the live structure, so the same allocation shuttles between undo/redo.
|
2026-07-25 18:49:18 +03:00
|
|
|
pub(super) structure: Option<StructureSnapshot>,
|
perf(undo): record entity deltas instead of cloning the whole document
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:11:17 +03:00
|
|
|
pub(super) label: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 18:49:18 +03:00
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub(super) enum StructureSnapshot {
|
|
|
|
|
/// Compatibility fallback for genuinely broad structural commands.
|
|
|
|
|
Full(CadDocument),
|
|
|
|
|
/// Exact layer-table entries touched by one command.
|
|
|
|
|
Layers(Vec<TableEntryDelta<acadrust::tables::Layer>>),
|
|
|
|
|
/// Exact text-style entries touched by one command.
|
|
|
|
|
TextStyles(Vec<TableEntryDelta<acadrust::tables::TextStyle>>),
|
|
|
|
|
/// Exact dimension-style entries touched by one command.
|
|
|
|
|
DimStyles(Vec<TableEntryDelta<acadrust::tables::DimStyle>>),
|
|
|
|
|
/// Exact object-map entries touched by one command. This supports commands
|
|
|
|
|
/// such as groups/dictionaries without retaining every unrelated object.
|
|
|
|
|
Objects(Vec<ObjectEntryDelta>),
|
|
|
|
|
/// The bounded set of style tables, style objects, current-style pointers,
|
|
|
|
|
/// and matching ribbon state touched by one Style Manager transaction.
|
|
|
|
|
Styles {
|
|
|
|
|
before: super::style_ops::StyleStateSnapshot,
|
|
|
|
|
after: super::style_ops::StyleStateSnapshot,
|
|
|
|
|
text_names: Vec<String>,
|
|
|
|
|
dim_names: Vec<String>,
|
|
|
|
|
object_handles: Vec<Handle>,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl StructureSnapshot {
|
|
|
|
|
pub(super) fn estimated_bytes(&self) -> usize {
|
|
|
|
|
match self {
|
|
|
|
|
Self::Full(doc) => doc.objects.len().saturating_mul(192),
|
|
|
|
|
Self::Layers(entries) => entries.len().saturating_mul(256),
|
|
|
|
|
Self::TextStyles(entries) => entries.len().saturating_mul(320),
|
|
|
|
|
Self::DimStyles(entries) => entries.len().saturating_mul(1024),
|
|
|
|
|
Self::Objects(entries) => entries.len().saturating_mul(384),
|
|
|
|
|
Self::Styles { before, after, .. } => before
|
|
|
|
|
.estimated_bytes()
|
|
|
|
|
.saturating_add(after.estimated_bytes()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn is_full(&self) -> bool {
|
|
|
|
|
matches!(self, Self::Full(_))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub(super) struct TableEntryDelta<T> {
|
|
|
|
|
pub(super) name: String,
|
|
|
|
|
pub(super) before: Option<T>,
|
|
|
|
|
pub(super) after: Option<T>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub(super) struct ObjectEntryDelta {
|
|
|
|
|
pub(super) handle: Handle,
|
|
|
|
|
pub(super) before: Option<acadrust::objects::ObjectType>,
|
|
|
|
|
pub(super) after: Option<acadrust::objects::ObjectType>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
#[derive(Default)]
|
|
|
|
|
pub(super) struct HistoryState {
|
|
|
|
|
pub(super) undo_stack: Vec<HistorySnapshot>,
|
|
|
|
|
pub(super) redo_stack: Vec<HistorySnapshot>,
|
2026-07-24 23:25:33 +03:00
|
|
|
pub(super) pending: Option<PendingHistorySnapshot>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) struct PendingHistorySnapshot {
|
|
|
|
|
pub(super) label: String,
|
|
|
|
|
pub(super) current_layout: String,
|
2026-08-15 19:56:35 +03:00
|
|
|
pub(super) active_layer: String,
|
2026-07-24 23:25:33 +03:00
|
|
|
pub(super) selected_before: Vec<Handle>,
|
|
|
|
|
pub(super) dirty_before: bool,
|
|
|
|
|
pub(super) structure_before: CadDocument,
|
|
|
|
|
pub(super) recorder: Arc<acadrust::document::EntityChangeRecorder>,
|
2026-03-22 22:44:32 +03:00
|
|
|
}
|