UI: one continuity control for every continuity choice

ui_kit::continuity_row is the one widget that chooses an order: a dropdown of the orders a
construction builds, labelled with the table's words and with a hover line per order naming
its code. Patch, Blend Surface, Boundary and the fillet's Corners row (tangent rolling ball or
curvature-continuous patch) all use it, and their dialogs keep a Continuity rather than an
index into a copied list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-27 20:26:35 -04:00
committed by Irakli Kourtis
parent 2fed39c85c
commit fd34314d56
6 changed files with 92 additions and 117 deletions
+2 -2
View File
@@ -47,7 +47,7 @@ fn edges_at_each_end_make_a_blend_and_come_back_in_an_edit() {
d.inputs[0].items = vec![from];
d.inputs[1].items = vec![to];
let Kind::Surface(Surf::Blend { continuity, bulge, .. }) = &mut d.kind else { panic!("{:?}", d.kind) };
*continuity = [2, 1];
*continuity = [Continuity::G2, Continuity::G1];
*bulge = [0.5, 0.0];
ok(&mut h);
let name = h.app.session.doc.features.last().unwrap().name.clone();
@@ -69,7 +69,7 @@ fn edges_at_each_end_make_a_blend_and_come_back_in_an_edit() {
h.step(&json!({"call":"ui.editFeature","params":{"feature":name}})).unwrap();
h.frames(8);
let d = h.app.dialog.as_mut().unwrap();
assert!(matches!(d.kind, Kind::Surface(Surf::Blend { continuity: [2, 1], stitch: false, .. })), "{:?}", d.kind);
assert!(matches!(d.kind, Kind::Surface(Surf::Blend { continuity: [Continuity::G2, Continuity::G1], stitch: false, .. })), "{:?}", d.kind);
assert_eq!((d.inputs[0].items.len(), d.inputs[1].items.len()), (1, 1), "{:?}", d.inputs);
let Kind::Surface(Surf::Blend { stitch, .. }) = &mut d.kind else { panic!() };
*stitch = true;
+3 -3
View File
@@ -70,7 +70,7 @@ fn edges_and_curves_make_a_network_and_come_back_in_an_edit() {
let d = h.app.dialog.as_mut().unwrap();
d.inputs[0].items = vec![rim.clone(), Sel::SketchCurve { id: top.clone(), sketch: None }];
let Kind::Surface(Surf::Boundary { continuity, join }) = &mut d.kind else { panic!("{:?}", d.kind) };
*continuity = 2;
*continuity = Continuity::G2;
*join = true;
ok(&mut h);
let name = h.app.session.doc.features.last().unwrap().name.clone();
@@ -91,7 +91,7 @@ fn edges_and_curves_make_a_network_and_come_back_in_an_edit() {
h.step(&json!({"call":"ui.editFeature","params":{"feature":name}})).unwrap();
h.frames(8);
let d = h.app.dialog.as_ref().unwrap();
assert!(matches!(d.kind, Kind::Surface(Surf::Boundary { continuity: 2, join: true })), "{:?}", d.kind);
assert!(matches!(d.kind, Kind::Surface(Surf::Boundary { continuity: Continuity::G2, join: true })), "{:?}", d.kind);
assert_eq!(d.inputs[0].items.len(), 2, "{:?}", d.inputs[0].items);
ok(&mut h);
let (u2, v2, op2) = network(&h);
@@ -112,7 +112,7 @@ fn curves_that_meet_are_one_chain() {
h.app.run("sketch.line", json!({"points":[[10,0],[10,8]],"ids":["d"]})).unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
let sketch = h.app.session.doc.find_feature("Net").unwrap().id;
let k = Surf::Boundary { continuity: 0, join: false };
let k = Surf::Boundary { continuity: Continuity::G0, join: false };
let (_, mut inputs) = crate::dialogs_surface::start(&h.app, "surface.boundary").unwrap();
inputs[0].items = ["a2", "b", "a1"].iter().map(|id| Sel::SketchCurve { id: (*id).into(), sketch: None }).collect();
inputs[1].items = ["c", "d"].iter().map(|id| Sel::SketchCurve { id: (*id).into(), sketch: None }).collect();
+21 -25
View File
@@ -2,6 +2,7 @@
use super::*;
use crate::ui_kit;
use crate::ui_kit::{choice_row, expr_row, hint_row};
use solidcraft_engine::kernel::Continuity;
pub(super) fn command(kind: &Kind) -> Option<&'static str> {
match kind {
@@ -107,10 +108,9 @@ const FILLET_TYPES: [(&str, &str); 5] = [
("Spline", "spline"),
];
/// What fills a fillet's corners where three or more rounded edges meet (labels and hover texts,
/// in the order of the `corner` words `rolling` and `g2`).
const FILLET_CORNERS: [(&str, &str); 2] =
[("ui.fillet_corner_ball", "ui.fillet_corner_ball_tip"), ("ui.fillet_corner_patch", "ui.fillet_corner_patch_tip")];
/// How a fillet's corners where three or more rounded edges meet continue the rounds: tangent (a
/// rolling ball, the `corner` default) or curvature continuous (`corner: g2`, a smooth patch).
const FILLET_CORNERS: [Continuity; 2] = [Continuity::G1, Continuity::G2];
/// A fillet's Corners row (`corner`) and, with a smooth patch, its Setback (`setbacks`, one
/// distance for every corner). Shown only while the edges have a corner where three or more
@@ -131,19 +131,15 @@ fn fillet_corner_rows(
let exact = extra.get("fit").and_then(Value::as_str).is_none_or(|fit| fit == "exact");
let mut enter = false;
ui.add_enabled_ui(rtype == 0 && exact, |ui| {
form_row(ui, "ui.corners", |ui| {
let mut choice = usize::from(patch);
let response = ui_kit::dropdown(ui, "fi_corner", &mut choice, &FILLET_CORNERS.map(|(label, _)| label));
if choice != usize::from(patch) {
if choice == 1 {
extra.insert("corner".into(), json!("g2"));
} else {
extra.remove("corner");
}
let mut corner = if patch { Continuity::G2 } else { Continuity::G1 };
ui_kit::continuity_row(ui, "ui.corners", "fi_corner", &mut corner, &FILLET_CORNERS);
if (corner == Continuity::G2) != patch {
if corner == Continuity::G2 {
extra.insert("corner".into(), json!(Continuity::G2.as_str()));
} else {
extra.remove("corner");
}
response
})
.on_hover_text(FILLET_CORNERS.map(|(_, tip)| tr(tip)).join("\n"));
}
if extra.get("corner").and_then(Value::as_str) == Some("g2") {
ui.scope(|ui| {
let hint = crate::i18n::format("ui.fillet_setback_hint", &[("factor", "1.5")]);
@@ -1169,9 +1165,9 @@ mod tests {
h.frames(3);
}
/// Corners shows only once three rounded edges meet at a corner; Smooth patch then writes
/// Corners shows only once three rounded edges meet at a corner; Curvature (a smooth patch) writes
/// `corner: g2`, its Setback (empty for the command's 1.5 × radius) goes to that corner as
/// `setbacks`, and Ball writes neither.
/// `setbacks`, and Tangent (a rolling ball) writes neither.
#[test]
fn corners_row_follows_the_selection_and_maps_to_corner_and_setbacks() {
let mut h = fillet_harness();
@@ -1186,7 +1182,7 @@ mod tests {
assert!(scenario::handle_at("dialog:ui.corners").is_some(), "three rounded edges meet at (10, 0, 5)");
assert!(scenario::handle_at("dialog:ui.fillet_setback").is_none(), "Setback goes with a smooth patch");
assert!(parameters(&h).get("corner").is_none(), "Ball is the default and writes nothing");
choose(&mut h, "ui.corners", "ui.fillet_corner_patch");
choose(&mut h, "ui.corners", Continuity::G2.label_id());
let p = parameters(&h);
assert_eq!(p["corner"], json!("g2"));
assert!(p.get("setbacks").is_none(), "an empty Setback leaves the command's default: {p}");
@@ -1195,12 +1191,12 @@ mod tests {
*setback = "3 mm".into();
}
assert_eq!(parameters(&h)["setbacks"], json!([{"vertex": [10.0, 0.0, 5.0], "distance": "3 mm"}]));
choose(&mut h, "ui.corners", "ui.fillet_corner_ball");
choose(&mut h, "ui.corners", Continuity::G1.label_id());
let p = parameters(&h);
assert!(p.get("corner").is_none() && p.get("setbacks").is_none(), "{p}");
assert!(scenario::handle_at("dialog:ui.fillet_setback").is_none());
// Another type clears the patch; the row stays, disabled, saying why in its hover text.
choose(&mut h, "ui.corners", "ui.fillet_corner_patch");
choose(&mut h, "ui.corners", Continuity::G2.label_id());
choose(&mut h, "Type", "Chord length");
let p = parameters(&h);
assert!(p.get("corner").is_none() && p.get("setbacks").is_none(), "{p}");
@@ -1212,15 +1208,15 @@ mod tests {
#[test]
fn fillet_labels_use_words_not_continuity_codes() {
let coded = |text: &str| ["G0", "G1", "G2", "G3"].iter().any(|code| text.contains(code));
for label in FILLET_TYPES.map(|(label, _)| label).into_iter().chain(FILLET_CORNERS.map(|(label, _)| label)) {
for label in FILLET_TYPES.map(|(label, _)| label).into_iter().chain(FILLET_CORNERS.map(Continuity::label_id)) {
assert!(!coded(crate::i18n::tr_label(label)), "{label}: {}", crate::i18n::tr_label(label));
}
for tip in FILLET_CORNERS.map(|(_, tip)| tip) {
for tip in FILLET_CORNERS.map(Continuity::tip_id) {
assert!(coded(crate::i18n::tr(tip)), "the hover text keeps the code for those who know it: {tip}");
}
let mut h = fillet_harness();
set_edges(&mut h, 3);
choose(&mut h, "ui.corners", "ui.fillet_corner_patch");
choose(&mut h, "ui.corners", Continuity::G2.label_id());
for section in ["Variable Radius", "Advanced"] {
h.step(&json!({"click":{"handle":format!("dialog:section:{section}")}})).unwrap();
}
@@ -1228,7 +1224,7 @@ mod tests {
assert!(scenario::handle_at("dialog:ui.fillet_setback").is_some() && scenario::handle_at("dialog:Radius Fit").is_some());
let tree = h.output.accesskit_update.as_ref().unwrap();
let labels: Vec<String> = tree.nodes.iter().flat_map(|(_, node)| [node.label(), node.value()]).flatten().map(str::to_string).collect();
assert!(labels.iter().any(|l| l == "Smooth patch"), "{labels:?}");
assert!(labels.iter().any(|l| l == "Curvature"), "{labels:?}");
let shown: Vec<&String> = labels.iter().filter(|l| coded(l)).collect();
assert!(shown.is_empty(), "labels with a continuity code: {shown:?}");
}
+42 -83
View File
@@ -59,24 +59,24 @@ crate::dialog_descriptor::dialog_descriptors! {
Loft {
rail_fit: usize,
},
/// `continuity`: an index into [`BLEND_ORDERS`] for each end, [from, to]; `bulge`: how
/// `continuity`: each end's, [from, to]; `bulge`: how
/// full each end leaves (0 is the plain bridge); `stitch`: sew the blend onto both bodies.
Blend {
continuity: [usize; 2],
continuity: [Continuity; 2],
bulge: [f64; 2],
stitch: bool,
},
/// `continuity`: an index into [`BOUNDARY_ORDERS`] (for a loop of sketch curves, how much of
/// `continuity`: for a loop of sketch curves, how much of
/// the sketch's plane each side repeats; for picked edges, how the surface meets the faces
/// beside them); `join`: sew the surface into the body of its edges.
Boundary {
continuity: usize,
continuity: Continuity,
join: bool,
},
/// `continuity`: an index into [`PATCH_ORDERS`] (how a patch on edges meets the faces
/// `continuity`: how a patch on edges meets the faces
/// beside its loop).
Patch {
continuity: usize,
continuity: Continuity,
},
Unstitch,
ReverseNormal,
@@ -138,51 +138,20 @@ impl Surf {
}
}
/// A dialog's continuity choices: the orders a construction builds, from the kernel's table
/// ([`Construction::offered`]), so an order a construction learns shows up here by itself. The
/// dialog keeps an index into them; labels are plain words, each order's code (G0 …) is in the
/// hover text.
#[derive(Clone, Copy, Debug)]
struct Orders(Construction);
/// What each dialog's continuity rows offer: the orders its construction builds, from the
/// kernel's table, so an order a construction learns shows up by itself.
fn patch_orders() -> Vec<Continuity> {
Construction::Patch.offered()
}
fn blend_orders() -> Vec<Continuity> {
Construction::Bridge.offered()
}
/// How a patch on edges meets the faces beside its loop.
const PATCH_ORDERS: Orders = Orders(Construction::Patch);
/// How much of the surface at its edges each end of a blend repeats.
const BLEND_ORDERS: Orders = Orders(Construction::Bridge);
/// A boundary's one row serves its loop patch (a bridge) and its curve network alike: the orders
/// a network builds, the fewer of the two.
const BOUNDARY_ORDERS: Orders = Orders(Construction::Network);
impl Orders {
fn all(self) -> Vec<Continuity> {
self.0.offered()
}
/// The message ids of the labels, in order.
fn labels(self) -> Vec<&'static str> {
self.all().into_iter().map(Continuity::label_id).collect()
}
/// The hover text: a line for each order.
fn tip(self) -> String {
self.all().into_iter().map(|g| crate::i18n::tr(g.tip_id())).collect::<Vec<_>>().join("\n")
}
/// The command word of the choice at `index`.
fn word(self, index: usize) -> Option<&'static str> {
self.all().get(index).map(|g| g.as_str())
}
/// The index of `g` among the choices (`None`: not offered).
fn index(self, g: Continuity) -> Option<usize> {
self.all().iter().position(|o| *o == g)
}
/// A Continuity row's segments (the segments draw their text as given).
fn segments(self, ui: &mut egui::Ui, selected: &mut usize) -> egui::Response {
let labels: Vec<&str> = self.labels().into_iter().map(crate::i18n::tr).collect();
ui.scope(|ui| ui_kit::segmented(ui, selected, &labels, Default::default())).response
}
fn boundary_orders() -> Vec<Continuity> {
Construction::Network.offered()
}
/// What a boundary surface becomes: a body of its own, or part of the body of its edges.
@@ -243,9 +212,9 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
"surface.revolve" => Surf::Revolve { angle: "360 deg".into(), symmetric: false, axis: None },
"surface.sweep" => Surf::Sweep(crate::dialogs_sweep::SweepOpts::default()),
"surface.loft" => Surf::Loft { rail_fit: 0 },
"surface.blend" => Surf::Blend { continuity: [1, 1], bulge: [0.0, 0.0], stitch: false },
"surface.boundary" => Surf::Boundary { continuity: 1, join: false },
"surface.patch" => Surf::Patch { continuity: 0 },
"surface.blend" => Surf::Blend { continuity: [Continuity::G1; 2], bulge: [0.0, 0.0], stitch: false },
"surface.boundary" => Surf::Boundary { continuity: Continuity::G1, join: false },
"surface.patch" => Surf::Patch { continuity: Continuity::G0 },
"surface.unstitch" => Surf::Unstitch,
"surface.reverse_normal" => Surf::ReverseNormal,
"surface.ruled" => {
@@ -315,23 +284,23 @@ pub fn rows(ui: &mut egui::Ui, k: &mut Surf) -> bool {
ui_kit::options(ui, "untrim_options", &mut [("Restore outer boundary", outer), ("In place (surfaces)", in_place)]);
}
Surf::Patch { continuity } => {
form_row(ui, "Continuity", |ui| PATCH_ORDERS.segments(ui, continuity)).on_hover_text(PATCH_ORDERS.tip());
ui_kit::continuity_row(ui, "Continuity", "pa_cont", continuity, &patch_orders());
}
Surf::Blend { continuity, bulge, stitch } => {
let [from, to] = continuity;
let [from_bulge, to_bulge] = bulge;
form_row(ui, "Start continuity", |ui| ui_kit::dropdown(ui, "bl_cont0", from, &BLEND_ORDERS.labels())).on_hover_text(BLEND_ORDERS.tip());
ui_kit::continuity_row(ui, "Start continuity", "bl_cont0", from, &blend_orders());
form_row(ui, "Start bulge", |ui| {
ui_kit::scalar_field(ui, from_bulge, -0.9..=3.0);
});
form_row(ui, "End continuity", |ui| ui_kit::dropdown(ui, "bl_cont1", to, &BLEND_ORDERS.labels())).on_hover_text(BLEND_ORDERS.tip());
ui_kit::continuity_row(ui, "End continuity", "bl_cont1", to, &blend_orders());
form_row(ui, "End bulge", |ui| {
ui_kit::scalar_field(ui, to_bulge, -0.9..=3.0);
});
ui_kit::options(ui, "blend_options", &mut [("Stitch to both bodies", stitch)]);
}
Surf::Boundary { continuity, join } => {
form_row(ui, "Continuity", |ui| BOUNDARY_ORDERS.segments(ui, continuity)).on_hover_text(BOUNDARY_ORDERS.tip());
ui_kit::continuity_row(ui, "Continuity", "bo_cont", continuity, &boundary_orders());
form_row(ui, "Operation", |ui| {
let mut op = usize::from(*join);
combo(ui, "bo_op", &BOUNDARY_OPS, &mut op);
@@ -584,11 +553,10 @@ pub fn commands(app: &SolveApp, k: &Surf, inputs: &[SelInput], extra: &Map<Strin
let (body, edges) = body_and_edges(items(inputs, i))?;
Ok(json!({"body": body, "edges": edges}))
};
let word = |i: usize| BLEND_ORDERS.word(i).unwrap_or("g1");
let mut p = json!({
"from": side(0)?,
"to": side(1)?,
"continuity": [word(continuity[0]), word(continuity[1])],
"continuity": continuity.map(Continuity::as_str),
"bulge": bulge,
});
if *stitch {
@@ -598,7 +566,7 @@ pub fn commands(app: &SolveApp, k: &Surf, inputs: &[SelInput], extra: &Map<Strin
}
Surf::Boundary { continuity, join } => {
need(0, "the curves (sketch curves or edges)")?;
let word = BOUNDARY_ORDERS.word(*continuity).unwrap_or("g0");
let word = continuity.as_str();
let across = items(inputs, 1);
let picks = items(inputs, 0);
let sketch_ids: Vec<String> = picks.curve_ids();
@@ -618,7 +586,7 @@ pub fn commands(app: &SolveApp, k: &Surf, inputs: &[SelInput], extra: &Map<Strin
Chain::Sketch { sketch, ids } => json!({"sketch": sketch, "curves": ids}),
Chain::Edges { body, points } => {
let mut v = json!({"body": body, "edges": points.iter().map(|p| pt(*p)).collect::<Vec<_>>()});
if *continuity > 0 {
if *continuity != Continuity::G0 {
v["continuity"] = json!(word);
}
v
@@ -647,8 +615,8 @@ pub fn commands(app: &SolveApp, k: &Surf, inputs: &[SelInput], extra: &Map<Strin
let edges: Vec<Value> = items(inputs, 0).edge_points().into_iter().map(pt).collect();
let body = items(inputs, 0).iter().find_map(|x| if let Sel::Edge { body, .. } = x { Some(body.clone()) } else { None });
let mut p = json!({"edges": edges, "body": body});
if let Some(word) = PATCH_ORDERS.word(*continuity).filter(|_| *continuity > 0) {
p["continuity"] = json!(word);
if *continuity != Continuity::G0 {
p["continuity"] = json!(continuity.as_str());
}
let points: Vec<Value> = items(inputs, 1)
.iter()
@@ -885,8 +853,7 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
)
}
FeatureKind::SurfaceBlend { from, to, continuity, bulge, align, seam, stitch } => {
let index = |c: &Continuity| BLEND_ORDERS.index(*c).unwrap_or(1);
let k = Surf::Blend { continuity: continuity.each_ref().map(index), bulge: *bulge, stitch: *stitch };
let k = Surf::Blend { continuity: *continuity, bulge: *bulge, stitch: *stitch };
// What the dialog does not show is kept as it was: the leave directions and the seams.
let mut extra = Map::new();
for (key, points) in [("align", align), ("seam", seam)] {
@@ -902,7 +869,7 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
}
FeatureKind::SurfaceBoundary { sketch, curves, orders, crown, samples, .. } => {
let first = orders.first().copied().unwrap_or(Continuity::G1);
let index = BOUNDARY_ORDERS.index(first);
let offered = boundary_orders().contains(&first);
// What the dialog does not show is kept as it was: a crown, the stations, and orders
// that differ from side to side or that the list does not offer (G3).
let mut extra = Map::new();
@@ -912,10 +879,10 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
if *samples != solidcraft_engine::doc::BOUNDARY_SAMPLES {
extra.insert("samples".into(), json!(samples));
}
if index.is_none() || orders.iter().any(|o| *o != first) {
if !offered || orders.iter().any(|o| *o != first) {
extra.insert("continuity".into(), json!(orders.iter().map(|o| o.as_str()).collect::<Vec<_>>()));
}
let k = Surf::Boundary { continuity: index.unwrap_or(1), join: false };
let k = Surf::Boundary { continuity: if offered { first } else { Continuity::G1 }, join: false };
let mut ins = inputs(&k);
if let Some(inp) = ins.first_mut() {
inp.items = curves.iter().map(|id| crate::selection::curve_pick(id, sketch.0, s.active_sketch)).collect();
@@ -944,8 +911,7 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
.collect()
};
let g = u.iter().chain(v).find(|c| c.body.is_some()).map_or(Continuity::G0, |c| c.continuity);
let index = BOUNDARY_ORDERS.index(g).unwrap_or(0);
(Surf::Boundary { continuity: index, join: *operation == solidcraft_engine::doc::Operation::Join }, vec![sels(u), sels(v)])
(Surf::Boundary { continuity: g, join: *operation == solidcraft_engine::doc::Operation::Join }, vec![sels(u), sels(v)])
}
FeatureKind::SurfaceLoft { sections, rails, rail_fit, .. } => {
let [rail, rail2] = crate::dialogs_sweep::rail_sels(rails, s.active_sketch);
@@ -976,7 +942,6 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
sels.extend((0..ss.profiles.len()).map(|index| Sel::Profile { sketch: sk.0, index }));
}
}
let index = PATCH_ORDERS.index(continuity.all).unwrap_or(0);
// What the dialog does not show is kept as it was: the crown, edges with a
// continuity of their own, and points that are not on a face any more.
let through: Vec<Sel> = points.iter().filter_map(|p| crate::dialogs::face_sel(s, p.0)).collect();
@@ -990,7 +955,7 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
if through.len() != points.len() {
extra.insert("points".into(), json!(points.iter().map(|p| pt(p.0)).collect::<Vec<_>>()));
}
let k = Surf::Patch { continuity: index };
let k = Surf::Patch { continuity: continuity.all };
let mut ins = inputs(&k);
for (input, picks) in ins.iter_mut().zip([sels, through]) {
input.items = picks;
@@ -1132,22 +1097,15 @@ mod tests {
Continuity::ALL.iter().any(|g| text.contains(g.code()))
}
/// The Patch, Blend and Boundary rows offer what their construction builds, from G0; their
/// labels are words in every complete catalogue (and the RTL ones), never a code, and each
/// hover line names its order's code for those who know it.
/// The Patch, Blend and Boundary rows offer what their construction builds, from G0; every
/// order's label is a word in each complete catalogue (and the RTL ones), never a code, and
/// its hover line names the code for those who know it.
#[test]
fn continuity_rows_are_words_and_keep_the_codes_for_the_hover_text() {
for orders in [PATCH_ORDERS, BLEND_ORDERS, BOUNDARY_ORDERS] {
let all = orders.all();
assert_eq!(all, orders.0.offered());
assert_eq!(all.first(), Some(&Continuity::G0), "{orders:?}");
for (i, g) in all.iter().enumerate() {
assert_eq!(orders.word(i), Some(g.as_str()));
assert_eq!(orders.index(*g), Some(i));
}
assert_eq!(orders.word(all.len()), None);
for orders in [patch_orders(), blend_orders(), boundary_orders()] {
assert_eq!(orders.first(), Some(&Continuity::G0), "{orders:?}");
}
assert_eq!(BLEND_ORDERS.all().last(), Some(&Continuity::G3), "a blend builds up to G3 today");
assert_eq!(blend_orders().last(), Some(&Continuity::G3), "a blend builds up to G3 today");
for lang in ["en", "ar", "he", "ja", "zh", "de", "ko"] {
let locale = crate::i18n::Locale::for_language(lang);
for g in Continuity::ALL {
@@ -1157,5 +1115,6 @@ mod tests {
assert!(tip.contains(g.code()), "{lang}: the {g:?} hover text names its code: {tip}");
}
}
assert_eq!(ui_kit::continuity_tip(&blend_orders()).lines().count(), 4);
}
}
+4 -4
View File
@@ -2215,7 +2215,7 @@ fn patch_dialog_takes_continuity_and_points_and_brings_them_back() {
d.inputs[0].items = rim;
d.inputs[1].items = vec![Sel::Vertex { body: "Tube".into(), point: Vec3::new(0.0, 0.0, 36.0) }];
if let Kind::Surface(crate::dialogs_surface::Surf::Patch { continuity }) = &mut d.kind {
*continuity = 1;
*continuity = solidcraft_engine::kernel::Continuity::G1;
}
let (id, p) = apply_commands(&app, &d).unwrap().remove(0);
assert_eq!((id.as_str(), &p["continuity"], &p["points"]), ("surface.patch", &json!("g1"), &json!([[0.0, 0.0, 36.0]])), "{p}");
@@ -2225,7 +2225,7 @@ fn patch_dialog_takes_continuity_and_points_and_brings_them_back() {
// Edit Feature: G1 again, and the point (on no face) kept as it was.
let fid = app.session.doc.features.last().unwrap().id;
let back = crate::dialogs::for_feature(&app, fid, None).unwrap();
assert!(matches!(back.kind, Kind::Surface(crate::dialogs_surface::Surf::Patch { continuity: 1 })));
assert!(matches!(back.kind, Kind::Surface(crate::dialogs_surface::Surf::Patch { continuity: solidcraft_engine::kernel::Continuity::G1 })));
let (_, p) = apply_commands(&app, &back).unwrap().remove(0);
assert_eq!((&p["params"]["continuity"], &p["params"]["points"]), (&json!("g1"), &json!([[0.0, 0.0, 36.0]])), "{p}");
}
@@ -2244,10 +2244,10 @@ fn patch_dialog_rows_are_translated() {
assert!(h.widget(through).is_some(), "{lang}: the points input must show {through:?}");
// The continuity in the language too, in words (the code is in the hover text).
if let Some(crate::dialogs::Dialog { kind: Kind::Surface(crate::dialogs_surface::Surf::Patch { continuity }), .. }) = h.app.dialog.as_mut() {
*continuity = 1;
*continuity = solidcraft_engine::kernel::Continuity::G1;
}
h.frames(2);
assert!(h.widget(tangent).is_some(), "{lang}: the Tangent segment must show {tangent:?}");
assert!(h.widget(tangent).is_some(), "{lang}: the Tangent choice must show {tangent:?}");
}
}
+20
View File
@@ -2,6 +2,7 @@
use super::{metrics::*, text::paint_text};
use crate::{scenario, theme::Tokens};
use egui::{Response, RichText, Ui, vec2};
use solidcraft_engine::kernel::Continuity;
pub(super) const FORM: &str = "sc_dense_form";
@@ -92,6 +93,25 @@ pub(crate) fn choice_row(ui: &mut Ui, label: &str, id: &str, selected: &mut usiz
row(ui, label, |ui| super::dropdown(ui, id, selected, options))
}
/// The one control that chooses a continuity order, wherever one is chosen: a dropdown of
/// `allowed` (what the construction builds, `Construction::offered`). Its labels are the orders'
/// plain words and its hover text a line per order naming its code, both from the kernel's one
/// table of orders; a value not in `allowed` shows the first. Tokens: `row, dropdown`.
pub(crate) fn continuity_row(ui: &mut Ui, label: &str, id: &str, value: &mut Continuity, allowed: &[Continuity]) -> Response {
let labels: Vec<&str> = allowed.iter().map(|g| g.label_id()).collect();
let mut index = allowed.iter().position(|g| g == value).unwrap_or(0);
let response = choice_row(ui, label, id, &mut index, &labels);
if let Some(g) = allowed.get(index) {
*value = *g;
}
response.on_hover_text(continuity_tip(allowed))
}
/// The hover text of a continuity choice: each order's own line, which names its code.
pub(crate) fn continuity_tip(allowed: &[Continuity]) -> String {
allowed.iter().map(|g| crate::i18n::tr(g.tip_id())).collect::<Vec<_>>().join("\n")
}
/// Internal form geometry for controls which need several lines (e.g. text areas).
pub(super) fn sized_row<R>(ui: &mut Ui, text: &str, height: f32, control: impl FnOnce(&mut Ui) -> R) -> R {
let t = Tokens::get();