View: Viewports > Named Viewports... lists, restores and deletes saved viewport configurations

-VPORTS saved, restored and deleted named model space viewport
configurations, and MVIEW Restore placed one in a layout, but View >
Viewports > Named Viewports... had no command. The new command
vports.named runs from the menu item. Typed or from the menu it opens
the Named Viewports dialog: the saved configurations, a preview of the
selected one's tiles, and in the Model tab the name of the configuration
whose tiles are where the Model tab's are, else *Active. Restore shows
the configuration in the Model tab, and in a layout it starts MVIEW
Restore with the name, which asks for the corners. Delete deletes the
saved configuration. The JSON form takes {name?, delete?, p1?, p2?,
layout?}, and without a name it returns the configurations and the
current one.

Catalog rows for the new menu label are added in all 15 languages. In
docs/gaps.md, U11 is narrowed, and F15, U11 and F21 no longer list MVIEW
Restore (#633) or PURGE of multiline styles, which are on main.
docs/parity-checklist.md is regenerated with cargo xtask parity.
This commit is contained in:
dotCooCoo
2026-10-25 16:07:11 -07:00
parent 004287d919
commit 7d0d475aee
24 changed files with 408 additions and 10 deletions
+91 -2
View File
@@ -6,11 +6,12 @@
//! tiles is an undo step. The session keeps each tile's view ([`crate::ModelTiles`]); the current
//! tile shows the model space view itself, so ZOOM, PAN and every pick work in it unchanged. The
//! UI draws the tiles from [`Session::model_tiles`] and hands clicks in other tiles to
//! [`Session::click_tile`]. In a layout -VPORTS is MVIEW.
//! [`Session::click_tile`]. In a layout -VPORTS is MVIEW. View > Viewports > Named Viewports...
//! restores or deletes a saved configuration.
use std::sync::Arc;
use cadcraft_doc::{Drawing, Space, VportConfig, VportTile};
use cadcraft_doc::{Drawing, EntityKind, Space, VportConfig, VportTile};
use cadcraft_geom::Vec2;
use serde_json::{Value, json};
@@ -37,6 +38,12 @@ pub fn specs() -> Vec<CommandSpec> {
)
.interactive(vports_factory),
CommandSpec::new("vports.activate", "Set Current Viewport", run_activate).params("{index: tile number from 1}").noundo(),
CommandSpec::new("vports.named", "Named Viewports...", run_named)
.menu(&["View", "Viewports", "Named Viewports..."])
.params(
"{name?: a saved viewport configuration, delete?: bool, p1?, p2?, layout?} (no name: → {current, configurations: [{name, viewports}]}; the Model tab shows the configuration's tiles, a layout gets one viewport per tile over p1-p2 or the printable area; typed or from the menu: the Named Viewports dialog)",
)
.interactive(named_factory),
]
}
@@ -388,7 +395,59 @@ fn run_activate(s: &mut Session, p: &Value) -> Result<Value> {
summary(s)
}
/// View > Viewports > Named Viewports...: without a name, the saved configurations and the one
/// the Model tab shows; with one, restore it (its tiles in the Model tab, MVIEW Restore in a
/// layout) or, with `delete`, delete it.
fn run_named(s: &mut Session, p: &Value) -> Result<Value> {
let delete = match p.get("delete") {
None | Some(Value::Null) => false,
Some(Value::Bool(b)) => *b,
Some(_) => return Err(bad("vports.named", "`delete` must be true or false")),
};
let Some(name) = str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()) else {
let configs: Vec<Value> =
s.doc()?.vports.iter().filter(|c| !c.is_active()).map(|c| json!({ "name": c.name, "viewports": c.tiles.len() })).collect();
return Ok(json!({ "current": s.viewport_configuration(), "configurations": configs }));
};
let saved = s
.doc()?
.vports
.iter()
.find(|c| !c.is_active() && c.name.eq_ignore_ascii_case(name))
.map(|c| c.name.clone())
.ok_or_else(|| EngineError::Other(format!("Could not find viewport configuration \"{}\".", name.chars().take(64).collect::<String>())))?;
if delete {
s.doc_mut()?.vports.retain(|c| c.is_active() || c.name != saved);
return Ok(json!({ "deleted": saved, "message": format!("Viewport configuration \"{saved}\" deleted.") }));
}
if s.layout_space() != Space::Model || p.get("layout").is_some() {
let mview = find_command("mview").ok_or_else(|| EngineError::UnknownCommand("mview".into()))?;
let mut p = p.clone();
if let Some(o) = p.as_object_mut() {
o.remove("delete");
o.insert("option".into(), json!("restore"));
o.insert("name".into(), json!(saved));
}
return (mview.run)(s, &p);
}
run_vports(s, &json!({ "option": "restore", "name": saved }))
}
/// Whether two configurations have their tiles in the same places (in any order).
fn same_places(a: &[VportTile], b: &[VportTile]) -> bool {
let near = |p: Vec2, q: Vec2| (p.x - q.x).abs() < EPS && (p.y - q.y).abs() < EPS;
a.len() == b.len() && a.iter().all(|t| b.iter().any(|u| near(t.min, u.min) && near(t.max, u.max)))
}
impl Session {
/// The saved viewport configuration whose tiles are where the Model tab's tiles are now (the
/// first such one in the drawing), if any.
pub fn viewport_configuration(&self) -> Option<String> {
let st = self.state().ok()?;
let (tiles, _) = live(st, self.viewport_px);
st.doc.vports.iter().filter(|c| !c.is_active()).find(|c| same_places(&c.tiles, &tiles)).map(|c| c.name.clone())
}
/// The model space tiles to draw (empty: one view, or not in the Model tab).
pub fn model_tiles(&self) -> Vec<ModelTile> {
let Ok(st) = self.state() else { return Vec::new() };
@@ -598,3 +657,33 @@ impl VportsM {
}
}
}
/// Named Viewports... without the dialog: -VPORTS Restore in the Model tab, MVIEW Restore in a
/// layout.
fn named_factory(s: &Session) -> Result<Box<dyn Interactive>> {
Ok(Box::new(Restoring(vports_factory(s)?)))
}
/// -VPORTS or MVIEW started at its Restore option.
struct Restoring(Box<dyn Interactive>);
impl Interactive for Restoring {
fn name(&self) -> &'static str {
self.0.name()
}
fn begin(&mut self, s: &mut Session) -> Result<Step> {
match self.0.begin(s)? {
Step::Continue => self.0.input(s, Input::Keyword("Restore".into())),
other => Ok(other),
}
}
fn prompt(&self, s: &Session) -> Prompt {
self.0.prompt(s)
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
self.0.input(s, i)
}
fn preview(&self, s: &Session, cursor: Vec2) -> Vec<EntityKind> {
self.0.preview(s, cursor)
}
}
+1
View File
@@ -75,6 +75,7 @@ mod modify_curves;
mod mtext_columns;
mod mtext_options;
mod multiline_attributes;
mod named_viewports;
mod named_views;
mod non_finite_edits;
mod offset_closed_spline;
+101
View File
@@ -0,0 +1,101 @@
//! View > Viewports > Named Viewports...: the saved model space viewport configurations, the one
//! the Model tab shows, and restoring or deleting one (its tiles in the Model tab, layout
//! viewports in a layout).
use cadcraft_engine::Session;
use cadcraft_engine::doc::{EntityKind, Space};
use serde_json::json;
/// A drawing with "Two" (two tiles side by side) and "Quad" (four tiles) saved, showing one view.
fn session() -> Session {
let mut s = Session::new();
s.viewport_px = (1200.0, 800.0);
s.execute("line", &json!({ "points": [[0, 0], [100, 50]] })).unwrap();
s.execute("-vports", &json!({ "option": "2", "arrangement": "vertical" })).unwrap();
s.execute("-vports", &json!({ "option": "save", "name": "Two" })).unwrap();
s.execute("-vports", &json!({ "option": "4", "applyTo": "display" })).unwrap();
s.execute("-vports", &json!({ "option": "save", "name": "Quad" })).unwrap();
s.execute("-vports", &json!({ "option": "single" })).unwrap();
s
}
fn rects(s: &Session) -> Vec<[f64; 4]> {
let mut r: Vec<[f64; 4]> = s.model_tiles().iter().map(|t| [t.min.x, t.min.y, t.max.x, t.max.y]).collect();
r.sort_by(|a, b| a.partial_cmp(b).unwrap());
r
}
fn layout_viewports(s: &Session) -> usize {
let d = s.doc().unwrap();
d.space(&Space::Paper("Layout1".into())).unwrap().iter().filter(|e| matches!(e.kind, EntityKind::Viewport(_))).count()
}
#[test]
fn named_viewports_lists_restores_and_deletes_in_the_model_tab() {
let mut s = session();
let list = s.execute("vports.named", &json!({})).unwrap();
assert_eq!(list["configurations"], json!([{ "name": "Two", "viewports": 2 }, { "name": "Quad", "viewports": 4 }]));
assert_eq!(list["current"], json!(null), "one view matches no saved configuration");
assert_eq!(s.viewport_configuration(), None);
s.execute("vports.named", &json!({ "name": "two" })).unwrap();
assert_eq!(rects(&s), vec![[0.0, 0.0, 0.5, 1.0], [0.5, 0.0, 1.0, 1.0]]);
assert_eq!(s.viewport_configuration().as_deref(), Some("Two"));
assert_eq!(s.execute("vports.named", &json!({})).unwrap()["current"], json!("Two"));
s.undo().unwrap();
assert!(rects(&s).is_empty(), "restoring is one undo step");
let r = s.execute("vports.named", &json!({ "name": "QUAD", "delete": true })).unwrap();
assert_eq!(r["deleted"], json!("Quad"));
let names: Vec<String> = s.doc().unwrap().vports.iter().filter(|c| !c.is_active()).map(|c| c.name.clone()).collect();
assert_eq!(names, vec!["Two".to_string()]);
s.undo().unwrap();
assert!(s.doc().unwrap().vports.iter().any(|c| c.name == "Quad"), "deleting is one undo step");
}
#[test]
fn named_viewports_refuses_unknown_names_and_a_delete_that_is_not_a_boolean() {
let mut s = session();
for bad in [
json!({ "name": "nope" }),
json!({ "name": "*Active" }),
json!({ "name": "Two", "delete": "yes" }),
json!({ "name": "nope", "delete": true }),
] {
assert!(s.execute("vports.named", &bad).is_err(), "{bad}");
}
// Only the exact name is deleted, never a wildcard match.
assert!(s.execute("vports.named", &json!({ "name": "*", "delete": true })).is_err());
assert_eq!(s.doc().unwrap().vports.iter().filter(|c| !c.is_active()).count(), 2);
}
#[test]
fn named_viewports_places_a_configuration_in_a_layout() {
let mut s = session();
s.execute("layout.set", &json!({ "name": "Layout1" })).unwrap();
let before = layout_viewports(&s);
let r = s.execute("vports.named", &json!({ "name": "Quad", "p1": [10, 10], "p2": [210, 110] })).unwrap();
assert_eq!(r["configuration"], json!("Quad"));
assert_eq!(layout_viewports(&s), before + 4);
assert!(s.model_tiles().is_empty(), "the Model tab's tiles do not change");
}
#[test]
fn typed_named_viewports_asks_for_the_configuration() {
let mut s = session();
s.start("vports.named").unwrap();
assert!(s.prompt_text().contains("Enter name of viewport configuration to restore"), "{}", s.prompt_text());
s.cmdline("Two").unwrap();
assert!(s.running.is_none());
assert_eq!(rects(&s).len(), 2);
s.execute("layout.set", &json!({ "name": "Layout1" })).unwrap();
let before = layout_viewports(&s);
s.start("vports.named").unwrap();
assert!(s.prompt_text().contains("Enter viewport configuration name"), "{}", s.prompt_text());
s.cmdline("Quad").unwrap();
assert!(s.prompt_text().contains("Specify first corner"), "{}", s.prompt_text());
s.cmdline("").unwrap();
assert!(s.running.is_none());
assert_eq!(layout_viewports(&s), before + 4, "Enter fits the configuration to the printable area");
}
+1
View File
@@ -587,6 +587,7 @@
Name الاسم
Name (A–Z) الاسم (أ–ي)
Named مسمى
Named Viewports... أطر العرض المسماة...
Named Views... طرق عرض مسماة...
Nearest الأقرب
New جديد
1 # CADCraft Arabic interface catalog — original translations.
587
588
589
590
591
592
593
+1
View File
@@ -537,6 +537,7 @@
N S
Name Název
Name (A–Z) Název (A–Z)
Named Viewports... Pojmenované výřezy...
Named Views... Pojmenované pohledy...
Nearest Nejbližší
New Nový
1 # CADCraft Czech interface catalog — original translations by @Losminek (AI-assisted).
537
538
539
540
541
542
543
+1
View File
@@ -457,6 +457,7 @@
N N
Name Name
Name (A–Z) Name (A–Z)
Named Viewports... Benannte Ansichtsfenster...
Nearest Nächstgelegener
New Neu
New (metric) Neu (metrisch)
1 # CADCraft German interface catalog — original translations by the CADCraft contributors.
457
458
459
460
461
462
463
+1
View File
@@ -640,6 +640,7 @@
Name Όνομα
Name (A–Z) Όνομα (Α–Ω)
Named Με όνομα
Named Viewports... Αποθηκευμένα παράθυρα προβολής...
Named Views... Αποθηκευμένες όψεις...
Nearest Πλησιέστερο (Nearest)
New Νέο
1 # Greek interface catalog for CADCraft. Original, AI-assisted translation by @Camthalion77 with ChatGPT.
640
641
642
643
644
645
646
+1
View File
@@ -567,6 +567,7 @@
N N
Name Nombre
Name (A–Z) Nombre (A–Z)
Named Viewports... Ventanas gráficas con nombre...
Named Views... Vistas con nombre...
Nearest Cercano
New Nuevo
1 # CADCraft Spanish interface catalog — original translations.
567
568
569
570
571
572
573
+1
View File
@@ -452,6 +452,7 @@
N N
Name Nom
Name (A–Z) Nom (A–Z)
Named Viewports... Fenêtres nommées...
Nearest Plus proche
New Nouveau
New (metric) Nouveau (métrique)
1 # CADCraft French interface catalog — original translations by @pechnikov2026-star.
452
453
454
455
456
457
458
+1
View File
@@ -650,6 +650,7 @@
Name Név
Name (A–Z) Név (A–Z)
Named Elnevezett
Named Viewports... Elnevezett nézetablakok...
Named Views... Elnevezett nézetek...
Nearest Legközelebbi
New Új
Can't render this file because it contains an unexpected character in line 16 and column 72.
+1
View File
@@ -549,6 +549,7 @@ menu Modify Edita
N N
Name Nome
Name (A–Z) Nome (A–Z)
Named Viewports... Finestre con nome...
Named Views... Viste con nome...
Nearest Vicino
New Nuovo
1 # CADCraft Italian interface catalog — original translations by Emanuele Bottelli (AI-assisted, Claude).
549
550
551
552
553
554
555
+1
View File
@@ -539,6 +539,7 @@
N 북
Name 이름
Name (A–Z) 이름(A–Z)
Named Viewports... 명명된 뷰포트...
Named Views... 명명된 뷰...
Nearest 근처점
New 새로 만들기
1 # CADCraft Korean interface catalog — original Korean translations.
539
540
541
542
543
544
545
+1
View File
@@ -565,6 +565,7 @@
N N
Name Nome
Name (A–Z) Nome (A–Z)
Named Viewports... Viewports nomeados...
Named Views... Vistas nomeadas...
Nearest Mais próximo
New Novo
1 # CADCraft Brazilian Portuguese (pt-BR) interface catalog.
565
566
567
568
569
570
571
+1
View File
@@ -452,6 +452,7 @@
N С
Name Название
Name (A–Z) Название (А–Я)
Named Viewports... Именованные видовые экраны...
Nearest Ближайшая точка
New Создать
New (metric) Создать (метрический)
1 # CADCraft Russian interface catalog — original translations by @pechnikov2026-star.
452
453
454
455
456
457
458
+1
View File
@@ -569,6 +569,7 @@
N N
Name Namn
Name (A–Z) Namn (A–Ö)
Named Viewports... Namngivna vyfönster...
Named Views... Namngivna vyer...
Nearest Närmaste
New Ny
1 # CADCraft Swedish interface catalog — original translations by the CADCraft contributors.
569
570
571
572
573
574
575
+1
View File
@@ -651,6 +651,7 @@
Name Ad
Name (A–Z) Ad (A–Z)
Named Adlandırılmış
Named Viewports... Adlandırılmış Görünüm Pencereleri...
Named Views... Adlandırılmış Görünümler...
Nearest En Yakın
New Yeni
Can't render this file because it contains an unexpected character in line 11 and column 13.
+1
View File
@@ -553,6 +553,7 @@
N Пн
Name Назва
Name (A–Z) Назва (А–Я)
Named Viewports... Іменовані видові екрани...
Named Views... Іменовані види...
Nearest Найближча точка
New Створити
1 # CADCraft Ukrainian interface catalog — original translations by @dmatviichuk.
553
554
555
556
557
558
559
+1
View File
@@ -1121,6 +1121,7 @@
Open Recent 打开最近的图形
Sheet Books to PDF... 图纸册转 PDF...
Drawing Recovery Manager... 图形恢复管理器...
Named Viewports... 命名视口...
Named Views... 命名视图...
Polygonal Viewport 多边形视口
From Template... 来自模板...
1 # CADCraft Simplified Chinese interface catalog — original translations.
1121
1122
1123
1124
1125
1126
1127
+6 -1
View File
@@ -1,5 +1,6 @@
//! Style and settings dialogs opened from the menus: Linetype Manager, Point Style, Text Style,
//! Table Style, Multileader Style, Page Setup Manager, Plot and Constraint Settings.
//! Table Style, Multileader Style, Page Setup Manager, Plot, Constraint Settings and Named
//! Viewports.
//!
//! Typed or chosen from a menu (no parameters) these commands open their dialog; JSON calls run
//! the command. The dialogs read the drawing and change it only through the same commands
@@ -16,6 +17,8 @@ use crate::theme::Tokens;
/// The Block Attribute Manager (BATTMAN).
mod battman;
/// Named Viewports (View > Viewports > Named Viewports...).
mod named_vports;
/// Open the dialog for a typed or menu-invoked command. `None` if `id` has no dialog here or the
/// call carries parameters.
@@ -32,6 +35,7 @@ pub fn route(app: &mut CadApp, id: &str, params: &Value) -> Option<Result<Value,
"mleaderstyle" | "mls" => "mleaderstyle",
"pagesetup" => "pagesetup",
"constraintsettings" | "csettings" => "csettings",
"vports.named" => "namedvports",
"plot" | "print" => {
PLOT_GEN.fetch_add(1, Ordering::Relaxed);
"plot"
@@ -53,6 +57,7 @@ pub fn dialog(app: &mut CadApp, ctx: &egui::Context, name: &str, open: &mut bool
"mleaderstyle" => mleader_style(app, ctx, open),
"pagesetup" => page_setup(app, ctx, open),
"csettings" => constraint_settings(app, ctx, open),
"namedvports" => named_vports::dialog(app, ctx, open),
"plot" => plot(app, ctx, open),
_ => {
*open = false;
+112
View File
@@ -0,0 +1,112 @@
//! Named Viewports (View > Viewports > Named Viewports...): the drawing's saved model space
//! viewport configurations, with the selected one's tiles drawn as a preview. Restore runs
//! `vports.named` in the Model tab; in a layout it starts MVIEW Restore with the name, so the
//! viewports' corners are picked next. Delete runs `vports.named` with `delete`.
use cadcraft_doc::{Space, VportConfig, VportTile};
use cadcraft_engine::Input;
use egui::{RichText, Sense, Stroke, vec2};
use serde_json::{Value, json};
use super::{set_temp, temp};
use crate::CadApp;
use crate::theme::Tokens;
/// Draws the tiles of a configuration in a 180 x 120 box; tile coordinates (0, 0) are the bottom left.
fn preview(ui: &mut egui::Ui, tiles: &[VportTile], t: &Tokens) {
let (rect, _) = ui.allocate_exact_size(vec2(180.0, 120.0), Sense::hover());
let p = ui.painter_at(rect);
p.rect_filled(rect, 0.0, t.panel);
let at = |x: f64, y: f64| egui::pos2(rect.min.x + x.clamp(0.0, 1.0) as f32 * rect.width(), rect.max.y - y.clamp(0.0, 1.0) as f32 * rect.height());
for tile in tiles.iter().filter(|t| t.min.is_finite() && t.max.is_finite()) {
let r = egui::Rect::from_two_pos(at(tile.min.x, tile.min.y), at(tile.max.x, tile.max.y));
p.rect_stroke(r, 0.0, Stroke::new(1.0, t.text_dim), egui::StrokeKind::Inside);
}
p.rect_stroke(rect, 0.0, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
}
/// Show the dialog; returns the command to run.
pub fn dialog(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Option<(&'static str, Value)> {
let t = Tokens::get();
let Ok(d) = app.session.doc() else {
egui::Window::new("Named Viewports").open(open).collapsible(false).resizable(false).show(ctx, |ui| {
ui.label("Open a drawing first.");
});
return None;
};
let configs: Vec<(String, Vec<VportTile>)> = d.vports.iter().filter(|c| !c.is_active()).map(|c| (c.name.clone(), c.tiles.clone())).collect();
let model = app.session.layout_space() == Space::Model;
let current = if model { app.session.viewport_configuration() } else { None };
let mut sel = temp::<String>(ctx, "namedvp_sel")
.filter(|s| configs.iter().any(|c| c.0 == *s))
.or_else(|| current.clone())
.or_else(|| configs.first().map(|c| c.0.clone()));
let mut action: Option<(&'static str, Value)> = None;
let mut restore = false;
let mut close = false;
egui::Window::new("Named Viewports").open(open).default_width(400.0).resizable(false).show(ctx, |ui| {
if model {
ui.label(format!("Current name: {}", current.as_deref().unwrap_or(VportConfig::ACTIVE)));
} else {
ui.label(RichText::new("Saved model space configurations to place in this layout").color(t.text_dim));
}
ui.separator();
if configs.is_empty() {
ui.label("This drawing has no named viewport configurations.");
}
ui.horizontal_top(|ui| {
egui::ScrollArea::vertical().id_salt("namedvp_list").max_height(120.0).min_scrolled_width(180.0).show(ui, |ui| {
ui.set_min_width(180.0);
for (name, _) in &configs {
let r = ui.selectable_label(sel.as_deref() == Some(name.as_str()), name);
if r.clicked() {
sel = Some(name.clone());
}
if r.double_clicked() {
sel = Some(name.clone());
restore = true;
}
}
});
let tiles = sel.as_ref().and_then(|s| configs.iter().find(|c| c.0 == *s)).map(|c| c.1.as_slice()).unwrap_or_default();
preview(ui, tiles, &t);
});
ui.separator();
ui.horizontal(|ui| {
let hint = if model { "Show this configuration's viewports" } else { "Place this configuration's viewports in the layout" };
if ui.add_enabled(sel.is_some(), egui::Button::new("Restore")).on_hover_text(hint).clicked() {
restore = true;
}
if ui.add_enabled(sel.is_some(), egui::Button::new("Delete")).on_hover_text("Delete the saved configuration").clicked()
&& let Some(n) = &sel
{
action = Some(("vports.named", json!({ "name": n, "delete": true })));
}
if ui.button("Close").clicked() {
close = true;
}
});
});
if let Some(n) = sel.clone() {
set_temp(ctx, "namedvp_sel", n);
}
if close {
*open = false;
}
let Some(name) = sel.filter(|_| restore) else { return action };
*open = false;
if model {
return Some(("vports.named", json!({ "name": name })));
}
// The viewports' corners are picked in the layout, as for MVIEW Restore.
app.start("mview");
if app.session.running.as_ref().is_some_and(|r| r.id == "mview") {
for i in [Input::Keyword("Restore".into()), Input::Text(name)] {
if let Err(e) = app.session.input(i) {
app.session.echo(e.to_string());
break;
}
}
}
None
}
+1
View File
@@ -17,6 +17,7 @@ mod lisp_files;
mod measure_distance_snapping;
mod menubar_hover;
mod mspace_viewport_view;
mod named_viewports_dialog;
mod options_dialog;
mod parameters_rename;
mod pickdrag;
@@ -0,0 +1,74 @@
//! View > Viewports > Named Viewports... opens a dialog that lists the saved viewport
//! configurations: Restore shows one in the Model tab or starts placing it in a layout, and Delete
//! deletes it.
use cadcraft_engine::Session;
use cadcraft_ui_egui::{CadApp, Services, dialogs, menus};
use egui::vec2;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use serde_json::json;
/// A drawing with "Two" (two tiles side by side) saved, showing one view, and the dialog open.
fn dialog() -> Harness<'static, CadApp> {
let mut app = CadApp::new(Session::default(), Services::default());
app.run("line", json!({ "points": [[0, 0], [100, 50]] })).unwrap();
app.run("-vports", json!({ "option": "2", "arrangement": "vertical" })).unwrap();
app.run("-vports", json!({ "option": "save", "name": "Two" })).unwrap();
app.run("-vports", json!({ "option": "single" })).unwrap();
menus::activate(&mut app, "vports.named");
assert_eq!(app.ui.dialog.as_deref(), Some("namedvports"));
let mut h = Harness::builder().with_size(vec2(900.0, 700.0)).build_ui_state(
|ui, app: &mut CadApp| {
app.logic(&ui.ctx().clone());
dialogs::show(app, ui.ctx());
},
app,
);
h.run_steps(3);
h
}
#[test]
fn restore_and_delete_in_the_model_tab() {
let mut h = dialog();
h.get_by_label("Current name: *Active");
h.get_by_label("Two").click();
h.run_steps(2);
h.get_by_label("Restore").click();
h.run_steps(3);
let app = h.state_mut();
assert_eq!(app.session.model_tiles().len(), 2);
assert_eq!(app.ui.dialog, None, "Restore closes the dialog");
menus::activate(app, "vports.named");
h.run_steps(3);
h.get_by_label("Current name: Two");
h.get_by_label("Delete").click();
h.run_steps(3);
let app = h.state_mut();
assert!(app.session.doc().unwrap().vports.iter().all(|c| c.name != "Two"));
assert_eq!(app.ui.dialog.as_deref(), Some("namedvports"), "Delete keeps the dialog open");
h.get_by_label("This drawing has no named viewport configurations.");
}
#[test]
fn restore_in_a_layout_asks_where_the_viewports_go() {
let mut h = dialog();
let app = h.state_mut();
app.ui.dialog = None;
app.run("layout.set", json!({ "name": "Layout1" })).unwrap();
menus::activate(app, "vports.named");
h.run_steps(3);
h.get_by_label("Two").click();
h.run_steps(2);
h.get_by_label("Restore").click();
h.run_steps(3);
let app = h.state_mut();
assert!(app.session.prompt_text().contains("Specify first corner"), "{}", app.session.prompt_text());
let before = app.session.doc().unwrap().layout("Layout1").unwrap().entities.iter().count();
app.cmdline("");
assert!(app.session.running.is_none());
let after = app.session.doc().unwrap().layout("Layout1").unwrap().entities.iter().count();
assert_eq!(after, before + 2, "Enter fits the two viewports to the printable area");
}
+5 -4
View File
@@ -1,6 +1,6 @@
# Where CADCraft falls short of AutoCAD
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (DIMTFILL limits in the `*D` block, EXPLODE and DXF: F7 updated) · **Target:** Autodesk AutoCAD 2027
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (Named Viewports dialog: U11 narrowed; MVIEW Restore and PURGE of multiline styles had landed: F15 and F21 corrected) · **Target:** Autodesk AutoCAD 2027
Every known shortfall, one row each, ranked within each section by user impact. This is the work
list: agents pick from here (top of a section first), and remove or update a row in the same PR
@@ -37,12 +37,12 @@ do them first.
| F12 | Annotative scaling done (#468): scale list (SCALELISTEDIT), CANNOSCALE, OBJECTSCALE, ANNOALLVISIBLE/ANNOAUTOSCALE, viewport scale, DXF, status bar list. Left: per-scale positions (ANNORESET is a no-op), a viewport annotation scale apart from its viewport scale, annotative objects inside blocks and old LEADERs, AutoCAD files' per-object scale contexts (read as not annotative) | `cmd/annoscale.rs`, `doc/src/anno.rs` | Low-medium | 4–8 |
| F14 | **[alpha blocker]** Plotting leftovers: binary `.ctb`/`.stb`/`.pc3` files can't be read; model-space PDF plots keep the default margins (a device paper's margins apply to layouts and PNG plots); no system printers (H3); the page setup's "Plot transparency" is not saved in DXF/DWG (the public DXF Reference has no plot layout flag for it), so it lasts for the session and in JSON only. The Plot dialog (#160), Plot Preview and PUBLISH (#186), plot styles (CTB and STB, [plot-styles.md](plot-styles.md), #410) with their end, join and fill styles, plotter devices with paper sizes and printable areas ([plotters.md](plotters.md), #564), and plotted transparency (page setup and Plot dialog option, PLOTTRANSPARENCYOVERRIDE) landed | `io/src/plotter.rs`, `cmd/plotters.rs` | Medium | 4–8 |
| F13 | Hatch: 23 patterns (AutoCAD ~85; ours must stay original); the Hatch and Gradient and Hatch Edit dialogs have no Preview or Inherit Properties (HPDLGMODE 0 keeps HATCH on the command line); no MPolygon; HATCHEDIT and -HATCHEDIT add boundaries from picked points or closed objects and remove chosen loops, but a hatch copied with its boundary isn't linked to the copy; HPGAPTOL closes gaps between curve ends only | `doc/src/library.rs`, `cmd/hatch.rs`, `cmd/hatch/edit.rs` | Medium | 6–10 |
| F15 | Viewports: MVIEW Restore prints "not available yet" (named viewport configurations into a layout, see U11); a clipped viewport and its clip boundary move and scale separately. Polygonal, object and clipped viewports, VPCLIP, MVIEW ON/OFF/Lock/2/3/4 and the status-bar scale list landed (#453) | `cmd/layout/viewports.rs` | Low | 2–4 |
| F15 | Viewports: a clipped viewport and its clip boundary move and scale separately. Polygonal, object and clipped viewports, VPCLIP, MVIEW ON/OFF/Lock/2/3/4 and the status-bar scale list landed (#453), MVIEW Restore in #633 | `cmd/layout/viewports.rs` | Low | 2–4 |
| F16 | Groups: no group bounding box or single grip when a group is selected (GROUPDISPLAYMODE), copying a whole group does not make a new group, no Object Grouping dialog (GROUP, GROUPEDIT, UNGROUP, -GROUP, PICKSTYLE group selection and the DXF/DWG round trip landed, #417); PICKSTYLE's associative-hatch bit is stored but unused | `cmd/groups.rs` | Low | 2–4 |
| F17 | Attributes (ATTSYNC, global -ATTEDIT, BATTMAN editing and DATAEXTRACTION/EATTEXT landed, #433): no verify, preset or lock-position modes, and multiline attributes (read, drawn and saved since 2026-10-12) can't be created or switched in BATTMAN; data extraction covers block references and attribute values (CSV or a table) only — no wizard, nested blocks, other objects' properties, `.dxe` templates or spreadsheet output | `cmd/blocks/battman.rs`, `cmd/blocks/extract.rs` | Low | 4–6 |
| F19 | MATCHPROP Settings is a keyword prompt (no dialog) and has no material, shadow or centre-object settings; TEXTTOFRONT and HATCHTOBACK act on the current space only; OVERKILL doesn't compare polyline segments with other objects (picker and Settings, DRAWORDER Above/Under, TEXTTOFRONT, HATCHTOBACK, OVERKILL tolerance/ignore/overlap/end-to-end options landed, #439) | `props/matchprop.rs`, `draworder.rs`, `overkill.rs` | Low | 2–4 |
| F20 | OPTIONS dialog done (#23): Display, Open and Save, Drafting, Selection, User Preferences and Profiles tabs, and the user profile (object snap modes, drafting toggles, Dynamic Input, PICKBOX, APERTURE, GRIPSIZE, CURSORSIZE, PICKFIRST, PICKADD, QPMODE, model and paper background, snap marker size, system cursor) survives restarts. Left: right-click customization, the autosave interval and default save version on Open and Save (the features themselves landed, #406, #421), AutoSnap magnet and tooltip toggles, colours of the other drawing-area elements, named profiles with import and export | `ui-egui/src/options/` | Low | 2–4 |
| F21 | MLINE, REGION and HELIX are objects since #510, with gaps: MLEDIT tees trim square; UNION/SUBTRACT/INTERSECT only combine regions whose boundaries don't meet; regions are saved as closed polylines (no ACIS data); PURGE skips multiline styles | `draw2/mline.rs`, `inquiry/regions.rs`, `dxf_region.rs` | Low | 6–10 |
| F21 | MLINE, REGION and HELIX are objects since #510, with gaps: MLEDIT tees trim square; UNION/SUBTRACT/INTERSECT only combine regions whose boundaries don't meet; regions are saved as closed polylines (no ACIS data) | `draw2/mline.rs`, `inquiry/regions.rs`, `dxf_region.rs` | Low | 6–10 |
| F22 | Inquiry leftovers (COUNT across spaces and nested blocks, the QuickCalc palette and COMPARE landed, #563; MASSPROP on regions, #510): COMPARE compares the tab shown, not every layout at once (COMPAREEXPORT, the comparison as a new drawing, landed); COMPAREEXPORT puts the two sets of objects in model-space groups, where AutoCAD puts them in blocks; QuickCalc keeps its history and variables for the session only | `file/compare.rs`, `palettes/quickcalc.rs` | Low | 3–5 |
| F23 | No Sheet Set Manager, eTransmit, Share, Traces, Markup Import, Activity Insights | menu items uncovered | Low-medium (team workflows) | 40–70 |
| F24 | No 3D: UCS, 3D views, orbit, solids, surfaces, meshes, sections, visual styles, materials, lights, rendering (150 menu items) | [parity-checklist](parity-checklist.md) | Medium (3D users) | 300–450 |
@@ -63,7 +63,7 @@ or allow clicks and drag (2, default), saved in the user profile. Drag-only mode
| U6 | Selection: lasso; the CLass keyword; isolation saved with the drawing (OBJECTISOLATIONMODE 1) and the status-bar isolate button. Press-drag windows (#63), the W, C, F, WP and CP keywords (#226), BOX, AUto, SIngle, Multiple, Group, Add, Remove and Undo, SELECTSIMILAR, ISOLATEOBJECTS/HIDEOBJECTS/UNISOLATEOBJECTS (#428) and selection cycling (#1143) landed | Medium | 3–5 |
| U7 | Right-click menus: no in-place layout tab rename or Move or Copy dialog, no Select All Layouts, no Paste to command line; palette menus are on the Layers rows and the Properties background only. The command menu (held right-click: Enter, Cancel, Recent Input, options, Snap Overrides, Pan/Zoom), the selection menu with object edits, command line, drawing tab, layout tab and palette menus landed (#512) | Low | 1–2 |
| U10 | SETVAR covers about 150 of ~1,000 variables (UNDO options, MREDO and UNDOCTL, and header variables such as CIRCLERAD, OFFSETDIST, POLYSIDES, HPNAME and the dimension variables in SETVAR ? landed, #461; typing a bare variable name as a command runs SETVAR on it) | Low-medium | 3–5 |
| U11 | MVIEW Restore (a saved viewport configuration into a layout); VIEW Orthographic, UCS and background settings; view twist is kept but not shown (Zoom Realtime and Dynamic, ZOOMFACTOR, named views (VIEW) and tiled model viewports with saved configurations (VPORTS) landed in #463) | Low | 1–2 |
| U11 | VIEW Orthographic, UCS and background settings; view twist is kept but not shown (Zoom Realtime and Dynamic, ZOOMFACTOR, named views (VIEW) and tiled model viewports with saved configurations (VPORTS) landed in #463, MVIEW Restore in #633, and the Named Viewports dialog, which restores or deletes a saved configuration) | Low | 1–2 |
| U12 | Workspaces, navigation bar, CUI customisation, ViewCube drag (#7) (the Command Aliases dialog landed, #1381) | Low-medium | 13–23 |
| U13 | `.dxf` from Finder fails to open on macOS (#158) (window controls fixed, #161) | Medium | 1–2 |
@@ -150,6 +150,7 @@ Detail: [localization-parity.md](localization-parity.md).
| Date | Change | Summary |
|---|---|---|
| 2026-10-25 | minor | Named Viewports (View ▸ Viewports ▸ Named Viewports..., `vports.named`) lists the saved viewport configurations with a preview and the current one's name, and restores or deletes one; in a layout Restore places it as MVIEW Restore does: U11 narrowed. F15 and U11 no longer list MVIEW Restore (#633), and F21 no longer says PURGE skips multiline styles |
| 2026-10-25 | minor | DIMTFILL limits: with DIMTFILL 1 (the drawing's background color) the written `*D` block and EXPLODE keep no background mask (a WIPEOUT over the text box would), and a true-color DIMTFILLCLR is written to DXF as the nearest ACI: F7 updated |
| 2026-10-24 | minor | COMPAREEXPORT (typed, or Export on the comparison bar) opens the comparison as a new untitled drawing, `compare_<this> vs <other>.dwg`: this drawing's objects in one group and the other drawing's differing objects in a second group, in the comparison colors (green, red, gray for objects in both), block contents and style colors included: F22 narrowed |
| 2026-10-24 | minor | DIMTFILL and DIMTFILLCLR: a dimension's text background (none, the drawing's background color, or a color) hides what is drawn under the text; SETVAR, DIMOVERRIDE and style fields, DIMSTYLE groups 69 and 70 in DXF from R2007 (CADCraft xdata before), and a SOLID in the `*D` block and from EXPLODE for DIMTFILL 2: F7 narrowed |
+2 -3
View File
@@ -4,13 +4,13 @@
Menu-breadth parity: a reference menu item counts as covered when a CADCraft command is registered under the same menu label. It measures breadth, not depth; see [target-app-parity.md](target-app-parity.md) for the weighted assessment.
**Menu breadth: 296 / 491 items (60%)** · 441 registered commands.
**Menu breadth: 297 / 491 items (60%)** · 442 registered commands.
| Menu | Items | Covered | % |
|---|---|---|---|
| File | 22 | 13 | 59% |
| Edit | 13 | 12 | 92% |
| View | 75 | 36 | 48% |
| View | 75 | 37 | 49% |
| Insert | 11 | 5 | 45% |
| Format | 27 | 26 | 96% |
| Tools | 85 | 66 | 78% |
@@ -37,7 +37,6 @@ Menu-breadth parity: a reference menu item counts as covered when a CADCraft com
- View > Orbit > Free Orbit
- View > Camera > Adjust Distance
- View > Camera > Swivel
- View > Viewports > Named Viewports...
- View > 3D Views > Viewpoint
- View > 3D Views > Plan View > Current UCS
- View > 3D Views > Plan View > World UCS