Browse / github.com/storytold/cadcraft / patch
Hatch: AR-CONC as closed stones among scattered dots
Hatch: AR-CONC as closed stones among scattered dots The library's AR-CONC drew loose, open strokes, so a concrete hatch did not read as concrete on screen or on paper. This redraws it as CADCraft's own pattern: per 540 × 450 mm tile, 20 small closed triangles (sides 12–34 mm, angles of 35° or more) and 70 dots, at the density a concrete hatch usually has on architectural drawings (one stone per ~12,000 mm², one dot per ~3,500 mm² in an ISO drawing at scale 1). - **Stays closed at any size.** Every line family repeats on one lattice, (540 mm, 0) and (190 mm, 450 mm), and each stone side runs along a lattice direction, so a stone's three sides
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Tangpixel2296 (imported from pull request #2020, open)
- Base
d3ae8a6af0b76185e33f6dc2735679fa5c2be77c- Size
- +176 −20 in 4 files
- Review
- pending
- Shared
- 2026-10-28
- Id
fa5680ce66a943e52c7e8282
How it went
No other operator has reported using it yet.
Diff
raw · crates/doc/src/library.rs crates/render/src/hatch.rs crates/render/src/tests.rs crates/render/tests/it/hatch_fill.rs
@@ -205,21 +205,119 @@ pub fn standard_patterns() -> Vec<HatchPattern> { description: "Staircase effect", lines: vec![pl(0.0, (0.0, 0.0), (0.125, 0.125), &[0.125, -0.125]), pl(90.0, (0.125, 0.0), (0.125, 0.125), &[0.125, -0.125])], },- // Drawn at the scale of the pattern CAD programs ship under this name (about 16x- // coarser than first drawn here: AR-CONC hatches came out hundreds of times too dense).- scaled_pattern(- HatchPattern {- name: "AR-CONC",- description: "Random dot and stone pattern",- lines: vec![- pl(50.0, (0.0, 0.0), (0.4, 0.25), &[0.15, -0.6]),- pl(355.0, (0.0, 0.0), (0.27, 0.3), &[0.12, -0.5]),- pl(100.0, (0.12, 0.05), (0.35, 0.33), &[0.1, -0.55]),- pl(0.0, (0.07, 0.02), (0.21, 0.17), &[0.0, -0.4]),- ],- },- 16.0,- ),+ // CADCraft's own drawing (2026-10-18): small closed stones (triangles of 12-34 mm sides,+ // angles 35° or more) among scattered dots, at the density a concrete hatch usually has+ // on architectural drawings (one stone per ~12 000 mm², a dot per ~3 500 mm² in an ISO+ // drawing at scale 1).+ // No line is taken from any vendor's pattern file. Every family repeats on one lattice+ // (540 mm, 0) / (190 mm, 450 mm), each stone side along a lattice direction, so the three+ // sides of a stone repeat together and it stays closed. 20 stones and 70 dots per tile.+ HatchPattern {+ name: "AR-CONC",+ description: "Random dot and stone pattern",+ lines: vec![+ pl(0.0, (20.481011, 9.635334), (7.480315, 17.716535), &[1.18168, -20.078162]),+ pl(247.109448, (21.662691, 9.635334), (10.9615, 19.585621), &[1.068911, -18.162073]),+ pl(307.874984, (20.481011, 9.635334), (13.052281, 16.781504), &[1.247521, -21.196877]),+ pl(247.109448, (19.07861, 2.645564), (10.9615, 19.585621), &[0.619114, -18.61187]),+ pl(127.874984, (18.837792, 2.075206), (13.052281, 16.781504), &[0.722565, -21.721833]),+ pl(180.0, (19.07861, 2.645564), (7.480315, 17.716535), &[0.68443, -20.575412]),+ pl(67.109448, (10.457133, 9.277081), (10.9615, 19.585621), &[0.877543, -18.35344]),+ pl(333.178019, (10.798473, 10.085518), (18.972555, 9.592865), &[0.597223, -38.666413]),+ pl(31.651273, (10.457133, 9.277081), (15.66439, 11.156058), &[1.027079, -32.734915]),+ pl(31.651273, (3.050302, 8.149389), (15.66439, 11.156058), &[0.689582, -33.072412]),+ pl(153.178019, (3.637314, 8.511246), (18.972555, 9.592865), &[0.801952, -38.461683]),+ pl(100.080598, (3.050302, 8.149389), (19.854913, 10.465822), &[0.735061, -35.253584]),+ pl(211.651273, (16.536125, 5.170862), (15.66439, 11.156058), &[1.220917, -32.541077]),+ pl(333.178019, (15.49681, 4.530188), (18.972555, 9.592865), &[0.709935, -38.5537]),+ pl(247.109448, (16.536125, 5.170862), (10.9615, 19.585621), &[1.04316, -18.187824]),+ pl(307.874984, (6.927619, 10.461012), (13.052281, 16.781504), &[0.706048, -21.738351]),+ pl(211.651273, (7.36109, 9.903692), (15.66439, 11.156058), &[1.062072, -32.699922]),+ pl(247.109448, (6.927619, 10.461012), (10.9615, 19.585621), &[1.209922, -18.021062]),+ pl(31.651273, (15.592914, 15.996194), (15.66439, 11.156058), &[0.621023, -33.140971]),+ pl(153.178019, (16.121565, 16.322074), (18.972555, 9.592865), &[0.722221, -38.541414]),+ pl(100.080598, (15.592914, 15.996194), (19.854913, 10.465822), &[0.66198, -35.326665]),+ pl(280.080598, (9.685289, 17.107032), (19.854913, 10.465822), &[0.912763, -35.075882]),+ pl(153.178019, (9.845053, 16.208359), (18.972555, 9.592865), &[0.995825, -38.267811]),+ pl(211.651273, (9.685289, 17.107032), (15.66439, 11.156058), &[0.85629, -32.905705]),+ pl(153.178019, (7.584278, 13.166973), (18.972555, 9.592865), &[0.644726, -38.61891]),+ pl(280.080598, (7.008916, 13.457886), (19.854913, 10.465822), &[0.590949, -35.397696]),+ pl(211.651273, (7.584278, 13.166973), (15.66439, 11.156058), &[0.554386, -33.207608]),+ pl(127.874984, (5.550531, 4.253355), (13.052281, 16.781504), &[1.03043, -21.413968]),+ pl(247.109448, (4.917908, 5.066727), (10.9615, 19.585621), &[0.882901, -18.348083]),+ pl(180.0, (5.550531, 4.253355), (7.480315, 17.716535), &[0.976047, -20.283796]),+ pl(247.109448, (9.798774, 2.144405), (10.9615, 19.585621), &[1.238914, -17.99207]),+ pl(31.651273, (9.316871, 1.003056), (15.66439, 11.156058), &[1.087522, -32.674473]),+ pl(307.874984, (9.798774, 2.144405), (13.052281, 16.781504), &[0.722966, -21.721432]),+ pl(31.651273, (19.772647, 4.666939), (15.66439, 11.156058), &[1.1044, -32.657595]),+ pl(127.874984, (20.712776, 5.24647), (13.052281, 16.781504), &[0.734186, -21.710212]),+ pl(67.109448, (19.772647, 4.666939), (10.9615, 19.585621), &[1.258142, -17.972842]),+ pl(333.178019, (15.27065, 10.074253), (18.972555, 9.592865), &[0.802463, -38.461173]),+ pl(211.651273, (15.986778, 9.712166), (15.66439, 11.156058), &[0.690021, -33.071973]),+ pl(280.080598, (15.27065, 10.074253), (19.854913, 10.465822), &[0.735529, -35.253116]),+ pl(31.651273, (0.612588, 17.042707), (15.66439, 11.156058), &[0.933964, -32.82803]),+ pl(153.178019, (1.407632, 17.532803), (18.972555, 9.592865), &[1.086157, -38.177478]),+ pl(100.080598, (0.612588, 17.042707), (19.854913, 10.465822), &[0.995561, -34.993085]),+ pl(307.874984, (8.960817, 5.655419), (13.052281, 16.781504), &[1.057289, -21.387109]),+ pl(67.109448, (9.60993, 4.820846), (10.9615, 19.585621), &[0.905914, -18.325069]),+ pl(0.0, (8.960817, 5.655419), (7.480315, 17.716535), &[1.001488, -20.258355]),+ pl(0.0, (19.206725, 14.739017), (7.480315, 17.716535), &[0.655568, -20.604274]),+ pl(127.874984, (19.862293, 14.739017), (13.052281, 16.781504), &[0.692095, -21.752303]),+ pl(67.109448, (19.206725, 14.739017), (10.9615, 19.585621), &[0.593007, -18.637977]),+ pl(211.651273, (13.119025, 15.340647), (15.66439, 11.156058), &[1.167, -32.594994]),+ pl(307.874984, (12.125607, 14.728266), (13.052281, 16.781504), &[0.775802, -21.668596]),+ pl(247.109448, (13.119025, 15.340647), (10.9615, 19.585621), &[1.329457, -17.901526]),+ pl(31.651273, (3.773733, 12.39632), (15.66439, 11.156058), &[0.651956, -33.110039]),+ pl(153.178019, (4.328715, 12.738433), (18.972555, 9.592865), &[0.758194, -38.505441]),+ pl(100.080598, (3.773733, 12.39632), (19.854913, 10.465822), &[0.694953, -35.293692]),+ pl(31.651273, (16.206885, 12.356449), (15.66439, 11.156058), &[1.083079, -32.678915]),+ pl(153.178019, (17.128864, 12.924792), (18.972555, 9.592865), &[1.259571, -38.004065]),+ pl(100.080598, (16.206885, 12.356449), (19.854913, 10.465822), &[1.15451, -34.834135]),+ pl(67.109448, (12.849427, 5.722968), (10.9615, 19.585621), &[0.853249, -18.377735]),+ pl(211.651273, (13.181317, 6.509024), (15.66439, 11.156058), &[0.998645, -32.763349]),+ pl(153.178019, (12.849427, 5.722968), (18.972555, 9.592865), &[0.580689, -38.682947]),+ pl(+ 19.510799,+ (1.403172, 0.497187),+ (33.006905, 7.100458),+ &[+ 0.0, -2.71837, 0.0, -2.122911, 0.0, -2.390294, 0.0, -3.002194, 0.0, -1.894831, 0.0, -4.06954, 0.0, -1.559613, 0.0, -2.89308,+ 0.0, -2.040845, 0.0, -3.324317, 0.0, -5.628361, 0.0, -2.660667, 0.0, -2.096617, 0.0, -7.476064, 0.0, -3.29102, 0.0,+ -2.316796, 0.0, -1.70561, 0.0, -1.854847,+ ],+ ),+ pl(+ 144.027613,+ (-6.861953, 4.98045),+ (38.763978, 6.243966),+ &[+ 0.0, -4.052168, 0.0, -1.36175, 0.0, -9.967512, 0.0, -1.280637, 0.0, -4.170196, 0.0, -1.41432, 0.0, -2.367455, 0.0, -1.486285,+ 0.0, -4.62738, 0.0, -4.002436, 0.0, -2.663116, 0.0, -1.934293, 0.0, -1.991448, 0.0, -2.637094, 0.0, -1.58119, 0.0, -2.780019,+ 0.0, -2.049279, 0.0, -9.955783,+ ],+ ),+ pl(+ 50.572198,+ (2.533777, 3.081621),+ (18.435492, 5.473881),+ &[+ 0.0, -3.332314, 0.0, -2.666002, 0.0, -3.562468, 0.0, -4.494214, 0.0, -5.221485, 0.0, -3.224885, 0.0, -3.111831, 0.0,+ -3.642425, 0.0, -7.151893, 0.0, -3.462364, 0.0, -3.165007, 0.0, -3.544732, 0.0, -1.308374, 0.0, -2.351488, 0.0, -5.583112,+ 0.0, -5.037525, 0.0, -7.948599,+ ],+ ),+ pl(+ 162.531977,+ (-3.708719, 1.167079),+ (20.279437, 6.381641),+ &[+ 0.0, -1.113427, 0.0, -2.237281, 0.0, -1.527796, 0.0, -6.108525, 0.0, -6.203357, 0.0, -4.227832, 0.0, -1.593644, 0.0,+ -4.181987, 0.0, -3.165928, 0.0, -2.379037, 0.0, -1.551491, 0.0, -6.066391, 0.0, -2.645402, 0.0, -4.557707, 0.0, -2.419574,+ 0.0, -1.646997, 0.0, -7.394611,+ ],+ ),+ ],+ }, // Drawn at the scale of the pattern CAD programs ship under this name (about 16x // coarser than first drawn here: AR-CONC hatches came out hundreds of times too dense). scaled_pattern(@@ -415,18 +415,24 @@ pub(crate) fn pattern_lines_until( const SWATCH_REPEATS: f64 = 6.0; /// Most runs a swatch holds. const MAX_SWATCH_RUNS: usize = 4000;+/// Runs a swatch over [`MAX_SWATCH_RUNS`] zooms in to: about one tile of AR-CONC, its stones a+/// few points across in a picker's swatch.+const ZOOMED_SWATCH_RUNS: usize = 125;+/// Most times a swatch zooms in (twice the scale each time).+const SWATCH_ZOOMS: usize = 6; /// A preview of a pattern for a pattern picker: the runs of its line families `lines` inside the /// `side` × `side` square with a corner at the origin, at a scale that shows a few repeats of the-/// pattern (each family's line spacing or dash repeat, whichever is longer). Empty for a pattern-/// with no lines (SOLID).+/// pattern (each family's line spacing or dash repeat, whichever is longer). A swatch that would+/// hold more than [`MAX_SWATCH_RUNS`] runs zooms in to [`ZOOMED_SWATCH_RUNS`] or fewer, so every+/// family shows at a readable size. Empty for a pattern with no lines (SOLID). pub fn pattern_swatch(lines: &[PatternLine], side: f64) -> Vec<Vec<Vec2>> { let repeat = lines.iter().map(|l| l.delta.1.abs().max(l.dashes.iter().map(|d| d.abs()).sum::<f64>())).filter(|r| r.is_finite()).fold(0.0, f64::max); if !side.is_finite() || side <= 0.0 || repeat < 1e-9 { return Vec::new(); }- let h = Hatch {+ let mut h = Hatch { pattern: String::new(), solid: false, loops: Vec::new(),@@ -440,8 +446,18 @@ pub fn pattern_swatch(lines: &[PatternLine], side: f64) -> Vec<Vec<Vec2>> { background: None, pattern_lines: lines.to_vec(), };- let square = vec![Vec2::new(0.0, 0.0), Vec2::new(side, 0.0), Vec2::new(side, side), Vec2::new(0.0, side)];- let mut runs = pattern_lines(&Drawing::default(), &h, &[square], 0.0, None);+ let square = [vec![Vec2::new(0.0, 0.0), Vec2::new(side, 0.0), Vec2::new(side, side), Vec2::new(0.0, side)]];+ let mut runs = pattern_lines(&Drawing::default(), &h, &square, 0.0, None);+ // A pattern whose dash repeats are mostly gaps (AR-CONC) spans many tiles at that scale: cut+ // at the cap, its last families would not show at all, and its marks would be specks.+ let target = if runs.len() > MAX_SWATCH_RUNS { ZOOMED_SWATCH_RUNS } else { MAX_SWATCH_RUNS };+ for _ in 0..SWATCH_ZOOMS {+ if runs.len() <= target {+ break;+ }+ h.scale *= 2.0;+ runs = pattern_lines(&Drawing::default(), &h, &square, 0.0, None);+ } runs.truncate(MAX_SWATCH_RUNS); runs }@@ -511,6 +511,35 @@ fn scattered_dot_pattern_reads_solid_once_dots_fill_the_pixels() { assert!(!crate::hatch::reads_solid(&cadcraft_doc::Drawing::new_imperial(), &h, 0.125)); } +/// AR-CONC's stones are closed triangles wherever they repeat: away from the boundary every side's+/// two ends meet the ends of the stone's other two sides. Its dots stand alone.+#[test]+fn ar_conc_stones_are_closed_triangles() {+ let d = cadcraft_doc::Drawing::new_imperial();+ let h = pattern_hatch("AR-CONC", 1.0, (0.0, 0.0, 60.0, 60.0));+ let runs = crate::hatch::pattern_lines(&d, &h, &loops_of(&h), 0.0, None);+ let inside = |p: Vec2| p.x > 2.0 && p.x < 58.0 && p.y > 2.0 && p.y < 58.0;+ let sides: Vec<[Vec2; 2]> = runs+ .iter()+ .filter_map(|r| match r.as_slice() {+ [a, b] if inside(*a) && inside(*b) => Some([*a, *b]),+ _ => None,+ })+ .collect();+ let dots = runs.iter().filter(|r| r.len() == 1).count();+ // 56² in² is about 8 tiles of 20 stones and 70 dots.+ assert!(sides.len() > 3 * 100, "{} sides", sides.len());+ assert!(dots > 400, "{dots} dots");+ // A stone's sides are under 1.4 (34 mm): those 4 from the boundary have both neighbours.+ let deep = |p: &Vec2| p.x > 4.0 && p.x < 56.0 && p.y > 4.0 && p.y < 56.0;+ for (i, s) in sides.iter().enumerate().filter(|(_, s)| s.iter().all(deep)) {+ for end in s {+ let meets = sides.iter().enumerate().filter(|(j, o)| *j != i && o.iter().any(|q| q.near(*end, 1e-4))).count();+ assert_eq!(meets, 1, "side {i} {s:?}: the end {end:?} meets {meets} other sides");+ }+ }+}+ /// Same polylines up to float noise: the view-limited path rotates into the family frame and back, /// so a start point can differ by ~1e-16 between platforms (#804). Structure must match exactly. fn assert_lines_close(a: &[Vec<Vec2>], b: &[Vec<Vec2>], what: &str) {@@ -182,6 +182,19 @@ fn pattern_swatches_stay_in_their_square_and_show_a_few_repeats() { assert!(cadcraft_render::pattern_swatch(&[], 1.0).is_empty()); } +#[test]+fn a_swatch_mostly_made_of_gaps_shows_every_family() {+ // AR-CONC's dash repeats are mostly gaps: at the usual scale its swatch would pass the cap and+ // lose its last families (the dots), leaving open stones.+ let Some(p) = cadcraft_doc::library::pattern("AR-CONC") else { panic!("AR-CONC") };+ let runs = cadcraft_render::pattern_swatch(&p.lines, 1.0);+ // Zoomed in to about one tile, so the stones read at a swatch's size.+ assert!(runs.len() <= 125, "{} runs", runs.len());+ assert!(runs.iter().any(|r| r.len() == 1), "no dots");+ // Zoomed in only that far: stones (three sides each) still show.+ assert!(runs.iter().filter(|r| r.len() == 2).count() >= 15, "too few stone sides");+}+ #[test] fn brick_joints_are_drawn_once_half_a_brick_apart_course_to_course() { let l = draw(hatch("BRICK", vec![square(0.0, 2.0)]), "0");