A tool window for sketch Offset, with a typed Distance, Flip and Chain (#731)
Sketch Offset gets its own tool window with a typed Distance, Flip and Chain, as a variant of the sketch modify window.
This commit is contained in:
1 parent
1a0100cdd9
commit
beca7eb126
10 files changed
+239
-69
No files matched your search
@@ -8,7 +8,7 @@
|
||||
use egui::{Color32, Pos2, Stroke, vec2};
|
||||
use solidcraft_engine::Sel;
|
||||
use solidcraft_engine::doc::expr::Kind as ValueKind;
|
||||
use solidcraft_engine::geom::{Mesh, Vec3};
|
||||
use solidcraft_engine::geom::{Mesh, Vec2, Vec3};
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::dialogs::{Dialog, Kind};
|
||||
@@ -50,7 +50,17 @@ pub fn anchor(app: &SolveApp, d: &Dialog) -> Option<(Vec3, Option<Vec3>)> {
|
||||
Sel::SketchCurve { id } => {
|
||||
let ss = st.sketches.iter().rev().find(|ss| ss.sketch.curve_index(id).is_some())?;
|
||||
let pts = ss.sketch.polyline(ss.sketch.curve_index(id)?);
|
||||
(ss.plane.to_world(*pts.get(pts.len() / 2)?), None)
|
||||
let (mid, tangent) = poly_mid(&pts)?;
|
||||
// An offset's drag arrow: the in-plane direction the distance grows along. The
|
||||
// command offsets a picked curve to its left, and the window's Flip row reverses it.
|
||||
let dir = match &d.kind {
|
||||
Kind::SketchModify(crate::sketch_modify_ui::Mod::Offset { flip, .. }) => {
|
||||
let n = if *flip { -tangent.perp() } else { tangent.perp() };
|
||||
Some(ss.plane.to_world(mid + n) - ss.plane.to_world(mid))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
(ss.plane.to_world(mid), dir)
|
||||
}
|
||||
Sel::Body { name } => (st.body(name)?.mesh().bounds().center(), None),
|
||||
Sel::Plane { name } => {
|
||||
@@ -75,10 +85,36 @@ pub fn anchor(app: &SolveApp, d: &Dialog) -> Option<(Vec3, Option<Vec3>)> {
|
||||
Kind::Sheet(_) => (at, crate::dialogs_sheet::arrow(app, d).or(normal)),
|
||||
Kind::Plastic(_) => (at, normal),
|
||||
Kind::PatternRect { .. } => (at, d.inputs.get(1).and_then(|i| i.items.first()).and_then(|x| crate::dialogs::axis_of(app, x)).map(|a| a.1)),
|
||||
// Already computed above, per picked curve; keep it rather than the default below.
|
||||
Kind::SketchModify(_) => (at, normal),
|
||||
_ => (at, None),
|
||||
})
|
||||
}
|
||||
|
||||
/// The point halfway along a polyline, with the unit tangent there. Indexing the middle sample
|
||||
/// instead would land on a two-point line's far end, putting the value box and its arrow on a
|
||||
/// corner. None for a polyline with no length.
|
||||
fn poly_mid(poly: &[Vec2]) -> Option<(Vec2, Vec2)> {
|
||||
let total: f64 = poly.windows(2).map(|w| w[0].dist(w[1])).sum();
|
||||
if !total.is_finite() || total <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let half = total / 2.0;
|
||||
let mut walked = 0.0;
|
||||
let mut last = None;
|
||||
for w in poly.windows(2) {
|
||||
let (a, b) = (w[0], w[1]);
|
||||
let Some(dir) = (b - a).normalized() else { continue };
|
||||
let len = a.dist(b);
|
||||
if walked + len >= half {
|
||||
return Some((a + dir * (half - walked), dir));
|
||||
}
|
||||
walked += len;
|
||||
last = Some((b, dir));
|
||||
}
|
||||
last
|
||||
}
|
||||
|
||||
fn profile_plane(app: &SolveApp, sketch: u64) -> Option<solidcraft_engine::geom::Plane> {
|
||||
let component = app.session.doc.feature(sketch)?.component;
|
||||
let state = app.session.model.state();
|
||||
|
||||
@@ -248,7 +248,7 @@ pub enum Kind {
|
||||
/// Edit Text: a sketch text's content and placement (`sketch_text`).
|
||||
Text(crate::sketch_text::Tx),
|
||||
TextMove(crate::sketch_text_move::TextMove),
|
||||
/// Sketch-space modify tools: scale, and later offset, fillet, chamfer, the patterns,
|
||||
/// Sketch-space modify tools: scale and offset, and later fillet, chamfer, the patterns,
|
||||
/// mirror and move (`sketch_modify_ui`).
|
||||
SketchModify(crate::sketch_modify_ui::Mod),
|
||||
}
|
||||
@@ -918,6 +918,13 @@ pub(crate) fn field(ui: &mut egui::Ui, s: &mut String) -> bool {
|
||||
r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))
|
||||
}
|
||||
|
||||
/// A checkbox row. Its row handle (`dialog:<row label>`) lets a scenario click it by the label
|
||||
/// beside it, the way `field` already publishes one for a value field.
|
||||
pub(crate) fn check(ui: &mut egui::Ui, on: &mut bool) {
|
||||
let r = ui.checkbox(on, "");
|
||||
publish_row(&r);
|
||||
}
|
||||
|
||||
pub(crate) fn combo(ui: &mut egui::Ui, id: &str, labels: &[&str], sel: &mut usize) {
|
||||
let row = ROW.with(|r| r.borrow().clone());
|
||||
let selected = labels.get(*sel).copied().map(tr).unwrap_or("");
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"ui.cancel": "Abbrechen",
|
||||
"ui.canvases": "Kanvas",
|
||||
"ui.centerline": "Mittellinie",
|
||||
"ui.chain": "Kette",
|
||||
"ui.chamfer": "Abschrägung",
|
||||
"ui.change_workspace": "Arbeitsbereich wechseln",
|
||||
"ui.chord_length": "Sehnenlänge",
|
||||
@@ -74,6 +75,7 @@
|
||||
"ui.create_sketch": "Skizze erstellen",
|
||||
"ui.current": "aktuell",
|
||||
"ui.curvature": "Krümmung",
|
||||
"ui.curves": "Kurven",
|
||||
"ui.cut": "Ausschneiden",
|
||||
"ui.cylinder": "Zylinder",
|
||||
"ui.dark": "Dunkel",
|
||||
@@ -143,6 +145,7 @@
|
||||
"ui.heading_mirror": "SPIEGELN",
|
||||
"ui.heading_modify": "ÄNDERN",
|
||||
"ui.heading_move": "VERSCHIEBEN",
|
||||
"ui.heading_offset": "ABSTAND",
|
||||
"ui.heading_offset_plane": "ABSTANDSEBENE",
|
||||
"ui.heading_ok": "OK",
|
||||
"ui.heading_parameters": "PARAMETER",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"ui.cancel": "Cancel",
|
||||
"ui.canvases": "Canvases",
|
||||
"ui.centerline": "Centerline",
|
||||
"ui.chain": "Chain",
|
||||
"ui.chamfer": "Chamfer",
|
||||
"ui.change_workspace": "Change workspace",
|
||||
"ui.chord_length": "Chord Length",
|
||||
@@ -74,6 +75,7 @@
|
||||
"ui.create_sketch": "Create Sketch",
|
||||
"ui.current": "current",
|
||||
"ui.curvature": "Curvature",
|
||||
"ui.curves": "Curves",
|
||||
"ui.cut": "Cut",
|
||||
"ui.cylinder": "Cylinder",
|
||||
"ui.dark": "Dark",
|
||||
@@ -143,6 +145,7 @@
|
||||
"ui.heading_mirror": "MIRROR",
|
||||
"ui.heading_modify": "MODIFY",
|
||||
"ui.heading_move": "MOVE",
|
||||
"ui.heading_offset": "OFFSET",
|
||||
"ui.heading_offset_plane": "OFFSET PLANE",
|
||||
"ui.heading_ok": "OK",
|
||||
"ui.heading_parameters": "PARAMETERS",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"ui.cancel": "キャンセル",
|
||||
"ui.canvases": "キャンバス",
|
||||
"ui.centerline": "中心線",
|
||||
"ui.chain": "チェーン",
|
||||
"ui.chamfer": "面取り",
|
||||
"ui.change_workspace": "ワークスペースの変更",
|
||||
"ui.chord_length": "弦長",
|
||||
@@ -74,6 +75,7 @@
|
||||
"ui.create_sketch": "スケッチを作成",
|
||||
"ui.current": "現在",
|
||||
"ui.curvature": "曲率",
|
||||
"ui.curves": "曲線",
|
||||
"ui.cut": "切り取り",
|
||||
"ui.cylinder": "円柱",
|
||||
"ui.dark": "ダーク",
|
||||
@@ -143,6 +145,7 @@
|
||||
"ui.heading_mirror": "ミラー",
|
||||
"ui.heading_modify": "修正",
|
||||
"ui.heading_move": "移動",
|
||||
"ui.heading_offset": "オフセット",
|
||||
"ui.heading_offset_plane": "オフセット平面",
|
||||
"ui.heading_ok": "OK",
|
||||
"ui.heading_parameters": "パラメータ",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"ui.cancel": "취소",
|
||||
"ui.canvases": "캔버스",
|
||||
"ui.centerline": "중심선",
|
||||
"ui.chain": "체인",
|
||||
"ui.chamfer": "모따기",
|
||||
"ui.change_workspace": "작업 공간 변경",
|
||||
"ui.chord_length": "현 길이",
|
||||
@@ -74,6 +75,7 @@
|
||||
"ui.create_sketch": "스케치 만들기",
|
||||
"ui.current": "현재",
|
||||
"ui.curvature": "곡률",
|
||||
"ui.curves": "곡선",
|
||||
"ui.cut": "잘라내기",
|
||||
"ui.cylinder": "원통",
|
||||
"ui.dark": "어둡게",
|
||||
@@ -143,6 +145,7 @@
|
||||
"ui.heading_mirror": "대칭 복사",
|
||||
"ui.heading_modify": "수정",
|
||||
"ui.heading_move": "이동",
|
||||
"ui.heading_offset": "오프셋",
|
||||
"ui.heading_offset_plane": "오프셋 평면",
|
||||
"ui.heading_ok": "확인",
|
||||
"ui.heading_parameters": "매개변수",
|
||||
|
||||
@@ -30,8 +30,26 @@ fn clicked_periodic_fit_closes_at_start_and_right_click_keeps_natural() {
|
||||
assert!(matches!(sk.curves[1].kind, CurveKind::Spline { periodic: false, .. }));
|
||||
}
|
||||
|
||||
/// Curve ids in the open dialog's first selection input.
|
||||
fn dialog_curves(h: &crate::scenario::Harness) -> Vec<String> {
|
||||
h.app
|
||||
.dialog
|
||||
.as_ref()
|
||||
.and_then(|d| d.inputs.first())
|
||||
.map(|i| i.items.iter().filter_map(|x| if let solidcraft_engine::Sel::SketchCurve { id } = x { Some(id.clone()) } else { None }).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Type `value` into the open Offset window's Distance row, then apply with OK.
|
||||
fn offset_distance(h: &mut crate::scenario::Harness, value: &str) {
|
||||
h.step(&json!({"click":{"handle":"dialog:Distance"}})).unwrap();
|
||||
h.step(&json!({"key":"A","cmd":true})).unwrap();
|
||||
h.step(&json!({"text":value})).unwrap();
|
||||
h.step(&json!({"widget":"OK"})).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicked_ellipse_offset_then_closed_offset_preserves_exact_distance_and_undo() {
|
||||
fn offset_window_distance_and_flip_offset_an_ellipse_exactly_and_undo_restores() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||
h.app.run("sketch.ellipse", json!({"center":[0,0],"major":[10,0],"minor_radius":5})).unwrap();
|
||||
@@ -40,19 +58,25 @@ fn clicked_ellipse_offset_then_closed_offset_preserves_exact_distance_and_undo()
|
||||
h.step(&json!({"start":"sketch.offset"})).unwrap();
|
||||
// Major tip is also a source handle: Offset must choose the actual curve.
|
||||
h.step(&json!({"click":{"world":[10,0,0]}})).unwrap();
|
||||
assert_eq!(h.app.tool.as_ref().unwrap().picks, vec!["e1"]);
|
||||
h.step(&json!({"click":{"world":[11,0,0]}})).unwrap();
|
||||
assert_eq!(dialog_curves(&h), vec!["e1".to_string()], "{:?}", h.app.status);
|
||||
// Nothing is committed until the window is applied.
|
||||
assert!(h.app.session.model.state().sketch(sid).unwrap().sketch.freeform_offsets.is_empty());
|
||||
h.step(&json!({"click":{"handle":"dialog:Flip"}})).unwrap();
|
||||
offset_distance(&mut h, "1 mm");
|
||||
assert!(h.app.dialog.is_none(), "{:?}", h.app.status);
|
||||
let sk = h.app.session.model.state().sketch(sid).unwrap().sketch.clone();
|
||||
assert_eq!(sk.freeform_offsets.len(), 1, "{:?}", h.app.status);
|
||||
assert!((sk.freeform_offsets[0].distance + 1.0).abs() < 1e-4);
|
||||
assert!((sk.freeform_offsets[0].distance + 1.0).abs() < 1e-4, "{:?}", sk.freeform_offsets[0].distance);
|
||||
let first = sk.freeform_offsets[0].output.clone();
|
||||
let saved = h.app.session.doc.to_json();
|
||||
h.step(&json!({"start":"sketch.offset"})).unwrap();
|
||||
h.step(&json!({"click":{"world":[11,0,0]}})).unwrap();
|
||||
assert_eq!(h.app.tool.as_ref().unwrap().picks, vec![first]);
|
||||
h.step(&json!({"click":{"world":[11.5,0,0]}})).unwrap();
|
||||
assert_eq!(dialog_curves(&h), vec![first], "{:?}", h.app.status);
|
||||
h.step(&json!({"click":{"handle":"dialog:Flip"}})).unwrap();
|
||||
offset_distance(&mut h, "0.5 mm");
|
||||
let sk = h.app.session.model.state().sketch(sid).unwrap().sketch.clone();
|
||||
assert_eq!(sk.freeform_offsets.len(), 2, "{:?}", h.app.status);
|
||||
assert!((sk.freeform_offsets[1].distance + 0.5).abs() < 1e-4);
|
||||
assert!((sk.freeform_offsets[1].distance + 0.5).abs() < 1e-4, "{:?}", sk.freeform_offsets[1].distance);
|
||||
assert!(sk.freeform_offsets.iter().all(|r| r.certificate == 2));
|
||||
assert_eq!(solidcraft_engine::sketch::find_profiles(&sk).len(), 3);
|
||||
let last = sk.curve_index(&sk.freeform_offsets[1].output).unwrap();
|
||||
@@ -61,6 +85,20 @@ fn clicked_ellipse_offset_then_closed_offset_preserves_exact_distance_and_undo()
|
||||
assert_eq!(h.app.session.doc.to_json(), saved);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_window_without_a_curve_reports_it_and_keeps_the_window_open() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||
h.app.run("sketch.ellipse", json!({"center":[0,0],"major":[10,0],"minor_radius":5})).unwrap();
|
||||
h.settle();
|
||||
let sid = h.app.session.active_sketch.unwrap();
|
||||
h.step(&json!({"start":"sketch.offset"})).unwrap();
|
||||
h.step(&json!({"widget":"OK"})).unwrap();
|
||||
let error = h.app.dialog.as_ref().and_then(|d| d.error.clone());
|
||||
assert_eq!(error.as_deref(), Some("Select the curves to offset first"));
|
||||
assert!(h.app.session.model.state().sketch(sid).unwrap().sketch.freeform_offsets.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicked_extend_endpoint_reaches_exact_ellipse_and_preserves_support() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
//! Tool windows for sketch-space modify commands, on the `dialogs_surface` pattern (#19): one
|
||||
//! `Mod` enum behind one `Kind::SketchModify` variant, grown by one variant and one arm per
|
||||
//! command rather than by a new module each time. The first variant here is Scale, whose
|
||||
//! toolbar button reached no tool and no dialog at all, so a press ran `sketch.scale` with no
|
||||
//! parameters and the engine answered "`factor` must be a positive number".
|
||||
//! `sketch.offset`, `sketch.fillet`, the three `sketch.chamfer.*` commands, the sketch
|
||||
//! patterns, `sketch.mirror` and `sketch.move` are the same gap and join this module later,
|
||||
//! each its own change.
|
||||
//! command rather than by a new module each time. Scale came first, whose toolbar button
|
||||
//! reached no tool and no dialog at all, so a press ran `sketch.scale` with no parameters and
|
||||
//! the engine answered "`factor` must be a positive number". Offset joins it here (upstream
|
||||
//! issue #162 item 1: "Offset does not create a constraint, the size of the offset seems
|
||||
//! random"). `sketch.fillet`, the three `sketch.chamfer.*` commands, the sketch patterns,
|
||||
//! `sketch.mirror` and `sketch.move` are the same gap and join this module later, each its own
|
||||
//! change.
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
use solidcraft_engine::Sel;
|
||||
use solidcraft_engine::doc::expr::Kind as ValueKind;
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::dialogs::{Kind, field, row_label};
|
||||
use crate::dialogs::{Kind, check, field, row_label};
|
||||
use crate::selection::{CURVES, POINTS, SelInput};
|
||||
|
||||
/// A sketch modify tool window's values.
|
||||
@@ -22,12 +23,17 @@ pub enum Mod {
|
||||
/// input; with none picked the command scales about the sketch origin, as its own default
|
||||
/// already does.
|
||||
Scale { factor: String },
|
||||
/// `sketch.offset`: the curves to offset, the distance typed and whether the connected
|
||||
/// chain follows. A positive distance is left of the chain, as the command defines it;
|
||||
/// `flip` reverses whatever sign the field holds.
|
||||
Offset { distance: String, chain: bool, flip: bool },
|
||||
}
|
||||
|
||||
impl Mod {
|
||||
pub fn title(&self) -> &'static str {
|
||||
match self {
|
||||
Mod::Scale { .. } => "SKETCH SCALE",
|
||||
Mod::Offset { .. } => "OFFSET",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +41,7 @@ impl Mod {
|
||||
pub fn primary(&mut self) -> Option<(&'static str, ValueKind, &mut String)> {
|
||||
Some(match self {
|
||||
Mod::Scale { factor } => ("Factor", ValueKind::Unitless, factor),
|
||||
Mod::Offset { distance, .. } => ("Distance", ValueKind::Length, distance),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -42,6 +49,7 @@ impl Mod {
|
||||
fn inputs(k: &Mod) -> Vec<SelInput> {
|
||||
match k {
|
||||
Mod::Scale { .. } => vec![SelInput::new("Entities", CURVES | POINTS, true), SelInput::new("Base point", POINTS, false)],
|
||||
Mod::Offset { .. } => vec![SelInput::new("Curves", CURVES, true)],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +57,7 @@ fn inputs(k: &Mod) -> Vec<SelInput> {
|
||||
pub fn start(_app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||||
let k = match id {
|
||||
"sketch.scale" => Mod::Scale { factor: "2".into() },
|
||||
"sketch.offset" => Mod::Offset { distance: "2 mm".into(), chain: true, flip: false },
|
||||
_ => return None,
|
||||
};
|
||||
let i = inputs(&k);
|
||||
@@ -64,6 +73,17 @@ pub fn rows(ui: &mut egui::Ui, k: &mut Mod) -> bool {
|
||||
enter |= field(ui, factor);
|
||||
ui.end_row();
|
||||
}
|
||||
Mod::Offset { distance, chain, flip } => {
|
||||
row_label(ui, "Distance");
|
||||
enter |= field(ui, distance);
|
||||
ui.end_row();
|
||||
row_label(ui, "Flip");
|
||||
check(ui, flip);
|
||||
ui.end_row();
|
||||
row_label(ui, "Chain");
|
||||
check(ui, chain);
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
enter
|
||||
}
|
||||
@@ -94,6 +114,11 @@ fn entity_ids(inputs: &[SelInput], i: usize) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Sketch curve ids picked in input `i`.
|
||||
fn curve_ids(inputs: &[SelInput], i: usize) -> Vec<String> {
|
||||
items(inputs, i).iter().filter_map(|x| if let Sel::SketchCurve { id } = x { Some(id.clone()) } else { None }).collect()
|
||||
}
|
||||
|
||||
/// A typed scale factor as the command wants it: the field evaluated against the document's
|
||||
/// parameters, in the range the command accepts. Checking the range here lets the window say
|
||||
/// so in its own error line instead of the status bar.
|
||||
@@ -105,9 +130,21 @@ fn factor(app: &SolveApp, text: &str) -> Result<f64, String> {
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// A typed distance as the command wants it: the field evaluated against the document's
|
||||
/// parameters, with the sign reversed when Flip is set. A bare number means the design's units,
|
||||
/// as a typed sketch value does everywhere else.
|
||||
fn signed_length(app: &SolveApp, text: &str, flip: bool) -> Result<f64, String> {
|
||||
let doc = &app.session.doc;
|
||||
let v = doc.eval(&doc.with_design_unit(text.trim(), ValueKind::Length), ValueKind::Length).map_err(|e| format!("Distance: {e}"))?;
|
||||
if !v.is_finite() || v.abs() <= 1e-9 {
|
||||
return Err("Distance must be a non-zero number".into());
|
||||
}
|
||||
Ok(if flip { -v } else { v })
|
||||
}
|
||||
|
||||
/// The command a sketch modify dialog makes. `sketch.scale` takes a plain evaluated number for
|
||||
/// its factor, not an expression string: it has no model parameter to carry one, unlike
|
||||
/// `sketch.fillet`'s radius.
|
||||
/// its factor and `sketch.offset` one for its distance, not an expression string: neither has a
|
||||
/// model parameter to carry one, unlike `sketch.fillet`'s radius.
|
||||
pub fn commands(app: &SolveApp, k: &Mod, inputs: &[SelInput], extra: &Map<String, Value>) -> Result<Vec<(String, Value)>, String> {
|
||||
let (cmd, mut params) = match k {
|
||||
Mod::Scale { factor: text } => {
|
||||
@@ -122,6 +159,14 @@ pub fn commands(app: &SolveApp, k: &Mod, inputs: &[SelInput], extra: &Map<String
|
||||
}
|
||||
("sketch.scale", p)
|
||||
}
|
||||
Mod::Offset { distance, chain, flip } => {
|
||||
let curves = curve_ids(inputs, 0);
|
||||
if curves.is_empty() {
|
||||
return Err("Select the curves to offset first".into());
|
||||
}
|
||||
let d = signed_length(app, distance, *flip)?;
|
||||
("sketch.offset", json!({"curves": curves, "distance": d, "chain": chain}))
|
||||
}
|
||||
};
|
||||
if let Value::Object(m) = &mut params {
|
||||
for (key, value) in extra {
|
||||
|
||||
@@ -357,7 +357,6 @@ fn mode(id: &str) -> Option<Mode> {
|
||||
"sketch.text" => Mode::Points(1),
|
||||
"sketch.edit_text" => Mode::Entities(1),
|
||||
"sketch.blend_curve" => Mode::Points(2),
|
||||
"sketch.offset" => Mode::Entities(1),
|
||||
"sketch.fillet" | "sketch.chamfer.equal_distance" | "sketch.chamfer.distance_angle" | "sketch.chamfer.two_distance" => Mode::Entities(1),
|
||||
"sketch.constraint.curvature" | "sketch.circle.two_tangent" => Mode::Entities(2),
|
||||
"sketch.circle.three_tangent" => Mode::Entities(3),
|
||||
@@ -415,7 +414,6 @@ pub fn hint(id: &str) -> Option<String> {
|
||||
"sketch.text" => "Text: click where the text starts",
|
||||
"sketch.edit_text" => "Edit Text: click a letter interior or outline",
|
||||
"sketch.blend_curve" => "Blend curve: click the ends of two curves",
|
||||
"sketch.offset" => "Offset: click a curve (its chain is offset), then the side and distance",
|
||||
"sketch.fillet" => "Fillet: click the corner of two lines",
|
||||
"sketch.chamfer.equal_distance" | "sketch.chamfer.distance_angle" | "sketch.chamfer.two_distance" => {
|
||||
"Chamfer: click the corner of two lines"
|
||||
@@ -709,30 +707,6 @@ pub fn on_click(app: &mut SolveApp, tool: &mut Tool, proj: &Proj, pos: Pos2) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if cmd == "sketch.offset" && !tool.picks.is_empty() {
|
||||
// Dense derived fit nodes must not snap a nearby side click back
|
||||
// onto the source, reducing a deliberate offset to zero.
|
||||
let state = app.session.world_state();
|
||||
let side = active_sketch(app).and_then(|sid| state.sketch(sid)).and_then(|ss| {
|
||||
let (o, d) = proj.ray(pos);
|
||||
ss.plane.intersect_ray(o, d).map(|p| ss.plane.to_local(p))
|
||||
});
|
||||
if let Some(p) = side {
|
||||
offset_side(app, tool, p);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if cmd == "sketch.offset" {
|
||||
if let Some(sid) = active_sketch(app)
|
||||
&& let Some(id) = hits.iter().find_map(|hit| match hit {
|
||||
Hit::SketchCurve { sketch, id, .. } if *sketch == sid => Some(id.clone()),
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
tool.picks.push(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if cmd == "sketch.edit_text" {
|
||||
let selection = hits.first().and_then(|hit| match hit {
|
||||
Hit::SketchCurve { sketch, id, .. } if Some(*sketch) == active_sketch(app) => {
|
||||
@@ -961,30 +935,6 @@ pub fn finish(app: &mut SolveApp, tool: &mut Tool) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Offset: the second click sets the side and distance.
|
||||
fn offset_side(app: &mut SolveApp, tool: &mut Tool, p: Vec2) -> bool {
|
||||
if tool.cmd != "sketch.offset" || tool.picks.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let st = app.session.world_state();
|
||||
let d = active_sketch(app)
|
||||
.and_then(|s| st.sketch(s))
|
||||
.and_then(|ss| {
|
||||
let curve = ss.sketch.curve_index(&tool.picks[0])?;
|
||||
if let Some(shape) = ss.sketch.shape(curve) {
|
||||
Some(shape.dist(p))
|
||||
} else {
|
||||
ss.sketch.exact_segs(curve)?.iter().map(|s| s.point_at(s.closest_param(p)).dist(p)).min_by(f64::total_cmp)
|
||||
}
|
||||
})
|
||||
.unwrap_or(0.0);
|
||||
if d > 1e-6 {
|
||||
let _ = app.run("sketch.offset", json!({"curves": [tool.picks[0]], "distance": d, "side": xy(p)}));
|
||||
}
|
||||
tool.picks.clear();
|
||||
true
|
||||
}
|
||||
|
||||
/// Rubber band for point tools and pick markers.
|
||||
pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) {
|
||||
let Some(sid) = active_sketch(app) else { return };
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
[
|
||||
{"note": "#162 item 1 (\"the size of the offset seems random\"): sketch.offset gets a tool window carrying Curves, Distance, Flip and Chain, instead of a bare click-pick that measured the distance off the second click."},
|
||||
{"start": "sketch.create"},
|
||||
{"click": {"plane": "XY"}},
|
||||
{"expect": {"sketching": true}},
|
||||
{"start": "sketch.circle.center"},
|
||||
{"click": {"sketch": [0, 0]}},
|
||||
{"click": {"sketch": [10, 0]}},
|
||||
{"key": "Escape"},
|
||||
{"expect": {"sketch_curves": {"own": 1}, "errors": 0}},
|
||||
|
||||
{"note": "Starting the command opens the window and commits nothing."},
|
||||
{"start": "sketch.offset"},
|
||||
{"expect": {"dialog": "Offset", "sketch_curves": {"own": 1}, "errors": 0}},
|
||||
{"click": {"sketch": [10, 0]}},
|
||||
{"expect": {"dialog": "Offset", "sketch_curves": {"own": 1}, "errors": 0}},
|
||||
|
||||
{"note": "The typed number reaches the command unchanged: a 10 mm circle offset 3 mm is a 7 mm circle."},
|
||||
{"click": {"handle": "dialog:Distance"}},
|
||||
{"key": "A", "cmd": true},
|
||||
{"text": "3"},
|
||||
{"key": "Enter"},
|
||||
{"expect": {"dialog": null, "errors": 0, "sketch_curves": {"own": 2}}},
|
||||
{"expect": {"query": {"command": "sketch.inspect", "path": ["curves", 1, "radius"], "value": 7}}},
|
||||
|
||||
{"note": "Flip reverses the side: the same 3 mm grows the circle to 13 mm."},
|
||||
{"start": "sketch.offset"},
|
||||
{"click": {"sketch": [10, 0]}},
|
||||
{"click": {"handle": "dialog:Flip"}},
|
||||
{"click": {"handle": "dialog:Distance"}},
|
||||
{"key": "A", "cmd": true},
|
||||
{"text": "3"},
|
||||
{"key": "Enter"},
|
||||
{"expect": {"dialog": null, "errors": 0, "sketch_curves": {"own": 3}}},
|
||||
{"expect": {"query": {"command": "sketch.inspect", "path": ["curves", 2, "radius"], "value": 13}}},
|
||||
{"shot": "sketch_offset_circle"},
|
||||
{"start": "sketch.finish"},
|
||||
|
||||
{"note": "Chain on (the default): one picked edge of a rectangle offsets the whole connected chain, four curves. The inner rectangle is 34 by 24, so the offset came from the typed 3 mm on every side."},
|
||||
{"start": "sketch.create"},
|
||||
{"click": {"plane": "XZ"}},
|
||||
{"start": "sketch.rectangle.two_point"},
|
||||
{"click": {"sketch": [0, 0]}},
|
||||
{"click": {"sketch": [40, 30]}},
|
||||
{"key": "Escape"},
|
||||
{"expect": {"sketch_curves": {"own": 4}, "errors": 0}},
|
||||
{"start": "sketch.offset"},
|
||||
{"click": {"sketch": [20, 0]}},
|
||||
{"expect": {"dialog": "Offset", "sketch_curves": {"own": 4}}},
|
||||
{"click": {"handle": "dialog:Distance"}},
|
||||
{"key": "A", "cmd": true},
|
||||
{"text": "3"},
|
||||
{"key": "Enter"},
|
||||
{"expect": {"dialog": null, "errors": 0, "sketch_curves": {"own": 8}}},
|
||||
{"expect": {"query": {"command": "sketch.inspect", "path": ["profiles", 1, "area_mm2"], "value": 816}}},
|
||||
{"shot": "sketch_offset_chain"},
|
||||
{"start": "sketch.finish"},
|
||||
|
||||
{"note": "Chain cleared: the same pick offsets that one curve, so the sketch gains one."},
|
||||
{"start": "sketch.create"},
|
||||
{"click": {"plane": "YZ"}},
|
||||
{"start": "sketch.rectangle.two_point"},
|
||||
{"click": {"sketch": [0, 0]}},
|
||||
{"click": {"sketch": [40, 30]}},
|
||||
{"key": "Escape"},
|
||||
{"expect": {"sketch_curves": {"own": 4}, "errors": 0}},
|
||||
{"start": "sketch.offset"},
|
||||
{"click": {"sketch": [20, 0]}},
|
||||
{"click": {"handle": "dialog:Chain"}},
|
||||
{"click": {"handle": "dialog:Distance"}},
|
||||
{"key": "A", "cmd": true},
|
||||
{"text": "3"},
|
||||
{"key": "Enter"},
|
||||
{"expect": {"dialog": null, "errors": 0, "sketch_curves": {"own": 5}}},
|
||||
|
||||
{"note": "Escape closes the window and leaves the sketch exactly as it was."},
|
||||
{"start": "sketch.offset"},
|
||||
{"click": {"sketch": [20, 0]}},
|
||||
{"expect": {"dialog": "Offset"}},
|
||||
{"key": "Escape"},
|
||||
{"expect": {"dialog": null, "errors": 0, "sketch_curves": {"own": 5}}}
|
||||
]
|
||||
Reference in new issue
Block a user