4806209761
Whiteboard: zoom bar and dot grid, layers panel, appear motion, sound cues, focus rings and AccessKit labels; .draw files with legacy .excalidraw still readable; notescraft-cli export whiteboard. Security: export.pdf and file.export_whiteboard_svg accept vault-relative out paths only (they are reachable by the assistant and the control channel). The CLI places absolute --out files itself via the new notescraft_engine::render_pdf; the UI export worker renders and the live session writes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
179 lines
6.9 KiB
Rust
179 lines
6.9 KiB
Rust
//! Per-element roughness, dash and fill behaviour:
|
|
//! 0 is clean geometry, 1..=3 scale jitter. Output is deterministic per seed.
|
|
|
|
use notescraft_whiteboard::rough::fill_polylines;
|
|
use notescraft_whiteboard::rough::{Polyline, Style, dash as dash_rough};
|
|
use notescraft_whiteboard::{Element, FillStyle, IdGen, StrokeStyle, rough};
|
|
|
|
fn bits(lines: &[Polyline]) -> Vec<u32> {
|
|
lines.iter().flat_map(|line| line.iter().flat_map(|p| [p[0].to_bits(), p[1].to_bits()])).collect()
|
|
}
|
|
|
|
fn rect_with_roughness(roughness: u8, seed: u64) -> Element {
|
|
let mut ids = IdGen::new();
|
|
let mut rect = Element::rectangle(&mut ids, 0.0, 0.0, 100.0, 60.0);
|
|
rect.roughness = roughness;
|
|
rect.seed = seed;
|
|
rect
|
|
}
|
|
|
|
#[test]
|
|
fn roughness_zero_matches_clean_for_every_kind() {
|
|
for seed in 1..=5u64 {
|
|
let mut ids = IdGen::new();
|
|
let mut elements = vec![
|
|
Element::rectangle(&mut ids, 0.0, 0.0, 100.0, 60.0),
|
|
Element::ellipse(&mut ids, 0.0, 0.0, 100.0, 60.0),
|
|
Element::diamond(&mut ids, 0.0, 0.0, 100.0, 60.0),
|
|
Element::line(&mut ids, 0.0, 0.0, 100.0, 40.0),
|
|
Element::arrow(&mut ids, 0.0, 0.0, 100.0, 0.0),
|
|
];
|
|
for element in &mut elements {
|
|
element.seed = seed;
|
|
element.roughness = 0;
|
|
}
|
|
for element in &elements {
|
|
let clean = rough::outline(element, Style::Clean);
|
|
let hand = rough::outline(element, Style::HandDrawn);
|
|
assert_eq!(bits(&clean), bits(&hand), "roughness 0 should match Clean for seed {seed}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn roughness_two_differs_from_zero_with_same_seed() {
|
|
let zero = rect_with_roughness(0, 42);
|
|
let two = rect_with_roughness(2, 42);
|
|
let zero_out = rough::outline(&zero, Style::HandDrawn);
|
|
let two_out = rough::outline(&two, Style::HandDrawn);
|
|
assert_eq!(zero_out.len(), 1, "roughness 0 ⇒ single stroke");
|
|
assert_eq!(two_out.len(), 2, "roughness 2 ⇒ double stroke");
|
|
assert_ne!(bits(&zero_out), bits(&two_out), "jitter must move the geometry");
|
|
}
|
|
|
|
#[test]
|
|
fn same_seed_and_roughness_give_identical_output() {
|
|
let a = rect_with_roughness(2, 99);
|
|
let b = rect_with_roughness(2, 99);
|
|
let first = rough::outline(&a, Style::HandDrawn);
|
|
let second = rough::outline(&b, Style::HandDrawn);
|
|
let again = rough::outline(&a, Style::HandDrawn);
|
|
assert_eq!(bits(&first), bits(&second));
|
|
assert_eq!(bits(&first), bits(&again));
|
|
}
|
|
|
|
#[test]
|
|
fn higher_roughness_perturbs_more() {
|
|
let r1 = rect_with_roughness(1, 7);
|
|
let r3 = rect_with_roughness(3, 7);
|
|
let one = rough::outline(&r1, Style::HandDrawn);
|
|
let three = rough::outline(&r3, Style::HandDrawn);
|
|
assert_eq!(one.len(), 2);
|
|
assert_eq!(three.len(), 2);
|
|
assert_ne!(bits(&one), bits(&three), "higher roughness should perturb the geometry differently");
|
|
}
|
|
|
|
#[test]
|
|
fn preview_force_clean_with_style_argument() {
|
|
let mut ids = IdGen::new();
|
|
let mut rect = Element::rectangle(&mut ids, 0.0, 0.0, 100.0, 60.0);
|
|
rect.roughness = 1;
|
|
let clean = rough::outline(&rect, Style::Clean);
|
|
assert_eq!(clean.len(), 1, "Style::Clean is an explicit clean override");
|
|
assert_eq!(clean[0].len(), 5, "4 corners plus the closing point");
|
|
}
|
|
|
|
#[test]
|
|
fn solid_stroke_returns_one_polyline() {
|
|
let strokes = vec![vec![[0.0, 0.0], [10.0, 0.0], [20.0, 0.0]]];
|
|
let dashed = dash_rough(&strokes, StrokeStyle::Solid);
|
|
assert_eq!(dashed, strokes);
|
|
}
|
|
|
|
#[test]
|
|
fn dashed_long_line_produces_many_segments() {
|
|
let mut ids = IdGen::new();
|
|
let line = Element::line(&mut ids, 0.0, 0.0, 1000.0, 0.0);
|
|
let strokes = rough::outline(&line, Style::Clean);
|
|
let dashed = dash_rough(&strokes, StrokeStyle::Dashed);
|
|
assert!(dashed.len() >= 2, "a 1000-pt line should dash into >= 2 segments, got {}", dashed.len());
|
|
for polyline in &dashed {
|
|
assert!(polyline.len() >= 2);
|
|
assert!(polyline.iter().all(|p| p[0].is_finite() && p[1].is_finite()));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn dotted_long_line_produces_many_dots() {
|
|
let mut ids = IdGen::new();
|
|
let line = Element::line(&mut ids, 0.0, 0.0, 1000.0, 0.0);
|
|
let strokes = rough::outline(&line, Style::Clean);
|
|
let dotted = dash_rough(&strokes, StrokeStyle::Dotted);
|
|
assert!(dotted.len() >= 50, "dotting a 1000-pt line should produce >= 50 dots");
|
|
}
|
|
|
|
#[test]
|
|
fn dash_does_not_explode_on_hostile_input() {
|
|
let strokes = vec![vec![[0.0, 0.0], [1.0e9, 0.0]]];
|
|
let dashed = dash_rough(&strokes, StrokeStyle::Dashed);
|
|
assert!(dashed.len() <= 1024, "hostile coordinates must not blow the segment cap: {}", dashed.len());
|
|
}
|
|
|
|
#[test]
|
|
fn fill_solid_returns_one_closed_outline() {
|
|
let mut ids = IdGen::new();
|
|
let rect = Element::rectangle(&mut ids, 0.0, 0.0, 100.0, 60.0);
|
|
let lines = fill_polylines(&rect);
|
|
assert_eq!(lines.len(), 1);
|
|
let polyline = &lines[0];
|
|
assert!(polyline.len() >= 4);
|
|
assert_eq!(polyline.first(), polyline.last());
|
|
}
|
|
|
|
#[test]
|
|
fn fill_hachure_spacing_matches_stroke_width_and_is_deterministic() {
|
|
let mut ids = IdGen::new();
|
|
let mut rect = Element::rectangle(&mut ids, 0.0, 0.0, 100.0, 60.0);
|
|
rect.stroke_width = 4.0;
|
|
rect.fill_style = FillStyle::Hachure;
|
|
let lines = fill_polylines(&rect);
|
|
assert!(lines.len() >= 3, "100x60 box should hatch into >= 3 lines, got {}", lines.len());
|
|
for polyline in &lines {
|
|
for point in polyline {
|
|
assert!(point[0].is_finite() && point[1].is_finite(), "non-finite point {point:?}");
|
|
}
|
|
}
|
|
let again = fill_polylines(&rect);
|
|
assert_eq!(bits(&lines), bits(&again), "fill output is bit-identical per element");
|
|
rect.stroke_width = 0.5;
|
|
let thin = fill_polylines(&rect);
|
|
let thick = fill_polylines(&Element { stroke_width: 8.0, ..rect.clone() });
|
|
assert!(thin.len() > thick.len(), "thinner stroke packs more lines: {} vs {}", thin.len(), thick.len());
|
|
}
|
|
|
|
#[test]
|
|
fn fill_cross_hatch_is_more_lines_than_hachure() {
|
|
let mut ids = IdGen::new();
|
|
let mut rect = Element::rectangle(&mut ids, 0.0, 0.0, 100.0, 60.0);
|
|
rect.fill_style = FillStyle::Hachure;
|
|
let hachure_count = fill_polylines(&rect).len();
|
|
rect.fill_style = FillStyle::CrossHatch;
|
|
let cross_count = fill_polylines(&rect).len();
|
|
assert!(cross_count > hachure_count, "cross-hatch must produce more lines than hachure ({cross_count} vs {hachure_count})");
|
|
for polyline in fill_polylines(&rect) {
|
|
for point in polyline {
|
|
assert!(point[0] >= -0.001 && point[0] <= 100.001, "x escaped the box: {point:?}");
|
|
assert!(point[1] >= -0.001 && point[1] <= 60.001, "y escaped the box: {point:?}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn fill_on_unsupported_kind_returns_empty() {
|
|
let mut ids = IdGen::new();
|
|
let line = Element::line(&mut ids, 0.0, 0.0, 100.0, 0.0);
|
|
assert_eq!(fill_polylines(&line), Vec::<Polyline>::new());
|
|
let text = Element::text(&mut ids, 0.0, 0.0, "hi");
|
|
assert_eq!(fill_polylines(&text), Vec::<Polyline>::new());
|
|
}
|