Compare commits

...
Author SHA1 Message Date
skylar taylor-barrick ef24afc10c #618: move the OFFSET canvas test into the ui-egui test binary
Main now builds one integration-test binary per crate (#1989); only tests that install process-wide state stay separate. This test only drives its own session, so it joins tests/it.
2026-10-23 12:18:39 -06:00
skylar taylor-barrick 6528bb564b Merge remote-tracking branch 'origin/main' into test/backfill-regression-tests 2026-10-23 12:09:53 -06:00
skylar taylor-barrick d0281360c2 #618: finish the grid-snapped OFFSET in the canvas test
The test stopped once the source line was picked, so a broken side-point step could leave the drawing unchanged and still pass. It now clicks a side point above the line and checks that the copy lands 5 units up, at y = 7. It also checks that OFFSET asks for the next object and that Enter ends it.
2026-10-22 21:16:48 -06:00
skylar taylor-barrick de5aecfe8b #651: compare small text against the plot build path
The full-detail baseline used build(), which takes the on-screen path; plots take build_plot(), which keeps glyphs at full detail and records them as plot text. Comparing against build_plot() checks what the test's name says. With the screen-detail change reverted it still fails (480 triangles on screen, 480 in the plot).
2026-10-22 16:49:30 -06:00
skylar taylor-barrick 546b5a1cf3 #651: test that small filled text is simplified on screen and keeps full detail in plots
Filled TrueType text was triangulated at full outline detail at every zoom, which froze the canvas on pages of small text. The test builds four O glyphs on screen at 10 px and under 2 px per em and checks the triangle count drops (to one box per glyph at the smallest), while a plot keeps every triangle. Without the fix the on-screen build has the same 1656 triangles as the plot. It skips when no system font is installed, like the other TrueType render tests.
2026-10-22 16:34:32 -06:00
skylar taylor-barrick 2ab09850bb #1336: test that the DXF LAYER table stops at the record cap
The LAYER, LTYPE, STYLE and DIMSTYLE tables had no record cap, unlike VIEW, UCS and VPORT. The test reads a DXF with over a million LAYER records and checks the layer list stays at the cap; without the guard it holds every record (1000102 layers). The other three tables scan their lists linearly per record, so a million-record file is too slow to build in a test and they stay uncovered.
2026-10-22 16:34:32 -06:00
skylar taylor-barrick 4ab8c384b3 #618: test that OFFSET takes a clicked distance and picks its source under grid snap
OFFSET only accepted a typed distance, and with grid snap on a click meant to pick the source object was moved to the nearest grid point. The engine test clicks two points for the distance and checks the guide, the distance and the resulting offset; the canvas test does the same through grid-snapped clicks and then picks the source line. Without the fix the engine test stays at the distance prompt and the canvas test never picks the line.
2026-10-22 16:34:32 -06:00
5 changed files with 148 additions and 0 deletions
+23
View File
@@ -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();
+16
View File
@@ -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();
+25
View File
@@ -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");
+1
View File
@@ -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;
+83
View File
@@ -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");
}