Fix face Extrude Join targets in assemblies (#1375)
A new face-based Extrude Join (and outward Press Pull) joins the picked face's body by default and stores that scope, so an edge-touching neighbour is no longer tried first or absorbed. Explicit targets and participants still win. Related to #226.
This commit is contained in:
1 parent
4674f0a61c
commit
8a6332be83
10 files changed
+258
-19
No files matched your search
@@ -21,6 +21,8 @@ offsets and plastic face placements, XZ (+Y normal) with a v6 migration preservi
|
||||
designs, interactive angle placement, file-picker sketch imports, Home/recent controls,
|
||||
toolbar overflow, and the remaining component-frame and assembly WIP work. The enclosure
|
||||
acceptance scenarios now include board resizes; STEP reopening is automated. See [WIP reconciliation](docs/wip-reconciliation.md).
|
||||
Face-based Extrude Join (including outward Press Pull) now defaults to the picked body,
|
||||
so edge-touching assembly neighbours stay separate regardless of body order (#226).
|
||||
|
||||
In scope, in priority order:
|
||||
1. SOLID and SKETCH (now).
|
||||
|
||||
@@ -128,8 +128,9 @@ fn xy(v: Vec2) -> Value {
|
||||
}
|
||||
|
||||
/// Start a sketch on the planar face at `point` holding a copy of the face boundary. Returns
|
||||
/// the sketch id and a point inside the face (sketch coordinates). The sketch is left active.
|
||||
pub(super) fn sketch_of_face(s: &mut Session, point: Vec3, cmd: &str, face_body: Option<&str>) -> Result<(u64, Vec2)> {
|
||||
/// the sketch id, a point inside the face (sketch coordinates), and the picked body's name.
|
||||
/// The sketch is left active.
|
||||
pub(super) fn sketch_of_face(s: &mut Session, point: Vec3, cmd: &str, face_body: Option<&str>) -> Result<(u64, Vec2, String)> {
|
||||
let none = || match face_body {
|
||||
Some(b) => bad(cmd, format!("no face of `{b}` at `face`")),
|
||||
None => bad(cmd, "no body face at `face`"),
|
||||
@@ -199,7 +200,7 @@ pub(super) fn sketch_of_face(s: &mut Session, point: Vec3, cmd: &str, face_body:
|
||||
// The face's edges came in as linked projections: they follow the face when earlier
|
||||
// features change, so nothing is copied.
|
||||
if r.get("projected").is_some_and(|x| !x.is_null()) {
|
||||
return Ok((sid, plane.to_local(author_inside)));
|
||||
return Ok((sid, plane.to_local(author_inside), body));
|
||||
}
|
||||
for e in &edges {
|
||||
let pts: Vec<Vec2> = e.iter().map(|p| plane.to_local(*p)).collect();
|
||||
@@ -237,9 +238,13 @@ pub(super) fn sketch_of_face(s: &mut Session, point: Vec3, cmd: &str, face_body:
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok((sid, plane.to_local(author_inside)))
|
||||
Ok((sid, plane.to_local(author_inside), body))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "face_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "face_join_tests.rs"]
|
||||
mod join_tests;
|
||||
@@ -0,0 +1,127 @@
|
||||
//! #226: a face's outward extrusion must not join an assembly neighbour it only touches.
|
||||
#![allow(clippy::unwrap_used, clippy::panic)]
|
||||
|
||||
use crate::Session;
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_doc::{Document, FeatureKind};
|
||||
use solidcraft_geom::Vec3;
|
||||
|
||||
fn run(s: &mut Session, command: &str, params: Value) -> Value {
|
||||
s.execute(command, ¶ms).unwrap_or_else(|error| panic!("{command}: {error}"))
|
||||
}
|
||||
|
||||
fn assembly(cap_first: bool) -> Session {
|
||||
let mut s = Session::default();
|
||||
let wall = json!({"length":40,"width":30,"height":20,"body_name":"Wall"});
|
||||
let cap = json!({"corner":[0,0,20],"length":40,"width":30,"height":5,"body_name":"Cap"});
|
||||
for params in if cap_first { [cap, wall] } else { [wall, cap] } {
|
||||
run(&mut s, "solid.box", params);
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn check_wall(s: &Session, count: usize) {
|
||||
assert!(s.model.results.iter().all(|result| result.error.is_none() && result.warning.is_none()), "{:?}", s.model.results);
|
||||
let state = s.model.state();
|
||||
assert_eq!(state.bodies.len(), count);
|
||||
let wall = state.body("Wall").unwrap();
|
||||
let measure = solidcraft_kernel::measure(&wall.body).unwrap();
|
||||
assert!((measure.volume - 30_000.0).abs() < 1e-6, "{measure:?}");
|
||||
assert!(measure.bbox.max.dist(Vec3::new(50.0, 30.0, 20.0)) < 1e-9);
|
||||
assert!(wall.body.contains(Vec3::new(49.0, 15.0, 10.0)));
|
||||
let cap = solidcraft_kernel::measure(&state.body("Cap").unwrap().body).unwrap();
|
||||
assert!((cap.volume - 6000.0).abs() < 1e-6);
|
||||
assert!(cap.bbox.max.dist(Vec3::new(40.0, 30.0, 25.0)) < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_join_and_press_pull_leave_the_edge_touching_cap_in_either_body_order() {
|
||||
for cap_first in [true, false] {
|
||||
for operation in ["join", "auto", "press_pull"] {
|
||||
let mut s = assembly(cap_first);
|
||||
let before = s.model.state();
|
||||
let params = json!({"face":[40,15,10],"distance":10,"operation":operation});
|
||||
let command = if operation == "press_pull" { "solid.press_pull" } else { "solid.extrude" };
|
||||
let result = run(&mut s, command, params);
|
||||
assert_eq!(result["changed_bodies"], json!(["Wall"]));
|
||||
assert_eq!(result["removed_bodies"], json!([]));
|
||||
assert!(s.model.state().body("Cap").unwrap().body.same(&before.body("Cap").unwrap().body));
|
||||
check_wall(&s, 2);
|
||||
let saved = s.doc.to_json();
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(s.model.state().body("Wall").unwrap().body.same(&before.body("Wall").unwrap().body));
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
assert_eq!(s.doc.to_json(), saved);
|
||||
check_wall(&Session::new(Document::from_json(&saved).unwrap()), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_join_respects_explicit_targets_and_participants() {
|
||||
for key in ["targets", "participants"] {
|
||||
for all in [false, true] {
|
||||
let mut s = assembly(!all);
|
||||
let mut params = json!({"face":[40,15,10],"distance":10,"operation":"join"});
|
||||
// Joining the wall first permits the cap's full base face to join afterwards.
|
||||
params[key] = if all { json!(["*"]) } else { json!(["Wall", "Cap"]) };
|
||||
run(&mut s, "solid.extrude", params);
|
||||
let state = s.model.state();
|
||||
assert_eq!(state.bodies.len(), 1, "{key}");
|
||||
assert!((solidcraft_kernel::measure(&state.body("Wall").unwrap().body).unwrap().volume - 36_000.0).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_join_maps_the_picked_instance_body_and_point_together() {
|
||||
let mut s = Session::default();
|
||||
let component = run(&mut s, "component.create", json!({"name":"Repeated"}))["component"].as_u64().unwrap();
|
||||
run(&mut s, "solid.box", json!({"length":40,"width":30,"height":20,"body_name":"Wall"}));
|
||||
run(&mut s, "component.activate", json!({"component":0}));
|
||||
let occurrence = run(&mut s, "occurrence.copy", json!({"component":component,"translate":[100,0,0]}))["occurrence"].as_u64().unwrap();
|
||||
run(&mut s, "component.activate", json!({"component":component,"occurrence":occurrence}));
|
||||
let world = s.world_state();
|
||||
let body = world.bodies.iter().find(|body| body.body.contains(Vec3::new(139.0, 15.0, 10.0))).unwrap().name.clone();
|
||||
assert_ne!(body, "Wall", "exercise an instance body name");
|
||||
let result = run(&mut s, "solid.extrude", json!({"face":[140,15,10],"face_body":body,"distance":10,"operation":"join"}));
|
||||
let FeatureKind::Extrude { targets, .. } = &s.doc.feature(result["feature"].as_u64().unwrap()).unwrap().kind else { panic!("extrude") };
|
||||
assert_eq!(targets, &["Wall"]);
|
||||
let world = s.world_state();
|
||||
assert_eq!(world.bodies.len(), 2);
|
||||
for point in [Vec3::new(49.0, 15.0, 10.0), Vec3::new(149.0, 15.0, 10.0)] {
|
||||
assert!(world.bodies.iter().any(|body| body.body.contains(point)), "{point:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_join_of_a_step_import_keeps_all_32_neighbours_unchanged() {
|
||||
let mut source = assembly(true);
|
||||
for i in 0..31 {
|
||||
run(&mut source, "solid.box", json!({"corner":[100+20*i,100,0],"length":5,"width":5,"height":5,"body_name":format!("Distant{i}")}));
|
||||
}
|
||||
let step = solidcraft_io::export(&source.model.state(), &[], solidcraft_io::Format::Step, "Original assembly").unwrap();
|
||||
let imported = solidcraft_io::step_import_feature(&step, "original-assembly.step").unwrap();
|
||||
let mut doc = Document::new("Imported assembly");
|
||||
let feature = doc.add_feature(imported.kind, None).unwrap();
|
||||
doc.feature_mut(feature).unwrap().body_names = imported.body_names;
|
||||
let mut s = Session::new(doc);
|
||||
assert_eq!(s.doc.features.len(), 1);
|
||||
let before = s.model.state();
|
||||
let result = run(&mut s, "solid.extrude", json!({"face":[40,15,10],"distance":"10 mm","direction":"positive","operation":"join"}));
|
||||
assert_eq!(result["changed_bodies"], json!(["Wall"]));
|
||||
check_wall(&s, 33);
|
||||
let reopened = Session::new(Document::from_json(&s.doc.to_json()).unwrap());
|
||||
check_wall(&reopened, 33);
|
||||
for body in before.bodies.iter().filter(|body| body.name != "Wall") {
|
||||
let after = s.model.state();
|
||||
assert!(body.body.same(&after.body(&body.name).unwrap().body), "{}", body.name);
|
||||
let cold = reopened.model.state();
|
||||
let original = solidcraft_kernel::measure(&body.body).unwrap();
|
||||
let restored = solidcraft_kernel::measure(&cold.body(&body.name).unwrap().body).unwrap();
|
||||
assert!((original.volume - restored.volume).abs() < 1e-9, "{}", body.name);
|
||||
assert!(original.bbox.min.dist(restored.bbox.min) < 1e-9 && original.bbox.max.dist(restored.bbox.max) < 1e-9);
|
||||
}
|
||||
let FeatureKind::Extrude { targets, .. } = &s.doc.feature(result["feature"].as_u64().unwrap()).unwrap().kind else { panic!("extrude") };
|
||||
assert_eq!(targets, &["Wall"]);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("extrude")
|
||||
.key("E")
|
||||
.params("distance: expr (or through_all: true); taper?: angle expr; sketch?: id|name (default: active or last sketch); profiles?: all | [index] | [{curves: [ids]}] (the profile whose outer loop is made of exactly these curves, or pieces of them; found again on every recompute, so it follows parameter changes; islands inside are its holes; an error when no profile or several match) | [{point:[x,y]}] (index and point picks are stored by their curves when that finds the same profile); face?: [x,y,z] (extrude a planar body face instead of a sketch profile); face_body?: name (whose face, where faces of several bodies meet at the point); direction?: positive|negative|symmetric (distance is total thickness, split evenly around the start plane); convention?: total_distance|legacy_per_side (default total_distance; legacy preserves older symmetric extents); distance2?; start_offset?: expr (along the sketch normal, also with direction: negative); to?: [x,y,z] (To Object: a face, vertex or point; to_offset?: expr past it); from?: [x,y,z] (From Object: start at that face or point); operation?: new|join|cut|intersect|auto (cut into a body, otherwise join existing solids; new in an empty component); targets?: [body]; name?; body_name?; thin?: true (Thin: a wall along sketch curves instead of the profiles) with curves?: [curve ids] (open chains and closed loops, joined where they meet end to end; default: every drawn curve of the sketch), thickness: expr, side?: center|inside|outside|two_sides (a closed loop's inside is the area it encloses, an open chain's the side it bends toward, a straight line's the left of its first curve as drawn; default center), thickness2?: expr (two_sides: the thickness outside); corners are mitred, crossing walls are one"),
|
||||
.params("distance: expr (or through_all: true); taper?: angle expr; sketch?: id|name (default: active or last sketch); profiles?: all | [index] | [{curves: [ids]}] (the profile whose outer loop is made of exactly these curves, or pieces of them; found again on every recompute, so it follows parameter changes; islands inside are its holes; an error when no profile or several match) | [{point:[x,y]}] (index and point picks are stored by their curves when that finds the same profile); face?: [x,y,z] (extrude a planar body face instead of a sketch profile); face_body?: name (whose face, where faces of several bodies meet at the point); direction?: positive|negative|symmetric (distance is total thickness, split evenly around the start plane); convention?: total_distance|legacy_per_side (default total_distance; legacy preserves older symmetric extents); distance2?; start_offset?: expr (along the sketch normal, also with direction: negative); to?: [x,y,z] (To Object: a face, vertex or point; to_offset?: expr past it); from?: [x,y,z] (From Object: start at that face or point); operation?: new|join|cut|intersect|auto (cut into a body, otherwise join existing solids; new in an empty component); targets?: [body] (Join from a face defaults to that face's body; explicit targets or participants override); name?; body_name?; thin?: true (Thin: a wall along sketch curves instead of the profiles) with curves?: [curve ids] (open chains and closed loops, joined where they meet end to end; default: every drawn curve of the sketch), thickness: expr, side?: center|inside|outside|two_sides (a closed loop's inside is the area it encloses, an open chain's the side it bends toward, a straight line's the left of its first curve as drawn; default center), thickness2?: expr (two_sides: the thickness outside); corners are mitred, crossing walls are one"),
|
||||
CommandSpec::new("solid.revolve", "Revolve", revolve)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("revolve")
|
||||
@@ -514,12 +514,12 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) ->
|
||||
fn extrude(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "solid.extrude";
|
||||
// A planar body face extrudes like a profile: its boundary is copied into a sketch on it.
|
||||
let (sketch, face_profile) = match p.get("face").and_then(vec3) {
|
||||
let (sketch, face_profile, face_body) = match p.get("face").and_then(vec3) {
|
||||
Some(fp) => {
|
||||
let (id, inside) = super::face::sketch_of_face(s, fp, cmd, str_(p, "face_body"))?;
|
||||
(id, Some(ProfileSel::Points { points: vec![inside] }))
|
||||
let (id, inside, body) = super::face::sketch_of_face(s, fp, cmd, str_(p, "face_body"))?;
|
||||
(id, Some(ProfileSel::Points { points: vec![inside] }), Some(body))
|
||||
}
|
||||
None => (feature_sketch(s, p, cmd)?, None),
|
||||
None => (feature_sketch(s, p, cmd)?, None, None),
|
||||
};
|
||||
let extent = extent_param(s, p, cmd)?;
|
||||
let thin = match bool_(p, "thin") {
|
||||
@@ -532,6 +532,17 @@ fn extrude(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
_ if p.get("thin").is_some_and(|v| !v.is_boolean()) => return Err(bad(cmd, "`thin` must be true or false")),
|
||||
_ => None,
|
||||
};
|
||||
let operation = match str_(p, "operation") {
|
||||
Some(o) if o.eq_ignore_ascii_case("auto") => Operation::parse(auto_operation(s, p).unwrap_or("new")).unwrap_or(Operation::NewBody),
|
||||
_ => operation(p, cmd)?,
|
||||
};
|
||||
let mut targets = string_list(p, "targets");
|
||||
if operation == Operation::Join && targets.is_empty() && string_list(p, "participants").is_empty() {
|
||||
// A face's outward extension joins that body. Assembly neighbours can touch only an
|
||||
// edge of the tool: trying to join them first fails, or absorbs unrelated parts (#226).
|
||||
// Store the resolved target so recomputes and reopening keep the same scope.
|
||||
targets.extend(face_body);
|
||||
}
|
||||
let kind = FeatureKind::Extrude {
|
||||
sketch,
|
||||
profiles: match face_profile {
|
||||
@@ -540,11 +551,8 @@ fn extrude(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
None => profiles(p, cmd)?,
|
||||
},
|
||||
extent,
|
||||
operation: match str_(p, "operation") {
|
||||
Some(o) if o.eq_ignore_ascii_case("auto") => Operation::parse(auto_operation(s, p).unwrap_or("new")).unwrap_or(Operation::NewBody),
|
||||
_ => operation(p, cmd)?,
|
||||
},
|
||||
targets: string_list(p, "targets"),
|
||||
operation,
|
||||
targets,
|
||||
thin,
|
||||
};
|
||||
add_feature(s, p, kind)
|
||||
@@ -659,7 +667,7 @@ fn revolve(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// A planar body face revolves like a profile (its edges projected into a sketch on it).
|
||||
let (sketch, face_profile) = match p.get("face").and_then(vec3) {
|
||||
Some(fp) => {
|
||||
let (id, inside) = super::face::sketch_of_face(s, fp, cmd, str_(p, "face_body"))?;
|
||||
let (id, inside, _) = super::face::sketch_of_face(s, fp, cmd, str_(p, "face_body"))?;
|
||||
s.active_sketch = None;
|
||||
(id, Some(ProfileSel::Points { points: vec![inside] }))
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ use K::*;
|
||||
/// The parameters each command takes in world coordinates.
|
||||
fn table(id: &str) -> &'static [(&'static str, K)] {
|
||||
match id {
|
||||
"solid.extrude" => &[("face", Pick), ("from", Make), ("to", Make), ("targets", Body), ("participants", Body)],
|
||||
"solid.extrude" => &[("face", Pick), ("face_body", Body), ("from", Make), ("to", Make), ("targets", Body), ("participants", Body)],
|
||||
"solid.revolve" => &[("face", Pick), ("axis", Axis), ("to", Make), ("targets", Body), ("participants", Body)],
|
||||
"solid.box" | "solid.sphere" => &[("corner", Make), ("center", Make)],
|
||||
"solid.cylinder" | "solid.coil" | "solid.torus" => &[("base", Make), ("center", Make), ("axis", Dir)],
|
||||
@@ -390,7 +390,7 @@ pub fn to_local(s: &Session, id: &str, params: &Value) -> Option<Value> {
|
||||
command_frames.sketch = frame;
|
||||
}
|
||||
let mut m = Mapper { s, f: command_frames, first: None, changed: false, here: false };
|
||||
let body = obj.get("body").and_then(Value::as_str).map(str::to_string);
|
||||
let body = obj.get("body").or_else(|| obj.get("face_body")).and_then(Value::as_str).map(str::to_string);
|
||||
let mut out: Map<String, Value> = obj.clone();
|
||||
// Picks first: their frame turns the directions.
|
||||
for pass in [true, false] {
|
||||
|
||||
@@ -3395,8 +3395,10 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
out.push(("solid.extrude".to_string(), with(json!({"sketch": sketch, "profiles": idx}))));
|
||||
}
|
||||
// Each planar body face extrudes on its own.
|
||||
for p in face_points(0) {
|
||||
out.push(("solid.extrude".to_string(), with(json!({ "face": p }))));
|
||||
for pick in sels(d, 0) {
|
||||
if let Sel::Face { body, point, .. } = pick {
|
||||
out.push(("solid.extrude".to_string(), with(json!({"face": pt(*point), "face_body": body}))));
|
||||
}
|
||||
}
|
||||
return Ok(out);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,24 @@ use solidcraft_engine::{Sel, Session};
|
||||
use crate::dialogs::{Dialog, Kind, apply_commands};
|
||||
use crate::{Services, SolveApp};
|
||||
|
||||
#[test]
|
||||
fn a_face_extrude_keeps_the_picked_body_when_faces_coincide() {
|
||||
let mut app = app_with(json!([
|
||||
{"command":"solid.box","params":{"length":40,"width":30,"height":20,"body_name":"Other"}},
|
||||
{"command":"solid.box","params":{"length":40,"width":30,"height":20,"body_name":"Wall"}}
|
||||
]));
|
||||
let before = app.session.model.state();
|
||||
let pick = Sel::Face { body: "Wall".into(), index: 2, point: Vec3::new(40.0, 15.0, 10.0) };
|
||||
let d = start(&mut app, vec![pick], "solid.extrude");
|
||||
let (command, params) = apply_commands(&app, &d).unwrap().remove(0);
|
||||
assert_eq!(params["face_body"], json!("Wall"));
|
||||
app.session.execute(&command, ¶ms).unwrap();
|
||||
let after = app.session.model.state();
|
||||
assert_eq!(after.bodies.len(), 2);
|
||||
assert!(after.body("Other").unwrap().body.same(&before.body("Other").unwrap().body));
|
||||
assert!(after.body("Wall").unwrap().body.contains(Vec3::new(49.0, 15.0, 10.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_plane_click_retains_source_and_previews_then_edits_in_place() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{"note":"#226: a cap created before a wall touches only the edge of the wall's outward extrusion. Picking the wall face in Extrude must extend Wall and preserve Cap as a separate body."},
|
||||
{"call":"engine.execute","params":{"command":"solid.box","params":{"corner":[0,0,20],"length":40,"width":30,"height":5,"body_name":"Cap"}}},
|
||||
{"call":"engine.execute","params":{"command":"solid.box","params":{"length":40,"width":30,"height":20,"body_name":"Wall"}}},
|
||||
{"call":"ui.camera","params":{"target":[20,15,12.5],"yaw":-0.785398,"pitch":0.61548,"distance":110}},
|
||||
{"start":"solid.extrude"},
|
||||
{"click":{"world":[40,15,10]}},
|
||||
{"text":"10"},
|
||||
{"key":"Enter"},
|
||||
{"expect":{"dialog":null,"bodies":2,"errors":0,"body_volume":{"body":"Wall","volume":30000},"bbox":{"body":"Wall","min":[0,0,0],"max":[50,30,20]}}},
|
||||
{"expect":{"body_volume":{"body":"Cap","volume":6000},"bbox":{"body":"Cap","min":[0,0,20],"max":[40,30,25]}}},
|
||||
{"shot":"face_join_keeps_cap"}
|
||||
]
|
||||
@@ -11620,6 +11620,63 @@
|
||||
"Body1/v[F6:end & F6:side:l7 & F6:side:l8]": "vertex.plane_three_points vertex.point",
|
||||
"design": "load"
|
||||
},
|
||||
"typical/multi-body-face-extrude": {
|
||||
"Cap/F1:+x": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Cap/F1:+x|F1:+y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Cap/F1:+x|F1:+z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Cap/F1:+x|F1:-y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Cap/F1:+x|F1:-z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Cap/F1:+y": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Cap/F1:+y|F1:+z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Cap/F1:+y|F1:-x": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Cap/F1:+y|F1:-z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Cap/F1:+z": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Cap/F1:+z|F1:-x": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Cap/F1:+z|F1:-y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Cap/F1:-x": "face.angle_plane face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Cap/F1:-x|F1:-y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Cap/F1:-x|F1:-z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Cap/F1:-y": "face.angle_plane face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Cap/F1:-y|F1:-z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Cap/F1:-z": "face.angle_plane face.draft_neutral !face.extrude face.hole face.offset_plane !face.press_pull face.shell face.sketch",
|
||||
"Cap/body": "body.copy body.mirror body.move body.pattern",
|
||||
"Cap/v[F1:+x & F1:+y & F1:+z]": "vertex.plane_three_points vertex.point",
|
||||
"Cap/v[F1:+x & F1:+y & F1:-z]": "vertex.plane_three_points vertex.point",
|
||||
"Cap/v[F1:+x & F1:+z & F1:-y]": "vertex.plane_three_points vertex.point",
|
||||
"Cap/v[F1:+x & F1:-y & F1:-z]": "vertex.plane_three_points vertex.point",
|
||||
"Cap/v[F1:+y & F1:+z & F1:-x]": "vertex.plane_three_points vertex.point",
|
||||
"Cap/v[F1:+y & F1:-x & F1:-z]": "vertex.plane_three_points vertex.point",
|
||||
"Cap/v[F1:+z & F1:-x & F1:-y]": "vertex.plane_three_points vertex.point",
|
||||
"Cap/v[F1:-x & F1:-y & F1:-z]": "vertex.plane_three_points vertex.point",
|
||||
"Wall/F2:+x": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Wall/F2:+x|F2:+y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Wall/F2:+x|F2:+z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Wall/F2:+x|F2:-y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Wall/F2:+x|F2:-z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Wall/F2:+y": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Wall/F2:+y|F2:+z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Wall/F2:+y|F2:-x": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Wall/F2:+y|F2:-z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Wall/F2:+z": "face.angle_plane face.draft_neutral !face.extrude face.hole face.midplane face.offset_plane !face.press_pull face.shell face.sketch",
|
||||
"Wall/F2:+z|F2:-x": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Wall/F2:+z|F2:-y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Wall/F2:-x": "face.angle_plane face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Wall/F2:-x|F2:-y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Wall/F2:-x|F2:-z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Wall/F2:-y": "face.angle_plane face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Wall/F2:-y|F2:-z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Wall/F2:-z": "face.angle_plane face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Wall/body": "body.copy body.mirror body.move body.pattern",
|
||||
"Wall/v[F2:+x & F2:+y & F2:+z]": "vertex.plane_three_points vertex.point",
|
||||
"Wall/v[F2:+x & F2:+y & F2:-z]": "vertex.plane_three_points vertex.point",
|
||||
"Wall/v[F2:+x & F2:+z & F2:-y]": "vertex.plane_three_points vertex.point",
|
||||
"Wall/v[F2:+x & F2:-y & F2:-z]": "vertex.plane_three_points vertex.point",
|
||||
"Wall/v[F2:+y & F2:+z & F2:-x]": "vertex.plane_three_points vertex.point",
|
||||
"Wall/v[F2:+y & F2:-x & F2:-z]": "vertex.plane_three_points vertex.point",
|
||||
"Wall/v[F2:+z & F2:-x & F2:-y]": "vertex.plane_three_points vertex.point",
|
||||
"Wall/v[F2:-x & F2:-y & F2:-z]": "vertex.plane_three_points vertex.point",
|
||||
"design": "load"
|
||||
},
|
||||
"typical/multi-body-join-cut": {
|
||||
"Block/F1:+x": "face.angle_plane !face.draft_neutral face.extrude face.extrude+edit face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Block/F1:+x|F1:+y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.normal_plane+edit edge.project",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Issue #226: the upper cap precedes the wall in body order and touches only the edge of a 10 mm outward extrusion from the wall's x=40 face.",
|
||||
"commands": [
|
||||
{"command":"solid.box","params":{"corner":[0,0,20],"length":40,"width":30,"height":5,"body_name":"Cap"}},
|
||||
{"command":"solid.box","params":{"length":40,"width":30,"height":20,"body_name":"Wall"}}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user