mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-10 18:55:14 +00:00
Sheet metal: a junction cut trimmed away is clipped to the material left, a name follows the surviving stretch (#5927, #5928)
This commit is contained in:
+67
-2
@@ -452,6 +452,56 @@ fn slit_loop(lp: &mut Loop2, at: Vec2, tip: Vec2) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Clip a junction cut to the material that remains when the ends it was computed from were cut
|
||||
/// away: walk the junction line outward from the bend line and end the cut where that line meets the
|
||||
/// outer loop. Returns the two ends to slit between, boundary end first, or None when the line has no
|
||||
/// material outward of the bend line: that cut has nothing left to part, an empty stretch of the
|
||||
/// line rather than a failed lookup.
|
||||
fn clip_junction_slit(lp: &Loop2, at: Vec2, tip: Vec2) -> Option<(Vec2, Vec2)> {
|
||||
let d = tip - at;
|
||||
let len = d.len();
|
||||
if len <= 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let u = d / len;
|
||||
// Where the junction line crosses the boundary, nearest the bend line first. Free-form segments
|
||||
// follow the 0.001 mm chords the planar booleans use; lines keep their exact ends.
|
||||
let mut hits: Vec<(f64, Vec2)> = Vec::new();
|
||||
for seg in &lp.segs {
|
||||
let pts = seg.polyline(1e-3);
|
||||
for w in pts.windows(2) {
|
||||
let (a, b) = (w[0], w[1]);
|
||||
let e = b - a;
|
||||
let den = u.cross(e);
|
||||
if den.abs() <= 1e-12 {
|
||||
continue;
|
||||
}
|
||||
let t = (a - at).cross(e) / den;
|
||||
let s = (at - a).cross(u) / e.cross(u);
|
||||
if t < -1e-9 || !(-1e-9..=1.0 + 1e-9).contains(&s) {
|
||||
continue;
|
||||
}
|
||||
hits.push((t.max(0.0), at + u * t));
|
||||
}
|
||||
}
|
||||
hits.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
hits.dedup_by(|a, b| (a.0 - b.0).abs() <= 1e-7);
|
||||
if lp.contains(at) {
|
||||
// The bend line is in material: the cut runs from it out to the nearest boundary crossing.
|
||||
return hits.first().map(|(_, p)| (*p, at));
|
||||
}
|
||||
// The bend line itself was cut away: the cut spans the stretch of material the line still
|
||||
// crosses, both ends on the boundary.
|
||||
let (pairs, _) = hits.as_chunks::<2>();
|
||||
for [enter, leave] in pairs {
|
||||
let mid = at + u * ((enter.0 + leave.0) / 2.0);
|
||||
if lp.contains(mid) {
|
||||
return Some((enter.1, leave.1));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Split the loop's segment that runs through `p` into two there, so `p` is a vertex of the loop.
|
||||
/// A `p` already at a vertex splits nothing. None when no segment runs through `p`.
|
||||
fn split_loop_at(lp: &mut Loop2, p: Vec2) -> Option<usize> {
|
||||
@@ -2650,8 +2700,23 @@ impl SheetBody {
|
||||
// unchanged: the slit is a zero-width line, so it adds no area and no kernel boolean sees
|
||||
// a self-touching loop.
|
||||
for (at, tip) in self.junction_slits() {
|
||||
for r in &mut regions {
|
||||
if slit_loop(&mut r.outer, at, tip) {
|
||||
if regions.iter_mut().any(|r| slit_loop(&mut r.outer, at, tip)) {
|
||||
continue;
|
||||
}
|
||||
// Neither computed end is a vertex of the material that is left: clip the cut to what
|
||||
// remains, so a required junction cut is never dropped (a notch through the far edge,
|
||||
// for one). A cut with no material outward of the bend line parts nothing, which
|
||||
// `clip_junction_slit` says by returning none: an empty stretch of the line, not a
|
||||
// failed lookup.
|
||||
let holder = regions.iter().position(|r| r.outer.contains(at));
|
||||
let order: Vec<usize> = match holder {
|
||||
Some(h) => std::iter::once(h).chain((0..regions.len()).filter(|&i| i != h)).collect(),
|
||||
None => (0..regions.len()).collect(),
|
||||
};
|
||||
for i in order {
|
||||
if let Some((anchor, inner)) = clip_junction_slit(®ions[i].outer, at, tip)
|
||||
&& slit_loop(&mut regions[i].outer, anchor, inner)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1767,6 +1767,17 @@ fn reorder_features(
|
||||
Ok((next, ordered, broken, Some(after)))
|
||||
}
|
||||
|
||||
/// The edge and stretch (`from`, `to`) a `sheet.flange` segment input's key names:
|
||||
/// `sheet.flange.edge<edge>.stretch<from bits>.<to bits>.<field>`.
|
||||
fn segment_span(key: &str) -> Option<(usize, f64, f64)> {
|
||||
let rest = key.strip_prefix("sheet.flange.edge")?;
|
||||
let mut parts = rest.split('.');
|
||||
let edge: usize = parts.next()?.parse().ok()?;
|
||||
let from = f64::from_bits(parts.next()?.strip_prefix("stretch")?.parse::<u64>().ok()?);
|
||||
let to = f64::from_bits(parts.next()?.parse::<u64>().ok()?);
|
||||
Some((edge, from, to))
|
||||
}
|
||||
|
||||
fn input_keys(kind: &FeatureKind) -> Vec<(String, solidcraft_doc::expr::Kind)> {
|
||||
let mut keys: Vec<_> = kind.inputs().into_iter().map(|(label, _, unit)| (label.to_string(), unit)).collect();
|
||||
if let FeatureKind::Fillet { style, .. } = kind {
|
||||
@@ -1962,28 +1973,67 @@ fn redefine(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// old index. Constructor-assigned names for new inputs cannot reuse the old dimensions.
|
||||
// A key names the input itself where its label doesn't (a segment's stretch, a fillet's
|
||||
// control); it is matched first, so inserting or reordering stretches moves no name onto
|
||||
// another stretch. An input whose key changed (a stretch's fractions edited) still keeps the
|
||||
// name of the input with the same label and kind, as it did before keys were unique.
|
||||
let old_labels: Vec<String> = old.kind.inputs().into_iter().map(|(label, _, _)| label.to_string()).collect();
|
||||
let new_labels: Vec<String> = new.kind.inputs().into_iter().map(|(label, _, _)| label.to_string()).collect();
|
||||
let mut old_inputs: Vec<(String, String, solidcraft_doc::expr::Kind, String)> = input_keys(&old.kind)
|
||||
// another stretch. An input whose key changed (a stretch's fractions edited) is matched to the
|
||||
// material it still is, below: the value it still has first, then the span it still covers.
|
||||
let new_inputs = new.kind.inputs();
|
||||
let new_labels: Vec<String> = new_inputs.iter().map(|(label, _, _)| label.to_string()).collect();
|
||||
let mut old_inputs: Vec<(String, String, solidcraft_doc::expr::Kind, String, String)> = input_keys(&old.kind)
|
||||
.into_iter()
|
||||
.zip(old_labels)
|
||||
.zip(old.kind.inputs())
|
||||
.zip(&old.param_names)
|
||||
.map(|(((key, kind), label), name)| (key, label, kind, name.clone()))
|
||||
.map(|(((key, kind), (label, expression, _)), name)| (key, label.to_string(), kind, name.clone(), expression))
|
||||
.collect();
|
||||
let keys = input_keys(&new.kind);
|
||||
let mut param_names: Vec<String> = vec![String::new(); keys.len()];
|
||||
for (i, (key, kind)) in keys.iter().enumerate() {
|
||||
if let Some(k) = old_inputs.iter().position(|(old_key, _, old_kind, _)| old_key == key && old_kind == kind) {
|
||||
if let Some(k) = old_inputs.iter().position(|(old_key, _, old_kind, _, _)| old_key == key && old_kind == kind) {
|
||||
param_names[i] = old_inputs.remove(k).3.clone();
|
||||
}
|
||||
}
|
||||
for (i, (_, kind)) in keys.iter().enumerate() {
|
||||
// A name follows the material it was given to, not the order of the list. When a segment input's
|
||||
// key misses (a stretch deleted or merged, so the survivor's fractions are new), it is matched to
|
||||
// the old input of the same label and kind that carries the same expression first: the survivor
|
||||
// still has the value it had. Only then does the greatest span overlap on the same edge carry a
|
||||
// name. A deleted stretch's name never lands on a stretch whose value came from elsewhere
|
||||
// (#5927, #5928).
|
||||
for (i, (key, kind)) in keys.iter().enumerate() {
|
||||
if !param_names[i].is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(k) = old_inputs.iter().position(|(_, old_label, old_kind, _)| *old_label == new_labels[i] && old_kind == kind) {
|
||||
if let Some((edge, from, to)) = segment_span(key) {
|
||||
let mut best: Option<(usize, bool, f64)> = None;
|
||||
for (k, (old_key, old_label, old_kind, _, old_expression)) in old_inputs.iter().enumerate() {
|
||||
if *old_label != new_labels[i] || old_kind != kind {
|
||||
continue;
|
||||
}
|
||||
let Some((old_edge, old_from, old_to)) = segment_span(old_key) else { continue };
|
||||
if old_edge != edge {
|
||||
continue;
|
||||
}
|
||||
let same_value = *old_expression == new_inputs[i].1;
|
||||
let overlap = (old_to.min(to) - old_from.max(from)).max(0.0);
|
||||
if !same_value && overlap <= 1e-9 {
|
||||
continue;
|
||||
}
|
||||
let better = match best {
|
||||
None => true,
|
||||
Some((_, best_value, best_overlap)) => (same_value && !best_value) || (same_value == best_value && overlap > best_overlap),
|
||||
};
|
||||
if better {
|
||||
best = Some((k, same_value, overlap));
|
||||
}
|
||||
}
|
||||
if let Some((k, _, _)) = best {
|
||||
param_names[i] = old_inputs.remove(k).3.clone();
|
||||
}
|
||||
// No old input of this edge carries the stretch's value or overlaps it: the
|
||||
// constructor's own name stays, rather than another stretch's.
|
||||
continue;
|
||||
}
|
||||
if let Some(k) = old_inputs
|
||||
.iter()
|
||||
.position(|(old_key, old_label, old_kind, _, _)| *old_label == new_labels[i] && old_kind == kind && segment_span(old_key).is_none())
|
||||
{
|
||||
param_names[i] = old_inputs.remove(k).3.clone();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1794,3 +1794,94 @@ fn redefining_a_segmented_flange_keeps_a_named_height_on_its_stretch() {
|
||||
let relief_index = inputs.iter().position(|(label, _, _)| *label == "Corner relief").unwrap();
|
||||
assert_eq!(f.param_names[relief_index], "Relief");
|
||||
}
|
||||
|
||||
/// A name follows the material it was given to, not the order of the input list: deleting one of two
|
||||
/// named stretches keeps the survivor's own name, drops the deleted stretch's, and an expression that
|
||||
/// reads the survivor's name still reads its height. A deleted stretch's name never lands on a
|
||||
/// stretch whose value came from elsewhere (#5927, #5928).
|
||||
#[test]
|
||||
fn deleting_a_stretch_keeps_the_surviving_stretches_own_name() {
|
||||
fn param(s: &mut Session, name: &str) -> Value {
|
||||
let list = run(s, "parameters.list", json!({}));
|
||||
list["parameters"].as_array().and_then(|a| a.iter().find(|p| p["name"] == name)).cloned().unwrap_or(Value::Null)
|
||||
}
|
||||
fn has(s: &mut Session, name: &str) -> bool {
|
||||
param(s, name)["value"].as_f64().is_some()
|
||||
}
|
||||
fn input_row(s: &mut Session, feature: &str, input: &str) -> Value {
|
||||
let list = run(s, "parameters.list", json!({}));
|
||||
list["parameters"].as_array().and_then(|a| a.iter().find(|p| p["feature"] == feature && p["input"] == input)).cloned().unwrap_or(Value::Null)
|
||||
}
|
||||
// The name on each "Segment height" input of `Lip`, in the walk's order.
|
||||
fn segment_names(s: &mut Session) -> Vec<(String, String)> {
|
||||
let f = s.doc.find_feature("Lip").unwrap();
|
||||
f.kind
|
||||
.inputs()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, (label, _, _))| *label == "Segment height")
|
||||
.map(|(i, (_, expression, _))| (f.param_names[i].clone(), expression.clone()))
|
||||
.collect()
|
||||
}
|
||||
// A 200 × 100 base, a two-stretch flange on its y = 0 edge with each height named, and a flange on
|
||||
// the opposite edge whose height reads one of those names.
|
||||
fn built(heights: [&str; 2], spans: [f64; 2], reader: &str) -> Session {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sheet.manage_rules", json!({"name":"AL2","thickness":"2 mm","k_factor":0.5,"bend_radius":"1 mm","active":true}));
|
||||
run(&mut s, "sketch.create", json!({"plane":"XY","name":"Base"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[200,100]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "sheet.flange", json!({"sketch":"Base"}));
|
||||
run(
|
||||
&mut s,
|
||||
"sheet.flange",
|
||||
json!({"type":"edge","edges":[{"point":[100,0,2],"segments":[
|
||||
{"from":0,"to":spans[0],"height":heights[0]},
|
||||
{"from":spans[0],"to":1,"height":heights[1]}]}],
|
||||
"height":"20 mm","angle":"90 deg","name":"Lip"}),
|
||||
);
|
||||
let names: Vec<String> = segment_names(&mut s).into_iter().map(|(n, _)| n).collect();
|
||||
assert_eq!(names.len(), 2, "{names:?}");
|
||||
run(&mut s, "parameters.rename", json!({"name": names[0], "new_name": "Short"}));
|
||||
run(&mut s, "parameters.rename", json!({"name": names[1], "new_name": "Tall"}));
|
||||
run(&mut s, "sheet.flange", json!({"edges":[[100,100,2]],"height":reader,"angle":"90 deg","name":"Reader"}));
|
||||
s
|
||||
}
|
||||
let redefine = |s: &mut Session, segments: Value| {
|
||||
run(
|
||||
s,
|
||||
"timeline.redefine",
|
||||
json!({"feature":"Lip","command":"sheet.flange","params":{
|
||||
"type":"edge","edges":[{"point":[100,0,2],"segments":segments}],
|
||||
"height":"20 mm","angle":"90 deg"}}),
|
||||
);
|
||||
};
|
||||
// Deleting the first stretch: the survivor is the 15 mm stretch, and keeps its own name.
|
||||
let mut s = built(["20 mm", "15 mm"], [0.5, 1.0], "Tall");
|
||||
assert_eq!(param(&mut s, "Short")["value"].as_f64(), Some(20.0));
|
||||
assert_eq!(param(&mut s, "Tall")["value"].as_f64(), Some(15.0));
|
||||
redefine(&mut s, json!([{"from":0,"to":1,"height":"15 mm"}]));
|
||||
assert_eq!(segment_names(&mut s), [("Tall".to_string(), "15 mm".to_string())], "the survivor keeps its own name");
|
||||
assert!(has(&mut s, "Tall") && !has(&mut s, "Short"), "the deleted stretch's name is gone: {:?}", run(&mut s, "parameters.list", json!({})));
|
||||
let reader = input_row(&mut s, "Reader", "Height");
|
||||
assert_eq!(reader["expression"], "Tall");
|
||||
assert_eq!(reader["value"].as_f64(), Some(15.0), "an expression reading Tall still reads the survivor's height: {reader}");
|
||||
assert!(reader["error"].is_null(), "{reader}");
|
||||
// Deleting the second stretch: the survivor is the 20 mm stretch, and keeps its own name.
|
||||
let mut s = built(["20 mm", "15 mm"], [0.5, 1.0], "Short");
|
||||
redefine(&mut s, json!([{"from":0,"to":1,"height":"20 mm"}]));
|
||||
assert_eq!(segment_names(&mut s), [("Short".to_string(), "20 mm".to_string())], "the survivor keeps its own name");
|
||||
assert!(has(&mut s, "Short") && !has(&mut s, "Tall"), "{:?}", run(&mut s, "parameters.list", json!({})));
|
||||
let reader = input_row(&mut s, "Reader", "Height");
|
||||
assert_eq!(reader["expression"], "Short");
|
||||
assert_eq!(reader["value"].as_f64(), Some(20.0), "an expression reading Short still reads the survivor's height: {reader}");
|
||||
// A merge whose surviving value came from the first stretch: it keeps the first stretch's name
|
||||
// even though the second stretch's span is the larger part of the merged stretch.
|
||||
let mut s = built(["20 mm", "15 mm"], [0.25, 1.0], "Short");
|
||||
redefine(&mut s, json!([{"from":0,"to":1,"height":"20 mm"}]));
|
||||
assert_eq!(segment_names(&mut s), [("Short".to_string(), "20 mm".to_string())], "the merged stretch keeps the name of the value it carries");
|
||||
assert!(has(&mut s, "Short") && !has(&mut s, "Tall"), "{:?}", run(&mut s, "parameters.list", json!({})));
|
||||
let reader = input_row(&mut s, "Reader", "Height");
|
||||
assert_eq!(reader["value"].as_f64(), Some(20.0), "{reader}");
|
||||
assert!(s.model.results.iter().all(|r| r.error.is_none()), "{:?}", s.model.results);
|
||||
}
|
||||
|
||||
@@ -738,3 +738,55 @@ fn segments_on_opposite_sides_cut_the_blank_at_the_junction() {
|
||||
let regions = same.model.state().sheets[0].flat_regions().unwrap_or_default();
|
||||
assert!(!regions.iter().any(|r| cut(&r.outer, from, to) && cut(&r.outer, to, from)), "a same-side junction stays joined");
|
||||
}
|
||||
|
||||
/// A notch through a junction's far edge takes both ends the junction cut was computed from off the
|
||||
/// blank: the far edge at x = 50 is gone, and the bend-line end lies inside the material. The cut is
|
||||
/// clipped to the material that remains, from the bend line out to the notch's edge, and written in
|
||||
/// the flat loop and the DXF. Dropped, the blank stayed joined where the folded part separates
|
||||
/// (FR-7 #5932).
|
||||
#[test]
|
||||
fn a_notch_through_a_junctions_far_edge_still_parts_the_blank() {
|
||||
let mut s = plate_in(sheet_of_2mm(), &[]);
|
||||
run(
|
||||
&mut s,
|
||||
"sheet.flange",
|
||||
json!({"type":"edge","edges":[{"point":[50,0,2],"segments":[
|
||||
{"from":0,"to":0.5,"angle":"90 deg","height":"20 mm","flip":false},
|
||||
{"from":0.5,"to":1,"angle":"90 deg","height":"20 mm","flip":true}]}],
|
||||
"height":"20 mm","angle":"90 deg"}),
|
||||
);
|
||||
// The notch: x 49…51, y -18…-10, in the flat frame. The flange's far edge is at y -17.14, so
|
||||
// the notch takes it and the material between it and y -10 away at the junction.
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
run(&mut s, "sketch.create", json!({"plane":"XY","name":"Notch"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0":[49,-18],"p1":[51,-10]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch":"Notch","through_all":true,"operation":"cut","direction":"symmetric"}));
|
||||
// The bend line, and where the material left ends outward of it: the notch's edge.
|
||||
let bend = Vec2::new(50.0, 3.0);
|
||||
let left = Vec2::new(50.0, -10.0);
|
||||
let cut = |lp: &solidcore_record::Loop2, a: Vec2, b: Vec2| {
|
||||
lp.segs.iter().any(|s| matches!(s, Seg2::Line { a: p, b: q } if p.dist(a) < 1e-6 && q.dist(b) < 1e-6))
|
||||
};
|
||||
let regions = s.model.state().sheets[0].flat_regions().unwrap_or_default();
|
||||
assert!(
|
||||
regions.iter().any(|r| cut(&r.outer, bend, left) && cut(&r.outer, left, bend)),
|
||||
"the blank is not cut from the bend line to the notch's edge: {:?}",
|
||||
regions.iter().map(|r| r.outer.segs.len()).collect::<Vec<_>>()
|
||||
);
|
||||
let outline: Vec<(Vec2, Vec2)> = entities(&export(&mut s, json!({})))
|
||||
.iter()
|
||||
.filter(|e| e.0 == "LINE" && e.1.iter().any(|(c, v)| *c == 8 && v == "OUTLINE"))
|
||||
.map(|e| (point(e, 10), point(e, 11)))
|
||||
.collect();
|
||||
let near = |p: Vec2, q: Vec2| p.dist(q) < 1e-6;
|
||||
let both_ways = |lines: &[(Vec2, Vec2)], a: Vec2, b: Vec2| {
|
||||
lines.iter().any(|&(p, q)| near(p, a) && near(q, b)) && lines.iter().any(|&(p, q)| near(p, b) && near(q, a))
|
||||
};
|
||||
assert!(both_ways(&outline, bend, left), "the DXF has no clipped junction cut: {outline:?}");
|
||||
// The cut leaves the part one piece, and refolding then unfolding builds it.
|
||||
run(&mut s, "sheet.refold", json!({}));
|
||||
run(&mut s, "sheet.unfold", json!({}));
|
||||
let errors: Vec<String> = s.model.results.iter().filter_map(|r| r.error.clone()).collect();
|
||||
assert!(errors.is_empty(), "unfold: {errors:?}");
|
||||
}
|
||||
|
||||
@@ -205,7 +205,10 @@ stretch's side outright: `true` bends it the other way and `false` the same way
|
||||
an override rather than a turn relative to it. No bend relief is cut where two segments meet, and
|
||||
where the two stretches bend to opposite sides the blank is cut along their junction, from the bend
|
||||
line out to the shorter stretch's far edge, in the flat pattern and the DXF: a blank still joined
|
||||
there cannot fold one stretch up and the next down, while same-side stretches stay one piece.
|
||||
there cannot fold one stretch up and the next down, while same-side stretches stay one piece. Where
|
||||
a cut-out took that far edge away, the cut is clipped to the material that is left: it runs from the
|
||||
bend line to the nearest boundary the junction line meets (a notch's edge, for one), so the blank is
|
||||
parted as far as its remaining material allows.
|
||||
`then` is refused on a split edge: bend each segment's far edge in its own
|
||||
feature. Stretches that meet within 1e-9 (the junction tolerance, `SEGMENT_JOIN_EPS`) are one
|
||||
partition: the junction is snapped shut whatever the two stretches' angle, height or side are, so
|
||||
|
||||
@@ -118,7 +118,11 @@ falls back to the edge's own value, then the feature's.
|
||||
two bends butt along the junction line. Only the outermost segments take part in corner closing
|
||||
and end relief. Where the two stretches bend to opposite sides the blank is cut along their
|
||||
junction, from the bend line out to the shorter stretch's far edge, in the flat pattern and the
|
||||
DXF: a blank joined there cannot fold, one stretch up and the next down. Same-side stretches stay
|
||||
DXF: a blank joined there cannot fold, one stretch up and the next down. Where a cut-out took that
|
||||
far edge away, the cut is clipped to the material that is left: it runs from the bend line to the
|
||||
nearest boundary the junction line meets (a notch's edge, for one), so the blank is parted as far
|
||||
as its remaining material allows; a junction with no material left outward of the bend line has
|
||||
nothing to part and is left alone. Same-side stretches stay
|
||||
one piece. A junction the command accepts within `SEGMENT_JOIN_EPS` is snapped shut, and a
|
||||
stretch narrower than that tolerance is no bend of its own: the run before it takes that stretch
|
||||
(its angle and height do not build), so the bends still tile the edge with nothing left uncovered,
|
||||
@@ -269,6 +273,11 @@ which edge is "the long edge" on a near-square part) before building.
|
||||
the only stretch left makes the whole edge. The dialog never submits a list with a gap.
|
||||
- **Per-edge versus per-segment:** `angle` and `height` at the edge entry are that edge's values; a
|
||||
segment's own values override them; the feature's values are the last fallback.
|
||||
- **A named stretch keeps its name with its material:** redefining a split edge after a stretch is
|
||||
deleted or merged keeps the surviving stretch's own model parameter name (the name on the value it
|
||||
still has; failing that, the name whose span it still covers), and the deleted stretch's name goes
|
||||
away. Expressions that read the survivor's name still read its height; a deleted stretch's name
|
||||
never lands on a stretch whose value came from elsewhere.
|
||||
- **`flip` is per segment:** it sets that bend's side outright: `true` bends it the other way and
|
||||
`false` the same way as the feature's `flip`, an override rather than a turn relative to it.
|
||||
- **`then` is refused with `segments`:** bend each segment's far edge in its own feature. The
|
||||
|
||||
Reference in New Issue
Block a user