mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 10:56:51 -06:00
Xref paths saved in a drawing (on open, on Reload, in the External References list, XOPEN, nested xrefs) and table data-link paths re-read by DATALINKUPDATE go through doc_path::resolve (craft-docpath): relative paths resolve beside the drawing that stores them, and paths to other computers, devices and URLs are not opened. Such an xref is Unresolved with the reason, unless a file of its name is beside the host drawing. Paths given to XATTACH, -XREF Path and DATALINK are used as given. One load of xrefs reads at most 1 GB from referenced files in total.
174 lines
8.3 KiB
Rust
174 lines
8.3 KiB
Rust
//! Table breaking (#534): parts with repeated labels, part grips, TABLEDIT in any part; data
|
|
//! links filling a table from a CSV file and DATALINKUPDATE.
|
|
|
|
use cadcraft_engine::Session;
|
|
use cadcraft_engine::doc::{BreakDirection, EntityKind, Handle, Table};
|
|
use cadcraft_engine::geom::Vec2;
|
|
use cadcraft_engine::render;
|
|
use serde_json::json;
|
|
|
|
fn table(s: &Session, h: &str) -> Table {
|
|
match &s.doc().unwrap().entity(Handle::parse_hex(h).unwrap()).unwrap().kind {
|
|
EntityKind::Table(t) => t.clone(),
|
|
_ => panic!("not a table"),
|
|
}
|
|
}
|
|
|
|
/// Title, header and 8 data rows, each 1 high, 2 columns of 3.
|
|
fn session() -> (Session, String) {
|
|
let mut s = Session::new();
|
|
let cells: Vec<Vec<String>> = (1..=8).map(|i| vec![format!("r{i}"), i.to_string()]).collect();
|
|
let r = s
|
|
.execute("table", &json!({ "at": [0, 0], "title": "T", "header": ["Name", "N"], "cells": cells, "rowHeight": 1.0, "colWidth": 3.0 }))
|
|
.unwrap();
|
|
let h = r["handle"].as_str().unwrap().to_string();
|
|
// Same height for the title row.
|
|
let hh = Handle::parse_hex(&h).unwrap();
|
|
s.doc_mut()
|
|
.unwrap()
|
|
.modify_entity(hh, |e| {
|
|
if let EntityKind::Table(t) = &mut e.kind {
|
|
t.row_heights[0] = 1.0;
|
|
}
|
|
})
|
|
.unwrap();
|
|
(s, h)
|
|
}
|
|
|
|
#[test]
|
|
fn breaking_draws_parts_with_repeated_labels() {
|
|
let (mut s, h) = session();
|
|
let r = s.execute("table.break", &json!({ "handle": h, "height": 6.0, "spacing": 1.0 })).unwrap();
|
|
// 2 label rows + 4 data rows per part.
|
|
assert_eq!(r["parts"], 2);
|
|
let t = table(&s, &h);
|
|
let g = render::table_geometry(s.doc().unwrap(), &t);
|
|
// The header text appears in both parts.
|
|
assert_eq!(g.texts.iter().filter(|x| x.text == "Name").count(), 2);
|
|
let r5 = g.texts.iter().find(|x| x.text == "r5").unwrap();
|
|
assert_eq!(r5.row, 6);
|
|
assert!(r5.at.x > 7.0 && r5.at.y > -3.0, "r5 starts the second part at x 7: {:?}", r5.at);
|
|
// Extents cover both parts.
|
|
let b = cadcraft_engine::doc::entity_bounds(s.doc().unwrap(), s.doc().unwrap().entity(Handle::parse_hex(&h).unwrap()).unwrap(), 0);
|
|
assert!((b.max.x - 13.0).abs() < 1e-9 && (b.min.y + 6.0).abs() < 1e-9, "{b:?}");
|
|
// TABLEDIT finds cells in the second part.
|
|
s.execute("tabledit", &json!({ "at": [11.5, -2.5], "text": "five" })).unwrap();
|
|
assert_eq!(table(&s, &h).cells[6][1].text, "five");
|
|
// Down, with manual heights; Off.
|
|
s.execute("table.break", &json!({ "handle": h, "direction": "down", "heights": [4.0] })).unwrap();
|
|
let t = table(&s, &h);
|
|
assert_eq!(t.breaks.as_ref().unwrap().direction, BreakDirection::Down);
|
|
assert_eq!(t.parts().first().unwrap().rows, vec![0, 1, 2, 3]);
|
|
let r = s.execute("table.break", &json!({ "handle": h, "enabled": false })).unwrap();
|
|
assert_eq!(r["parts"], 1);
|
|
assert!(s.execute("table.break", &json!({ "handle": h, "direction": "up" })).is_err());
|
|
assert!(s.execute("table.break", &json!({ "handle": h, "height": -1 })).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn part_grips_move_parts() {
|
|
let (mut s, h) = session();
|
|
s.execute("table.break", &json!({ "handle": h, "height": 6.0, "spacing": 1.0 })).unwrap();
|
|
let k = EntityKind::Table(table(&s, &h));
|
|
let grips = k.grips();
|
|
assert_eq!(grips, vec![Vec2::new(0.0, 0.0), Vec2::new(7.0, 0.0)]);
|
|
// Drag the second part's grip; the first part stays.
|
|
let moved = cadcraft_engine::grips::stretch_grip(&k, 1, Vec2::new(9.0, -2.0)).unwrap();
|
|
let EntityKind::Table(t) = &moved else { panic!() };
|
|
let parts = t.parts();
|
|
assert_eq!((parts[0].origin, parts[1].origin), (Vec2::new(0.0, 0.0), Vec2::new(9.0, -2.0)));
|
|
// The insertion grip moves everything.
|
|
let all = cadcraft_engine::grips::stretch_grip(&moved, 0, Vec2::new(1.0, 1.0)).unwrap();
|
|
let EntityKind::Table(t) = &all else { panic!() };
|
|
assert_eq!(t.parts()[1].origin, Vec2::new(10.0, -1.0));
|
|
// Typed: select the table by a point, then the height.
|
|
s.execute("table.break", &json!({ "handle": h, "enabled": false })).unwrap();
|
|
s.cmdline("table.break").unwrap();
|
|
s.cmdline("1,-0.5").unwrap();
|
|
s.cmdline("4").unwrap();
|
|
assert_eq!(table(&s, &h).parts().len(), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn data_links_fill_and_update() {
|
|
let dir = std::env::temp_dir().join(format!("cadcraft-datalink-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let path = dir.join("parts.csv");
|
|
std::fs::write(&path, "Part,Qty,Note\r\nbolt,4,\"M6, zinc\"\nnut,8,\n").unwrap();
|
|
let mut s = Session::new();
|
|
let r = s.execute("table", &json!({ "at": [0, 0], "title": "Parts", "cols": 2, "rows": 1 })).unwrap();
|
|
let h = r["handle"].as_str().unwrap().to_string();
|
|
let r = s.execute("datalink", &json!({ "handle": h, "path": path.to_str().unwrap() })).unwrap();
|
|
assert_eq!((r["rows"].as_u64(), r["cols"].as_u64()), (Some(4), Some(3)));
|
|
let t = table(&s, &h);
|
|
assert_eq!(t.cells[0][0].merged, Some((1, 3)));
|
|
assert_eq!(t.cells[1][0].text, "Part");
|
|
assert_eq!(t.cells[2][2].text, "M6, zinc");
|
|
// An update re-reads the file and resizes the table to it.
|
|
std::fs::write(&path, "Part,Qty\nwasher,12\n").unwrap();
|
|
let r = s.execute("datalinkupdate", &json!({})).unwrap();
|
|
assert_eq!(r["updated"], 1);
|
|
let t = table(&s, &h);
|
|
assert_eq!((t.cells.len(), t.col_widths.len()), (3, 2));
|
|
assert_eq!(t.cells[2][1].text, "12");
|
|
// Typed DATALINKUPDATE with a selection.
|
|
s.cmdline("datalinkupdate").unwrap();
|
|
s.cmdline("1,-0.2").unwrap();
|
|
s.cmdline("").unwrap();
|
|
// Missing files and missing paths are errors, not panics.
|
|
assert!(s.execute("datalink", &json!({ "handle": h })).is_err());
|
|
assert!(s.execute("datalink", &json!({ "handle": h, "path": dir.join("missing.csv").to_str().unwrap() })).is_err());
|
|
std::fs::remove_file(&path).unwrap();
|
|
let r = s.execute("datalinkupdate", &json!({})).unwrap();
|
|
assert_eq!(r["updated"], 0);
|
|
s.execute("datalink", &json!({ "handle": h, "remove": true })).unwrap();
|
|
assert!(table(&s, &h).data_link.is_none());
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn data_links_saved_in_a_drawing_are_read_beside_it_or_on_this_computer_only() {
|
|
cadcraft_engine::cmd::file::set_io(cadcraft_engine::cmd::file::IoHooks {
|
|
read: |b, name| cadcraft_io::read(b, name).map_err(|e| e.to_string()),
|
|
write: |d, name| cadcraft_io::write(d, name).map_err(|e| e.to_string()),
|
|
plot: None,
|
|
});
|
|
let base = std::env::temp_dir().join(format!("cadcraft-datalink-saved-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&base);
|
|
let (dir, elsewhere) = (base.join("plans"), base.join("elsewhere"));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
std::fs::create_dir_all(&elsewhere).unwrap();
|
|
std::fs::write(dir.join("parts.csv"), "Part,Qty\nbolt,4\n").unwrap();
|
|
std::fs::write(elsewhere.join("far.csv"), "Part,Qty\nnut,8\n").unwrap();
|
|
|
|
let mut links = vec![r"\\192.0.2.1\share\far.csv".to_string(), "file://192.0.2.1/s/far.csv".to_string(), "parts.csv".to_string()];
|
|
if cfg!(unix) {
|
|
// Read as a local path by Linux, and as a network path by the resolver.
|
|
links.push(format!("/{}", elsewhere.join("far.csv").display()));
|
|
}
|
|
let mut s = Session::new();
|
|
for (i, link) in links.iter().enumerate() {
|
|
let r = s.execute("table", &json!({ "at": [0, 10.0 * i as f64], "cols": 2, "rows": 1 })).unwrap();
|
|
let h = Handle::parse_hex(r["handle"].as_str().unwrap()).unwrap();
|
|
s.doc_mut()
|
|
.unwrap()
|
|
.modify_entity(h, |e| {
|
|
if let EntityKind::Table(t) = &mut e.kind {
|
|
t.data_link = Some(cadcraft_engine::doc::DataLink { path: link.clone(), start_row: 0 });
|
|
}
|
|
})
|
|
.unwrap();
|
|
}
|
|
let host = dir.join("host.dxf");
|
|
s.execute("saveas", &json!({ "path": host.to_str().unwrap() })).unwrap();
|
|
let mut s = Session::new();
|
|
s.execute("open", &json!({ "path": host.to_str().unwrap() })).unwrap();
|
|
|
|
let r = s.execute("datalinkupdate", &json!({})).unwrap();
|
|
assert_eq!(r["updated"], 1, "only the file beside the drawing: {r}");
|
|
let errors: Vec<&str> = r["errors"].as_array().unwrap().iter().filter_map(|e| e.as_str()).collect();
|
|
assert_eq!(errors.len(), links.len() - 1, "{errors:?}");
|
|
assert!(errors.iter().all(|e| e.contains("another computer")), "{errors:?}");
|
|
let _ = std::fs::remove_dir_all(&base);
|
|
}
|