mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 02:46:51 -06:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef24afc10c | ||
|
|
6528bb564b | ||
|
|
d0281360c2 | ||
|
|
de5aecfe8b | ||
|
|
546b5a1cf3 | ||
|
|
2ab09850bb | ||
|
|
4ab8c384b3 |
@@ -1,7 +1,9 @@
|
||||
//! OFFSET Erase/Layer/Multiple/Undo and the MOVE/COPY Displacement option at the command line.
|
||||
|
||||
use cadcraft_engine::Input;
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::doc::EntityKind;
|
||||
use cadcraft_engine::geom::Vec2;
|
||||
use serde_json::json;
|
||||
|
||||
/// Every line as ((ax, ay), (bx, by), layer), sorted by the first y.
|
||||
@@ -62,6 +64,27 @@ fn offset_multiple_and_undo() {
|
||||
assert_eq!(lines(&s), vec![l((0.0, 0.0), (10.0, 0.0), "0"), l((0.0, 2.0), (10.0, 2.0), "0"), l((0.0, 4.0), (10.0, 4.0), "0")]);
|
||||
}
|
||||
|
||||
// Regression test for #618: OFFSET's distance prompt took only a typed number; clicking two points
|
||||
// (the distance between them) was rejected, and the second click had no distance guide.
|
||||
#[test]
|
||||
fn offset_distance_from_two_clicked_points() {
|
||||
let mut s = one_line();
|
||||
s.start("offset").unwrap();
|
||||
s.input(Input::Point(Vec2::new(20.0, 20.0))).unwrap();
|
||||
let p = s.current_prompt().unwrap();
|
||||
assert!(p.message.contains("second point"), "{}", s.prompt_text());
|
||||
assert!(p.base.is_some_and(|b| b.near(Vec2::new(20.0, 20.0), 1e-9)), "the second click is measured from the first");
|
||||
// The guide from the first click to the cursor is the previewed distance.
|
||||
let guide = s.preview(Vec2::new(20.0, 22.5));
|
||||
assert!(matches!(guide.first().map(|e| &e.kind), Some(EntityKind::Line(g)) if g.a.xy().near(Vec2::new(20.0, 20.0), 1e-9)), "{guide:?}");
|
||||
s.input(Input::Point(Vec2::new(20.0, 22.5))).unwrap();
|
||||
assert!(s.current_prompt().unwrap().message.contains("Select object"), "{}", s.prompt_text());
|
||||
assert_eq!(s.doc().unwrap().header.f64("OFFSETDIST", -1.0), 2.5);
|
||||
s.script("5,0 5,1\n\n").unwrap();
|
||||
assert!(s.running.is_none(), "{}", s.prompt_text());
|
||||
assert_eq!(lines(&s), vec![l((0.0, 0.0), (10.0, 0.0), "0"), l((0.0, 2.5), (10.0, 2.5), "0")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_json_erase_and_layer() {
|
||||
let mut s = one_line();
|
||||
|
||||
@@ -349,6 +349,22 @@ fn hostile_dxf_does_not_panic() {
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test for #1336: the LAYER table of a DXF had no record cap, so a hostile file could
|
||||
// grow the layer list without bound (its VIEW/UCS/VPORT peers stop at one million records).
|
||||
#[test]
|
||||
fn dxf_layer_table_stops_at_the_record_cap() {
|
||||
use std::fmt::Write as _;
|
||||
let records = 1_000_100;
|
||||
let mut text = String::from("0\nSECTION\n2\nTABLES\n0\nTABLE\n2\nLAYER\n");
|
||||
for i in 0..records {
|
||||
let _ = write!(text, "0\nLAYER\n2\nL{i}\n");
|
||||
}
|
||||
text.push_str("0\nENDTAB\n0\nENDSEC\n0\nEOF\n");
|
||||
let d = read(text.as_bytes(), "layers.dxf").unwrap();
|
||||
assert!(d.layers.len() <= 1_000_000, "{} layers read from {records} records", d.layers.len());
|
||||
assert!(d.layers.len() > 900_000, "the records below the cap are kept: {}", d.layers.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_and_png_export() {
|
||||
let d = sample();
|
||||
|
||||
@@ -297,6 +297,31 @@ fn truetype_text_is_filled_or_outlined() {
|
||||
assert!(l.prims.len() >= 3, "O has two contours, H one");
|
||||
}
|
||||
|
||||
// Regression test for #651: filled TrueType text was triangulated at full outline detail at every
|
||||
// zoom, so a page of small text on screen produced millions of triangles and froze the canvas.
|
||||
#[test]
|
||||
fn small_filled_text_on_screen_is_simplified_but_plots_keep_full_detail() {
|
||||
let Some(font) = system_font() else { return };
|
||||
let d = text_doc(font, "OOOO");
|
||||
let tris = |l: &DisplayList| l.prims.iter().filter(|p| p.kind == Kind::Tris).map(|p| l.points(p).len() / 3).sum::<usize>();
|
||||
let plot = build_plot(&d, &Space::Model, &Options::default());
|
||||
let full = tris(&plot);
|
||||
assert!(full > 100, "full-detail O glyphs are many triangles: {full}");
|
||||
// One pixel is a tenth of the cap height: the outlines are simplified, the shapes stay.
|
||||
let reduced = build(&d, &Space::Model, &screen(0.1, None));
|
||||
assert!(tris(&reduced) < full / 2, "{} triangles at 10 px per cap height, {full} in a plot", tris(&reduced));
|
||||
assert!(tris(&reduced) > 0 && reduced.bounds.width() > plot.bounds.width() * 0.9, "{:?} vs {:?}", reduced.bounds, plot.bounds);
|
||||
// An em under two pixels is drawn as the ink's box: two triangles a glyph, same extent.
|
||||
let tiny = build(&d, &Space::Model, &screen(1.0, None));
|
||||
assert_eq!(tris(&tiny), 2 * 4, "one box per glyph");
|
||||
assert!(
|
||||
tiny.bounds.width() > plot.bounds.width() * 0.9 && tiny.bounds.height() > plot.bounds.height() * 0.9,
|
||||
"{:?} vs {:?}",
|
||||
tiny.bounds,
|
||||
plot.bounds
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_font_falls_back_to_stroke() {
|
||||
let d = text_doc("NoSuchFontAnywhere.ttf", "OH");
|
||||
|
||||
@@ -16,6 +16,7 @@ mod lisp_files;
|
||||
mod measure_distance_snapping;
|
||||
mod menubar_hover;
|
||||
mod mspace_viewport_view;
|
||||
mod offset_clicks;
|
||||
mod options_dialog;
|
||||
mod parameters_rename;
|
||||
mod pickdrag;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//! OFFSET on the canvas: the distance can be clicked, and picking the source is not moved by the
|
||||
//! grid snap (#618).
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::doc::{Common, Drawing, EntityKind, Line, Space};
|
||||
use cadcraft_engine::geom::Vec2;
|
||||
use cadcraft_ui_egui::{CadApp, Services};
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2, vec2};
|
||||
use egui_kittest::Harness;
|
||||
use serde_json::json;
|
||||
|
||||
fn harness() -> Harness<'static, CadApp> {
|
||||
let mut d = Drawing::new_imperial();
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Line(Line { a: Vec2::new(0.0, 2.0).to3(0.0), b: Vec2::new(10.0, 2.0).to3(0.0) })).unwrap();
|
||||
let mut session = Session::empty();
|
||||
session.open_drawing(d, "Offset clicks", None);
|
||||
session.execute("view.set", &json!({"center":[5,5],"height":20})).unwrap();
|
||||
session.settings.osmode = 0;
|
||||
session.settings.snapmode = true;
|
||||
session.execute("setvar", &json!({"name":"SNAPUNIT","value":[5,5]})).unwrap();
|
||||
session.settings.orthomode = false;
|
||||
let mut app = CadApp::new(session, Services::default());
|
||||
app.canvas.zoom_pending = false;
|
||||
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_ui_state(
|
||||
|ui, app: &mut CadApp| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(5);
|
||||
h
|
||||
}
|
||||
|
||||
fn click(h: &mut Harness<'_, CadApp>, world: Vec2) {
|
||||
let pos: Pos2 = h.state().canvas.xf.unwrap().to_screen(world);
|
||||
h.event(Event::PointerMoved(pos));
|
||||
h.run_steps(2);
|
||||
for pressed in [true, false] {
|
||||
h.event(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
|
||||
h.run_steps(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test for #618: OFFSET's distance could not be clicked, and with grid snap on a click
|
||||
// meant to pick the source object was moved to the nearest grid point, missing the object.
|
||||
#[test]
|
||||
fn offset_takes_a_clicked_distance_and_picks_its_source_under_grid_snap() {
|
||||
let mut h = harness();
|
||||
h.state_mut().start("offset");
|
||||
h.run_steps(2);
|
||||
// Two clicks, snapped to the grid points (5, 5) and (10, 5): a distance of 5.
|
||||
click(&mut h, Vec2::new(5.1, 4.9));
|
||||
assert!(h.state().session.prompt_text().contains("second point"), "{}", h.state().session.prompt_text());
|
||||
click(&mut h, Vec2::new(9.9, 5.1));
|
||||
assert_eq!(h.state().session.doc().unwrap().header.f64("OFFSETDIST", -1.0), 5.0);
|
||||
assert!(h.state().session.prompt_text().contains("Select object"), "{}", h.state().session.prompt_text());
|
||||
// The source line is at y = 2, two units from the grid point (5, 0) the click would snap to.
|
||||
click(&mut h, Vec2::new(7.0, 2.04));
|
||||
let prompt = h.state().session.prompt_text();
|
||||
assert!(prompt.contains("side to offset"), "the line was not picked: {prompt}");
|
||||
// A side click above the line (snapped to (5, 5)) makes the copy 5 units up, at y = 7.
|
||||
click(&mut h, Vec2::new(7.0, 7.0));
|
||||
let lines: Vec<(f64, f64)> = h
|
||||
.state()
|
||||
.session
|
||||
.doc()
|
||||
.unwrap()
|
||||
.model
|
||||
.iter()
|
||||
.filter_map(|e| match &e.kind {
|
||||
EntityKind::Line(line) => Some((line.a.y, line.b.y)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(lines.len(), 2, "OFFSET added no line: {lines:?}");
|
||||
assert!(lines.iter().any(|&(a, b)| (a - 7.0).abs() < 1e-9 && (b - 7.0).abs() < 1e-9), "no copy at y = 7: {lines:?}");
|
||||
// OFFSET asks for the next object; Enter ends it.
|
||||
assert!(h.state().session.prompt_text().contains("Select object to offset"), "{}", h.state().session.prompt_text());
|
||||
h.key_press(egui::Key::Enter);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().session.prompt_text(), "Command:", "Enter ends OFFSET");
|
||||
}
|
||||
Reference in New Issue
Block a user