Thanks @jijinbei.
This commit is contained in:
1 parent
487aa64e10
commit
2acc344aa4
14 files changed
+661
-7
No files matched your search
@@ -340,10 +340,16 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
if !visible(app, ctx) {
|
||||
return;
|
||||
}
|
||||
let (opts, dof_line, ok) = {
|
||||
let (opts, dof_line, ok, open_ends, untied_ends) = {
|
||||
let st = app.session.world_state();
|
||||
let Some(ss) = app.session.active_sketch.and_then(|s| st.sketch(s)) else { return };
|
||||
(ss.sketch.view.clone(), dof_text(ss.report.dof, ss.report.ok()), ss.report.ok() && ss.report.dof == 0)
|
||||
(
|
||||
ss.sketch.view.clone(),
|
||||
dof_text(ss.report.dof, ss.report.ok()),
|
||||
ss.report.ok() && ss.report.dof == 0,
|
||||
ss.ends.open.len(),
|
||||
ss.ends.untied.len(),
|
||||
)
|
||||
};
|
||||
let t = Tokens::get();
|
||||
let vp = viewport(app, ctx);
|
||||
@@ -369,6 +375,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
let mut linetype: Option<u8> = None;
|
||||
let mut look = false;
|
||||
let mut finish = false;
|
||||
let mut tie = false;
|
||||
let mut start: Option<&'static str> = None;
|
||||
let feature = active_feature(app);
|
||||
let has_curves = !selected_curves(app).is_empty();
|
||||
@@ -429,7 +436,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
});
|
||||
if !ofold {
|
||||
// Keep room for the footer: status heading and line, a line, Finish Sketch, a line.
|
||||
let footer = 5.0 * line + ui.spacing().interact_size.y + 6.0 * gap + 8.0;
|
||||
let ends_rows = if open_ends > 0 { line + gap } else { 0.0 } + if untied_ends > 0 { ui.spacing().interact_size.y + gap } else { 0.0 };
|
||||
let footer = 5.0 * line + ui.spacing().interact_size.y + 6.0 * gap + 8.0 + ends_rows;
|
||||
let options_height = (bottom - ui.cursor().top() - footer).max(24.0);
|
||||
let options =
|
||||
egui::ScrollArea::vertical().id_salt("sc_palette_options").max_height(options_height).auto_shrink([false, true]).show(ui, |ui| {
|
||||
@@ -537,6 +545,22 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
ui.label(RichText::new(app.tr("ui.sketch_status")).size(12.0));
|
||||
});
|
||||
ui.label(RichText::new(crate::i18n::tr_dyn(&dof_line)).color(if ok { t.sketch_accent } else { t.text_dim }));
|
||||
// Where a profile can fail (the viewport rings and diamonds them).
|
||||
if open_ends > 0 {
|
||||
let text = crate::i18n::format("sketch.open_ends", &[("count", &open_ends.to_string())]);
|
||||
ui.label(RichText::new(text).color(t.warning)).on_hover_text(app.tr("ui.open_ends_tip"));
|
||||
}
|
||||
if untied_ends > 0 {
|
||||
ui.horizontal(|ui| {
|
||||
let text = crate::i18n::format("sketch.untied_ends", &[("count", &untied_ends.to_string())]);
|
||||
ui.label(RichText::new(text).color(t.warning)).on_hover_text(app.tr("ui.untied_ends_tip"));
|
||||
let button = ui.button(app.tr("ui.tie_ends")).on_hover_text(app.tr("ui.tie_ends_tip"));
|
||||
crate::scenario::publish_handle("sketch_palette:tie_ends", button.rect.center());
|
||||
if button.clicked() {
|
||||
tie = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
// One line between the status and Finish Sketch, and below it to the edge.
|
||||
ui.add_space((line - gap).max(0.0));
|
||||
// A row as tall as the button, so the gap below it is just the line.
|
||||
@@ -577,6 +601,9 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
if look {
|
||||
crate::dialogs::look_at_sketch_again(app);
|
||||
}
|
||||
if tie && let Err(e) = app.run("sketch.loose_ends.tie", json!({})) {
|
||||
app.set_status(e, true);
|
||||
}
|
||||
if finish {
|
||||
app.finish_sketch();
|
||||
}
|
||||
@@ -748,6 +775,28 @@ mod tests {
|
||||
assert!(yaw.abs() < 0.01, "must align with the sketch's own x axis: yaw={yaw}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_counts_loose_ends_and_tie_closes_the_untied_ones() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||
// A square of separate lines (closed by position only) and one lone line (two open ends).
|
||||
let c = [[0, 0], [10, 0], [10, 10], [0, 10]];
|
||||
for i in 0..4 {
|
||||
h.app.run("sketch.line", json!({"points": [c[i], c[(i + 1) % 4]], "infer": false, "connect": false})).unwrap();
|
||||
}
|
||||
h.app.run("sketch.line", json!({"points": [[20, 0], [30, 5]], "infer": false, "connect": false})).unwrap();
|
||||
h.settle();
|
||||
assert!(h.widget("Open 2").is_some(), "the status counts the open ends");
|
||||
assert!(h.widget("Untied 4").is_some(), "the status counts the untied ends");
|
||||
let at = crate::scenario::handle_at("sketch_palette:tie_ends").unwrap();
|
||||
h.call("ui.click", json!({"x": at.x, "y": at.y}));
|
||||
h.settle();
|
||||
assert!(h.widget("Untied 4").is_none(), "Tie constrains every untied end");
|
||||
assert!(h.widget("Open 2").is_some(), "open ends have nothing to tie to");
|
||||
let r = h.app.run("sketch.loose_ends", json!({})).unwrap();
|
||||
assert!(r["untied"].as_array().is_some_and(Vec::is_empty), "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_disclosure_triangle_folds_and_unfolds_the_section() {
|
||||
OPTIONS_FOLDED.with(|v| v.set(false));
|
||||
|
||||
@@ -3177,6 +3177,7 @@ fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
painter.rect_filled(Rect::from_center_size(sp, vec2(5.0, 5.0)), 0.0, c);
|
||||
}
|
||||
}
|
||||
loose_end_markers(sk, &ss.ends, &ss.plane, painter, proj, &t);
|
||||
// Sketch status.
|
||||
if let Some(r) = app.viewport.rect {
|
||||
let msg = if !ss.report.ok() {
|
||||
@@ -3191,6 +3192,28 @@ fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a profile can fail: a ring round each open end (it touches no other curve) and a
|
||||
/// diamond round each untied one (it touches another curve only by position), told apart by
|
||||
/// shape. Both are warnings, not errors: an open sketch (a sweep path, an axis) is valid.
|
||||
fn loose_end_markers(
|
||||
sk: &solidcraft_engine::sketch::Sketch,
|
||||
ends: &solidcraft_engine::sketch::EndReport,
|
||||
plane: &solidcraft_engine::geom::Plane,
|
||||
painter: &egui::Painter,
|
||||
proj: &Proj,
|
||||
t: &Tokens,
|
||||
) {
|
||||
let screen = |i: usize| sk.point(i).and_then(|p| proj.to_screen(plane.to_world(p)));
|
||||
for sp in ends.open.iter().filter_map(|&i| screen(i)) {
|
||||
painter.circle_stroke(sp, 7.0, Stroke::new(1.8, t.warning));
|
||||
}
|
||||
for sp in ends.untied.iter().filter_map(|u| screen(u.point)) {
|
||||
let r = 7.5;
|
||||
let diamond = vec![pos2(sp.x, sp.y - r), pos2(sp.x + r, sp.y), pos2(sp.x, sp.y + r), pos2(sp.x - r, sp.y)];
|
||||
painter.add(egui::Shape::closed_line(diamond, Stroke::new(1.8, t.warning)));
|
||||
}
|
||||
}
|
||||
|
||||
/// The colours of a headless picture of the model (`ui.render`, the agent's view image) in the
|
||||
/// dark or the light theme and a colour vision mode, from the tokens the window draws with, so the
|
||||
/// picture matches the window. The light theme's tokens (Off) are the engine's own colours, so a light
|
||||
|
||||
Reference in new issue
Block a user