mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-25 19:36:49 -06:00
Squash of PR #504 by @vangel76. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
6.8 KiB
Rust
156 lines
6.8 KiB
Rust
//! Tiled model space viewports (#463): -VPORTS SIngle, 2/3/4, Join, Save, Restore, Delete and ?,
|
|
//! View > Viewports > n in the Model tab, the current tile and its own view. (The DXF VPORT table
|
|
//! round trip is tested in `cadcraft-io`'s `view_tables_roundtrip`.)
|
|
|
|
use cadcraft_engine::doc::{EntityKind, Space};
|
|
use cadcraft_engine::geom::Vec2;
|
|
use cadcraft_engine::{ModelTile, Session, View};
|
|
use serde_json::json;
|
|
|
|
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("view.set", &json!({ "center": [10, 10], "height": 20 })).unwrap();
|
|
s
|
|
}
|
|
|
|
fn typed(s: &mut Session, lines: &[&str]) {
|
|
for l in lines {
|
|
s.cmdline(l).unwrap();
|
|
}
|
|
}
|
|
|
|
fn tiles(s: &Session) -> Vec<ModelTile> {
|
|
s.model_tiles()
|
|
}
|
|
|
|
fn current(s: &Session) -> usize {
|
|
tiles(s).iter().position(|t| t.active).unwrap()
|
|
}
|
|
|
|
fn rects(s: &Session) -> Vec<[f64; 4]> {
|
|
let mut r: Vec<[f64; 4]> = tiles(s).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 view(s: &Session) -> View {
|
|
s.state().unwrap().view()
|
|
}
|
|
|
|
#[test]
|
|
fn split_activate_and_each_tile_keeps_its_view() {
|
|
let mut s = session();
|
|
assert!(tiles(&s).is_empty(), "one view: no tiles");
|
|
s.execute("-vports", &json!({ "option": "2", "arrangement": "vertical" })).unwrap();
|
|
assert_eq!(rects(&s), vec![[0.0, 0.0, 0.5, 1.0], [0.5, 0.0, 1.0, 1.0]]);
|
|
// Every new tile starts with the view that was split.
|
|
assert!(tiles(&s).iter().all(|t| t.view == View { center: Vec2::new(10.0, 10.0), height: 20.0 }));
|
|
let first = current(&s);
|
|
let other = 1 - first;
|
|
s.activate_tile(other).unwrap();
|
|
assert_eq!(current(&s), other);
|
|
s.execute("view.set", &json!({ "center": [50, 25], "height": 5 })).unwrap();
|
|
s.execute("vports.activate", &json!({ "index": first + 1 })).unwrap();
|
|
assert_eq!(view(&s), View { center: Vec2::new(10.0, 10.0), height: 20.0 });
|
|
assert_eq!(tiles(&s)[other].view, View { center: Vec2::new(50.0, 25.0), height: 5.0 });
|
|
// Switching tiles changes no drawing data; arranging them is an undo step.
|
|
assert_eq!(s.state().unwrap().undo.len(), 2, "line + VPORTS");
|
|
s.undo().unwrap();
|
|
assert!(tiles(&s).is_empty());
|
|
assert!(s.execute("vports.activate", &json!({ "index": 2 })).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn typed_three_right_then_join_and_single() {
|
|
let mut s = session();
|
|
// Enter takes 3, Enter takes Right: a big tile on the right (current) and two on the left.
|
|
typed(&mut s, &["-vports", "", ""]);
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
assert_eq!(rects(&s), vec![[0.0, 0.0, 0.5, 0.5], [0.0, 0.5, 0.5, 1.0], [0.5, 0.0, 1.0, 1.0]]);
|
|
let cur = tiles(&s)[current(&s)];
|
|
assert_eq!((cur.min, cur.max), (Vec2::new(0.5, 0.0), Vec2::new(1.0, 1.0)));
|
|
// Join the two left tiles: the dominant one's view wins.
|
|
let t = tiles(&s);
|
|
let top = t.iter().position(|t| t.min == Vec2::new(0.0, 0.5)).unwrap();
|
|
let bottom = t.iter().position(|t| t.min == Vec2::ZERO).unwrap();
|
|
s.activate_tile(top).unwrap();
|
|
s.execute("view.set", &json!({ "center": [1, 2], "height": 3 })).unwrap();
|
|
typed(&mut s, &["-vports", "j", ""]);
|
|
// A click in the other tile answers "Select viewport to join".
|
|
s.click_tile(bottom).unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
assert_eq!(rects(&s), vec![[0.0, 0.0, 0.5, 1.0], [0.5, 0.0, 1.0, 1.0]]);
|
|
assert_eq!(view(&s), View { center: Vec2::new(1.0, 2.0), height: 3.0 });
|
|
// Tiles that don't make a rectangle can't be joined.
|
|
s.execute("-vports", &json!({ "option": "3", "arrangement": "above" })).unwrap();
|
|
let t = tiles(&s);
|
|
// The left tile split: bottom left (0,0)-(0.25,0.5) and the right tile don't touch.
|
|
let (a, b) = (t.iter().position(|t| t.max.x <= 0.25).unwrap(), t.iter().position(|t| t.min.x >= 0.5).unwrap());
|
|
assert!(s.execute("-vports", &json!({ "option": "join", "dominant": a + 1, "join": b + 1 })).is_err());
|
|
// SIngle keeps the current tile's view.
|
|
s.execute("view.set", &json!({ "center": [7, 7], "height": 7 })).unwrap();
|
|
typed(&mut s, &["-vports", "si"]);
|
|
assert!(tiles(&s).is_empty());
|
|
assert_eq!(view(&s), View { center: Vec2::new(7.0, 7.0), height: 7.0 });
|
|
}
|
|
|
|
#[test]
|
|
fn named_configurations_save_restore_list_delete() {
|
|
let mut s = session();
|
|
s.execute("-vports", &json!({ "option": "4" })).unwrap();
|
|
assert_eq!(tiles(&s).len(), 4);
|
|
typed(&mut s, &["-vports", "s", "Quad"]);
|
|
s.execute("-vports", &json!({ "option": "single" })).unwrap();
|
|
assert!(tiles(&s).is_empty());
|
|
typed(&mut s, &["-vports", "r", "quad"]);
|
|
assert_eq!(tiles(&s).len(), 4);
|
|
let list = s.execute("-vports", &json!({ "option": "list" })).unwrap();
|
|
assert_eq!(list["configurations"][0]["name"], json!("Quad"));
|
|
assert_eq!(list["tiles"].as_array().unwrap().len(), 4);
|
|
typed(&mut s, &["-vports", "d", "Q*"]);
|
|
assert!(s.doc().unwrap().vports.iter().all(|c| c.name != "Quad"));
|
|
for bad in [
|
|
json!({ "option": "save", "name": "*Active" }),
|
|
json!({ "option": "restore", "name": "nope" }),
|
|
json!({ "option": "join", "join": 99 }),
|
|
json!({ "option": "3", "arrangement": "diagonal" }),
|
|
json!({ "option": "7" }),
|
|
] {
|
|
assert!(s.execute("-vports", &bad).is_err(), "{bad}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn menu_viewports_in_the_model_tab_and_limits() {
|
|
let mut s = session();
|
|
// View > Viewports > 2 Viewports asks for the arrangement in the Model tab.
|
|
s.start("vports.2").unwrap();
|
|
typed(&mut s, &["h"]);
|
|
assert_eq!(rects(&s), vec![[0.0, 0.0, 1.0, 0.5], [0.0, 0.5, 1.0, 1.0]]);
|
|
s.execute("vports.1", &json!({})).unwrap();
|
|
assert!(tiles(&s).is_empty());
|
|
s.execute("vports.3", &json!({ "arrangement": "vertical" })).unwrap();
|
|
// MVIEW / VPORTS JSON stay layout commands.
|
|
assert!(s.execute("vports", &json!({ "count": 2 })).is_err());
|
|
assert_eq!(tiles(&s).len(), 3);
|
|
// Splitting the current tile again and again stops at a bound, without a panic.
|
|
let mut errors = 0;
|
|
for _ in 0..40 {
|
|
if s.execute("-vports", &json!({ "option": "4" })).is_err() {
|
|
errors += 1;
|
|
}
|
|
}
|
|
assert!(errors > 0 && tiles(&s).len() <= 64, "{} tiles", tiles(&s).len());
|
|
// A layout has no tiles; -VPORTS there makes layout viewports (MVIEW).
|
|
s.execute("layout.set", &json!({ "name": "Layout1" })).unwrap();
|
|
assert!(tiles(&s).is_empty());
|
|
s.execute("-vports", &json!({ "option": "2" })).unwrap();
|
|
let vps = s.doc().unwrap().layout("Layout1").unwrap().entities.iter().filter(|e| matches!(e.kind, EntityKind::Viewport(_))).count();
|
|
assert!(vps >= 2, "{vps}");
|
|
s.execute("layout.set", &json!({ "name": "Model" })).unwrap();
|
|
assert_eq!(s.layout_space(), Space::Model);
|
|
assert!(!tiles(&s).is_empty(), "the tiles come back with the Model tab");
|
|
}
|