mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
doc: an extrude's taper without a corner rule picks one per profile
Extent.corner is optional: unset, each profile preserves curvature where every curve is a line or an arc and takes the tangent rule where another curve has a corner, with a note in the feature's warning; a rule the user gave is kept. draft_corners reports not_exact as the kernel builds (any free curve in a cornered profile). Builds (release CLI); cases not yet run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
@@ -274,10 +274,6 @@ impl ExtrudeConvention {
|
||||
}
|
||||
}
|
||||
|
||||
fn draft_corner_is_default(c: &solidcraft_kernel::DraftCorner) -> bool {
|
||||
*c == solidcraft_kernel::DraftCorner::default()
|
||||
}
|
||||
|
||||
/// Extent of an extrude.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)]
|
||||
pub struct Extent {
|
||||
@@ -305,10 +301,13 @@ pub struct Extent {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[walk(angle = "Taper angle")]
|
||||
pub taper: Option<Expr>,
|
||||
/// How a taper closes the profile's sharp corners ([`solidcraft_kernel::draft_corners`] says
|
||||
/// what each rule gives at each corner).
|
||||
#[serde(default, skip_serializing_if = "draft_corner_is_default")]
|
||||
pub corner: solidcraft_kernel::DraftCorner,
|
||||
/// How a taper closes the profile's sharp corners, when the user chose
|
||||
/// ([`solidcraft_kernel::draft_corners`] says what each rule gives at each corner). Without
|
||||
/// it each profile takes preserve curvature where the kernel can build it exactly and the
|
||||
/// tangent rule where a curve that is neither a line nor an arc has a corner, and the
|
||||
/// feature's warning says so.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub corner: Option<solidcraft_kernel::DraftCorner>,
|
||||
/// To Object: end at the face (or vertex/point) at this point, instead of `distance`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub to: Option<FacePick>,
|
||||
|
||||
+35
-5
@@ -2457,6 +2457,28 @@ pub fn draft_corner_options(ss: &SolvedSketch, sel: &ProfileSel, distance: f64,
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The corner rule a taper takes for profile `r` of `ss` when the user chose none: preserve
|
||||
/// curvature, unless a corner of the profile cannot keep it exactly (a curve that is neither a
|
||||
/// line nor an arc), then the tangent rule, with a note in `notes` saying where and why.
|
||||
fn draft_corner_for(ss: &SolvedSketch, r: &Region2, distance: f64, taper: f64, notes: &mut Vec<String>) -> Result<kernel::DraftCorner> {
|
||||
let reports = kernel::draft_corners(r, distance, taper)?;
|
||||
let inexact = reports.iter().find(|c| {
|
||||
c.options.iter().any(|o| o.rule == kernel::DraftCorner::PreserveCurvature && o.fill == Err(kernel::CornerRefusal::NotExact))
|
||||
});
|
||||
Ok(match inexact {
|
||||
Some(c) => {
|
||||
notes.push(format!(
|
||||
"the taper's corners of profile at ({}, {}) in {} use the tangent rule: a curve of it is neither a line nor an arc, so its walls cannot continue as themselves exactly; corner: round also builds",
|
||||
show_mm(c.at.x),
|
||||
show_mm(c.at.y),
|
||||
ss.name
|
||||
));
|
||||
kernel::DraftCorner::Tangent
|
||||
}
|
||||
None => kernel::DraftCorner::PreserveCurvature,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_profiles(ss: &SolvedSketch, sel: &ProfileSel) -> Result<Vec<Region2>> {
|
||||
let ps = &ss.profiles;
|
||||
if ps.is_empty() {
|
||||
@@ -4466,6 +4488,7 @@ fn feature_tools_with_constructor(
|
||||
st: &ModelState,
|
||||
constructor: &mut Option<ExtrudeConstructor>,
|
||||
to_body: &mut Option<String>,
|
||||
warning: &mut Option<String>,
|
||||
) -> Result<Vec<Traced>> {
|
||||
*to_body = None;
|
||||
let unnamed = |bodies: Vec<Body>| -> Vec<Traced> { bodies.into_iter().map(Traced::unnamed).collect() };
|
||||
@@ -4553,15 +4576,22 @@ fn feature_tools_with_constructor(
|
||||
];
|
||||
// How far a tool grows past its profile, at most: its sides' run over its length.
|
||||
let growth = len * taper.tan().abs();
|
||||
let mut notes = Vec::new();
|
||||
let tools: Vec<Traced> = regions
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let roles =
|
||||
crate::lineage::ProfileRoles { sketch: Some(&ss), region: Some(r), caps: caps.clone(), thin: thin_roles.as_ref() };
|
||||
let body = kernel::extrude_tapered_between(&start_plane, r, 0.0, len, 0.0, sign, taper, extent.corner)?;
|
||||
let roles = crate::lineage::ProfileRoles { sketch: Some(&ss), region: Some(r), caps: caps.clone(), thin: thin_roles.as_ref() };
|
||||
let corner = match extent.corner {
|
||||
Some(c) => c,
|
||||
None => draft_corner_for(&ss, r, len, taper, &mut notes)?,
|
||||
};
|
||||
let body = kernel::extrude_tapered_between(&start_plane, r, 0.0, len, 0.0, sign, taper, corner)?;
|
||||
Ok(roles.tool(body, None))
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
for note in notes {
|
||||
append_warning(warning, note);
|
||||
}
|
||||
// Profiles apart by more than the tools grow (the letters of a text): the tools
|
||||
// are apart too, and go into the body as one, in one boolean rather than one per
|
||||
// profile, each on a body grown or cut by the ones before. (A join meeting several
|
||||
@@ -5898,7 +5928,7 @@ fn replay(
|
||||
// points follow their named faces, as the feature's own did (#2121): a To Object
|
||||
// extrude's copies reach the face where it is now, not where it was picked.
|
||||
let moved = with_named_face_points(doc, st, src).map(|(g, _)| g);
|
||||
let tools = feature_tools_with_constructor(doc, vals, moved.as_ref().unwrap_or(src), st, &mut None, &mut None)?;
|
||||
let tools = feature_tools_with_constructor(doc, vals, moved.as_ref().unwrap_or(src), st, &mut None, &mut None, &mut None)?;
|
||||
for (k, m) in mats.iter().enumerate() {
|
||||
let start = copies.len();
|
||||
for t in &tools {
|
||||
@@ -6426,7 +6456,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
| FeatureKind::Sweep { operation, targets, .. } => {
|
||||
let mut constructor = None;
|
||||
let mut to_body = None;
|
||||
let tools = feature_tools_with_constructor(doc, vals, f, st, &mut constructor, &mut to_body)?;
|
||||
let tools = feature_tools_with_constructor(doc, vals, f, st, &mut constructor, &mut to_body, warning)?;
|
||||
// Cuts through sheet metal become cut-outs of its flat pattern.
|
||||
if matches!(operation, Operation::Cut | Operation::Intersect) && matches!(f.kind, FeatureKind::Extrude { .. }) && !st.sheets.is_empty() {
|
||||
let bodies: Vec<Body> = tools.iter().map(|t| t.body.clone()).collect();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! An extrude's taper saves how it closes the profile's sharp corners: `extent.corner`,
|
||||
//! `tangent` or `round` (each offset curve continued straight along its end tangent, or an arc
|
||||
//! about the corner); absent for `preserve_curvature`, the default, where each wall continues as
|
||||
//! itself until it meets its neighbour. An older design has none and takes the default, the rule
|
||||
//! its tapers were built with before SMLib (the walls meeting where the curves' offsets cross). The
|
||||
//! last release refuses a design with this change (the newer format number).
|
||||
//! An extrude's taper saves how it closes the profile's sharp corners when the user chose:
|
||||
//! `extent.corner`, `preserve_curvature` (each wall continues as itself until it meets its
|
||||
//! neighbour), `tangent` (each offset curve continued straight along its end tangent) or `round`
|
||||
//! (an arc about the corner). Without it each profile preserves curvature where every curve is a
|
||||
//! line or an arc and takes the tangent rule where another curve has a corner, the feature's
|
||||
//! warning saying so; an older design has none and gets that choice. The last release refuses a
|
||||
//! design with this change (the newer format number).
|
||||
|
||||
@@ -25,7 +25,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.key("E")
|
||||
.contract(&EXTRUDE_CONTRACT)
|
||||
.output(output_schema_for::<FeatureResult>)
|
||||
.params("distance: expr (or through_all: true); taper?: angle expr (positive: outward, so the far end is larger along the extrusion direction; negative: inward, so the far end is smaller); corner?: preserve_curvature|tangent|round (how a taper closes a sharp corner its offset opens: each wall continues as itself, a line's plane or an arc's cone, until it meets its neighbour; each continues straight along its end tangent; or an arc about the corner. Default preserve_curvature, which needs lines and arcs at the corners); 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; a pick on a drawn profile that reference geometry, such as the projected outline of the face the sketch is on, cuts into regions takes the whole drawn profile); exact_regions?: bool (each index or point pick is exactly the region it names, as the dialogs send); default: every drawn profile, except that a loop inside another is its hole and a loop inside that hole is material again; face?: [x,y,z] (extrude a planar body face instead of a sketch profile); faces?: [[x,y,z],…] (coplanar faces of one body); 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?: expr (Two Sides: a second distance the other way from the first side, which may be through_all or to; with direction: negative the first side goes against the normal and distance2 along it); 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, or from a sketch on a body's 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"),
|
||||
.params("distance: expr (or through_all: true); taper?: angle expr (positive: outward, so the far end is larger along the extrusion direction; negative: inward, so the far end is smaller); corner?: preserve_curvature|tangent|round (how a taper closes a sharp corner its offset opens: each wall continues as itself, a line's plane or an arc's cone, until it meets its neighbour; each continues straight along its end tangent; or an arc about the corner. Without it, each profile preserves curvature where every curve is a line or an arc and takes tangent where another curve has a corner, and the feature's warning says so; a corner rule given is kept, and refused where the kernel cannot build it); 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; a pick on a drawn profile that reference geometry, such as the projected outline of the face the sketch is on, cuts into regions takes the whole drawn profile); exact_regions?: bool (each index or point pick is exactly the region it names, as the dialogs send); default: every drawn profile, except that a loop inside another is its hole and a loop inside that hole is material again; face?: [x,y,z] (extrude a planar body face instead of a sketch profile); faces?: [[x,y,z],…] (coplanar faces of one body); 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?: expr (Two Sides: a second distance the other way from the first side, which may be through_all or to; with direction: negative the first side goes against the normal and distance2 along it); 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, or from a sketch on a body's 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")
|
||||
@@ -1109,8 +1109,8 @@ pub(super) fn extent_param(s: &Session, p: &Value, cmd: &str) -> Result<Extent>
|
||||
check_expr(s, t, Kind::Angle, cmd, "taper")?;
|
||||
}
|
||||
let corner = match p.get("corner") {
|
||||
None => solidcraft_kernel::DraftCorner::default(),
|
||||
Some(value) => serde_json::from_value(value.clone()).map_err(|_| bad(cmd, "`corner` must be preserve_curvature, tangent or round"))?,
|
||||
None => None,
|
||||
Some(value) => Some(serde_json::from_value(value.clone()).map_err(|_| bad(cmd, "`corner` must be preserve_curvature, tangent or round"))?),
|
||||
};
|
||||
let direction = match str_(p, "direction").map(str::to_ascii_lowercase).as_deref() {
|
||||
None | Some("positive") | Some("one_side") => Direction::Positive,
|
||||
|
||||
@@ -39,7 +39,7 @@ impl DraftCorner {
|
||||
pub enum CornerRefusal {
|
||||
/// The continued curves do not meet (two parallel lines, two circles that part).
|
||||
Apart,
|
||||
/// A curve at the corner is neither a line nor an arc: its wall has no exact continuation.
|
||||
/// A curve of the profile is neither a line nor an arc: its wall has no exact continuation.
|
||||
NotExact,
|
||||
/// A wall at the corner closes before the far end (its arc shrinks to its centre).
|
||||
WallCloses,
|
||||
@@ -97,6 +97,9 @@ pub fn draft_corners(region: &Region2, distance: f64, taper: f64) -> Result<Vec<
|
||||
let segs = &outer.segs;
|
||||
let d = distance * taper.tan();
|
||||
let n = segs.len();
|
||||
// The walls are built as one exact set: a profile with a sharp corner keeps its curvature only
|
||||
// when every curve is a line or an arc.
|
||||
let free = segs.iter().any(|s| matches!(carrier(s, d), Carrier::Free));
|
||||
let mut out = Vec::new();
|
||||
for k in 0..n {
|
||||
let (Some(a), Some(b)) = (segs.get((k + n - 1) % n), segs.get(k)) else { continue };
|
||||
@@ -111,7 +114,7 @@ pub fn draft_corners(region: &Region2, distance: f64, taper: f64) -> Result<Vec<
|
||||
let (ea, eb) = (a.end() + right(ta) * d, b.start() + right(tb) * d);
|
||||
// A left turn opens a gap when the profile grows, a right turn when it shrinks.
|
||||
let gap = (turn > 0.0) == (d > 0.0);
|
||||
let curvature = continued(ca, cb, ea, eb, p, d);
|
||||
let curvature = if free { Err(CornerRefusal::NotExact) } else { continued(ca, cb, ea, eb, p, d) };
|
||||
let options = if gap {
|
||||
[
|
||||
CornerOption { rule: DraftCorner::PreserveCurvature, fill: curvature },
|
||||
|
||||
Reference in New Issue
Block a user