Sketch on every side wall of one sketch extruded up and down (#957)
* Name a side drawn on several sketch curves by all of them An extrude named each side wall after the one sketch curve its profile segment lies on. A wall along two coincident lines (squares drawn edge to edge) or along collinear lines merged into one profile segment had no single curve, so it was named from geometry and could not carry a sketch attachment. One sketch extruded up and down and joined (the logo) refused a sketch on every one of its side walls. Such a side is now named by every curve it lies on, in sketch order: the first is its name, the others aliases. The set is fixed by the sketch, so the name is exact. The refusal message no longer blames merged faces. Adds the reported logo design and a synthetic up-and-down join to the action-matrix designs, and a regression test that sketches and extrudes on every flat face, fresh and after save and reopen. Fixes #954 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Action matrix: record the up-and-down join designs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9f76cca8b4
commit
ea23401710
9 files changed
+305
-13
No files matched your search
@@ -192,7 +192,8 @@ fn capture_impl(s: &Session, value: &Value, cmd: &str, internal: bool) -> Result
|
||||
{
|
||||
return Err(bad(
|
||||
cmd,
|
||||
"this face can't keep an attachment: it was merged from more than one feature's faces or reshaped by an edit. \
|
||||
"this face can't keep an attachment: its history can't tell it apart from other faces (an edit reshaped it, \
|
||||
or more than one face could claim its name). \
|
||||
Pick another face, or use a fixed plane here (fixed:true), which won't follow later changes to the face",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -582,3 +582,98 @@ fn offset_rectangle_pocket_has_one_logical_outer_wall_and_closed_face_projection
|
||||
assert!((mesh.measure().volume - expected).abs() < 1e-6, "actual {}, expected {expected}", mesh.measure().volume);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every flat face of every body: (body, face index, a point well inside it, its normal). The
|
||||
/// middle of the face's largest triangle, so an L-shaped face is picked on itself.
|
||||
fn flat_faces(s: &Session) -> Vec<(String, usize, Vec3, Vec3)> {
|
||||
let world = s.world_state();
|
||||
let mut out = Vec::new();
|
||||
for body in &world.bodies {
|
||||
let mesh = body.mesh();
|
||||
let mut inside: std::collections::BTreeMap<usize, (Vec3, f64)> = std::collections::BTreeMap::new();
|
||||
for (triangle, face) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
||||
if let Some([a, b, c]) = mesh.tri(triangle) {
|
||||
let area = (b - a).cross(c - a).len();
|
||||
let entry = inside.entry(*face as usize).or_insert(((a + b + c) * (1. / 3.), -1.));
|
||||
if area > entry.1 {
|
||||
*entry = ((a + b + c) * (1. / 3.), area);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (index, (point, _)) in inside {
|
||||
if let Some(plane) = body.body.planar_face(index) {
|
||||
out.push((body.name.clone(), index, point, plane.normal()));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn timeline_errors(s: &Session) -> Vec<String> {
|
||||
s.model.results.iter().filter_map(|r| r.error.as_ref().map(|e| format!("{}: {e}", r.name))).collect()
|
||||
}
|
||||
|
||||
fn save_and_reopen(s: &mut Session, dir: &std::path::Path, name: &str) -> Session {
|
||||
let path = dir.join(name);
|
||||
run(s, "file.save_as", json!({"path": path.to_string_lossy()}));
|
||||
let mut reopened = Session::default();
|
||||
run(&mut reopened, "doc.open", json!({"path": path.to_string_lossy()}));
|
||||
reopened
|
||||
}
|
||||
|
||||
/// A sketch on every flat face of `s`'s bodies (as the desktop picks it: body, index, a point on
|
||||
/// it) stays attached to that face, and a square drawn on it extrudes out of the face, fresh and
|
||||
/// after the result is saved and opened again.
|
||||
fn sketch_and_extrude_on_every_flat_face(s: &Session, dir: &std::path::Path, label: &str) -> usize {
|
||||
use solidcraft_doc::FaceAttachmentStatus;
|
||||
let faces = flat_faces(s);
|
||||
let before = timeline_errors(s);
|
||||
for (body, index, point, normal) in &faces {
|
||||
let what = format!("{label}: {body} face {index} at {point:?}");
|
||||
let mut t = s.scratch();
|
||||
let pick = json!({"body": body, "index": index, "face": [point.x, point.y, point.z]});
|
||||
let sketch =
|
||||
t.execute("sketch.create", &json!({"plane": pick, "project_edges": false})).unwrap_or_else(|error| panic!("{what}: {error}"))["sketch"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
let plane = t.model.state().sketch(sketch).unwrap().plane;
|
||||
assert!(plane.normal().dot(*normal) > 1. - 1e-9, "{what}: the sketch faces out of the face");
|
||||
let local = [(*point - plane.origin).dot(plane.x), (*point - plane.origin).dot(plane.y)];
|
||||
run(&mut t, "sketch.rectangle.two_point", json!({"p0": [local[0] - 0.5, local[1] - 0.5], "p1": [local[0] + 0.5, local[1] + 0.5]}));
|
||||
run(&mut t, "sketch.finish", json!({}));
|
||||
t.execute("solid.extrude", &json!({"sketch": sketch, "distance": 1, "operation": "join"})).unwrap_or_else(|error| panic!("{what}: {error}"));
|
||||
let check = |t: &Session, when: &str| {
|
||||
assert!(
|
||||
matches!(t.placed_document().sketch_face_attachment(sketch, &t.model), FaceAttachmentStatus::Healthy { .. }),
|
||||
"{what} ({when}): the sketch must stay attached to its face"
|
||||
);
|
||||
assert_eq!(timeline_errors(t), before, "{what} ({when}): no new timeline errors");
|
||||
};
|
||||
check(&t, "fresh");
|
||||
let reopened = save_and_reopen(&mut t, dir, &format!("{label}-{index}.solidcraft"));
|
||||
check(&reopened, "after save and reopen");
|
||||
}
|
||||
faces.len()
|
||||
}
|
||||
|
||||
/// The logo: one sketch extruded up and the same sketch extruded down, joined. Its side walls
|
||||
/// are merged from both extrudes' sides, or run along two coincident sketch lines (squares drawn
|
||||
/// edge to edge); each still takes a sketch that follows it, as the tops and bottoms do.
|
||||
#[test]
|
||||
fn every_flat_face_of_one_sketch_extruded_up_and_down_takes_an_attached_sketch() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/action_matrix/designs");
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-up-down-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let script: Value = serde_json::from_str(&std::fs::read_to_string(root.join("up-down-join.json")).unwrap()).unwrap();
|
||||
let mut s = Session::default();
|
||||
s.run_script(&script).unwrap();
|
||||
assert_eq!(s.world_state().bodies.len(), 1, "the two extrudes join into one body");
|
||||
assert_eq!(sketch_and_extrude_on_every_flat_face(&s, &dir, "synthetic"), 12, "8 side walls, top, bottom, 2 at the sketch plane");
|
||||
let reopened = save_and_reopen(&mut s, &dir, "synthetic.solidcraft");
|
||||
assert_eq!(sketch_and_extrude_on_every_flat_face(&reopened, &dir, "synthetic-reopened"), 12);
|
||||
// The logo design as it was reported (format 17, with a removed shelled body before it).
|
||||
let mut logo = Session::default();
|
||||
run(&mut logo, "doc.open", json!({"path": root.join("logo-up-down-join.solidcraft").to_string_lossy()}));
|
||||
assert_eq!(sketch_and_extrude_on_every_flat_face(&logo, &dir, "logo"), 12);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
Reference in new issue
Block a user