Dialog parity with Fusion: header with fold glyph, label column, Select/N selected chips, info glyph and OK greyed until valid, Fusion colours; Extrude rows Start (profile plane/offset), Direction (one side, two sides, symmetric), Extent Type (distance/all), Taper Angle; Hole rows Placement (single/multiple sketch points), Extents, Hole Tap Type (tapped size), Drill Point, Tip Angle; pattern Type switch; Mirror operation; pre-selection routes every item to the input that takes it (planar-only inputs, fillet tangent chains), with tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
a8d54e0a3f
commit
89c2c440ac
7 files changed
+570
-145
No files matched your search
@@ -176,9 +176,14 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
|
|||||||
taper_handle(ui, painter, proj, base - e * (40.0 * px), n * l, -e, ang, taper);
|
taper_handle(ui, painter, proj, base - e * (40.0 * px), n * l, -e, ang, taper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let (Kind::Hole { depth, .. }, Some(n)) = (&mut d.kind, dir) {
|
if let (Kind::Hole { depth, opts, .. }, Some(n)) = (&mut d.kind, dir) {
|
||||||
let l = app.session.doc.eval(depth, ValueKind::Length).ok().filter(|v| v.is_finite()).unwrap_or(0.0);
|
let l = if opts.all { 0.0 } else { app.session.doc.eval(depth, ValueKind::Length).ok().filter(|v| v.is_finite()).unwrap_or(0.0) };
|
||||||
|
let before = depth.clone();
|
||||||
drag_arrow(ui, painter, proj, base, bs, -n, l, false, egui::Id::new("sc_manipulator_depth"), half_height, depth);
|
drag_arrow(ui, painter, proj, base, bs, -n, l, false, egui::Id::new("sc_manipulator_depth"), half_height, depth);
|
||||||
|
// Dragging the depth turns "through all" into a distance.
|
||||||
|
if *depth != before {
|
||||||
|
opts.all = false;
|
||||||
|
}
|
||||||
dir = None;
|
dir = None;
|
||||||
}
|
}
|
||||||
let Some((label, kind, value)) = d.primary() else { return };
|
let Some((label, kind, value)) = d.primary() else { return };
|
||||||
|
|||||||
+406
-135
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
use egui::{Color32, RichText, vec2};
|
use egui::{Color32, RichText, Stroke, vec2};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use solvecraft_engine::Sel;
|
use solvecraft_engine::Sel;
|
||||||
use solvecraft_engine::Session;
|
use solvecraft_engine::Session;
|
||||||
@@ -20,7 +20,6 @@ use crate::viewport::Hit;
|
|||||||
const OPS: [&str; 4] = ["new", "join", "cut", "intersect"];
|
const OPS: [&str; 4] = ["new", "join", "cut", "intersect"];
|
||||||
const OP_LABELS: [&str; 4] = ["New Body", "Join", "Cut", "Intersect"];
|
const OP_LABELS: [&str; 4] = ["New Body", "Join", "Cut", "Intersect"];
|
||||||
const DIRS: [&str; 4] = ["positive", "negative", "symmetric", "positive"];
|
const DIRS: [&str; 4] = ["positive", "negative", "symmetric", "positive"];
|
||||||
const DIR_LABELS: [&str; 4] = ["One side", "Flip", "Symmetric", "Two sides"];
|
|
||||||
/// The "Two sides" direction (a second distance the other way).
|
/// The "Two sides" direction (a second distance the other way).
|
||||||
pub const TWO_SIDES: usize = 3;
|
pub const TWO_SIDES: usize = 3;
|
||||||
const HOLE_TYPES: [&str; 3] = ["simple", "counterbore", "countersink"];
|
const HOLE_TYPES: [&str; 3] = ["simple", "counterbore", "countersink"];
|
||||||
@@ -39,8 +38,12 @@ pub enum Kind {
|
|||||||
auto_op: bool,
|
auto_op: bool,
|
||||||
/// The other side's distance (two sides).
|
/// The other side's distance (two sides).
|
||||||
distance2: String,
|
distance2: String,
|
||||||
/// Taper angle (empty: none).
|
/// Taper angle (empty or zero: none).
|
||||||
taper: String,
|
taper: String,
|
||||||
|
/// Start offset from the profile plane (empty: start on the profile plane).
|
||||||
|
start: String,
|
||||||
|
/// Extent type All (through everything) instead of a distance.
|
||||||
|
all: bool,
|
||||||
},
|
},
|
||||||
Revolve {
|
Revolve {
|
||||||
angle: String,
|
angle: String,
|
||||||
@@ -166,6 +169,7 @@ pub enum Kind {
|
|||||||
cb_depth: String,
|
cb_depth: String,
|
||||||
cs_diameter: String,
|
cs_diameter: String,
|
||||||
cs_angle: String,
|
cs_angle: String,
|
||||||
|
opts: HoleOpts,
|
||||||
},
|
},
|
||||||
Primitive {
|
Primitive {
|
||||||
cmd: &'static str,
|
cmd: &'static str,
|
||||||
@@ -196,6 +200,27 @@ pub enum Kind {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The rest of the Hole dialog: placement, extents, tap type and drill point.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct HoleOpts {
|
||||||
|
/// Placement at several sketch points instead of one face point.
|
||||||
|
pub multiple: bool,
|
||||||
|
/// Extents: through all instead of a distance.
|
||||||
|
pub all: bool,
|
||||||
|
/// Drill point: an angled tip instead of a flat bottom.
|
||||||
|
pub angled: bool,
|
||||||
|
pub tip_angle: String,
|
||||||
|
/// Tap type: tapped (a cosmetic thread) instead of simple.
|
||||||
|
pub tapped: bool,
|
||||||
|
pub thread: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HoleOpts {
|
||||||
|
fn default() -> Self {
|
||||||
|
HoleOpts { multiple: false, all: false, angled: true, tip_angle: "118 deg".into(), tapped: false, thread: "M5".into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Dialog {
|
pub struct Dialog {
|
||||||
pub kind: Kind,
|
pub kind: Kind,
|
||||||
@@ -207,6 +232,8 @@ pub struct Dialog {
|
|||||||
pub editing: Option<(u64, Option<usize>)>,
|
pub editing: Option<(u64, Option<usize>)>,
|
||||||
/// Give the on-canvas value box the keyboard (and select its text) on the next frame.
|
/// Give the on-canvas value box the keyboard (and select its text) on the next frame.
|
||||||
pub focus: bool,
|
pub focus: bool,
|
||||||
|
/// Folded to its header (the "−" glyph).
|
||||||
|
pub collapsed: bool,
|
||||||
/// Parameters the dialog doesn't show but the command needs (kept when editing).
|
/// Parameters the dialog doesn't show but the command needs (kept when editing).
|
||||||
pub extra: serde_json::Map<String, Value>,
|
pub extra: serde_json::Map<String, Value>,
|
||||||
}
|
}
|
||||||
@@ -228,7 +255,7 @@ fn fits(a: Accept, s: &Sel) -> bool {
|
|||||||
|
|
||||||
impl Dialog {
|
impl Dialog {
|
||||||
fn new(kind: Kind, inputs: Vec<SelInput>) -> Dialog {
|
fn new(kind: Kind, inputs: Vec<SelInput>) -> Dialog {
|
||||||
Dialog { kind, inputs, active: 0, error: None, editing: None, focus: true, extra: serde_json::Map::new() }
|
Dialog { kind, inputs, active: 0, error: None, editing: None, focus: true, collapsed: false, extra: serde_json::Map::new() }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_command(app: &SolveApp, id: &str) -> Option<Dialog> {
|
pub fn for_command(app: &SolveApp, id: &str) -> Option<Dialog> {
|
||||||
@@ -243,13 +270,15 @@ impl Dialog {
|
|||||||
operation: usize::from(has_bodies),
|
operation: usize::from(has_bodies),
|
||||||
auto_op: true,
|
auto_op: true,
|
||||||
distance2: "10 mm".into(),
|
distance2: "10 mm".into(),
|
||||||
taper: String::new(),
|
taper: "0 deg".into(),
|
||||||
|
start: String::new(),
|
||||||
|
all: false,
|
||||||
},
|
},
|
||||||
vec![SelInput::new("Profiles", PROFILES | PLANAR_FACES, true)],
|
vec![SelInput::new("Profiles", PROFILES | PLANAR_FACES, true)],
|
||||||
),
|
),
|
||||||
"Revolve" => Dialog::new(
|
"Revolve" => Dialog::new(
|
||||||
Kind::Revolve { angle: "360 deg".into(), operation: 0 },
|
Kind::Revolve { angle: "360 deg".into(), operation: 0 },
|
||||||
vec![SelInput::new("Profiles", PROFILES, true), SelInput::new("Axis", AXES, false)],
|
vec![SelInput::new("Profile", PROFILES, true), SelInput::new("Axis", AXES, false)],
|
||||||
),
|
),
|
||||||
"FusionFilletEdgesCommand" => {
|
"FusionFilletEdgesCommand" => {
|
||||||
Dialog::new(Kind::Fillet { radius: "2 mm".into(), chamfer: false, chain: true }, vec![SelInput::new("Edges", EDGES | FACES, true)])
|
Dialog::new(Kind::Fillet { radius: "2 mm".into(), chamfer: false, chain: true }, vec![SelInput::new("Edges", EDGES | FACES, true)])
|
||||||
@@ -257,13 +286,13 @@ impl Dialog {
|
|||||||
"FusionChamferCommand" => {
|
"FusionChamferCommand" => {
|
||||||
Dialog::new(Kind::Fillet { radius: "1 mm".into(), chamfer: true, chain: true }, vec![SelInput::new("Edges", EDGES | FACES, true)])
|
Dialog::new(Kind::Fillet { radius: "1 mm".into(), chamfer: true, chain: true }, vec![SelInput::new("Edges", EDGES | FACES, true)])
|
||||||
}
|
}
|
||||||
"FusionShellBodyCommand" => Dialog::new(Kind::Shell { thickness: "2 mm".into() }, vec![SelInput::new("Faces", FACES, true)]),
|
"FusionShellBodyCommand" => Dialog::new(Kind::Shell { thickness: "2 mm".into() }, vec![SelInput::new("Faces/Body", FACES, true)]),
|
||||||
"FusionDraftCommand" => Dialog::new(
|
"FusionDraftCommand" => Dialog::new(
|
||||||
Kind::Draft { angle: "5 deg".into() },
|
Kind::Draft { angle: "5 deg".into() },
|
||||||
vec![SelInput::new("Faces", FACES, true), SelInput::new("Neutral plane", PLANES | PLANAR_FACES, false)],
|
vec![SelInput::new("Faces", FACES, true), SelInput::new("Neutral plane", PLANES | PLANAR_FACES, false)],
|
||||||
),
|
),
|
||||||
"MirrorCommand" => {
|
"MirrorCommand" => {
|
||||||
Dialog::new(Kind::Mirror, vec![SelInput::new("Bodies", BODIES, true), SelInput::new("Mirror plane", PLANES | PLANAR_FACES, false)])
|
Dialog::new(Kind::Mirror, vec![SelInput::new("Objects", BODIES, true), SelInput::new("Mirror Plane", PLANES | PLANAR_FACES, false)])
|
||||||
}
|
}
|
||||||
"FusionRibCommand" | "FusionWebCommand" => Dialog::new(
|
"FusionRibCommand" | "FusionWebCommand" => Dialog::new(
|
||||||
Kind::Rib { web: id == "FusionWebCommand", thickness: "2 mm".into(), depth: String::new(), flip: false },
|
Kind::Rib { web: id == "FusionWebCommand", thickness: "2 mm".into(), depth: String::new(), flip: false },
|
||||||
@@ -330,7 +359,7 @@ impl Dialog {
|
|||||||
"FusionMoveCommand" => {
|
"FusionMoveCommand" => {
|
||||||
Dialog::new(Kind::Move { x: "0 mm".into(), y: "0 mm".into(), z: "10 mm".into() }, vec![SelInput::new("Bodies", BODIES, true)])
|
Dialog::new(Kind::Move { x: "0 mm".into(), y: "0 mm".into(), z: "10 mm".into() }, vec![SelInput::new("Bodies", BODIES, true)])
|
||||||
}
|
}
|
||||||
"FusionHoleCommand" => Dialog::new(hole_defaults(), vec![SelInput::new("Position", FACES, true)]),
|
"FusionHoleCommand" => Dialog::new(hole_defaults(), vec![SelInput::new("Face", PLANAR_FACES, true)]),
|
||||||
"PrimitiveBox" => Dialog::new(
|
"PrimitiveBox" => Dialog::new(
|
||||||
Kind::Primitive {
|
Kind::Primitive {
|
||||||
cmd: "PrimitiveBox",
|
cmd: "PrimitiveBox",
|
||||||
@@ -360,7 +389,7 @@ impl Dialog {
|
|||||||
),
|
),
|
||||||
"FusionCombineCommand" => Dialog::new(
|
"FusionCombineCommand" => Dialog::new(
|
||||||
Kind::Combine { operation: 1, keep_tools: false },
|
Kind::Combine { operation: 1, keep_tools: false },
|
||||||
vec![SelInput::new("Target body", BODIES, false), SelInput::new("Tool bodies", BODIES, true)],
|
vec![SelInput::new("Target Body", BODIES, false), SelInput::new("Tool Bodies", BODIES, true)],
|
||||||
),
|
),
|
||||||
"ChangeParameterCommand" => Dialog::new(Kind::Params { new_name: String::new(), new_expr: String::new() }, vec![]),
|
"ChangeParameterCommand" => Dialog::new(Kind::Params { new_name: String::new(), new_expr: String::new() }, vec![]),
|
||||||
_ => return None,
|
_ => return None,
|
||||||
@@ -380,8 +409,36 @@ impl Dialog {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(inp) = d.inputs.iter_mut().find(|i| fits(i.accept, sel) && (i.multi || i.items.is_empty())) {
|
// Planar-only inputs take planar faces only.
|
||||||
inp.items.push(sel.clone());
|
let planar_ok = match sel {
|
||||||
|
Sel::Face { body, index, .. } => planar_face(s, body, *index).is_some(),
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
let takes = |i: &SelInput| {
|
||||||
|
fits(i.accept, sel) && (i.multi || i.items.is_empty()) && (planar_ok || i.accept & FACES != 0 || !matches!(sel, Sel::Face { .. }))
|
||||||
|
};
|
||||||
|
if let Some(inp) = d.inputs.iter_mut().find(|i| takes(i)) {
|
||||||
|
// Fillets and chamfers take an edge with its tangent chain, as a pick does.
|
||||||
|
let picked = match (&d.kind, sel) {
|
||||||
|
(Kind::Fillet { chain: true, .. }, Sel::Edge { body, index, .. }) => s
|
||||||
|
.model
|
||||||
|
.state()
|
||||||
|
.body(body)
|
||||||
|
.map(|b| {
|
||||||
|
let m = b.mesh();
|
||||||
|
m.tangent_chain(*index, 2f64.to_radians())
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|i| m.edges.get(i).map(|e| Sel::Edge { body: body.clone(), index: i, point: polyline_mid(e) }))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| vec![sel.clone()]),
|
||||||
|
_ => vec![sel.clone()],
|
||||||
|
};
|
||||||
|
for x in picked {
|
||||||
|
if !inp.items.contains(&x) {
|
||||||
|
inp.items.push(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// With nothing pre-selected, a single profile in the sketch being used is taken.
|
// With nothing pre-selected, a single profile in the sketch being used is taken.
|
||||||
@@ -686,6 +743,71 @@ fn hint(inp: &SelInput) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Width of the label column; longer labels are cut with "…".
|
||||||
|
const LABEL_W: f32 = 92.0;
|
||||||
|
|
||||||
|
/// A row label in the dialog's label column.
|
||||||
|
fn row_label(ui: &mut egui::Ui, text: &str) {
|
||||||
|
ui.allocate_ui_with_layout(vec2(LABEL_W, 22.0), egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||||
|
ui.set_width(LABEL_W);
|
||||||
|
ui.add(egui::Label::new(text).truncate()).on_hover_text(text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rows above the selection inputs: the pattern type, the Hole placement (one face point, or
|
||||||
|
/// sketch points).
|
||||||
|
fn placement_row(d: &mut Dialog, ui: &mut egui::Ui) {
|
||||||
|
let current = match d.kind {
|
||||||
|
Kind::PatternRect { .. } => Some(0),
|
||||||
|
Kind::PatternCirc { .. } => Some(1),
|
||||||
|
Kind::PathPattern { .. } => Some(2),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(cur) = current {
|
||||||
|
row_label(ui, "Type");
|
||||||
|
let mut ty = cur;
|
||||||
|
combo(ui, "pat_type", &["Rectangular", "Circular", "Path"], &mut ty);
|
||||||
|
ui.end_row();
|
||||||
|
if ty != cur {
|
||||||
|
// Same objects, the new pattern's other inputs.
|
||||||
|
let objects = d.inputs.first().map(|i| i.items.clone()).unwrap_or_default();
|
||||||
|
let (kind, mut inputs) = match ty {
|
||||||
|
0 => (
|
||||||
|
Kind::PatternRect { count: "3".into(), spacing: "20 mm".into(), count2: "1".into(), spacing2: "20 mm".into() },
|
||||||
|
vec![SelInput::new("Objects", BODIES, true), SelInput::new("Direction", AXES, false), SelInput::new("Direction 2", AXES, false)],
|
||||||
|
),
|
||||||
|
1 => (
|
||||||
|
Kind::PatternCirc { count: "6".into(), angle: "360 deg".into() },
|
||||||
|
vec![SelInput::new("Objects", BODIES, true), SelInput::new("Axis", AXES, false)],
|
||||||
|
),
|
||||||
|
_ => (
|
||||||
|
Kind::PathPattern { count: "4".into(), spacing: "20 mm".into() },
|
||||||
|
vec![SelInput::new("Objects", BODIES, true), SelInput::new("Path", CURVES, true)],
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if let Some(i) = inputs.first_mut() {
|
||||||
|
i.items = objects;
|
||||||
|
}
|
||||||
|
d.kind = kind;
|
||||||
|
d.inputs = inputs;
|
||||||
|
d.extra.clear();
|
||||||
|
d.advance();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Kind::Hole { opts, .. } = &mut d.kind else { return };
|
||||||
|
row_label(ui, "Placement");
|
||||||
|
let mut m = usize::from(opts.multiple);
|
||||||
|
combo(ui, "hole_place", &["Single", "Multiple"], &mut m);
|
||||||
|
ui.end_row();
|
||||||
|
if (m == 1) != opts.multiple {
|
||||||
|
opts.multiple = m == 1;
|
||||||
|
d.inputs = vec![if opts.multiple { SelInput::new("Points", selection::POINTS, true) } else { SelInput::new("Face", PLANAR_FACES, true) }];
|
||||||
|
d.active = 0;
|
||||||
|
d.extra.remove("direction");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Selection input rows: label, "N selected" (or a hint) and a clear button; clicking a row
|
/// Selection input rows: label, "N selected" (or a hint) and a clear button; clicking a row
|
||||||
/// makes it the active input.
|
/// makes it the active input.
|
||||||
fn input_rows(d: &mut Dialog, ui: &mut egui::Ui) {
|
fn input_rows(d: &mut Dialog, ui: &mut egui::Ui) {
|
||||||
@@ -693,20 +815,24 @@ fn input_rows(d: &mut Dialog, ui: &mut egui::Ui) {
|
|||||||
let mut clear: Option<usize> = None;
|
let mut clear: Option<usize> = None;
|
||||||
let mut activate: Option<usize> = None;
|
let mut activate: Option<usize> = None;
|
||||||
for (i, inp) in d.inputs.iter().enumerate() {
|
for (i, inp) in d.inputs.iter().enumerate() {
|
||||||
ui.label(inp.label);
|
row_label(ui, inp.label);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
let active = i == d.active;
|
let active = i == d.active;
|
||||||
let text = if inp.items.is_empty() {
|
// A chip: "↖ Select" (outlined) while empty, "N selected" (filled) once picked.
|
||||||
RichText::new(hint(inp)).color(if active { t.warning } else { t.text_dim })
|
let filled = !inp.items.is_empty();
|
||||||
|
let text = if filled {
|
||||||
|
RichText::new(format!("↖ {} selected", inp.items.len())).color(Color32::WHITE)
|
||||||
} else {
|
} else {
|
||||||
RichText::new(format!("{} selected", inp.items.len())).color(t.text)
|
RichText::new("↖ Select").color(t.text)
|
||||||
};
|
};
|
||||||
|
let stroke = if active { Stroke::new(1.5, t.accent) } else { Stroke::new(1.0, t.dialog_border.gamma_multiply(1.6)) };
|
||||||
// Not in the Tab order: Tab moves between values.
|
// Not in the Tab order: Tab moves between values.
|
||||||
let b = egui::Button::new(text)
|
let b = egui::Button::new(text)
|
||||||
.fill(if active { t.accent_soft } else { Color32::TRANSPARENT })
|
.fill(if filled { t.chip } else { Color32::TRANSPARENT })
|
||||||
.min_size(vec2(FIELD_W, 22.0))
|
.stroke(stroke)
|
||||||
|
.min_size(vec2(0.0, 22.0))
|
||||||
.sense(egui::Sense::CLICK);
|
.sense(egui::Sense::CLICK);
|
||||||
if ui.add(b).on_hover_text("Click to make this the input that viewport picks go to").clicked() {
|
if ui.add(b).on_hover_text(format!("{} (click to pick into this input)", hint(inp))).clicked() {
|
||||||
activate = Some(i);
|
activate = Some(i);
|
||||||
}
|
}
|
||||||
if !inp.items.is_empty() && ui.small_button("×").on_hover_text("Clear the selection").clicked() {
|
if !inp.items.is_empty() && ui.small_button("×").on_hover_text("Clear the selection").clicked() {
|
||||||
@@ -727,10 +853,10 @@ fn input_rows(d: &mut Dialog, ui: &mut egui::Ui) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Width limits of a docked dialog: shrink-wrapped to its content within these.
|
/// Width limits of a docked dialog: shrink-wrapped to its content within these.
|
||||||
const DIALOG_MIN_W: f32 = 260.0;
|
const DIALOG_MIN_W: f32 = 252.0;
|
||||||
const DIALOG_MAX_W: f32 = 380.0;
|
const DIALOG_MAX_W: f32 = 380.0;
|
||||||
/// Width of value fields and choice boxes in a dialog.
|
/// Width of value fields and choice boxes in a dialog.
|
||||||
const FIELD_W: f32 = 140.0;
|
const FIELD_W: f32 = 124.0;
|
||||||
|
|
||||||
/// Measure again when the picked items changed.
|
/// Measure again when the picked items changed.
|
||||||
fn update_measure(app: &SolveApp, d: &mut Dialog) {
|
fn update_measure(app: &SolveApp, d: &mut Dialog) {
|
||||||
@@ -850,14 +976,17 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
let mut enter = false;
|
let mut enter = false;
|
||||||
let wide = matches!(d.kind, Kind::Params { .. });
|
let wide = matches!(d.kind, Kind::Params { .. });
|
||||||
let heading = if d.editing.is_some() { format!("EDIT {}", title(&d.kind)) } else { title(&d.kind).to_string() };
|
let heading = if d.editing.is_some() { format!("EDIT {}", title(&d.kind)) } else { title(&d.kind).to_string() };
|
||||||
let frame = egui::Frame::window(&ctx.global_style()).corner_radius(egui::CornerRadius { nw: 6, sw: 6, ne: 0, se: 0 }).shadow(egui::Shadow {
|
let frame = egui::Frame::window(&ctx.global_style())
|
||||||
offset: [-2, 2],
|
.fill(t.dialog_bg)
|
||||||
blur: 8,
|
.stroke(Stroke::new(1.0, t.dialog_border))
|
||||||
spread: 0,
|
.corner_radius(egui::CornerRadius { nw: 4, sw: 4, ne: 0, se: 0 })
|
||||||
color: Color32::from_black_alpha(40),
|
.shadow(egui::Shadow { offset: [-2, 2], blur: 8, spread: 0, color: Color32::from_black_alpha(40) });
|
||||||
});
|
// OK is offered once the inputs make a feature.
|
||||||
egui::Window::new(RichText::new(heading).strong().size(13.0))
|
let has_ok = !matches!(d.kind, Kind::Sketch | Kind::Params { .. } | Kind::Measure { .. } | Kind::Preferences);
|
||||||
|
let valid = !has_ok || !d.previews() || (apply_commands(app, &d).is_ok() && app.preview.error.is_none());
|
||||||
|
egui::Window::new(RichText::new(heading.clone()).strong().size(13.0))
|
||||||
.id(egui::Id::new("sc_dialog"))
|
.id(egui::Id::new("sc_dialog"))
|
||||||
|
.title_bar(false)
|
||||||
.frame(frame)
|
.frame(frame)
|
||||||
.pivot(egui::Align2::RIGHT_TOP)
|
.pivot(egui::Align2::RIGHT_TOP)
|
||||||
.fixed_pos(anchor)
|
.fixed_pos(anchor)
|
||||||
@@ -872,29 +1001,73 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
let margin = 2.0 * ui.style().spacing.window_margin.leftf();
|
let margin = 2.0 * ui.style().spacing.window_margin.leftf();
|
||||||
ui.set_min_width(if wide { 480.0 } else { DIALOG_MIN_W } - margin);
|
ui.set_min_width(if wide { 480.0 } else { DIALOG_MIN_W } - margin);
|
||||||
ui.set_max_width(if wide { 560.0 } else { DIALOG_MAX_W } - margin);
|
ui.set_max_width(if wide { 560.0 } else { DIALOG_MAX_W } - margin);
|
||||||
|
// Header: "−" folds the dialog, the command name, "»" at the right.
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let fold = if d.collapsed { "+" } else { "−" };
|
||||||
|
if ui
|
||||||
|
.add(egui::Button::new(RichText::new(fold).size(14.0)).frame(false))
|
||||||
|
.on_hover_text(if d.collapsed { "Expand" } else { "Collapse" })
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
d.collapsed = !d.collapsed;
|
||||||
|
}
|
||||||
|
ui.label(RichText::new(heading.as_str()).strong().size(12.5));
|
||||||
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
|
ui.label(RichText::new("»").color(t.text_dim));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if d.collapsed {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ui.add_space(2.0);
|
||||||
egui::Grid::new("sc_dialog_grid").num_columns(2).spacing(vec2(10.0, 8.0)).show(ui, |ui| {
|
egui::Grid::new("sc_dialog_grid").num_columns(2).spacing(vec2(10.0, 8.0)).show(ui, |ui| {
|
||||||
|
placement_row(&mut d, ui);
|
||||||
input_rows(&mut d, ui);
|
input_rows(&mut d, ui);
|
||||||
match &mut d.kind {
|
match &mut d.kind {
|
||||||
Kind::Sketch => {}
|
Kind::Sketch => {}
|
||||||
Kind::Extrude { distance, direction, operation, auto_op, distance2, taper } => {
|
Kind::Extrude { distance, direction, operation, auto_op, distance2, taper, start, all } => {
|
||||||
ui.label("Direction");
|
row_label(ui, "Start");
|
||||||
combo(ui, "ex_dir", &DIR_LABELS, direction);
|
let mut st = usize::from(!start.is_empty());
|
||||||
|
combo(ui, "ex_start", &["Profile Plane", "Offset"], &mut st);
|
||||||
|
if st == 1 && start.is_empty() {
|
||||||
|
*start = "5 mm".into();
|
||||||
|
} else if st == 0 {
|
||||||
|
start.clear();
|
||||||
|
}
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Distance");
|
if !start.is_empty() {
|
||||||
enter |= field(ui, distance);
|
row_label(ui, "Offset");
|
||||||
ui.end_row();
|
enter |= field(ui, start);
|
||||||
if *direction == TWO_SIDES {
|
|
||||||
ui.label("Distance 2");
|
|
||||||
enter |= field(ui, distance2);
|
|
||||||
ui.end_row();
|
|
||||||
} else if *direction < 2 {
|
|
||||||
ui.label("Taper angle");
|
|
||||||
let r = ui.add(egui::TextEdit::singleline(taper).hint_text("0 deg"));
|
|
||||||
crate::params_dialog::complete(ui, &r, taper);
|
|
||||||
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
ui.label("Operation");
|
row_label(ui, "Direction");
|
||||||
|
// One Side, Two Sides, Symmetric (a flip is a negative distance).
|
||||||
|
let order = [0usize, TWO_SIDES, 2];
|
||||||
|
let mut pos = order.iter().position(|o| o == direction).unwrap_or(0);
|
||||||
|
combo(ui, "ex_dir", &["One Side", "Two Sides", "Symmetric"], &mut pos);
|
||||||
|
*direction = order.get(pos).copied().unwrap_or(0);
|
||||||
|
ui.end_row();
|
||||||
|
row_label(ui, "Extent Type");
|
||||||
|
let mut ext = usize::from(*all);
|
||||||
|
combo(ui, "ex_ext", &["Distance", "All"], &mut ext);
|
||||||
|
*all = ext == 1;
|
||||||
|
ui.end_row();
|
||||||
|
if !*all {
|
||||||
|
row_label(ui, "Distance");
|
||||||
|
enter |= field(ui, distance);
|
||||||
|
ui.end_row();
|
||||||
|
if *direction == TWO_SIDES {
|
||||||
|
row_label(ui, "Distance 2");
|
||||||
|
enter |= field(ui, distance2);
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if *direction < 2 {
|
||||||
|
row_label(ui, "Taper Angle");
|
||||||
|
enter |= field(ui, taper);
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
row_label(ui, "Operation");
|
||||||
let before = *operation;
|
let before = *operation;
|
||||||
combo(ui, "ex_op", &OP_LABELS, operation);
|
combo(ui, "ex_op", &OP_LABELS, operation);
|
||||||
// A choice made by hand sticks.
|
// A choice made by hand sticks.
|
||||||
@@ -904,111 +1077,117 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Revolve { angle, operation } => {
|
Kind::Revolve { angle, operation } => {
|
||||||
ui.label("Angle");
|
row_label(ui, "Angle");
|
||||||
enter |= field(ui, angle);
|
enter |= field(ui, angle);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Operation");
|
row_label(ui, "Operation");
|
||||||
combo(ui, "rv_op", &OP_LABELS, operation);
|
combo(ui, "rv_op", &OP_LABELS, operation);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Fillet { radius, chamfer, chain } => {
|
Kind::Fillet { radius, chamfer, chain } => {
|
||||||
ui.label(if *chamfer { "Distance" } else { "Radius" });
|
row_label(ui, if *chamfer { "Distance" } else { "Radius" });
|
||||||
enter |= field(ui, radius);
|
enter |= field(ui, radius);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Tangent chain");
|
row_label(ui, "Tangent Chain");
|
||||||
ui.checkbox(chain, "");
|
ui.checkbox(chain, "");
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Shell { thickness } => {
|
Kind::Shell { thickness } => {
|
||||||
ui.label("Inside thickness");
|
row_label(ui, "Inside Thickness");
|
||||||
enter |= field(ui, thickness);
|
enter |= field(ui, thickness);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Draft { angle } => {
|
Kind::Draft { angle } => {
|
||||||
ui.label("Angle");
|
row_label(ui, "Angle");
|
||||||
enter |= field(ui, angle);
|
enter |= field(ui, angle);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Mirror => {}
|
Kind::Mirror => {
|
||||||
|
row_label(ui, "Operation");
|
||||||
|
let mut op = usize::from(d.extra.get("combine").and_then(Value::as_bool).unwrap_or(false));
|
||||||
|
combo(ui, "mi_op", &["New Body", "Join"], &mut op);
|
||||||
|
d.extra.insert("combine".into(), json!(op == 1));
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
Kind::Measure { result, .. } => measure_rows(ui, result.as_ref()),
|
Kind::Measure { result, .. } => measure_rows(ui, result.as_ref()),
|
||||||
Kind::Preferences => preferences_rows(app, ui),
|
Kind::Preferences => preferences_rows(app, ui),
|
||||||
Kind::OffsetPlane { offset } => {
|
Kind::OffsetPlane { offset } => {
|
||||||
ui.label("Distance");
|
row_label(ui, "Distance");
|
||||||
enter |= field(ui, offset);
|
enter |= field(ui, offset);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::AnglePlane { angle } => {
|
Kind::AnglePlane { angle } => {
|
||||||
ui.label("Angle");
|
row_label(ui, "Angle");
|
||||||
enter |= field(ui, angle);
|
enter |= field(ui, angle);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Split | Kind::ReplaceFace | Kind::Remove => {}
|
Kind::Split | Kind::ReplaceFace | Kind::Remove => {}
|
||||||
Kind::Rib { thickness, depth, flip, .. } => {
|
Kind::Rib { thickness, depth, flip, .. } => {
|
||||||
ui.label("Thickness");
|
row_label(ui, "Thickness");
|
||||||
enter |= field(ui, thickness);
|
enter |= field(ui, thickness);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Depth");
|
row_label(ui, "Depth");
|
||||||
let r = ui.add(egui::TextEdit::singleline(depth).hint_text("to the next face"));
|
let r = ui.add(egui::TextEdit::singleline(depth).hint_text("to the next face"));
|
||||||
crate::params_dialog::complete(ui, &r, depth);
|
crate::params_dialog::complete(ui, &r, depth);
|
||||||
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Flip");
|
row_label(ui, "Flip");
|
||||||
ui.checkbox(flip, "");
|
ui.checkbox(flip, "");
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Emboss { depth, deboss } => {
|
Kind::Emboss { depth, deboss } => {
|
||||||
ui.label("Depth");
|
row_label(ui, "Depth");
|
||||||
enter |= field(ui, depth);
|
enter |= field(ui, depth);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Type");
|
row_label(ui, "Type");
|
||||||
let mut i = usize::from(*deboss);
|
let mut i = usize::from(*deboss);
|
||||||
combo(ui, "emb_mode", &["Emboss", "Deboss"], &mut i);
|
combo(ui, "emb_mode", &["Emboss", "Deboss"], &mut i);
|
||||||
*deboss = i == 1;
|
*deboss = i == 1;
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Align { flip } => {
|
Kind::Align { flip } => {
|
||||||
ui.label("Flip");
|
row_label(ui, "Flip");
|
||||||
ui.checkbox(flip, "");
|
ui.checkbox(flip, "");
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::PathPattern { count, spacing } => {
|
Kind::PathPattern { count, spacing } => {
|
||||||
for (l, v) in [("Quantity", count), ("Spacing", spacing)] {
|
for (l, v) in [("Quantity", count), ("Spacing", spacing)] {
|
||||||
ui.label(l);
|
row_label(ui, l);
|
||||||
enter |= field(ui, v);
|
enter |= field(ui, v);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Kind::Pipe { diameter, wall } => {
|
Kind::Pipe { diameter, wall } => {
|
||||||
ui.label("Diameter");
|
row_label(ui, "Diameter");
|
||||||
enter |= field(ui, diameter);
|
enter |= field(ui, diameter);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Wall");
|
row_label(ui, "Wall");
|
||||||
let r = ui.add(egui::TextEdit::singleline(wall).hint_text("solid"));
|
let r = ui.add(egui::TextEdit::singleline(wall).hint_text("solid"));
|
||||||
crate::params_dialog::complete(ui, &r, wall);
|
crate::params_dialog::complete(ui, &r, wall);
|
||||||
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Stock { margin } => {
|
Kind::Stock { margin } => {
|
||||||
ui.label("Margin");
|
row_label(ui, "Margin");
|
||||||
enter |= field(ui, margin);
|
enter |= field(ui, margin);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Scale { factor } => {
|
Kind::Scale { factor } => {
|
||||||
ui.label("Scale factor");
|
row_label(ui, "Scale factor");
|
||||||
enter |= field(ui, factor);
|
enter |= field(ui, factor);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::OffsetFaces { distance } => {
|
Kind::OffsetFaces { distance } => {
|
||||||
ui.label("Distance");
|
row_label(ui, "Distance");
|
||||||
enter |= field(ui, distance);
|
enter |= field(ui, distance);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Thread { designation, length } => {
|
Kind::Thread { designation, length } => {
|
||||||
ui.label("Size");
|
row_label(ui, "Size");
|
||||||
let r = ui.add(egui::TextEdit::singleline(designation).hint_text("fits the face, e.g. M8"));
|
let r = ui.add(egui::TextEdit::singleline(designation).hint_text("fits the face, e.g. M8"));
|
||||||
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Length");
|
row_label(ui, "Length");
|
||||||
let r = ui.add(egui::TextEdit::singleline(length).hint_text("whole face"));
|
let r = ui.add(egui::TextEdit::singleline(length).hint_text("whole face"));
|
||||||
crate::params_dialog::complete(ui, &r, length);
|
crate::params_dialog::complete(ui, &r, length);
|
||||||
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||||
@@ -1016,95 +1195,123 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
Kind::Material { index } => {
|
Kind::Material { index } => {
|
||||||
let names: Vec<&str> = solvecraft_engine::doc::MATERIALS.iter().map(|(n, _)| *n).collect();
|
let names: Vec<&str> = solvecraft_engine::doc::MATERIALS.iter().map(|(n, _)| *n).collect();
|
||||||
ui.label("Material");
|
row_label(ui, "Material");
|
||||||
combo(ui, "mat", &names, index);
|
combo(ui, "mat", &names, index);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Section { offset, flip } => {
|
Kind::Section { offset, flip } => {
|
||||||
ui.label("Distance");
|
row_label(ui, "Distance");
|
||||||
enter |= field(ui, offset);
|
enter |= field(ui, offset);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Flip");
|
row_label(ui, "Flip");
|
||||||
ui.checkbox(flip, "");
|
ui.checkbox(flip, "");
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::PatternRect { count, spacing, count2, spacing2 } => {
|
Kind::PatternRect { count, spacing, count2, spacing2 } => {
|
||||||
for (l, v) in [("Quantity", count), ("Spacing", spacing), ("Quantity 2", count2), ("Spacing 2", spacing2)] {
|
for (l, v) in [("Quantity", count), ("Spacing", spacing), ("Quantity 2", count2), ("Spacing 2", spacing2)] {
|
||||||
ui.label(l);
|
row_label(ui, l);
|
||||||
enter |= field(ui, v);
|
enter |= field(ui, v);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Kind::PatternCirc { count, angle } => {
|
Kind::PatternCirc { count, angle } => {
|
||||||
for (l, v) in [("Quantity", count), ("Total angle", angle)] {
|
for (l, v) in [("Quantity", count), ("Total angle", angle)] {
|
||||||
ui.label(l);
|
row_label(ui, l);
|
||||||
enter |= field(ui, v);
|
enter |= field(ui, v);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Kind::Loft { operation } => {
|
Kind::Loft { operation } => {
|
||||||
ui.label("Operation");
|
row_label(ui, "Operation");
|
||||||
combo(ui, "lf_op", &OP_LABELS, operation);
|
combo(ui, "lf_op", &OP_LABELS, operation);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Sweep { operation } => {
|
Kind::Sweep { operation } => {
|
||||||
ui.label("Operation");
|
row_label(ui, "Operation");
|
||||||
combo(ui, "sw_op", &OP_LABELS, operation);
|
combo(ui, "sw_op", &OP_LABELS, operation);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Move { x, y, z } => {
|
Kind::Move { x, y, z } => {
|
||||||
for (l, v) in [("X distance", x), ("Y distance", y), ("Z distance", z)] {
|
for (l, v) in [("X distance", x), ("Y distance", y), ("Z distance", z)] {
|
||||||
ui.label(l);
|
row_label(ui, l);
|
||||||
enter |= field(ui, v);
|
enter |= field(ui, v);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Kind::Hole { diameter, depth, kind, cb_diameter, cb_depth, cs_diameter, cs_angle } => {
|
Kind::Hole { diameter, depth, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } => {
|
||||||
ui.label("Type");
|
row_label(ui, "Extents");
|
||||||
|
let mut ext = usize::from(opts.all);
|
||||||
|
combo(ui, "hole_ext", &["Distance", "All"], &mut ext);
|
||||||
|
opts.all = ext == 1;
|
||||||
|
ui.end_row();
|
||||||
|
row_label(ui, "Hole Type");
|
||||||
combo(ui, "hole_kind", &HOLE_LABELS, kind);
|
combo(ui, "hole_kind", &HOLE_LABELS, kind);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Diameter");
|
row_label(ui, "Hole Tap Type");
|
||||||
|
let mut tap = usize::from(opts.tapped);
|
||||||
|
combo(ui, "hole_tap", &["Simple", "Tapped"], &mut tap);
|
||||||
|
opts.tapped = tap == 1;
|
||||||
|
ui.end_row();
|
||||||
|
if opts.tapped {
|
||||||
|
row_label(ui, "Size");
|
||||||
|
let r = ui.add(egui::TextEdit::singleline(&mut opts.thread).hint_text("M5"));
|
||||||
|
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
if *kind == 0 {
|
||||||
|
row_label(ui, "Drill Point");
|
||||||
|
let mut tip = usize::from(opts.angled);
|
||||||
|
combo(ui, "hole_tip", &["Flat", "Angle"], &mut tip);
|
||||||
|
opts.angled = tip == 1;
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
if !opts.all {
|
||||||
|
row_label(ui, "Depth");
|
||||||
|
enter |= field(ui, depth);
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
if *kind == 0 && opts.angled {
|
||||||
|
row_label(ui, "Tip Angle");
|
||||||
|
enter |= field(ui, &mut opts.tip_angle);
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
row_label(ui, "Diameter");
|
||||||
enter |= field(ui, diameter);
|
enter |= field(ui, diameter);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Depth");
|
|
||||||
let r = ui.add(egui::TextEdit::singleline(depth).hint_text("through all"));
|
|
||||||
crate::params_dialog::complete(ui, &r, depth);
|
|
||||||
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
|
||||||
ui.end_row();
|
|
||||||
if *kind == 1 {
|
if *kind == 1 {
|
||||||
ui.label("Counterbore Ø");
|
row_label(ui, "Counterbore Ø");
|
||||||
enter |= field(ui, cb_diameter);
|
enter |= field(ui, cb_diameter);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Counterbore depth");
|
row_label(ui, "Counterbore depth");
|
||||||
enter |= field(ui, cb_depth);
|
enter |= field(ui, cb_depth);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
if *kind == 2 {
|
if *kind == 2 {
|
||||||
ui.label("Countersink Ø");
|
row_label(ui, "Countersink Ø");
|
||||||
enter |= field(ui, cs_diameter);
|
enter |= field(ui, cs_diameter);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Countersink angle");
|
row_label(ui, "Countersink angle");
|
||||||
enter |= field(ui, cs_angle);
|
enter |= field(ui, cs_angle);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Kind::Primitive { fields, operation, .. } => {
|
Kind::Primitive { fields, operation, .. } => {
|
||||||
for (k, v) in fields.iter_mut() {
|
for (k, v) in fields.iter_mut() {
|
||||||
ui.label(*k);
|
row_label(ui, k);
|
||||||
enter |= field(ui, v);
|
enter |= field(ui, v);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
ui.label("Operation");
|
row_label(ui, "Operation");
|
||||||
combo(ui, "pr_op", &OP_LABELS, operation);
|
combo(ui, "pr_op", &OP_LABELS, operation);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Combine { operation, keep_tools } => {
|
Kind::Combine { operation, keep_tools } => {
|
||||||
ui.label("Operation");
|
row_label(ui, "Operation");
|
||||||
let mut op = operation.saturating_sub(1);
|
let mut op = operation.saturating_sub(1);
|
||||||
combo(ui, "cb_op", &OP_LABELS[1..], &mut op);
|
combo(ui, "cb_op", &OP_LABELS[1..], &mut op);
|
||||||
*operation = op + 1;
|
*operation = op + 1;
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Keep tools");
|
row_label(ui, "Keep Tools");
|
||||||
ui.checkbox(keep_tools, "");
|
ui.checkbox(keep_tools, "");
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
@@ -1120,7 +1327,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
Kind::Rename { name, .. } => {
|
Kind::Rename { name, .. } => {
|
||||||
ui.label("Name");
|
row_label(ui, "Name");
|
||||||
let r = ui.text_edit_singleline(name);
|
let r = ui.text_edit_singleline(name);
|
||||||
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||||
ok = true;
|
ok = true;
|
||||||
@@ -1130,45 +1337,48 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
Kind::ConfirmDelete { with, fail, .. } => {
|
Kind::ConfirmDelete { with, fail, .. } => {
|
||||||
if !with.is_empty() {
|
if !with.is_empty() {
|
||||||
ui.label("Also deletes");
|
row_label(ui, "Also deletes");
|
||||||
ui.label(with.join(", "));
|
ui.label(with.join(", "));
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
if !fail.is_empty() {
|
if !fail.is_empty() {
|
||||||
ui.label("Will fail");
|
row_label(ui, "Will fail");
|
||||||
ui.label(RichText::new(fail.join(", ")).color(t.warning));
|
ui.label(RichText::new(fail.join(", ")).color(t.warning));
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(e) = d.error.as_ref().or(app.preview.error.as_ref()) {
|
if let Some(e) = d.error.as_ref().or(app.preview.error.as_ref()) {
|
||||||
ui.label("");
|
row_label(ui, "");
|
||||||
ui.add(egui::Label::new(RichText::new(e.as_str()).color(t.error)).wrap());
|
ui.add(egui::Label::new(RichText::new(e.as_str()).color(t.error)).wrap());
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
|
// Footer: an info glyph at the left, OK and Cancel at the right.
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if !matches!(d.kind, Kind::Sketch | Kind::Params { .. } | Kind::Measure { .. } | Kind::Preferences)
|
let (r, resp) = ui.allocate_exact_size(vec2(16.0, 16.0), egui::Sense::hover());
|
||||||
&& ui
|
ui.painter().circle_stroke(r.center(), 7.0, Stroke::new(1.2, t.text_dim));
|
||||||
.add(
|
ui.painter().text(r.center(), egui::Align2::CENTER_CENTER, "i", egui::FontId::proportional(11.0), t.text_dim);
|
||||||
egui::Button::new(
|
resp.on_hover_text(heading.as_str());
|
||||||
RichText::new(if matches!(d.kind, Kind::ConfirmDelete { .. }) { "Delete" } else { "OK" }).color(Color32::WHITE),
|
|
||||||
)
|
|
||||||
.fill(t.accent)
|
|
||||||
.min_size(vec2(70.0, 24.0)),
|
|
||||||
)
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
ok = true;
|
|
||||||
}
|
|
||||||
let close = if matches!(d.kind, Kind::Params { .. } | Kind::Measure { .. } | Kind::Preferences) { "Close" } else { "Cancel" };
|
|
||||||
if ui.add(egui::Button::new(close).min_size(vec2(70.0, 24.0))).clicked() {
|
|
||||||
cancel = true;
|
|
||||||
}
|
|
||||||
if app.preview.busy {
|
if app.preview.busy {
|
||||||
ui.add(egui::Spinner::new().size(14.0)).on_hover_text("Updating the preview");
|
ui.add(egui::Spinner::new().size(14.0)).on_hover_text("Updating the preview");
|
||||||
}
|
}
|
||||||
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
|
let close = if matches!(d.kind, Kind::Params { .. } | Kind::Measure { .. } | Kind::Preferences) { "Close" } else { "Cancel" };
|
||||||
|
if ui.add(egui::Button::new(close).min_size(vec2(64.0, 24.0))).clicked() {
|
||||||
|
cancel = true;
|
||||||
|
}
|
||||||
|
if has_ok {
|
||||||
|
let label = if matches!(d.kind, Kind::ConfirmDelete { .. }) { "Delete" } else { "OK" };
|
||||||
|
let b = egui::Button::new(RichText::new(label).color(if valid { Color32::WHITE } else { t.text_dim }))
|
||||||
|
.fill(if valid { t.accent } else { Color32::TRANSPARENT })
|
||||||
|
.min_size(vec2(64.0, 24.0));
|
||||||
|
if ui.add_enabled(valid, b).clicked() {
|
||||||
|
ok = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// Keyboard: Enter applies (from a value field, or with nothing focused), Esc cancels.
|
// Keyboard: Enter applies (from a value field, or with nothing focused), Esc cancels.
|
||||||
@@ -1216,12 +1426,13 @@ pub fn cancel(app: &mut SolveApp) {
|
|||||||
fn hole_defaults() -> Kind {
|
fn hole_defaults() -> Kind {
|
||||||
Kind::Hole {
|
Kind::Hole {
|
||||||
diameter: "5 mm".into(),
|
diameter: "5 mm".into(),
|
||||||
depth: String::new(),
|
depth: "10 mm".into(),
|
||||||
kind: 0,
|
kind: 0,
|
||||||
cb_diameter: "9 mm".into(),
|
cb_diameter: "9 mm".into(),
|
||||||
cb_depth: "3 mm".into(),
|
cb_depth: "3 mm".into(),
|
||||||
cs_diameter: "10 mm".into(),
|
cs_diameter: "10 mm".into(),
|
||||||
cs_angle: "90 deg".into(),
|
cs_angle: "90 deg".into(),
|
||||||
|
opts: HoleOpts::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1333,14 +1544,21 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
|||||||
sels(d, i).iter().filter_map(|x| if let Sel::Face { point, .. } = x { Some(pt(*point)) } else { None }).collect()
|
sels(d, i).iter().filter_map(|x| if let Sel::Face { point, .. } = x { Some(pt(*point)) } else { None }).collect()
|
||||||
};
|
};
|
||||||
let (cmd, params): (&str, Value) = match &d.kind {
|
let (cmd, params): (&str, Value) = match &d.kind {
|
||||||
Kind::Extrude { distance, direction, operation, distance2, taper, .. } => {
|
Kind::Extrude { distance, direction, operation, distance2, taper, start, all, .. } => {
|
||||||
need(0, "profiles or a planar face")?;
|
need(0, "profiles or a planar face")?;
|
||||||
let mut common = json!({"distance": distance, "direction": DIRS.get(*direction).copied().unwrap_or("positive"), "operation": OPS.get(*operation).copied().unwrap_or("new")});
|
let mut common = json!({"distance": distance, "direction": DIRS.get(*direction).copied().unwrap_or("positive"), "operation": OPS.get(*operation).copied().unwrap_or("new")});
|
||||||
|
let tapered = s.doc.eval(taper, ValueKind::Angle).map(|v| v.abs() > 1e-12).unwrap_or(!taper.trim().is_empty());
|
||||||
if *direction == TWO_SIDES {
|
if *direction == TWO_SIDES {
|
||||||
common["distance2"] = json!(distance2);
|
common["distance2"] = json!(distance2);
|
||||||
} else if *direction < 2 && !taper.trim().is_empty() {
|
} else if *direction < 2 && tapered {
|
||||||
common["taper"] = json!(taper);
|
common["taper"] = json!(taper);
|
||||||
}
|
}
|
||||||
|
if !start.trim().is_empty() {
|
||||||
|
common["start_offset"] = json!(start);
|
||||||
|
}
|
||||||
|
if *all {
|
||||||
|
common["through_all"] = json!(true);
|
||||||
|
}
|
||||||
let with = |extra: Value| -> Value {
|
let with = |extra: Value| -> Value {
|
||||||
let mut p = common.clone();
|
let mut p = common.clone();
|
||||||
if let (Value::Object(m), Value::Object(e)) = (&mut p, extra) {
|
if let (Value::Object(m), Value::Object(e)) = (&mut p, extra) {
|
||||||
@@ -1641,16 +1859,45 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
|||||||
need(0, "bodies")?;
|
need(0, "bodies")?;
|
||||||
("FusionMoveCommand", json!({"bodies": body_names(0), "translate": [x, y, z]}))
|
("FusionMoveCommand", json!({"bodies": body_names(0), "translate": [x, y, z]}))
|
||||||
}
|
}
|
||||||
Kind::Hole { diameter, depth, kind, cb_diameter, cb_depth, cs_diameter, cs_angle } => {
|
Kind::Hole { diameter, depth, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } => {
|
||||||
need(0, "a face position")?;
|
need(0, if opts.multiple { "sketch points" } else { "a face position" })?;
|
||||||
let ty = HOLE_TYPES.get(*kind).copied().unwrap_or("simple");
|
let ty = match *kind {
|
||||||
// One hole per picked position.
|
0 if opts.angled => "drilled",
|
||||||
|
k => HOLE_TYPES.get(k).copied().unwrap_or("simple"),
|
||||||
|
};
|
||||||
|
// One hole per picked face position, or one Hole feature per sketch for sketch points.
|
||||||
|
let mut places: Vec<Value> = Vec::new();
|
||||||
|
if opts.multiple {
|
||||||
|
let mut by_sketch: Vec<(u64, Vec<String>)> = Vec::new();
|
||||||
|
for x in sels(d, 0) {
|
||||||
|
if let Sel::SketchPoint { id } = x
|
||||||
|
&& let Some((sk, pid)) = id.split_once(':')
|
||||||
|
&& let Ok(sk) = sk.parse::<u64>()
|
||||||
|
{
|
||||||
|
match by_sketch.iter_mut().find(|(s, _)| *s == sk) {
|
||||||
|
Some((_, v)) => v.push(pid.to_string()),
|
||||||
|
None => by_sketch.push((sk, vec![pid.to_string()])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
places.extend(by_sketch.into_iter().map(|(sk, ids)| json!({"sketch": sk, "points": ids})));
|
||||||
|
} else {
|
||||||
|
places.extend(face_points(0).into_iter().map(|p| json!({ "position": p })));
|
||||||
|
}
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for p in face_points(0) {
|
for place in places {
|
||||||
let mut params = json!({"position": p, "diameter": diameter, "type": ty});
|
let mut params = place;
|
||||||
if !depth.trim().is_empty() {
|
params["diameter"] = json!(diameter);
|
||||||
|
params["type"] = json!(ty);
|
||||||
|
if !opts.all {
|
||||||
params["depth"] = json!(depth);
|
params["depth"] = json!(depth);
|
||||||
}
|
}
|
||||||
|
if ty == "drilled" {
|
||||||
|
params["tip_angle"] = json!(opts.tip_angle);
|
||||||
|
}
|
||||||
|
if opts.tapped && !opts.thread.trim().is_empty() {
|
||||||
|
params["thread"] = json!(opts.thread.trim());
|
||||||
|
}
|
||||||
match *kind {
|
match *kind {
|
||||||
1 => {
|
1 => {
|
||||||
params["cb_diameter"] = json!(cb_diameter);
|
params["cb_diameter"] = json!(cb_diameter);
|
||||||
@@ -1860,17 +2107,22 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
|||||||
operation: op_index(operation),
|
operation: op_index(operation),
|
||||||
auto_op: false,
|
auto_op: false,
|
||||||
distance2: extent.distance2.clone().unwrap_or_else(|| "10 mm".into()),
|
distance2: extent.distance2.clone().unwrap_or_else(|| "10 mm".into()),
|
||||||
taper: extent.taper.clone().unwrap_or_default(),
|
taper: extent.taper.clone().unwrap_or_else(|| "0 deg".into()),
|
||||||
|
start: extent.start_offset.clone().unwrap_or_default(),
|
||||||
|
all: extent.through_all,
|
||||||
};
|
};
|
||||||
|
// The dialog has no "flip": a flipped extrude is a negative distance.
|
||||||
|
if let Kind::Extrude { direction, distance, .. } = &mut d.kind
|
||||||
|
&& *direction == 1
|
||||||
|
{
|
||||||
|
*direction = 0;
|
||||||
|
*distance = format!("-({distance})");
|
||||||
|
}
|
||||||
let items = st.sketch(*sketch).map(|ss| profile_indices(ss, profiles)).unwrap_or_default();
|
let items = st.sketch(*sketch).map(|ss| profile_indices(ss, profiles)).unwrap_or_default();
|
||||||
if let Some(inp) = d.inputs.first_mut() {
|
if let Some(inp) = d.inputs.first_mut() {
|
||||||
inp.items = items.into_iter().map(|index| Sel::Profile { sketch: *sketch, index }).collect();
|
inp.items = items.into_iter().map(|index| Sel::Profile { sketch: *sketch, index }).collect();
|
||||||
}
|
}
|
||||||
for (k, v) in [
|
for (k, v) in [("targets", (!targets.is_empty()).then(|| json!(targets)))] {
|
||||||
("start_offset", extent.start_offset.as_ref().map(|x| json!(x))),
|
|
||||||
("through_all", extent.through_all.then_some(json!(true))),
|
|
||||||
("targets", (!targets.is_empty()).then(|| json!(targets))),
|
|
||||||
] {
|
|
||||||
if let Some(v) = v {
|
if let Some(v) = v {
|
||||||
d.extra.insert(k.into(), v);
|
d.extra.insert(k.into(), v);
|
||||||
}
|
}
|
||||||
@@ -1926,12 +2178,21 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
|||||||
d.extra.insert("pull".into(), pt3(*pull));
|
d.extra.insert("pull".into(), pt3(*pull));
|
||||||
d
|
d
|
||||||
}
|
}
|
||||||
FeatureKind::Hole { position, direction, diameter, depth, hole, .. } => {
|
FeatureKind::Hole { position, direction, diameter, depth, hole, points, thread } => {
|
||||||
let mut d = start("FusionHoleCommand")?;
|
let mut d = start("FusionHoleCommand")?;
|
||||||
let mut k = hole_defaults();
|
let mut k = hole_defaults();
|
||||||
if let Kind::Hole { diameter: dia, depth: dep, kind, cb_diameter, cb_depth, cs_diameter, cs_angle } = &mut k {
|
if let Kind::Hole { diameter: dia, depth: dep, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } = &mut k {
|
||||||
*dia = diameter.clone();
|
*dia = diameter.clone();
|
||||||
*dep = depth.clone().unwrap_or_default();
|
opts.all = depth.is_none();
|
||||||
|
opts.angled = matches!(hole, HoleKind::Drilled { .. });
|
||||||
|
opts.multiple = points.is_some();
|
||||||
|
if let Some(th) = thread {
|
||||||
|
opts.tapped = true;
|
||||||
|
opts.thread = th.clone();
|
||||||
|
}
|
||||||
|
if let Some(dd) = depth {
|
||||||
|
*dep = dd.clone();
|
||||||
|
}
|
||||||
match hole {
|
match hole {
|
||||||
HoleKind::Counterbore { cb_diameter: a, cb_depth: b } => {
|
HoleKind::Counterbore { cb_diameter: a, cb_depth: b } => {
|
||||||
*kind = 1;
|
*kind = 1;
|
||||||
@@ -1944,16 +2205,26 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
|||||||
*cs_angle = b.clone();
|
*cs_angle = b.clone();
|
||||||
}
|
}
|
||||||
HoleKind::Drilled { tip_angle } => {
|
HoleKind::Drilled { tip_angle } => {
|
||||||
d.extra.insert("tip_angle".into(), json!(tip_angle));
|
opts.tip_angle = tip_angle.clone();
|
||||||
}
|
}
|
||||||
HoleKind::Simple => {}
|
HoleKind::Simple => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.kind = k;
|
d.kind = k;
|
||||||
if let Some(inp) = d.inputs.first_mut() {
|
match points {
|
||||||
inp.items = vec![face_sel(s, *position).unwrap_or(Sel::Face { body: String::new(), index: 0, point: *position })];
|
Some(sp) => {
|
||||||
|
d.inputs = vec![SelInput::new("Points", selection::POINTS, true)];
|
||||||
|
if let Some(inp) = d.inputs.first_mut() {
|
||||||
|
inp.items = sp.ids.iter().map(|id| Sel::SketchPoint { id: format!("{}:{id}", sp.sketch) }).collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if let Some(inp) = d.inputs.first_mut() {
|
||||||
|
inp.items = vec![face_sel(s, *position).unwrap_or(Sel::Face { body: String::new(), index: 0, point: *position })];
|
||||||
|
}
|
||||||
|
d.extra.insert("direction".into(), pt3(*direction));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
d.extra.insert("direction".into(), pt3(*direction));
|
|
||||||
d
|
d
|
||||||
}
|
}
|
||||||
FeatureKind::Box { corner, length, width, height, operation } => {
|
FeatureKind::Box { corner, length, width, height, operation } => {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ pub mod icons;
|
|||||||
mod menu_tests;
|
mod menu_tests;
|
||||||
pub mod palette;
|
pub mod palette;
|
||||||
pub mod params_dialog;
|
pub mod params_dialog;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod preselect_tests;
|
||||||
pub mod preview;
|
pub mod preview;
|
||||||
pub mod ref_images;
|
pub mod ref_images;
|
||||||
pub mod selection;
|
pub mod selection;
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
//! Pre-selection fills every input of a command, each item going to the input that takes its
|
||||||
|
//! type, whether the command starts from a shortcut, the toolbar or a menu.
|
||||||
|
|
||||||
|
use serde_json::json;
|
||||||
|
use solvecraft_engine::geom::Vec3;
|
||||||
|
use solvecraft_engine::{Sel, Session};
|
||||||
|
|
||||||
|
use crate::dialogs::{Dialog, Kind, apply_commands};
|
||||||
|
use crate::{Services, SolveApp};
|
||||||
|
|
||||||
|
fn app_with(script: serde_json::Value) -> SolveApp {
|
||||||
|
let mut s = Session::default();
|
||||||
|
s.run_script(&script).unwrap();
|
||||||
|
SolveApp::new(s, Services::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn boxed() -> SolveApp {
|
||||||
|
app_with(json!([{"command": "PrimitiveBox", "params": {"length": 40, "width": 30, "height": 20}}]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The face of Body1 whose centre is at `c`.
|
||||||
|
fn face(app: &SolveApp, c: Vec3) -> Sel {
|
||||||
|
let st = app.session.model.state();
|
||||||
|
let b = st.body("Body1").unwrap();
|
||||||
|
let f = b.body.faces(0.01).unwrap().into_iter().find(|f| f.centroid.dist(c) < 1e-6).unwrap();
|
||||||
|
Sel::Face { body: "Body1".into(), index: f.index, point: c }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edge(app: &SolveApp, p: Vec3) -> Sel {
|
||||||
|
let st = app.session.model.state();
|
||||||
|
let m = st.body("Body1").unwrap().mesh();
|
||||||
|
let i = m.edges.iter().position(|e| e.windows(2).any(|w| p.dist_to_segment(w[0], w[1]) < 1e-9)).unwrap();
|
||||||
|
Sel::Edge { body: "Body1".into(), index: i, point: p }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(app: &mut SolveApp, sels: Vec<Sel>, id: &str) -> Dialog {
|
||||||
|
app.session.selection = sels;
|
||||||
|
app.start(id);
|
||||||
|
app.dialog.clone().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn profiles_then_extrude_takes_them_all() {
|
||||||
|
let mut app = app_with(json!([
|
||||||
|
{"command": "SketchCreate", "params": {"plane": "XY"}},
|
||||||
|
{"command": "CircleCenterRadius", "params": {"center": [0, 0], "radius": 5}},
|
||||||
|
{"command": "CircleCenterRadius", "params": {"center": [20, 0], "radius": 5}},
|
||||||
|
{"command": "SketchStop", "params": {}},
|
||||||
|
]));
|
||||||
|
let sk = app.session.doc.features.first().unwrap().id;
|
||||||
|
let d = start(&mut app, vec![Sel::Profile { sketch: sk, index: 0 }, Sel::Profile { sketch: sk, index: 1 }], "Extrude");
|
||||||
|
assert_eq!(d.inputs[0].items.len(), 2);
|
||||||
|
assert!(d.focus, "the value box takes the keyboard");
|
||||||
|
assert!(apply_commands(&app, &d).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn edges_then_fillet_takes_them_with_chains() {
|
||||||
|
let mut app = boxed();
|
||||||
|
let (a, b) = (edge(&app, Vec3::new(0.0, 0.0, 10.0)), edge(&app, Vec3::new(20.0, 0.0, 20.0)));
|
||||||
|
let d = start(&mut app, vec![a, b], "FusionFilletEdgesCommand");
|
||||||
|
assert_eq!(d.inputs[0].items.len(), 2, "box edges are not tangent to others");
|
||||||
|
let cmds = apply_commands(&app, &d).unwrap();
|
||||||
|
assert_eq!(cmds[0].1["edges"].as_array().unwrap().len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn face_then_extrude_press_pulls_it() {
|
||||||
|
let mut app = boxed();
|
||||||
|
let top = face(&app, Vec3::new(20.0, 15.0, 20.0));
|
||||||
|
let d = start(&mut app, vec![top], "Extrude");
|
||||||
|
let cmds = apply_commands(&app, &d).unwrap();
|
||||||
|
assert!(cmds[0].1.get("face").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn faces_then_shell_and_face_then_hole() {
|
||||||
|
let mut app = boxed();
|
||||||
|
let top = face(&app, Vec3::new(20.0, 15.0, 20.0));
|
||||||
|
let front = face(&app, Vec3::new(20.0, 0.0, 10.0));
|
||||||
|
let d = start(&mut app, vec![top.clone(), front], "FusionShellBodyCommand");
|
||||||
|
assert_eq!(d.inputs[0].items.len(), 2);
|
||||||
|
let d = start(&mut app, vec![top], "FusionHoleCommand");
|
||||||
|
assert_eq!(d.inputs[0].items.len(), 1);
|
||||||
|
let cmds = apply_commands(&app, &d).unwrap();
|
||||||
|
assert!(cmds[0].1.get("position").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn line_and_profile_then_revolve_routes_each() {
|
||||||
|
let mut app = app_with(json!([
|
||||||
|
{"command": "SketchCreate", "params": {"plane": "XZ"}},
|
||||||
|
{"command": "ShapeRectangleTwoPoint", "params": {"p0": [10, 0], "p1": [20, 10]}},
|
||||||
|
{"command": "DrawPolyline", "params": {"points": [[0, 0], [0, 30]], "ids": ["axis"]}},
|
||||||
|
]));
|
||||||
|
let sk = app.session.active_sketch.unwrap();
|
||||||
|
let d = start(&mut app, vec![Sel::SketchCurve { id: "axis".into() }, Sel::Profile { sketch: sk, index: 0 }], "Revolve");
|
||||||
|
assert_eq!(d.inputs[0].items, vec![Sel::Profile { sketch: sk, index: 0 }]);
|
||||||
|
assert_eq!(d.inputs[1].items, vec![Sel::SketchCurve { id: "axis".into() }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn face_then_move_takes_the_body_and_odd_items_are_ignored() {
|
||||||
|
let mut app = boxed();
|
||||||
|
let top = face(&app, Vec3::new(20.0, 15.0, 20.0));
|
||||||
|
let d = start(&mut app, vec![top, Sel::Axis { name: "X".into() }], "FusionMoveCommand");
|
||||||
|
assert_eq!(d.inputs[0].items, vec![Sel::Body { name: "Body1".into() }]);
|
||||||
|
assert!(matches!(d.kind, Kind::Move { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hole_at_sketch_points() {
|
||||||
|
let mut app = app_with(json!([
|
||||||
|
{"command": "PrimitiveBox", "params": {"length": 40, "width": 30, "height": 20}},
|
||||||
|
{"command": "SketchCreate", "params": {"plane": {"face": [20, 15, 20]}}},
|
||||||
|
{"command": "DrawPoint", "params": {"point": [10, 10], "id": "h1"}},
|
||||||
|
{"command": "DrawPoint", "params": {"point": [30, 20], "id": "h2"}},
|
||||||
|
{"command": "SketchStop", "params": {}},
|
||||||
|
]));
|
||||||
|
let sk = app.session.doc.features.iter().find(|f| matches!(f.kind, solvecraft_engine::doc::FeatureKind::Sketch { .. })).unwrap().id;
|
||||||
|
app.start("FusionHoleCommand");
|
||||||
|
let mut d = app.dialog.clone().unwrap();
|
||||||
|
if let Kind::Hole { opts, .. } = &mut d.kind {
|
||||||
|
opts.multiple = true;
|
||||||
|
}
|
||||||
|
d.inputs = vec![crate::selection::SelInput::new("Points", crate::selection::POINTS, true)];
|
||||||
|
d.inputs[0].items = vec![Sel::SketchPoint { id: format!("{sk}:h1") }, Sel::SketchPoint { id: format!("{sk}:h2") }];
|
||||||
|
let cmds = apply_commands(&app, &d).unwrap();
|
||||||
|
assert_eq!(cmds.len(), 1);
|
||||||
|
assert_eq!(cmds[0].1["points"], json!(["h1", "h2"]));
|
||||||
|
let r = app.session.execute(&cmds[0].0, &cmds[0].1);
|
||||||
|
assert!(r.is_ok(), "{r:?}");
|
||||||
|
}
|
||||||
@@ -23,6 +23,8 @@ pub const AXES: Accept = 64;
|
|||||||
pub const VERTICES: Accept = 128;
|
pub const VERTICES: Accept = 128;
|
||||||
/// Any sketch curve (a sweep path).
|
/// Any sketch curve (a sweep path).
|
||||||
pub const CURVES: Accept = 256;
|
pub const CURVES: Accept = 256;
|
||||||
|
/// Sketch points of any visible sketch (hole centres); picked as "<sketch id>:<point id>".
|
||||||
|
pub const POINTS: Accept = 512;
|
||||||
|
|
||||||
/// One selection input of a command dialog ("Profiles", "Edges", "Plane", ...).
|
/// One selection input of a command dialog ("Profiles", "Edges", "Plane", ...).
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
@@ -53,6 +55,7 @@ impl SelInput {
|
|||||||
Hit::Axis { name } if a & AXES != 0 => Some(Sel::Axis { name: name.clone() }),
|
Hit::Axis { name } if a & AXES != 0 => Some(Sel::Axis { name: name.clone() }),
|
||||||
Hit::SketchCurve { id, straight: true, .. } if a & AXES != 0 => Some(Sel::SketchCurve { id: id.clone() }),
|
Hit::SketchCurve { id, straight: true, .. } if a & AXES != 0 => Some(Sel::SketchCurve { id: id.clone() }),
|
||||||
Hit::SketchCurve { id, .. } if a & CURVES != 0 => Some(Sel::SketchCurve { id: id.clone() }),
|
Hit::SketchCurve { id, .. } if a & CURVES != 0 => Some(Sel::SketchCurve { id: id.clone() }),
|
||||||
|
Hit::SketchPoint { sketch, id, .. } if a & POINTS != 0 => Some(Sel::SketchPoint { id: format!("{sketch}:{id}") }),
|
||||||
Hit::Vertex { body, point } if a & VERTICES != 0 => Some(Sel::Vertex { body: body.clone(), point: *point }),
|
Hit::Vertex { body, point } if a & VERTICES != 0 => Some(Sel::Vertex { body: body.clone(), point: *point }),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ pub struct Tokens {
|
|||||||
/// Sketch points: fully constrained, free.
|
/// Sketch points: fully constrained, free.
|
||||||
pub sketch_point: Color32,
|
pub sketch_point: Color32,
|
||||||
pub sketch_point_free: Color32,
|
pub sketch_point_free: Color32,
|
||||||
|
/// Command dialogs: panel, border, and the filled selection chip.
|
||||||
|
pub dialog_bg: Color32,
|
||||||
|
pub dialog_border: Color32,
|
||||||
|
pub chip: Color32,
|
||||||
/// Inside of bodies cut by a section.
|
/// Inside of bodies cut by a section.
|
||||||
pub section_cap: Color32,
|
pub section_cap: Color32,
|
||||||
/// Closed sketch profiles (translucent fill).
|
/// Closed sketch profiles (translucent fill).
|
||||||
@@ -178,6 +182,9 @@ impl Tokens {
|
|||||||
dimension_driven: Color32::from_rgb(120, 126, 138),
|
dimension_driven: Color32::from_rgb(120, 126, 138),
|
||||||
sketch_point_free: Color32::from_rgb(30, 90, 200),
|
sketch_point_free: Color32::from_rgb(30, 90, 200),
|
||||||
section_cap: Color32::from_rgb(214, 128, 72),
|
section_cap: Color32::from_rgb(214, 128, 72),
|
||||||
|
dialog_bg: Color32::from_rgb(250, 251, 252),
|
||||||
|
dialog_border: Color32::from_rgb(208, 212, 220),
|
||||||
|
chip: Color32::from_rgb(38, 120, 218),
|
||||||
profile_fill: Color32::from_rgba_unmultiplied_const(255, 196, 120, 70),
|
profile_fill: Color32::from_rgba_unmultiplied_const(255, 196, 120, 70),
|
||||||
glyph_bg: Color32::WHITE,
|
glyph_bg: Color32::WHITE,
|
||||||
glyph_edge: Color32::from_rgb(90, 150, 90),
|
glyph_edge: Color32::from_rgb(90, 150, 90),
|
||||||
@@ -233,6 +240,9 @@ impl Tokens {
|
|||||||
t.dimension_driven = Color32::from_rgb(142, 150, 164);
|
t.dimension_driven = Color32::from_rgb(142, 150, 164);
|
||||||
t.sketch_point_free = Color32::from_rgb(92, 150, 255);
|
t.sketch_point_free = Color32::from_rgb(92, 150, 255);
|
||||||
t.profile_fill = Color32::from_rgba_unmultiplied_const(120, 150, 200, 40);
|
t.profile_fill = Color32::from_rgba_unmultiplied_const(120, 150, 200, 40);
|
||||||
|
t.dialog_bg = Color32::from_rgb(59, 68, 83);
|
||||||
|
t.dialog_border = Color32::from_rgb(72, 83, 99);
|
||||||
|
t.chip = Color32::from_rgb(59, 101, 168);
|
||||||
t.glyph_bg = Color32::from_rgb(44, 50, 60);
|
t.glyph_bg = Color32::from_rgb(44, 50, 60);
|
||||||
t.glyph_edge = Color32::from_rgb(96, 170, 110);
|
t.glyph_edge = Color32::from_rgb(96, 170, 110);
|
||||||
t.glyph_text = Color32::from_rgb(150, 220, 160);
|
t.glyph_text = Color32::from_rgb(150, 220, 160);
|
||||||
|
|||||||
@@ -347,11 +347,12 @@ pub fn pick(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
|
|||||||
let st = s.world_state();
|
let st = s.world_state();
|
||||||
let mut hits = Vec::new();
|
let mut hits = Vec::new();
|
||||||
let (o, d) = proj.ray(pos);
|
let (o, d) = proj.ray(pos);
|
||||||
// Sketch points of the active sketch.
|
// Sketch points of the active sketch (of every shown sketch while a command takes points).
|
||||||
if let Some(sid) = s.active_sketch
|
let wants_points = app.dialog.as_ref().and_then(|d| d.active_input()).is_some_and(|i| i.accept & crate::selection::POINTS != 0);
|
||||||
&& let Some(ss) = st.sketch(sid)
|
let point_sketches: Vec<u64> = if wants_points { visible_sketches(app) } else { s.active_sketch.into_iter().collect() };
|
||||||
{
|
let mut best: Option<(f32, Hit)> = None;
|
||||||
let mut best: Option<(f32, Hit)> = None;
|
for ss in point_sketches.iter().filter_map(|sid| st.sketch(*sid)) {
|
||||||
|
let sid = ss.feature;
|
||||||
for p in &ss.sketch.points {
|
for p in &ss.sketch.points {
|
||||||
if let Some(sp) = proj.to_screen(ss.plane.to_world(p.pos)) {
|
if let Some(sp) = proj.to_screen(ss.plane.to_world(p.pos)) {
|
||||||
let dd = sp.distance(pos);
|
let dd = sp.distance(pos);
|
||||||
@@ -360,9 +361,9 @@ pub fn pick(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some((_, h)) = best {
|
}
|
||||||
hits.push(h);
|
if let Some((_, h)) = best {
|
||||||
}
|
hits.push(h);
|
||||||
}
|
}
|
||||||
// Sketch curves (the active sketch wins ties).
|
// Sketch curves (the active sketch wins ties).
|
||||||
let mut bestc: Option<(f32, Hit)> = None;
|
let mut bestc: Option<(f32, Hit)> = None;
|
||||||
|
|||||||
Reference in new issue
Block a user