diff --git a/crates/doc/src/config_certificates.rs b/crates/doc/src/config_certificates.rs index 7e84b5b2..25e9b94d 100644 --- a/crates/doc/src/config_certificates.rs +++ b/crates/doc/src/config_certificates.rs @@ -457,6 +457,46 @@ impl<'a> ExactSketchCurves<'a> { } } +impl ExactSketchCurves<'_> { + /// The sketch curves a generated segment is drawn on: [`find_along`](Self::find_along)'s one + /// curve, else every drawn curve that contains it or runs along and overlaps it (projections + /// only where no drawn curve does), in the sketch's order. A segment two coincident curves + /// were drawn over (four squares drawn edge to edge), or one a profile merged from collinear + /// curves (the outline of several adjacent profiles extruded together), is drawn on all of + /// them: the set is exact and fixed by the sketch, so its face is named by it (the first its + /// name, the others aliases) rather than not at all. Empty when it lies on none. + pub(crate) fn find_all_along(&self, generated: &Seg2) -> Vec { + if let Some(id) = self.find_along(generated) { + return vec![id]; + } + let runs_along = |original: &Seg2| { + if exact_segment_contains(original, generated) { + return true; + } + let (Seg2::Line { a, b }, Seg2::Line { a: c, b: d }) = (*original, *generated) else { return false }; + let direction = b - a; + let scale = direction.len().max((d - c).len()).max(1.); + if direction.len() <= 1e-12 + || direction.cross(c - a).abs() > 1e-10 * scale * scale + || direction.cross(d - a).abs() > 1e-10 * scale * scale + { + return false; + } + let (tc, td) = ((c - a).dot(direction) / direction.len2(), (d - a).dot(direction) / direction.len2()); + tc.min(td).max(0.) < tc.max(td).min(1.) - 1e-9 + }; + let pick = |linked: bool| -> Vec { + self.segments + .iter() + .filter(|(_, l, originals)| *l == linked && originals.iter().any(runs_along)) + .map(|(id, _, _)| (*id).to_owned()) + .collect() + }; + let drawn = pick(false); + if drawn.is_empty() { pick(true) } else { drawn } + } +} + fn exact_segment_contains(original: &Seg2, generated: &Seg2) -> bool { match (original, generated) { (Seg2::Line { a, b }, Seg2::Line { a: c, b: d }) => { diff --git a/crates/doc/src/lineage.rs b/crates/doc/src/lineage.rs index 3424254c..d55f7c57 100644 --- a/crates/doc/src/lineage.rs +++ b/crates/doc/src/lineage.rs @@ -207,23 +207,30 @@ impl<'a> ProfileRoles<'a> { let segments: Vec<&solidcraft_geom::Seg2> = self.region.map(|r| std::iter::once(&r.outer).chain(&r.holes).flat_map(|l| &l.segs).collect()).unwrap_or_default(); let curves = self.sketch.map(crate::config::ExactSketchCurves::new); - let curve = |segment: usize| -> Option { curves.as_ref()?.find_along(segments.get(segment)?).map(|id| format!("side:{id}")) }; + // Every curve the segment is drawn on (one, or several coincident or collinear ones). + let curve = |segment: usize| -> Vec { + let (Some(curves), Some(segment)) = (curves.as_ref(), segments.get(segment)) else { return Vec::new() }; + curves.find_all_along(segment).into_iter().map(|id| format!("side:{id}")).collect() + }; let roles = history .faces .iter() .map(|face| { let mut roles: Vec = Vec::new(); for origin in &face.origins { - let role = match *origin { + let found = match *origin { FaceOrigin::Profile { segment } => curve(segment), - FaceOrigin::Cap { end } => self.caps.get(usize::from(end)).cloned().flatten(), - _ => None, + FaceOrigin::Cap { end } => self.caps.get(usize::from(end)).cloned().flatten().into_iter().collect(), + _ => Vec::new(), }; - match role { - Some(role) if !roles.contains(&role) => roles.push(role), - Some(_) => {} - // One unknown origin leaves the face unknown: never a partial guess. - None => return Vec::new(), + // One unknown origin leaves the face unknown: never a partial guess. + if found.is_empty() { + return Vec::new(); + } + for role in found { + if !roles.contains(&role) { + roles.push(role); + } } } roles diff --git a/crates/engine/src/cmd/sketch_face.rs b/crates/engine/src/cmd/sketch_face.rs index 810b0f75..985cdf0c 100644 --- a/crates/engine/src/cmd/sketch_face.rs +++ b/crates/engine/src/cmd/sketch_face.rs @@ -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", )); } diff --git a/crates/engine/src/cmd/sketch_face_tests.rs b/crates/engine/src/cmd/sketch_face_tests.rs index ba684c29..3d5f226b 100644 --- a/crates/engine/src/cmd/sketch_face_tests.rs +++ b/crates/engine/src/cmd/sketch_face_tests.rs @@ -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 = 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 { + 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); +} diff --git a/docs/action-matrix.md b/docs/action-matrix.md index 27ea4f9c..2e6dda52 100644 --- a/docs/action-matrix.md +++ b/docs/action-matrix.md @@ -44,11 +44,14 @@ lists which still fail; skip `action_matrix_` to leave out the matrix itself). `demo/pi-case`: the Raspberry Pi 4 case of the 0.6 README demo, built through the desktop UI and saved by `examples/demo/enclosure_ui_mcp.py --story pi-0.6 --save`; `demo/pi-case-0.8`: the 0.8 README demo's case (`--story pi`), with a rounded rim, engraved - text and a PETG material. + text and a PETG material; + `demo/logo-up-down-join`: the SolidCraft logo as it was reported in #954, one sketch extruded + up and down and joined. - `typical/*`: [`tests/action_matrix/designs`](../tests/action_matrix/designs/), parts made the ordinary way: a stacked extrude, a filleted and shelled box, a revolved knob, a plate with a counterbored hole patterned 3 × 2 and a chamfered outline, a sheet metal L bracket with holes, a - join and a cut of three bodies, a loft on a plate, and a sweep. + join and a cut of three bodies, a loft on a plate, a sweep, and one sketch of squares drawn edge + to edge extruded up and down and joined (`up-down-join`). ## The actions diff --git a/docs/face-attachments.md b/docs/face-attachments.md index fda3074a..1a6a7d8a 100644 --- a/docs/face-attachments.md +++ b/docs/face-attachments.md @@ -36,7 +36,12 @@ keep the history of the body they go on and name their own faces by the part the (`F4:post:top`, `F5:rest:side:0`, `F6:arm:top`), so those take attachments too. A side an extrude grew to overlap a flush body before joining it (its neighbours lengthened past the sketch's corners) is still named by the sketch line it runs along, so a wall a Join merged from two extrudes' walls -attaches by the first one's name and answers to the other's. +attaches by the first one's name and answers to the other's. A side drawn on several sketch +curves at once (two squares drawn edge to edge leave two coincident lines along the shared edge; +several adjacent profiles extruded together leave one wall along collinear lines) is named by +every curve it lies on, the first in the sketch's order its name and the others aliases: the set +is fixed by the sketch, so one sketch extruded up and down and joined (a logo) takes an attached +sketch on every wall, not only on its tops and bottoms. Faces whose history is not exact (faces a feature rebuilt without reporting its history), ambiguous sources, and the pieces of a face split on purpose with Split Face (each piece is its diff --git a/tests/action_matrix/baseline.json b/tests/action_matrix/baseline.json index 93d3da5d..272e459e 100644 --- a/tests/action_matrix/baseline.json +++ b/tests/action_matrix/baseline.json @@ -1662,6 +1662,71 @@ "Enclosure/v[F4:inner:F2:side:l3 & F4:inner:F3:blend:F2:side:l2|F2:side:l3] (2)": "vertex.point", "design": "load" }, + "demo/logo-up-down-join": { + "Body2/F6:side:l1": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch face.sketch+edit", + "Body2/F6:side:l10": "face.angle_plane face.angle_plane+edit face.draft_neutral face.extrude face.hole face.midplane face.midplane+edit face.offset_plane face.press_pull !face.shell face.sketch face.sketch+edit", + "Body2/F6:side:l10|F6:side:l7": "edge.axis edge.chamfer edge.chamfer+edit edge.fillet edge.fillet+edit edge.normal_plane edge.project", + "Body2/F6:side:l10|F6:side:l9": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body2/F6:side:l10|F6:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body2/F6:side:l10|F7:bottom": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.normal_plane+edit edge.project", + "Body2/F6:side:l10|F7:start": "edge.axis edge.axis+edit edge.chamfer edge.fillet edge.normal_plane edge.project edge.project+edit", + "Body2/F6:side:l1|F6:side:l10": "edge.axis edge.chamfer edge.chamfer+edit edge.fillet edge.fillet+edit edge.normal_plane edge.normal_plane+edit edge.project", + "Body2/F6:side:l1|F6:side:l4": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project edge.project+edit", + "Body2/F6:side:l1|F6:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body2/F6:side:l1|F7:start": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body2/F6:side:l4": "face.angle_plane face.draft_neutral face.extrude face.extrude+edit face.hole face.midplane face.offset_plane face.press_pull face.press_pull+edit face.shell face.sketch", + "Body2/F6:side:l4|F6:side:l7": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project edge.project+edit", + "Body2/F6:side:l4|F6:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body2/F6:side:l4|F7:start": "edge.axis edge.chamfer edge.chamfer+edit edge.fillet edge.normal_plane edge.normal_plane+edit edge.project", + "Body2/F6:side:l7": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.midplane+edit face.offset_plane face.press_pull !face.shell face.sketch face.sketch+edit", + "Body2/F6:side:l7|F6:side:l8": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project edge.project+edit", + "Body2/F6:side:l7|F6:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body2/F6:side:l7|F7:bottom": "edge.axis edge.chamfer edge.chamfer+edit edge.fillet edge.normal_plane edge.normal_plane+edit edge.project", + "Body2/F6:side:l7|F7:start": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body2/F6:side:l8": "face.angle_plane face.draft_neutral face.draft_neutral+edit face.extrude face.extrude+edit face.hole face.offset_plane face.press_pull face.press_pull+edit !face.shell face.sketch", + "Body2/F6:side:l8|F6:side:l9": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project edge.project+edit", + "Body2/F6:side:l8|F6:start": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project edge.project+edit", + "Body2/F6:side:l8|F6:top": "edge.axis edge.chamfer edge.chamfer+edit edge.fillet edge.fillet+edit edge.normal_plane edge.project", + "Body2/F6:side:l8|F7:bottom": "edge.axis edge.axis+edit edge.chamfer edge.fillet edge.normal_plane edge.project edge.project+edit", + "Body2/F6:side:l8|F7:side:l5": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body2/F6:side:l9": "face.angle_plane face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull !face.shell face.sketch", + "Body2/F6:side:l9|F6:start": "edge.axis edge.chamfer edge.chamfer+edit edge.fillet edge.fillet+edit edge.normal_plane edge.project", + "Body2/F6:side:l9|F6:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project edge.project+edit", + "Body2/F6:side:l9|F7:bottom": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body2/F6:side:l9|F7:side:l12": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body2/F6:start": "face.angle_plane face.angle_plane+edit face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch", + "Body2/F6:start|F7:side:l12": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body2/F6:start|F7:side:l5": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body2/F6:top": "face.angle_plane face.draft_neutral face.draft_neutral+edit face.extrude face.hole face.offset_plane face.press_pull !face.shell face.sketch", + "Body2/F7:bottom": "face.angle_plane face.angle_plane+edit face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull !face.shell face.sketch", + "Body2/F7:bottom|F7:side:l12": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body2/F7:bottom|F7:side:l5": "edge.axis edge.axis+edit edge.chamfer edge.fillet edge.normal_plane edge.project edge.project+edit", + "Body2/F7:side:l12": "face.angle_plane face.draft_neutral face.draft_neutral+edit face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch", + "Body2/F7:side:l12|F7:side:l5": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body2/F7:side:l5": "face.angle_plane face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch", + "Body2/F7:start": "face.angle_plane face.draft_neutral face.draft_neutral+edit face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch", + "Body2/body": "body.copy body.mirror body.mirror+edit body.move body.move+edit body.pattern", + "Body2/v[F6:side:l1 & F6:side:l10 & F6:top]": "vertex.plane_three_points vertex.point vertex.point+edit", + "Body2/v[F6:side:l1 & F6:side:l10 & F7:start]": "vertex.plane_three_points vertex.point", + "Body2/v[F6:side:l1 & F6:side:l4 & F6:start & F7:side:l12 & F7:side:l5 & F7:start]": "vertex.plane_three_points vertex.point", + "Body2/v[F6:side:l1 & F6:side:l4 & F6:top]": "vertex.plane_three_points vertex.plane_three_points+edit vertex.point", + "Body2/v[F6:side:l10 & F6:side:l7 & F7:bottom]": "vertex.plane_three_points vertex.plane_three_points+edit vertex.point", + "Body2/v[F6:side:l10 & F6:side:l7 & F7:start]": "vertex.plane_three_points vertex.point", + "Body2/v[F6:side:l10 & F6:side:l9 & F6:top]": "vertex.plane_three_points vertex.point", + "Body2/v[F6:side:l10 & F6:side:l9 & F7:bottom]": "vertex.plane_three_points vertex.point", + "Body2/v[F6:side:l4 & F6:side:l7 & F6:top]": "vertex.plane_three_points vertex.plane_three_points+edit vertex.point", + "Body2/v[F6:side:l4 & F6:side:l7 & F7:start]": "vertex.plane_three_points vertex.point", + "Body2/v[F6:side:l7 & F6:side:l8 & F6:top]": "vertex.plane_three_points vertex.point", + "Body2/v[F6:side:l7 & F6:side:l8 & F7:bottom]": "vertex.plane_three_points vertex.point", + "Body2/v[F6:side:l8 & F6:side:l9 & F6:start]": "vertex.plane_three_points vertex.point", + "Body2/v[F6:side:l8 & F6:side:l9 & F6:top]": "vertex.plane_three_points vertex.plane_three_points+edit vertex.point", + "Body2/v[F6:side:l8 & F6:start & F7:side:l5]": "vertex.plane_three_points vertex.plane_three_points+edit vertex.point", + "Body2/v[F6:side:l8 & F7:bottom & F7:side:l5]": "vertex.plane_three_points vertex.point", + "Body2/v[F6:side:l9 & F6:start & F7:side:l12]": "vertex.plane_three_points vertex.point vertex.point+edit", + "Body2/v[F6:side:l9 & F7:bottom & F7:side:l12]": "vertex.plane_three_points vertex.point", + "Body2/v[F7:bottom & F7:side:l12 & F7:side:l5]": "vertex.plane_three_points vertex.plane_three_points+edit vertex.point", + "design": "load" + }, "demo/pi-case": { "Body1/F11:side:l5": "face.angle_plane !face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch face.sketch+edit", "Body1/F11:side:l5|F11:side:l6": "edge.axis edge.axis+edit edge.chamfer edge.fillet edge.normal_plane edge.project", @@ -11275,6 +11340,71 @@ "Body1/v[F3:side:l3#2 & F3:side:l3#[F3:end] & F3:side:l4#2 & F3:side:l4#[F3:end]]": "vertex.plane_three_points vertex.point", "Body1/v[F3:side:l3#[F3:start] & F3:side:l4#[F3:start] & F3:start]": "vertex.plane_three_points vertex.point", "design": "load" + }, + "typical/up-down-join": { + "Body1/F2:side:l1": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch", + "Body1/F2:side:l10": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull !face.shell face.sketch", + "Body1/F2:side:l10|F2:side:l7": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l10|F2:side:l9": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l10|F2:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l10|F3:bottom": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l10|F3:start": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l1|F2:side:l10": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l1|F2:side:l4": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l1|F2:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l1|F3:start": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l4": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch", + "Body1/F2:side:l4|F2:side:l7": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l4|F2:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l4|F3:start": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l7": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull !face.shell face.sketch", + "Body1/F2:side:l7|F2:side:l8": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l7|F2:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l7|F3:bottom": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l7|F3:start": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l8": "face.angle_plane face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull !face.shell face.sketch", + "Body1/F2:side:l8|F2:side:l9": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l8|F2:start": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l8|F2:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l8|F3:bottom": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l8|F3:side:l5": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l9": "face.angle_plane face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull !face.shell face.sketch", + "Body1/F2:side:l9|F2:start": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l9|F2:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l9|F3:bottom": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:side:l9|F3:side:l12": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:start": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch", + "Body1/F2:start|F3:side:l12": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:start|F3:side:l5": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F2:top": "face.angle_plane face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull !face.shell face.sketch", + "Body1/F3:bottom": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull !face.shell face.sketch", + "Body1/F3:bottom|F3:side:l12": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F3:bottom|F3:side:l5": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F3:side:l12": "face.angle_plane face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch", + "Body1/F3:side:l12|F3:side:l5": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", + "Body1/F3:side:l5": "face.angle_plane face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch", + "Body1/F3:start": "face.angle_plane face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch", + "Body1/body": "body.copy body.mirror body.move body.pattern", + "Body1/v[F2:side:l1 & F2:side:l10 & F2:top]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l1 & F2:side:l10 & F3:start]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l1 & F2:side:l4 & F2:start & F3:side:l12 & F3:side:l5 & F3:start]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l1 & F2:side:l4 & F2:top]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l10 & F2:side:l7 & F3:bottom]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l10 & F2:side:l7 & F3:start]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l10 & F2:side:l9 & F2:top]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l10 & F2:side:l9 & F3:bottom]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l4 & F2:side:l7 & F2:top]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l4 & F2:side:l7 & F3:start]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l7 & F2:side:l8 & F2:top]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l7 & F2:side:l8 & F3:bottom]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l8 & F2:side:l9 & F2:start]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l8 & F2:side:l9 & F2:top]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l8 & F2:start & F3:side:l5]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l8 & F3:bottom & F3:side:l5]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l9 & F2:start & F3:side:l12]": "vertex.plane_three_points vertex.point", + "Body1/v[F2:side:l9 & F3:bottom & F3:side:l12]": "vertex.plane_three_points vertex.point", + "Body1/v[F3:bottom & F3:side:l12 & F3:side:l5]": "vertex.plane_three_points vertex.point", + "design": "load" } } } diff --git a/tests/action_matrix/designs/logo-up-down-join.solidcraft b/tests/action_matrix/designs/logo-up-down-join.solidcraft new file mode 100755 index 00000000..7ab9c5da Binary files /dev/null and b/tests/action_matrix/designs/logo-up-down-join.solidcraft differ diff --git a/tests/action_matrix/designs/up-down-join.json b/tests/action_matrix/designs/up-down-join.json new file mode 100644 index 00000000..f0ece523 --- /dev/null +++ b/tests/action_matrix/designs/up-down-join.json @@ -0,0 +1,11 @@ +{"description": "One sketch of four squares drawn edge to edge, three extruded up and three down from it and joined: every side wall spans both extrudes or runs along two coincident sketch lines.", + "commands": [ + {"command": "sketch.create", "params": {"plane": "XY", "name": "Squares"}}, + {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [10, 10]}}, + {"command": "sketch.rectangle.two_point", "params": {"p0": [-10, 0], "p1": [0, 10]}}, + {"command": "sketch.rectangle.two_point", "params": {"p0": [0, -10], "p1": [10, 0]}}, + {"command": "sketch.rectangle.two_point", "params": {"p0": [-10, -10], "p1": [0, 0]}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"sketch": "Squares", "profiles": [{"point": [-5, 5]}, {"point": [-5, -5]}, {"point": [5, -5]}], "distance": 10}}, + {"command": "solid.extrude", "params": {"sketch": "Squares", "profiles": [{"point": [5, 5]}, {"point": [5, -5]}, {"point": [-5, 5]}], "distance": 10, "direction": "negative", "operation": "join"}} + ]}