Compare commits

..

1 Commits

Author SHA1 Message Date
Brennen Herbruck c3576489c8 CI: build without sccache when its download fails
Since about 4:30 AM ET GitHub answers 504 for sccache's x86_64 release
asset, so every job that sets up the compiler cache fails before it builds,
including merge-queue runs. The setup step may now fail; the job then clears
RUSTC_WRAPPER and builds uncached, and the stats steps skip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 05:22:35 -05:00
13 changed files with 102 additions and 94 deletions
+33 -3
View File
@@ -505,9 +505,15 @@ jobs:
key: native-tests
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Set up compiler cache
id: sccache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
version: v0.16.0
# A failed download of sccache (GitHub release outages answer 504) must not fail the build.
continue-on-error: true
- name: Build without the compiler cache if it could not be set up
if: steps.sccache.outcome == 'failure'
run: echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
- uses: taiki-e/install-action@1168f6810a3cb30a3514bf5756a3b7f0422c6df8 # v2.87.28
with:
tool: cargo-nextest
@@ -528,7 +534,7 @@ jobs:
compression-level: 0
- name: Compiler cache statistics
if: always()
run: sccache --show-stats
run: if command -v sccache >/dev/null; then sccache --show-stats; else echo "sccache was not set up; built without it"; fi
tests:
name: Native Linux tests
@@ -576,9 +582,15 @@ jobs:
key: native-tests
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Set up compiler cache
id: sccache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
version: v0.16.0
# A failed download of sccache (GitHub release outages answer 504) must not fail the build.
continue-on-error: true
- name: Build without the compiler cache if it could not be set up
if: steps.sccache.outcome == 'failure'
run: echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
- uses: taiki-e/install-action@1168f6810a3cb30a3514bf5756a3b7f0422c6df8 # v2.87.28
with:
tool: cargo-nextest
@@ -619,7 +631,7 @@ jobs:
cargo xtask known-failing --junit "$junit"
- name: Compiler cache statistics
if: always()
run: sccache --show-stats
run: if command -v sccache >/dev/null; then sccache --show-stats; else echo "sccache was not set up; built without it"; fi
- name: Upload test timings
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
@@ -692,9 +704,15 @@ jobs:
key: native-gpu
save-if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && matrix.shard == 1 }}
- name: Set up compiler cache
id: sccache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
version: v0.16.0
# A failed download of sccache (GitHub release outages answer 504) must not fail the build.
continue-on-error: true
- name: Build without the compiler cache if it could not be set up
if: steps.sccache.outcome == 'failure'
run: echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
- uses: taiki-e/install-action@1168f6810a3cb30a3514bf5756a3b7f0422c6df8 # v2.87.28
with:
tool: cargo-nextest
@@ -715,7 +733,7 @@ jobs:
cargo xtask known-failing --junit "$junit"
- name: Compiler cache statistics
if: always()
run: sccache --show-stats
run: if command -v sccache >/dev/null; then sccache --show-stats; else echo "sccache was not set up; built without it"; fi
- name: Upload test timings
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
@@ -848,9 +866,15 @@ jobs:
key: doc-tests
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Set up compiler cache
id: sccache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
version: v0.16.0
# A failed download of sccache (GitHub release outages answer 504) must not fail the build.
continue-on-error: true
- name: Build without the compiler cache if it could not be set up
if: steps.sccache.outcome == 'failure'
run: echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
# nextest does not run doc tests.
- name: Doc tests
env:
@@ -906,9 +930,15 @@ jobs:
key: oracle
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Set up compiler cache
id: sccache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
version: v0.16.0
# A failed download of sccache (GitHub release outages answer 504) must not fail the build.
continue-on-error: true
- name: Build without the compiler cache if it could not be set up
if: steps.sccache.outcome == 'failure'
run: echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
- name: Build the command line
run: cargo build -p solidcraft-cli --profile ci --locked
- name: Replay tests/oracle and compare with expected.json
+6
View File
@@ -85,9 +85,15 @@ jobs:
key: fillet-rig
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
- name: Set up compiler cache
id: sccache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
version: v0.16.0
# A failed download of sccache (GitHub release outages answer 504) must not fail the build.
continue-on-error: true
- name: Build without the compiler cache if it could not be set up
if: steps.sccache.outcome == 'failure'
run: echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
- name: Build the CLI (release)
run: cargo build --release --locked -p solidcraft-cli
- name: Run the fillet cases against the known-failing list
+7 -1
View File
@@ -149,9 +149,15 @@ jobs:
key: kernel-rig
save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') }}
- name: Set up compiler cache
id: sccache
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
version: v0.16.0
# A failed download of sccache (GitHub release outages answer 504) must not fail the build.
continue-on-error: true
- name: Build without the compiler cache if it could not be set up
if: steps.sccache.outcome == 'failure'
run: echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
- name: Build the CLI (release) and the rig's runner
run: |
cargo build --release --locked -p solidcraft-cli
@@ -174,7 +180,7 @@ jobs:
retention-days: 1
- name: Compiler cache statistics
if: always()
run: sccache --show-stats
run: if command -v sccache >/dev/null; then sccache --show-stats; else echo "sccache was not set up; built without it"; fi
shard:
name: Kernel rig (${{ matrix.shard }})
+1 -6
View File
@@ -176,12 +176,7 @@ mod tests {
let sag = mesh
.triangles
.iter()
.zip(&mesh.tri_face)
// The side wall's own triangles: the flat caps have corners on the rim too.
.filter(|(_, f)| {
matches!(solidcraft_kernel::face_surface(&mb.body, **f as usize), Some(solidcraft_kernel::FaceSurface::Cylinder { .. }))
})
.map(|(t, _)| t.map(|i| mesh.positions[i as usize]))
.map(|t| t.map(|i| mesh.positions[i as usize]))
.filter(|t| t.iter().all(|p| (r(*p) - 20.0).abs() < 1e-9))
.map(|[a, b, c]| 20.0 - r((a + b + c) * (1.0 / 3.0)))
.fold(0.0, f64::max);
+4 -3
View File
@@ -1394,11 +1394,12 @@ mod tests {
assert!(bytes.starts_with(signature), "{cmd}");
}
// The triangles `file.export` writes (the 80-byte header holds the file's name), 84 + 50
// bytes each. A planar face is exact for any triangulation of its outline, so the mesher
// gives each of the box's six faces two triangles (kernel test `a_box_is_two_triangles_a_face`).
// bytes each. The mesher lays each planar face out as a regular grid whose sides are at
// most a fifth of the body's diagonal (kernel test `a_box_is_a_regular_grid_on_every_face`):
// 22.9 / 5 = 4.58 mm, so 5 × 3 × 2 cells along 20 × 10 × 5, two triangles each.
s.execute("file.export", &json!({"path": at("box.stl")})).unwrap();
assert_eq!(std::fs::read(at("StlBinary.txt")).unwrap()[80..], std::fs::read(at("box.stl")).unwrap()[80..]);
let triangles = 6 * 2;
let triangles = 2 * 2 * (5 * 3 + 5 * 2 + 3 * 2);
let stl = std::fs::read(at("box.stl")).unwrap();
assert_eq!(u32::from_le_bytes([stl[80], stl[81], stl[82], stl[83]]), u32::try_from(triangles).unwrap());
assert_eq!(stl.len(), 84 + 50 * triangles);
+7 -4
View File
@@ -1185,8 +1185,11 @@ mod tests {
}
/// OBJ shares the vertices faces meet at and keeps the body name as written, spaces included.
/// The 10 × 20 × 30 box meshes as two triangles a face (a flat face needs no interior
/// nodes), 12 triangles on its 8 corners (unwelded, 36), and one normal per face.
/// The 10 × 20 × 30 box meshes as the planner's grids, no side longer than a fifth of its
/// extent (the 37.4 mm diagonal): its edges in 2, 3 and 5 pieces, so its faces are grids of
/// 2 × 3, 2 × 5 and 3 × 5 cells, 2 · (6 + 10 + 15) · 2 = 124 triangles. Welded, their
/// corners are the 3 · 4 · 6 = 72 nodes of the box's lattice less the 1 · 2 · 4 inside it: 64
/// positions (unwelded, 372), and one normal per face.
#[test]
fn obj_export_is_welded_and_keeps_names() {
let (_, m) = model();
@@ -1197,10 +1200,10 @@ mod tests {
let o = String::from_utf8(export(&st, &[], Format::Obj, "t").unwrap()).unwrap();
let count = |p: &str| o.lines().filter(|l| l.starts_with(p)).count();
assert!(o.contains("\no Red Block\n"), "{o}");
assert_eq!((count("v "), count("vn "), count("f ")), (8, 6, 12), "{o}");
assert_eq!((count("v "), count("vn "), count("f ")), (64, 6, 124), "{o}");
let ids: Vec<usize> =
o.lines().filter(|l| l.starts_with("f ")).flat_map(|l| l.split([' ', '/']).skip(1)).filter_map(|x| x.parse().ok()).collect();
assert!(ids.iter().all(|i| (1..=8).contains(i)));
assert!(ids.iter().all(|i| (1..=64).contains(i)));
}
/// The OBJ export carries the colours of the bodies: the mesh file names the material
+7 -13
View File
@@ -179,10 +179,7 @@ pub(crate) fn plan(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result
.into_iter()
.collect::<Result<_>>()?;
let face_h = |f: usize| sizes.get(f).map_or(rules.max_len, |(h, _)| *h);
// A flat face asks for no size (infinite): an edge between flat faces is whole, one beside
// a curved face takes the curved face's size, so both meet at the same nodes.
let edge_h: Vec<f64> =
edge_faces.iter().map(|fs| if fs.is_empty() { rules.max_len } else { fs.iter().map(|&f| face_h(f)).fold(f64::INFINITY, f64::min) }).collect();
let edge_h: Vec<f64> = edge_faces.iter().map(|fs| fs.iter().map(|&f| face_h(f)).fold(rules.max_len, f64::min)).collect();
let jobs: Vec<(usize, f64)> = edge_h.into_iter().enumerate().collect();
let solved = b.edges.iter().filter(|e| matches!(e.curve, Curve::SurfaceIntersection { .. })).count();
let mut nodes: Vec<edges::Nodes> = crate::par::map("edge nodes", &jobs, solved >= PARALLEL_CURVES || jobs.len() >= PARALLEL_EDGES, |(e, h)| {
@@ -1030,18 +1027,15 @@ fn plan_face(
// Constrained Delaunay in (u, v) scaled to millimetres: equilateral seeds of side `h`,
// refined where triangles leave the surface, are too long or bend too much, then shaped by
// Ruppert refinement.
let planar = matches!(s, EvalSurface::Plane { .. });
let bare = planar && h.is_infinite();
let polys: Vec<&[[f64; 2]]> = ranges.iter().filter_map(|r| uvs.get(r.clone())).collect();
let step = [h / speed[0], h / speed[1]];
let delaunay = || {
// A flat face with no size asked of it: the boundary nodes alone, no interior seeds.
let seeds = if bare { Vec::new() } else { cdt::lattice(bbox, step, &polys, speed) };
let seeds = cdt::lattice(bbox, step, &polys, speed);
let eval = |u: f64, v: f64| s.derivs(u, v).map(|(p, su, sv)| (p, su.cross(sv).normalized()));
let sag = cdt::Sag { eval: &eval, tol, h, turn: rules.turn, scale: speed };
let fill = cdt::Fill {
seeds: &seeds,
refine: if bare { None } else { Some((sag, MAX_REFINE)) },
refine: Some((sag, MAX_REFINE)),
shape: !matches!(s, EvalSurface::Plane { .. }),
lenient: lenient == Lenient::Yes,
};
@@ -1049,6 +1043,7 @@ fn plan_face(
};
// Planar and curved faces alike: a structured grid where the boundary runs along the
// parameter box (a rectangle, a cylinder half), a Coons grid on four corners, else Delaunay.
let planar = matches!(s, EvalSurface::Plane { .. });
// The lenient fill is the constrained Delaunay alone: the caller names the face in the stats.
let (extra, triangles) = if lenient == Lenient::Yes {
delaunay()?
@@ -1056,7 +1051,7 @@ fn plan_face(
let mut us = uv::divide(s, 0, bbox[0], bbox[1], rules, h, MAX_LINES);
let mut vs = uv::divide(s, 1, bbox[1], bbox[0], rules, h, MAX_LINES);
grid::thin(&mut us, &mut vs, grid::MAX_FACE_GRID);
let structured = match (ranges.len(), if seamed || bare { None } else { grid::quad(&uvs) }) {
let structured = match (ranges.len(), if seamed { None } else { grid::quad(&uvs) }) {
(1, Some(q)) => {
// Interior lines: the boundary's own where opposite sides agree (refined where the
// surface turns more between them inside the face), else the division.
@@ -1081,8 +1076,7 @@ fn plan_face(
_ => None,
};
let structured = match structured {
// A flat face with no size asked of it is its boundary alone (the Delaunay below).
None if ranges.len() == 1 && !seamed && !bare => {
None if ranges.len() == 1 && !seamed => {
let fine = |a: [f64; 2], b: [f64; 2]| uv::fine_between(s, rules, h, a, b);
coons::coons(&uvs, &fixed, speed, grid::MAX_FACE_GRID, h, Some(&fine))
}
@@ -1114,7 +1108,7 @@ fn plan_face(
let trimmed = if seamed || strip {
None
} else if planar {
if ranges.len() == 1 && h.is_finite() { plan_pieces(&face) } else { None }
if ranges.len() == 1 { plan_pieces(&face) } else { None }
} else {
plan_pieces(&face).or_else(|| {
let traces = motorcycle::traces(&uvs, &ranges, speed, 0.25 * h);
+7 -12
View File
@@ -1,9 +1,7 @@
//! Target triangle size: one side length per face, so triangles come out about equilateral
//! (well-shaped triangles shade evenly) and as small as the surface needs.
//!
//! A flat face (a plane, or a spline lying in one) has no size: any triangulation of its
//! boundary is exact, so it gets no interior cells and its straight edges no extra nodes.
//! A curved face's size is the smallest of the body's longest side, the length over which the surface
//! A face's size is the smallest of the body's longest side, the length over which the surface
//! normal turns the caller's facet angle (when one is given, see [`Turn`]), and the chord that
//! sags the tolerance; an edge takes the smaller size of its two faces, so a curved face and
//! its flat neighbour meet at the same spacing.
@@ -109,12 +107,11 @@ pub(crate) fn extent(points: impl Iterator<Item = Vec3>) -> f64 {
/// so the size is never larger than the face needs).
pub(crate) fn face_size(s: &EvalSurface, dom: &Domain, rules: &Rules) -> (f64, [f64; 2]) {
if let EvalSurface::Plane { du, dv, .. } = s {
return (f64::INFINITY, [du.len(), dv.len()]);
return (rules.max_len, [du.len(), dv.len()]);
}
let (Some(ru), Some(rv)) = (dom.range[0], dom.range[1]) else { return (rules.max_len, [1.0, 1.0]) };
let flat = planar(s);
// A flat spline (sheet-metal walls, chamfers): no curvature to look for, only the speeds.
let samples = if flat { 3 } else { SAMPLES };
let samples = if planar(s) { 3 } else { SAMPLES };
let normal = |u: f64, v: f64| -> Option<(Vec3, Vec3, [f64; 2])> {
let (p, su, sv) = s.derivs(u, v)?;
Some((p, su.cross(sv).normalized()?, [su.len(), sv.len()]))
@@ -144,9 +141,7 @@ pub(crate) fn face_size(s: &EvalSurface, dom: &Domain, rules: &Rules) -> (f64, [
}
}
let speed = if count > 0 { [speed[0] / count as f64, speed[1] / count as f64] } else { [1.0, 1.0] };
// A flat face is exact for any triangulation of its boundary: no length cap.
let size = if flat { f64::INFINITY } else { rules.for_curvature(k_max) };
(size, speed.map(|x| if x.is_finite() && x > 0.0 { x } else { 1.0 }))
(rules.for_curvature(k_max), speed.map(|x| if x.is_finite() && x > 0.0 { x } else { 1.0 }))
}
/// A spline surface whose control points lie in one plane, with positive weights: the
@@ -216,7 +211,7 @@ mod tests {
assert!(!planar(&patch(0.5)));
let rules = Rules::new(0.01, 100.0, THIRTY);
let (h, speed) = face_size(&patch(0.0), &super::super::uv::domain(&patch(0.0)), &rules);
assert_eq!(h, f64::INFINITY);
assert_eq!(h, rules.max_len);
assert!((speed[0] - 4.0).abs() < 1e-9 && (speed[1] - 3.0).abs() < 1e-9, "{speed:?}");
// The twisted patch (a hyperbolic paraboloid) has curvature: a smaller side.
assert!(face_size(&patch(0.5), &super::super::uv::domain(&patch(0.5)), &rules).0 < rules.max_len);
@@ -237,9 +232,9 @@ mod tests {
}
#[test]
fn a_plane_has_no_size_cap() {
fn a_plane_takes_the_longest_side() {
let s = EvalSurface::Plane { origin: Vec3::ZERO, du: Vec3::X, dv: Vec3::Y };
let rules = Rules::new(0.01, 100.0, THIRTY);
assert_eq!(face_size(&s, &super::super::uv::domain(&s), &rules).0, f64::INFINITY);
assert_eq!(face_size(&s, &super::super::uv::domain(&s), &rules).0, 20.0);
}
}
+21 -36
View File
@@ -69,46 +69,31 @@ fn cut(a: &Body, b: &Body) -> Body {
const PI: f64 = std::f64::consts::PI;
/// A flat face is exact for any triangulation of its boundary, so a box is two triangles a
/// face whatever its size (it was a grid capped at a fifth of the body's diagonal: 124
/// triangles for 20 x 10 x 5).
#[test]
fn a_box_is_two_triangles_a_face() {
for (name, size) in [("20 x 10 x 5", Vec3::new(20.0, 10.0, 5.0)), ("40 x 30 x 20", Vec3::new(40.0, 30.0, 20.0))] {
let b = ok(crate::box_solid(Vec3::ZERO, size));
let (m, s) = plan_body(&b, 0.05);
assert_eq!(s.planar, 6);
assert_watertight(&m, name);
assert_eq!(m.triangles.len(), 12, "{name}: {:?}", (0..6).map(|i| m.tri_face.iter().filter(|t| **t == i).count()).collect::<Vec<_>>());
assert!(max_chord(&b, &m) <= 1e-9, "{name}: flat faces are exact");
/// Smallest corner angle (degrees) over the mesh.
fn min_angle(m: &Mesh) -> f64 {
let mut least = 180.0f64;
for t in &m.triangles {
let [a, b, c] = t.map(|i| m.positions[i as usize]);
for (p, q, r) in [(a, b, c), (b, c, a), (c, a, b)] {
let (u, v) = (q - p, r - p);
if let (Some(u), Some(v)) = (u.normalized(), v.normalized()) {
least = least.min(u.dot(v).clamp(-1.0, 1.0).acos().to_degrees());
}
}
}
least
}
/// Flat faces beside curved ones stay joined: the straight edge between a plane and a fillet
/// takes the fillet's spacing, and the plane's boundary uses those nodes.
#[test]
fn flat_faces_beside_curved_ones_stay_watertight_and_within_the_chord() {
let tol = 0.01;
let bx = ok(crate::box_solid(Vec3::ZERO, Vec3::new(20.0, 10.0, 5.0)));
let rounded =
ok(crate::fillet(&bx, &[Vec3::new(10.0, 0.0, 5.0), Vec3::new(20.0, 5.0, 5.0), Vec3::new(10.0, 10.0, 5.0), Vec3::new(0.0, 5.0, 5.0)], 1.5));
let pin = ok(crate::cylinder(Vec3::new(10.0, 5.0, -1.0), Vec3::Z, 2.0, 7.0));
let holed = cut(&bx, &pin);
for (name, body) in [("filleted box", &rounded), ("box with a hole", &holed)] {
let m = closed_display_mesh(name, body, tol);
let chord = max_chord(body, &m);
assert!(chord <= tol * 1.05, "{name}: chord {chord}");
// The flat faces carry no interior nodes: far fewer triangles than a grid at the
// curved neighbours' spacing would give.
let (flat, curved) = (0..body.face_count()).fold((0, 0), |(f, c), i| {
let n = m.tri_face.iter().filter(|t| **t as usize == i).count();
match crate::face_surface(body, i) {
Some(crate::FaceSurface::Plane { .. }) => (f + n, c),
_ => (f, c + n),
}
});
assert!(flat < curved, "{name}: {flat} flat vs {curved} curved triangles");
}
fn a_box_is_a_regular_grid_on_every_face() {
// 40 × 30 × 20: sides at most 1/5 of the diagonal (10.77 mm), so 4 × 3 × 2 cells.
let b = ok(crate::box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 20.0)));
let (m, s) = plan_body(&b, 0.05);
assert_eq!(s.planar, 6);
assert_watertight(&m, "box");
let cells = 2 * (4 * 3 + 4 * 2 + 3 * 2);
assert_eq!(m.triangles.len(), 2 * cells, "two triangles per grid cell");
assert!(min_angle(&m) > 40.0, "{}", min_angle(&m));
}
#[test]
+1 -9
View File
@@ -468,15 +468,7 @@ mod tests {
let sag = mesh
.triangles
.iter()
.zip(&mesh.tri_face)
// The side wall's own triangles: the flat caps have corners on the rim too.
.filter(|(_, f)| {
matches!(
solidcraft_engine::kernel::face_surface(&b.body, **f as usize),
Some(solidcraft_engine::kernel::FaceSurface::Cylinder { .. })
)
})
.map(|(t, _)| t.map(|i| mesh.positions[i as usize]))
.map(|t| t.map(|i| mesh.positions[i as usize]))
.filter(|t| t.iter().all(|p| (r(*p) - 10.0).abs() < 1e-9))
.map(|[a, b, c]| 10.0 - r((a + b + c) * (1.0 / 3.0)))
.fold(0.0, f64::max);
@@ -785,8 +785,7 @@ fn cylinder_with_cluster(sketch: Value, at: [f64; 2], view: &str) -> Cluster {
gap.x.max(gap.y).max(gap.z) <= 1e-3
};
let (body_tris, face_tris, near_tris) = (m.triangles.len(), face.len(), face.iter().filter(|t| near_points(t)).count());
// Other faces exist (the flat caps are two triangles each now, so the curved side can be most of the body).
assert!(body_tris > face_tris, "the body has other faces: {body_tris} and {face_tris} triangles");
assert!(body_tris >= 2 * face_tris, "the body has other faces: {body_tris} and {face_tris} triangles");
Cluster { h, many, click, body_tris, face_tris, near_tris }
}
+4 -3
View File
@@ -9179,9 +9179,10 @@ mod tests {
for style in 0..4 {
app.ui.visual_style = style;
let scene = build_scene(&app).merged();
// Twelve edges per placed box, each one segment (a flat face needs no nodes along its
// edges), 24 segments.
let lines = if style == 1 { 0 } else { 2 * 12 * crate::gpu::LINE_SIZE };
// Twelve edges per placed box, in the saved mesh's pieces: no side longer than a fifth
// of the 30 mm diagonal, so the eight 20 mm edges in four and the four 10 mm ones in
// two, 40 segments.
let lines = if style == 1 { 0 } else { 2 * 40 * crate::gpu::LINE_SIZE };
assert_eq!(scene.lines.len(), lines, "style {style}: twelve edges per placed box");
assert_eq!(scene.hidden.len(), if style == 3 { lines } else { 0 });
assert_eq!(scene.tris.is_empty(), style == 2, "wireframe draws only the edges");
@@ -243,8 +243,9 @@ fn an_opacity_class_change_keeps_vertices_and_rewrites_only_the_runs() {
assert!(block.bytes.opaque.is_empty() && !block.bytes.glass.is_empty(), "the triangles moved to the glass runs");
let triangles: u32 = block.bytes.glass.iter().map(|r| r.len() as u32).sum::<u32>() / 3;
assert_eq!(block.bytes.glass.len(), 1, "one body of one class is one run, drawn with no index buffer");
// Flat faces are their outline alone: two triangles a face.
assert_eq!(triangles, 12, "every triangle of the box");
// The planner's grids, no side longer than a fifth of the box's 26.9 mm diagonal: edges in
// 4, 3 and 2 pieces, faces of 4 × 3, 4 × 2 and 3 × 2 cells, 2 · (12 + 8 + 6) · 2 triangles.
assert_eq!(triangles, 104, "every triangle of the box");
let vertex = block.bytes.glass.first().map_or(0, |r| r.start) as usize;
let at = vertex * crate::gpu::TRI_SIZE;
let colour = first_vertex(&second, block.bytes.vertices.get(at..).unwrap_or_default()).1;