One walk over a feature's references (2/3, #876) (#988)

Part 2 of #876: one walk over the references a feature holds (the sketches it's built from, the features it names, the bodies it works on) replaces three hand-written matches, with a golden record covering every variant.
This commit is contained in:
JIJINBEI authored and GitHub committed 2026-10-14 11:14:25 -04:00
1 parent 110d00d07e
commit 86ea018d05
10 files changed
+778 -420

No files matched your search

+89 -154
View File
@@ -1,161 +1,22 @@
//! Golden record of what the walk over a feature's expressions reports, for every variant (and
//! every fillet style, pattern, hole and construction definition): `inputs` (label, expression,
//! kind, in order: the order names the `dN` parameters) and `expressions` (every expression,
//! placement points included). Each expression field holds its own name, so a moved, missing or
//! relabelled input shows in the diff. The records live in `feature_walk_tests/*.txt`; to record
//! an intended change, run the tests with `FEATURE_WALK_BLESS=1`.
//! Golden records of what the walks over a feature report, for every variant (and every fillet
//! style, pattern, hole and construction definition):
//! - expressions: `inputs` (label, expression, kind, in order: the order names the `dN`
//! parameters) and `expressions` (every expression, placement points included). Each
//! expression field holds its own name, so a moved, missing or relabelled input shows;
//! - references: the sketches a feature is built from, and the fields `remap_refs` and
//! `remap_bodies` rewrite when everything is copied under new ids and names.
//!
//! The records live in `feature_walk_tests/*.txt`; to record an intended change, run the tests
//! with `FEATURE_WALK_BLESS=1`.
use serde_json::{Value, json};
mod fixtures;
use std::collections::BTreeMap;
use fixtures::{changed, fixtures, with_refs};
use crate::FeatureKind;
fn plane(chain: &str) -> Value {
json!({"type": "at_angle", "angle": format!("{chain}.angle"), "axis_origin": [0, 0, 0], "axis_dir": [1, 0, 0],
"base": {"type": "offset", "distance": format!("{chain}.offset"), "base": {"type": "origin", "name": "XY"}}})
}
fn geo_plane(chain: &str) -> Value {
json!({"type": "plane", "plane": plane(chain)})
}
fn along(chain: &str) -> Value {
json!({"type": "along_path", "path": {"type": "edge", "at": [0, 0, 0]}, "t": format!("{chain}.t")})
}
fn fixtures() -> Vec<(&'static str, Value)> {
let o = [0, 0, 0];
let sk = serde_json::to_value(solidcraft_sketch::Sketch::new()).unwrap();
let extent = |n: &str| {
json!({"distance": format!("{n}.distance"), "distance2": format!("{n}.distance2"), "start_offset": format!("{n}.start_offset"),
"taper": format!("{n}.taper"), "to_offset": format!("{n}.to_offset")})
};
let fillet = |style: Value| json!({"type": "fillet", "edges": [o], "radius": "fillet.radius", "style": style});
let pattern = |p: Value| json!({"type": "pattern", "features": ["F"], "pattern": p});
let hole =
|h: Value| json!({"type": "hole", "position": o, "direction": [0, 0, 1], "diameter": "hole.diameter", "depth": "hole.depth", "hole": h});
vec![
(
"body_transfer",
json!({"type": "body_transfer", "bodies": ["B"], "from": 0, "to": 1, "transform": [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]}),
),
("sketch", json!({"type": "sketch", "plane": plane("sketch"), "sketch": sk.clone()})),
("sketch_along_path", json!({"type": "sketch", "plane": along("sketch"), "sketch": sk})),
("extrude", json!({"type": "extrude", "sketch": 1, "profiles": {"type": "all"}, "extent": extent("extrude")})),
(
"revolve",
json!({"type": "revolve", "sketch": 1, "profiles": {"type": "all"}, "axis": {"type": "world", "axis": "X"}, "angle": "revolve.angle", "angle2": "revolve.angle2"}),
),
("fillet_constant", fillet(json!({"type": "constant"}))),
("fillet_chord", fillet(json!({"type": "chord"}))),
("fillet_curvature", fillet(json!({"type": "curvature"}))),
("fillet_variable", fillet(json!({"type": "variable", "radius2": "fillet.radius2", "start": o, "mid": [[0.5, "fillet.mid"]]}))),
("fillet_spline", fillet(json!({"type": "spline", "start": o, "knots": [0.0, 1.0], "radii": ["fillet.r1", "fillet.r2", "fillet.r3"]}))),
(
"chamfer",
json!({"type": "chamfer", "edges": [o], "distance": "chamfer.distance", "distance2": "chamfer.distance2", "angle": "chamfer.angle"}),
),
("box", json!({"type": "box", "corner": ["box.x", "box.y", "box.z"], "length": "box.length", "width": "box.width", "height": "box.height"})),
("cylinder", json!({"type": "cylinder", "base": ["cyl.x", "cyl.y", "cyl.z"], "radius": "cyl.radius", "height": "cyl.height"})),
("sphere", json!({"type": "sphere", "center": ["sph.x", "sph.y", "sph.z"], "radius": "sph.radius"})),
("torus", json!({"type": "torus", "center": ["tor.x", "tor.y", "tor.z"], "major": "tor.major", "minor": "tor.minor"})),
("combine", json!({"type": "combine", "target": "A", "tools": ["B"], "operation": "join"})),
(
"pattern_rectangular",
pattern(
json!({"type": "rectangular", "dir1": [1, 0, 0], "count1": "rect.count1", "spacing1": "rect.spacing1", "dir2": [0, 1, 0], "count2": "rect.count2", "spacing2": "rect.spacing2"}),
),
),
("pattern_circular", pattern(json!({"type": "circular", "origin": o, "axis": [0, 0, 1], "count": "circ.count", "angle": "circ.angle"}))),
("pattern_path", pattern(json!({"type": "path", "path_sketch": 1, "path": ["l1"], "count": "path.count", "spacing": "path.spacing"}))),
("mirror", json!({"type": "mirror", "features": ["F"], "plane": plane("mirror")})),
("hole_simple", hole(json!({"type": "simple"}))),
("hole_drilled", hole(json!({"type": "drilled", "tip_angle": "hole.tip_angle"}))),
("hole_counterbore", hole(json!({"type": "counterbore", "cb_diameter": "hole.cb_diameter", "cb_depth": "hole.cb_depth"}))),
("hole_countersink", hole(json!({"type": "countersink", "cs_diameter": "hole.cs_diameter", "cs_angle": "hole.cs_angle"}))),
("thread", json!({"type": "thread", "face": o, "length": "thread.length"})),
("loft", json!({"type": "loft", "sections": []})),
("sweep", json!({"type": "sweep", "sketch": 1, "profiles": {"type": "all"}, "path_sketch": 2, "path": ["l1"]})),
("shell", json!({"type": "shell", "faces": [o], "thickness": "shell.thickness"})),
("draft", json!({"type": "draft", "faces": [o], "angle": "draft.angle", "neutral": plane("draft_neutral"), "pull": [0, 0, 1]})),
("construction_plane", json!({"type": "construction_plane", "plane": plane("cplane")})),
("split", json!({"type": "split", "body": "B", "plane": plane("split")})),
(
"construction_axis",
json!({"type": "construction_axis", "def": {"type": "two_planes", "a": geo_plane("axis_a"), "b": geo_plane("axis_b")}}),
),
(
"construction_point_planes",
json!({"type": "construction_point", "def": {"type": "three_planes", "a": geo_plane("pt_a"), "b": geo_plane("pt_b"), "c": geo_plane("pt_c")}}),
),
(
"construction_point_along",
json!({"type": "construction_point", "def": {"type": "along_path", "path": {"type": "edge", "at": o}, "t": "pt.t"}}),
),
("split_face", json!({"type": "split_face", "faces": [o], "plane": plane("split_face")})),
("scale", json!({"type": "scale", "bodies": ["B"], "factor": "scale.factor", "factors": ["scale.x", "scale.y", "scale.z"]})),
("offset_face", json!({"type": "offset_face", "faces": [o], "distance": "offset_face.distance"})),
("delete_face", json!({"type": "delete_face", "faces": [o]})),
("bounding_solid", json!({"type": "bounding_solid", "bodies": ["B"], "margin": "bounding.margin"})),
("pipe", json!({"type": "pipe", "path_sketch": 1, "path": ["l1"], "diameter": "pipe.diameter", "wall": "pipe.wall"})),
("emboss", json!({"type": "emboss", "sketch": 1, "profiles": {"type": "all"}, "depth": "emboss.depth"})),
("rib", json!({"type": "rib", "sketch": 1, "curves": ["l1"], "thickness": "rib.thickness", "depth": "rib.depth"})),
("replace_face", json!({"type": "replace_face", "faces": [o], "target": plane("replace_face")})),
("align", json!({"type": "align", "bodies": ["B"], "from": o, "to": o})),
(
"coil",
json!({"type": "coil", "base": ["coil.x", "coil.y", "coil.z"], "diameter": "coil.diameter", "pitch": "coil.pitch", "turns": "coil.turns", "section_size": "coil.section_size", "start_angle": "coil.start_angle"}),
),
("sheet_base", json!({"type": "sheet_base", "sketch": 1, "profiles": {"type": "all"}})),
("sheet_contour", json!({"type": "sheet_contour", "sketch": 1, "curves": ["l1"], "distance": "contour.distance"})),
(
"sheet_flange",
json!({"type": "sheet_flange", "edges": [{"point": o, "angle": "flange.e1_angle", "height": "flange.e1_height"}, {"point": o, "angle": null, "height": "flange.e2_height"}], "height": "flange.height", "angle": "flange.angle", "radius": "flange.radius"}),
),
("sheet_hem", json!({"type": "sheet_hem", "edges": [o], "length": "hem.length", "gap": "hem.gap"})),
(
"sheet_fold",
json!({"type": "sheet_fold", "a": o, "b": [1, 0, 0], "angle": "fold.angle", "radius": "fold.radius", "position": "centerline"}),
),
("sheet_unfold", json!({"type": "sheet_unfold"})),
("sheet_convert", json!({"type": "sheet_convert", "body": "B", "face": o})),
(
"boss",
json!({"type": "boss", "position": o, "diameter": "boss.diameter", "height": "boss.height", "hole_diameter": "boss.hole_diameter", "hole_depth": "boss.hole_depth",
"draft": "boss.draft", "fillet": "boss.fillet", "ribs": "boss.ribs", "rib_thickness": "boss.rib_thickness", "rib_length": "boss.rib_length", "rib_offset": "boss.rib_offset"}),
),
("lip", json!({"type": "lip", "face": o, "width": "lip.width", "height": "lip.height", "gap": "lip.gap"})),
(
"snap_fit",
json!({"type": "snap_fit", "position": o, "hook": [1, 0, 0], "length": "snap.length", "thickness": "snap.thickness", "width": "snap.width", "catch_depth": "snap.catch_depth", "catch_length": "snap.catch_length"}),
),
(
"rest",
json!({"type": "rest", "position": o, "width": "rest.width", "length": "rest.length", "height": "rest.height", "draft": "rest.draft", "thickness": "rest.thickness"}),
),
("boundary_fill", json!({"type": "boundary_fill", "tools": ["B"], "cells": [o]})),
("remove", json!({"type": "remove", "bodies": ["B"]})),
("patch", json!({"type": "patch"})),
("stitch", json!({"type": "stitch", "bodies": ["B"], "tolerance": "stitch.tolerance"})),
("thicken", json!({"type": "thicken", "bodies": ["B"], "thickness": "thicken.thickness", "operation": "new_body"})),
("surface_extrude", json!({"type": "surface_extrude", "sketch": 1, "extent": extent("surface_extrude")})),
(
"surface_revolve",
json!({"type": "surface_revolve", "sketch": 1, "axis": {"type": "world", "axis": "X"}, "angle": "srev.angle", "angle2": "srev.angle2"}),
),
("surface_sweep", json!({"type": "surface_sweep", "sketch": 1, "path_sketch": 2, "path": ["l1"]})),
("surface_loft", json!({"type": "surface_loft", "sections": []})),
("surface_offset", json!({"type": "surface_offset", "faces": [o], "distance": "soff.distance", "tolerance": "soff.tolerance"})),
("unstitch", json!({"type": "unstitch", "body": "B"})),
("reverse_normal", json!({"type": "reverse_normal", "body": "B"})),
("surface_extend", json!({"type": "surface_extend", "body": "B", "edges": [o], "distance": "extend.distance"})),
("surface_trim", json!({"type": "surface_trim", "body": "B", "plane": plane("trim"), "keep": o})),
("move", json!({"type": "move", "bodies": ["B"], "translate": ["move.x", "move.y", "move.z"], "angle": "move.angle"})),
("import", json!({"type": "import", "file": "a.step", "step": ""})),
("mesh_convert", json!({"type": "mesh_convert", "body": "B", "tolerance": "mesh_convert.tolerance"})),
("mesh_import", json!({"type": "mesh_import", "file": "a.stl", "meshes": []})),
]
}
/// Per fixture: its inputs, one per line (`label = expression (kind)`), then every expression.
fn record() -> String {
let mut out = String::new();
@@ -177,6 +38,43 @@ fn record() -> String {
out
}
/// Per fixture: the sketches it is built from, and the fields that copying with every sketch,
/// feature and body renamed rewrites (`remap_refs`: sketches and features; `remap_bodies`).
fn ref_record() -> String {
let sketches: BTreeMap<u64, u64> = (1..=8).map(|i| (i, i + 100)).collect();
let names = BTreeMap::from([("F".to_string(), "F2".to_string()), ("CP".to_string(), "CP2".to_string())]);
let bodies = BTreeMap::from([("B".to_string(), "B2".to_string())]);
let mut out = String::new();
for (name, v) in fixtures() {
let kind: FeatureKind = serde_json::from_value(with_refs(name, v)).unwrap_or_else(|e| panic!("{name}: {e}"));
let before = serde_json::to_value(&kind).unwrap();
let mut refs = kind.clone();
refs.remap_refs(&sketches, &names);
let mut by_refs = Vec::new();
changed(&before, &serde_json::to_value(&refs).unwrap(), "", &mut by_refs);
let mut body = kind.clone();
body.remap_bodies(&bodies);
let mut by_bodies = Vec::new();
changed(&before, &serde_json::to_value(&body).unwrap(), "", &mut by_bodies);
out += &format!("{name}\n");
let sketches = kind.input_sketches();
if !sketches.is_empty() {
out += &format!(" sketches: {sketches:?}\n");
}
if !by_refs.is_empty() {
out += &format!(" remap_refs: {}\n", by_refs.join(", "));
}
if !by_bodies.is_empty() {
out += &format!(" remap_bodies: {}\n", by_bodies.join(", "));
}
}
out
}
/// A variant with a field that refers to a sketch (a field named `*sketch*` holding an id, sketch
/// points or loft sections) must report it from `input_sketches`, or deleting the sketch leaves
/// the feature broken. New variants are caught by the walk's exhaustive match; this catches a
/// sketch field added to an existing variant (its fixture has to set it).
/// Compares `got` with the golden file `name` in `feature_walk_tests/`, line by line. With
/// `FEATURE_WALK_BLESS` set it rewrites the file instead, for an intended change (say why in the
/// pull request).
@@ -195,6 +93,43 @@ fn check_golden(name: &str, want: &str, got: &str) {
/// Recorded from the hand-written `inputs_mut` and `expressions` the walk replaced.
const GOLDEN_EXPRS: &str = include_str!("feature_walk_tests/golden_exprs.txt");
/// Recorded from the hand-written `input_sketches`, `remap_refs` and `remap_bodies` the walk
/// replaced, with two fixes: a coil's `targets` and a surface trim's plane are remapped too.
const GOLDEN_REFS: &str = include_str!("feature_walk_tests/golden_refs.txt");
#[test]
fn input_sketches_lists_every_variant_with_a_sketch_field() {
let src = include_str!("document.rs");
let body = src.split("pub enum FeatureKind {").nth(1).and_then(|s| s.split("\n}\n").next()).unwrap();
let mut with_sketch = Vec::new();
let mut variant = String::new();
for line in body.lines() {
let t = line.trim();
if line.starts_with(" ") && !line.starts_with(" ") && t.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
variant = t.split(|c: char| !c.is_alphanumeric()).next().unwrap_or("").to_lowercase();
continue;
}
let Some((name, ty)) = t.strip_prefix("pub ").unwrap_or(t).split_once(':') else { continue };
let refers = (name.contains("sketch") && ty.contains("u64")) || ty.contains("SketchPoints") || ty.contains("LoftSection");
if refers && !t.starts_with("//") && !with_sketch.contains(&variant) {
with_sketch.push(variant.clone());
}
}
assert!(with_sketch.len() >= 10, "the FeatureKind parse found too few sketch users: {with_sketch:?}");
for v in with_sketch {
let uses = fixtures().into_iter().filter(|(_, f)| f["type"].as_str().unwrap().replace('_', "") == v).any(|(name, f)| {
let kind: FeatureKind = serde_json::from_value(with_refs(name, f)).unwrap();
!kind.input_sketches().is_empty()
});
assert!(uses, "FeatureKind::{v} has a sketch field but input_sketches reports no sketch for its fixtures");
}
}
#[test]
fn references_match_the_golden_record() {
check_golden("golden_refs.txt", GOLDEN_REFS, &ref_record());
}
#[test]
fn inputs_and_expressions_match_the_golden_record() {
check_golden("golden_exprs.txt", GOLDEN_EXPRS, &record());