mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-09 20:10:12 -06:00
279 lines
12 KiB
Rust
279 lines
12 KiB
Rust
use super::view::{ModelTiles, View, usable_view};
|
|
use super::{Arc, Drawing, Handle, Space, Vec2, blockedit, undoctl};
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Snapshot {
|
|
pub label: String,
|
|
pub doc: Arc<Drawing>,
|
|
pub selection: Vec<Handle>,
|
|
}
|
|
|
|
/// One open drawing.
|
|
#[derive(Clone, Debug)]
|
|
pub struct DocState {
|
|
pub doc: Arc<Drawing>,
|
|
pub undo: Vec<Snapshot>,
|
|
pub redo: Vec<Snapshot>,
|
|
/// UNDO marks, BEgin/End group and Control (see [`undoctl`]).
|
|
pub undo_ctl: undoctl::UndoCtl,
|
|
pub selection: Vec<Handle>,
|
|
pub previous_selection: Vec<Handle>,
|
|
pub path: Option<String>,
|
|
pub title: String,
|
|
pub saved: Arc<Drawing>,
|
|
pub revision: u64,
|
|
pub space: Space,
|
|
/// Tiled model viewports (VPORTS).
|
|
pub tiles: ModelTiles,
|
|
/// MSPACE: the layout viewport whose model space is being edited (None = paper space).
|
|
pub mspace: Option<Handle>,
|
|
pub views: Vec<(Space, View)>,
|
|
pub view_history: Vec<View>,
|
|
pub uid: u64,
|
|
/// The drawing is saved as binary DXF (it was opened from one, or last saved as one).
|
|
pub binary_dxf: bool,
|
|
/// The Block Editor (BEDIT), when it is open: `doc` is then the editor's drawing, and the
|
|
/// drawing itself waits in it ([`DocState::drawing`]).
|
|
pub block_edit: Option<blockedit::BlockEdit>,
|
|
/// What the last erase removed, for OOPS.
|
|
pub(crate) erased: crate::cmd::Erased,
|
|
/// The fonts missing on Windows that a save last named: the next save names them again only
|
|
/// when they change.
|
|
pub(crate) fonts_noted: String,
|
|
}
|
|
|
|
static NEXT_UID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
|
|
|
impl DocState {
|
|
pub fn new(doc: Drawing, title: &str, path: Option<String>) -> Self {
|
|
let doc = Arc::new(doc);
|
|
DocState {
|
|
saved: doc.clone(),
|
|
doc,
|
|
undo: Vec::new(),
|
|
redo: Vec::new(),
|
|
undo_ctl: undoctl::UndoCtl::default(),
|
|
selection: Vec::new(),
|
|
previous_selection: Vec::new(),
|
|
path,
|
|
title: title.into(),
|
|
revision: 1,
|
|
space: Space::Model,
|
|
tiles: ModelTiles::default(),
|
|
mspace: None,
|
|
views: Vec::new(),
|
|
view_history: Vec::new(),
|
|
uid: NEXT_UID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
|
binary_dxf: false,
|
|
block_edit: None,
|
|
erased: Default::default(),
|
|
fonts_noted: String::new(),
|
|
}
|
|
}
|
|
/// The drawing has changes not saved to its file, or the Block Editor has changes not yet
|
|
/// saved into its block.
|
|
pub fn is_dirty(&self) -> bool {
|
|
!Arc::ptr_eq(self.drawing(), &self.saved) || self.block_unsaved()
|
|
}
|
|
/// Show the drawing an undo or redo step holds, with nothing selected; returns the drawing as
|
|
/// it was, as the step back.
|
|
pub(crate) fn swap_in(&mut self, snap: Snapshot) -> Snapshot {
|
|
let cur = Snapshot { label: snap.label, doc: std::mem::replace(&mut self.doc, snap.doc), selection: std::mem::take(&mut self.selection) };
|
|
self.follow_layout(&cur.doc);
|
|
self.reconcile_untiled_twist();
|
|
self.revision += 1;
|
|
cur
|
|
}
|
|
/// After undo/redo replaced the drawing (`before` is the one shown until then): if the current
|
|
/// layout no longer exists, follow it to its other name when it was renamed (same tab order,
|
|
/// a name `before` did not have), else go to the Model tab.
|
|
pub(crate) fn follow_layout(&mut self, before: &Drawing) {
|
|
let Space::Paper(name) = &self.space else { return };
|
|
if self.doc.layouts.iter().any(|l| l.name == *name) {
|
|
return;
|
|
}
|
|
let renamed = before
|
|
.layouts
|
|
.iter()
|
|
.find(|l| l.name == *name)
|
|
.and_then(|old| self.doc.layouts.iter().find(|l| l.tab_order == old.tab_order && !before.layouts.iter().any(|b| b.name == l.name)));
|
|
match renamed.map(|l| Space::Paper(l.name.clone())) {
|
|
Some(to) => {
|
|
let from = self.space.clone();
|
|
for (sp, _) in self.views.iter_mut() {
|
|
if *sp == from {
|
|
*sp = to.clone();
|
|
}
|
|
}
|
|
self.space = to;
|
|
}
|
|
None => {
|
|
self.space = Space::Model;
|
|
self.mspace = None;
|
|
}
|
|
}
|
|
}
|
|
/// The space edits, picks and snaps act on: model space inside an active viewport.
|
|
pub fn edit_space(&self) -> Space {
|
|
if self.active_viewport().is_some() { Space::Model } else { self.space.clone() }
|
|
}
|
|
/// The active MSPACE viewport, if it still exists in the current layout.
|
|
pub fn active_viewport(&self) -> Option<(Handle, cadcraft_doc::Viewport)> {
|
|
let h = self.mspace?;
|
|
let e = self.doc.space(&self.space)?.get(h)?;
|
|
match &e.kind {
|
|
cadcraft_doc::EntityKind::Viewport(v) if v.height > 1e-12 && v.view_height > 1e-12 => Some((h, v.clone())),
|
|
_ => None,
|
|
}
|
|
}
|
|
pub fn paper_view(&self) -> View {
|
|
self.views.iter().find(|(s, _)| *s == self.space).map(|(_, v)| *v).unwrap_or_default()
|
|
}
|
|
/// The screen view. Inside an MSPACE viewport this is the model-space view that maps the
|
|
/// screen consistently with the paper view, so picks, snaps and previews work in model units.
|
|
pub fn view(&self) -> View {
|
|
let pv = self.paper_view();
|
|
match self.active_viewport() {
|
|
Some((_, vp)) => {
|
|
let k = vp.view_height / vp.height;
|
|
// The viewport's twist turns the model view inside it.
|
|
View { center: vp.paper_to_model(pv.center), height: pv.height * k, twist: vp.view_twist() }
|
|
}
|
|
None => pv,
|
|
}
|
|
}
|
|
/// Map a requested screen view to the paper view, or (in MSPACE) to the viewport's model view.
|
|
/// A finite request can still map to a view too large to represent: then nothing changes and
|
|
/// this returns false.
|
|
fn store_view(&mut self, v: View) -> bool {
|
|
if let Some((h, vp)) = self.active_viewport() {
|
|
let pv = self.paper_view();
|
|
let k = v.height / pv.height.max(1e-12);
|
|
if vp.locked {
|
|
// A locked viewport keeps its scale: pan/zoom the sheet instead.
|
|
let k0 = vp.view_height / vp.height;
|
|
let pc = vp.model_to_paper(v.center);
|
|
let paper = View { center: pc, height: v.height / k0, twist: pv.twist };
|
|
if !usable_view(&paper) {
|
|
return false;
|
|
}
|
|
self.put_paper_view(paper);
|
|
} else {
|
|
// `v`'s twist is the one the viewport shows (a non-finite one keeps its own).
|
|
let twist = if v.twist.is_finite() { v.twist } else { vp.view_twist() };
|
|
let center = v.center - ((pv.center - vp.center.xy()) * k).rotate(-twist);
|
|
let height = vp.height * k;
|
|
if !usable_view(&View { center, height, twist }) {
|
|
return false;
|
|
}
|
|
let doc = Arc::make_mut(&mut self.doc);
|
|
if let Some(store) = doc.space_mut(&self.space.clone()) {
|
|
store.modify(h, |e| {
|
|
if let cadcraft_doc::EntityKind::Viewport(x) = &mut e.kind {
|
|
x.view_center = center;
|
|
x.view_height = height;
|
|
x.twist = twist;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
self.put_paper_view(v);
|
|
true
|
|
}
|
|
fn put_paper_view(&mut self, v: View) {
|
|
match self.views.iter_mut().find(|(s, _)| *s == self.space) {
|
|
Some((_, slot)) => *slot = v,
|
|
None => self.views.push((self.space.clone(), v)),
|
|
}
|
|
if self.space == Space::Model {
|
|
self.keep_untiled_twist(v);
|
|
}
|
|
}
|
|
/// An untiled Model tab keeps its view twist in the drawing as a lone `*Active` VPORT tile
|
|
/// (written as that record's group 51 and read back), so it survives save and reopen. Only a
|
|
/// change of twist touches the drawing; an untwisted view keeps no record.
|
|
/// After undo or redo has put back another drawing: its untiled-twist record follows the
|
|
/// Model tab's live view again (views are not part of the drawing's undo).
|
|
pub(crate) fn reconcile_untiled_twist(&mut self) {
|
|
if let Some(v) = self.views.iter().find(|(s, _)| *s == Space::Model).map(|(_, v)| *v) {
|
|
self.keep_untiled_twist(v);
|
|
}
|
|
}
|
|
fn keep_untiled_twist(&mut self, v: View) {
|
|
use cadcraft_doc::{VportConfig, VportTile};
|
|
let twist = cadcraft_geom::norm_angle(v.twist);
|
|
let active = self.doc.vports.iter().find(|c| c.is_active());
|
|
if active.is_some_and(|c| c.tiles.len() > 1) {
|
|
return;
|
|
}
|
|
if active.and_then(|c| c.tiles.first()).map_or(0.0, |t| t.twist) == twist {
|
|
return;
|
|
}
|
|
let d = Arc::make_mut(&mut self.doc);
|
|
d.vports.retain(|c| !c.is_active());
|
|
if twist != 0.0 {
|
|
let tile = VportTile { min: Vec2::ZERO, max: Vec2::new(1.0, 1.0), center: v.center, height: v.height, aspect: 1.0, twist };
|
|
d.vports.insert(0, VportConfig { name: VportConfig::ACTIVE.into(), tiles: vec![tile] });
|
|
}
|
|
}
|
|
/// Set the view, recording view history. Returns false, changing nothing, when the view (or,
|
|
/// in MSPACE, the viewport view it maps to) isn't finite with a positive height.
|
|
pub fn set_view(&mut self, v: View) -> bool {
|
|
if !usable_view(&v) {
|
|
return false;
|
|
}
|
|
let old = self.view();
|
|
if !self.store_view(v) {
|
|
return false;
|
|
}
|
|
if old != v {
|
|
self.view_history.push(old);
|
|
if self.view_history.len() > 50 {
|
|
self.view_history.remove(0);
|
|
}
|
|
}
|
|
true
|
|
}
|
|
/// The active MSPACE viewport when ZOOM works on its own view, as in AutoCAD: extents and
|
|
/// windows fit the viewport, and zooms keep its centre. A locked viewport keeps its scale, so
|
|
/// zooming it moves the sheet instead (see `store_view`).
|
|
pub fn zoom_viewport(&self) -> Option<cadcraft_doc::Viewport> {
|
|
self.active_viewport().map(|(_, v)| v).filter(|v| !v.locked && v.width > 1e-12)
|
|
}
|
|
/// The view ZOOM starts from: the model view of the active unlocked viewport, else [`Self::view`].
|
|
pub fn zoom_frame(&self) -> View {
|
|
match self.zoom_viewport() {
|
|
Some(v) => View { center: v.view_center, height: v.view_height, twist: v.view_twist() },
|
|
None => self.view(),
|
|
}
|
|
}
|
|
/// Width / height of the area ZOOM fits into: the active unlocked viewport, else `screen`.
|
|
pub fn zoom_aspect(&self, screen: f64) -> f64 {
|
|
self.zoom_viewport().map_or(screen, |v| v.width / v.height)
|
|
}
|
|
/// Set the view ZOOM works on (see [`Self::zoom_frame`]), recording view history.
|
|
pub fn set_zoom_frame(&mut self, f: View) {
|
|
let v = match self.zoom_viewport() {
|
|
// The screen view that `store_view` maps to this model view of the viewport.
|
|
Some(vp) => {
|
|
let pv = self.paper_view();
|
|
let k = f.height / vp.height;
|
|
// The frame's twist turns the viewport (a non-finite one keeps the viewport's).
|
|
let twist = if f.twist.is_finite() { f.twist } else { vp.view_twist() };
|
|
View { center: f.center + ((pv.center - vp.center.xy()) * k).rotate(-twist), height: pv.height * k, twist }
|
|
}
|
|
None => f,
|
|
};
|
|
self.set_view(v);
|
|
}
|
|
/// Set without recording view history (realtime pan/zoom frames).
|
|
pub fn set_view_quiet(&mut self, v: View) {
|
|
if !usable_view(&v) {
|
|
return;
|
|
}
|
|
self.store_view(v);
|
|
}
|
|
}
|