Construction mode like Fusion: with nothing selected, X (or the toolbar's Construction, or the Sketch Palette's Linetype) switches the drawing line type, so new curves are construction (or centerline) until switched back, the active mode highlighted in the palette and named in the status bar; with a selection they convert it as before; the line type is part of the drawing's undo step. Scenarios: s35 construction mode, s36 the Sketch Palette shows while sketching (also over the tools, back after a window closes, both themes) and goes with the sketch; harness shown/not_shown checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-08 16:07:33 +00:00
1 parent 35ac9569dc
commit 90873e51bb
6 files changed
+246 -13

No files matched your search

+8 -11
View File
@@ -340,6 +340,10 @@ impl SolveApp {
let t0 = now_ms();
let r = self.session.execute(id, &params).map_err(|e| e.to_string());
self.big_operation |= now_ms() - t0 >= recovery_ui::BIG_OPERATION_MS;
if let Ok(v) = &r {
// Curves drawn in construction (or centerline) mode take that line type.
sketch_palette::apply_mode(self, id, v);
}
match &r {
Ok(_) => {
self.session.echo(format!("{id}: done"));
@@ -416,17 +420,10 @@ impl SolveApp {
}
}
// Construction toggle: selected sketch curves switch at once, otherwise pick them.
if id == "sketch.construction" {
let curves: Vec<String> = self
.session
.selection
.iter()
.filter_map(|s| if let solvecraft_engine::Sel::SketchCurve { id } = s { Some(id.clone()) } else { None })
.collect();
if !curves.is_empty() {
let _ = self.run(id, json!({ "curves": curves }));
return;
}
// Construction (X): convert the selection, or switch construction mode.
if id == "sketch.construction" && self.session.active_sketch.is_some() {
sketch_palette::toggle_construction(self);
return;
}
if let Some(spec) = solvecraft_engine::find_command(id)
&& (tools::Tool::for_command(id).is_some() || dialogs::Dialog::for_command(self, id).is_some())
+7
View File
@@ -563,6 +563,13 @@ pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> {
}
}
}
"shown" | "not_shown" => {
// A widget with this text is (not) on screen.
let t = v.as_str().unwrap_or_default();
if h.widget(t).is_some() != (k == "shown") {
return Err(format!("widget `{t}`: want {}", if k == "shown" { "shown" } else { "not shown" }));
}
}
"home" => {
if ui["home"] != *v {
return Err(format!("start page open: got {}, want {v}", ui["home"]));
+83 -2
View File
@@ -4,6 +4,9 @@
//! profiles, points, dimensions, constraints, projected geometry, 3D sketch); the sketch's
//! degrees of freedom; Finish Sketch. Toggles run `sketch.options`, line types
//! `sketch.construction` / `sketch.centerline`.
//!
//! The line type is also a drawing mode, as in Fusion: with nothing selected, Construction (or
//! X) makes what is drawn next construction geometry until it is switched back.
use std::cell::Cell;
@@ -15,6 +18,8 @@ use crate::SolveApp;
use crate::theme::Tokens;
thread_local! {
/// The drawing line type (0 normal, 1 construction, 2 centerline) and the sketch it is for.
static MODE: Cell<(u8, Option<u64>)> = const { Cell::new((0, None)) };
static FOLDED: Cell<bool> = const { Cell::new(false) };
static OPTIONS_FOLDED: Cell<bool> = const { Cell::new(false) };
}
@@ -48,6 +53,74 @@ pub fn dof_text(dof: usize, ok: bool) -> String {
}
}
/// The line type new curves get in the sketch being edited (0 normal, 1 construction,
/// 2 centerline); a new sketch starts normal.
pub fn mode(app: &SolveApp) -> u8 {
let (m, sk) = MODE.with(Cell::get);
if sk.is_some() && sk == app.session.active_sketch { m } else { 0 }
}
/// Switch the drawing line type (with a status hint).
pub fn set_mode(app: &mut SolveApp, m: u8) {
MODE.with(|c| c.set((m, app.session.active_sketch)));
let hint = match m {
1 => "Construction mode: new curves are construction geometry (X or the palette's Normal to leave)",
2 => "Centerline mode: new lines are centerlines",
_ => "Normal mode",
};
app.set_status(hint, false);
}
/// X / the toolbar's Construction: convert the selected curves, or with nothing selected
/// switch construction mode on or off.
pub fn toggle_construction(app: &mut SolveApp) {
let curves = selected_curves(app);
if curves.is_empty() {
let m = if mode(app) == 1 { 0 } else { 1 };
set_mode(app, m);
} else {
let _ = app.run("sketch.construction", json!({ "curves": curves }));
}
}
/// Curves a drawing command just made take the drawing line type.
pub fn apply_mode(app: &mut SolveApp, id: &str, result: &Value) {
let m = mode(app);
if m == 0 || !solvecraft_engine::find_command(id).is_some_and(|c| c.tab == "SKETCH" && c.panel == "CREATE") {
return;
}
let curves: Vec<String> = result["curves"].as_array().into_iter().flatten().filter_map(|c| c.as_str().map(str::to_string)).collect();
if curves.is_empty() {
return;
}
let lines: Vec<String> = if m == 2 {
let st = app.session.model.state();
let Some(ss) = app.session.active_sketch.and_then(|s| st.sketch(s)) else { return };
curves
.iter()
.filter(|c| {
ss.sketch
.curve_index(c)
.and_then(|i| ss.sketch.curves.get(i))
.is_some_and(|c| matches!(c.kind, solvecraft_engine::sketch::CurveKind::Line { .. }))
})
.cloned()
.collect()
} else {
Vec::new()
};
let rest: Vec<String> = curves.iter().filter(|c| !lines.contains(c)).cloned().collect();
// Part of the drawing's own undo step.
let depth = app.session.undo.len();
if !lines.is_empty() {
let _ = app.session.execute("sketch.centerline", &json!({ "curves": lines, "value": true }));
}
if !rest.is_empty() {
let _ = app.session.execute("sketch.construction", &json!({ "curves": rest, "value": true }));
}
app.session.undo.truncate(depth);
}
/// Selected curves of the active sketch.
fn selected_curves(app: &SolveApp) -> Vec<String> {
app.session.selection.iter().filter_map(|s| if let Sel::SketchCurve { id } = s { Some(id.clone()) } else { None }).collect()
@@ -154,8 +227,12 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
egui::Grid::new("sc_palette_grid").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
ui.label("Linetype");
ui.horizontal(|ui| {
// With a selection: convert it; without: the mode new curves are drawn in.
let current = mode(app);
for (i, (label, tip)) in [("—", "Normal"), ("- -", "Construction"), ("-·-", "Centerline")].iter().enumerate() {
if ui.add_enabled(has_curves, egui::Button::new(*label).min_size(vec2(30.0, 20.0))).on_hover_text(*tip).clicked() {
let on = !has_curves && current == i as u8;
let tip = if has_curves { format!("Make the selection {tip}") } else { format!("Draw {tip} curves") };
if ui.add(egui::Button::new(*label).selected(on).min_size(vec2(30.0, 20.0))).on_hover_text(tip).clicked() {
linetype = Some(i as u8);
}
}
@@ -194,7 +271,11 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
}
}
if let Some(k) = linetype {
set_linetype(app, k);
if has_curves {
set_linetype(app, k);
} else {
set_mode(app, k);
}
}
if look {
crate::dialogs::look_at_sketch(app);
+2
View File
@@ -52,6 +52,8 @@ scenarios!(
s27_move_triad,
s33_move_x_arrow,
s34_component_move_carries_sketches,
s35_construction_mode,
s36_sketch_palette_visible
);
/// Scenarios written ahead of their fixes or features (the user's open bugs): run with
@@ -0,0 +1,47 @@
[
{"note": "Construction mode like Fusion: with nothing selected, X (or the palette's Construction linetype) makes new curves construction until switched back; with a selection X converts it."},
{"start": "SketchCreate"},
{"click": {"plane": "XY"}},
{"expect": {"dialog": null}},
{"key": "X"},
{"expect": {"tool": null}},
{"start": "ShapeRectangleTwoPoint"},
{"click": {"sketch": [0, 0]}},
{"click": {"sketch": [30, 20]}},
{"key": "Escape"},
{"start": "CircleCenterRadius"},
{"click": {"sketch": [15, 10]}},
{"click": {"sketch": [20, 10]}},
{"key": "Escape"},
{"expect": {"query": {"command": "sketch.inspect", "path": ["curves", 4, "construction"], "value": true}}},
{"expect": {"query": {"command": "sketch.inspect", "path": ["curves", 0, "construction"], "value": true}}},
{"expect": {"query": {"command": "sketch.inspect", "path": ["profiles"], "value": []}}},
{"key": "Z", "cmd": true},
{"expect": {"query": {"command": "sketch.inspect", "path": ["curves", 4], "value": null}}},
{"key": "Y", "cmd": true},
{"shot": "s35_construction_mode"},
{"widget": "—"},
{"start": "CircleCenterRadius"},
{"click": {"sketch": [40, 10]}},
{"click": {"sketch": [45, 10]}},
{"key": "Escape"},
{"expect": {"query": {"command": "sketch.inspect", "path": ["curves", 5, "construction"], "value": false}}},
{"widget": "- -"},
{"start": "DrawPolyline"},
{"click": {"sketch": [0, 30]}},
{"click": {"sketch": [30, 30]}},
{"key": "Escape"},
{"key": "Escape"},
{"expect": {"query": {"command": "sketch.inspect", "path": ["curves", 6, "construction"], "value": true}}},
{"key": "X"},
{"click": {"sketch": [40, 15]}},
{"key": "X"},
{"expect": {"query": {"command": "sketch.inspect", "path": ["curves", 5, "construction"], "value": true}}},
{"key": "Escape"},
{"start": "DrawPolyline"},
{"click": {"sketch": [0, 40]}},
{"click": {"sketch": [30, 40]}},
{"key": "Escape"},
{"key": "Escape"},
{"expect": {"query": {"command": "sketch.inspect", "path": ["curves", 7, "construction"], "value": false}}}
]
@@ -0,0 +1,99 @@
[
{
"note": "The Sketch Palette shows docked at the right whenever a sketch is active and no command dialog is open, comes back when the dialog closes, and goes with the sketch."
},
{
"expect": {
"not_shown": "SKETCH PALETTE"
}
},
{
"start": "SketchCreate"
},
{
"expect": {
"dialog": "Sketch",
"not_shown": "SKETCH PALETTE"
}
},
{
"click": {
"plane": "XY"
}
},
{
"expect": {
"sketching": true,
"dialog": null,
"shown": "SKETCH PALETTE"
}
},
{
"expect": {
"shown": "Finish Sketch"
}
},
{
"start": "ShapeRectangleTwoPoint"
},
{
"expect": {
"shown": "SKETCH PALETTE"
}
},
{
"click": {
"sketch": [
0,
0
]
}
},
{
"click": {
"sketch": [
20,
10
]
}
},
{
"key": "Escape"
},
{
"start": "ChangeParameterCommand"
},
{
"widget": "Close window"
},
{
"expect": {
"shown": "SKETCH PALETTE"
}
},
{
"call": "ui.set",
"params": {
"dark": false
}
},
{
"expect": {
"shown": "SKETCH PALETTE"
}
},
{
"expect": {
"shown": "Finish Sketch"
}
},
{
"widget": "Finish Sketch"
},
{
"expect": {
"sketching": false,
"not_shown": "SKETCH PALETTE"
}
}
]