Sketch Palette, docked at the right while sketching: line type of the selected curves (normal, construction, centerline), Look At, and options kept with each sketch (sketch.options: Sketch Grid, Snap, Slice, Show Profile, Show Points, Show Dimensions, Show Constraints, Show Projected Geometries, 3D Sketch), the degrees of freedom, Finish Sketch. Slice cuts the model at the sketch plane; Snap off places points at the cursor
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
2608ae6761
commit
bbd0a19260
10 files changed
+382
-7
No files matched your search
@@ -157,6 +157,9 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("sketch.dimension_text", "Move Dimension Text", dimension_text)
|
||||
.enabled(in_sketch)
|
||||
.params("dimension: constraint id or parameter name; at: [x,y] text centre (sketch coordinates) | reset: true (default place)"),
|
||||
CommandSpec::new("sketch.options", "Sketch Palette Options", sketch_options).icon("sketch").params(
|
||||
"sketch?: id|name (default active); any of show_profile, show_points, show_dimensions, show_constraints, show_projected, grid, snap, slice, sketch_3d: bool. Returns them all",
|
||||
),
|
||||
CommandSpec::new("sketch.solve", "Solve Sketch", solve_cmd)
|
||||
.enabled(in_sketch)
|
||||
.noundo()
|
||||
@@ -1514,6 +1517,55 @@ fn move_point(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"sketch": info}))
|
||||
}
|
||||
|
||||
/// The Sketch Palette options of a sketch, by their palette names (true = shown / on).
|
||||
fn options_json(v: &solvecraft_sketch::SketchView) -> Value {
|
||||
json!({
|
||||
"show_profile": !v.hide_profile,
|
||||
"show_points": !v.hide_points,
|
||||
"show_dimensions": !v.hide_dimensions,
|
||||
"show_constraints": !v.hide_constraints,
|
||||
"show_projected": !v.hide_projected,
|
||||
"grid": !v.hide_grid,
|
||||
"snap": !v.no_snap,
|
||||
"slice": v.slice,
|
||||
"sketch_3d": v.three_d,
|
||||
})
|
||||
}
|
||||
|
||||
fn sketch_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.options";
|
||||
let keys = ["show_profile", "show_points", "show_dimensions", "show_constraints", "show_projected", "grid", "snap", "slice", "sketch_3d"];
|
||||
if let Some(o) = p.as_object()
|
||||
&& let Some(k) = o.keys().find(|k| *k != "sketch" && !keys.contains(&k.as_str()))
|
||||
{
|
||||
return Err(bad(cmd, format!("unknown option `{k}`")));
|
||||
}
|
||||
let set = |k: &str| bool_(p, k);
|
||||
if keys.iter().all(|k| set(k).is_none()) {
|
||||
let id = target_sketch(s, p, cmd)?;
|
||||
return Ok(json!({"options": options_json(&s.doc.sketch(id)?.view)}));
|
||||
}
|
||||
let (v, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let v = &mut sk.view;
|
||||
let put = |k: &str, flag: &mut bool, shown_means_off: bool| {
|
||||
if let Some(b) = set(k) {
|
||||
*flag = if shown_means_off { !b } else { b };
|
||||
}
|
||||
};
|
||||
put("show_profile", &mut v.hide_profile, true);
|
||||
put("show_points", &mut v.hide_points, true);
|
||||
put("show_dimensions", &mut v.hide_dimensions, true);
|
||||
put("show_constraints", &mut v.hide_constraints, true);
|
||||
put("show_projected", &mut v.hide_projected, true);
|
||||
put("grid", &mut v.hide_grid, true);
|
||||
put("snap", &mut v.no_snap, true);
|
||||
put("slice", &mut v.slice, false);
|
||||
put("sketch_3d", &mut v.three_d, false);
|
||||
Ok(options_json(v))
|
||||
})?;
|
||||
Ok(json!({"options": v, "sketch": info}))
|
||||
}
|
||||
|
||||
fn solve_cmd(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = target_sketch(s, p, "sketch.solve")?;
|
||||
let st = s.model.state();
|
||||
|
||||
@@ -400,3 +400,36 @@ fn point_constraints_keep_the_first_pick() {
|
||||
assert!(same(&snap(&s, "p1"), &a0) && same(&snap(&s, "l1"), &l0));
|
||||
assert!(pt(&s, "p2").dist(solvecraft_geom::Vec2::new(3.0, -7.0)) < 1e-6, "{:?}", pt(&s, "p2"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Sketch Palette options
|
||||
|
||||
#[test]
|
||||
fn palette_options_are_kept_with_each_sketch() {
|
||||
let mut s = new_sketch();
|
||||
let o = run(&mut s, "sketch.options", json!({}));
|
||||
assert_eq!(o["options"]["show_dimensions"], true);
|
||||
assert_eq!(o["options"]["slice"], false);
|
||||
let o = run(&mut s, "sketch.options", json!({"show_dimensions": false, "snap": false, "slice": true, "sketch_3d": true}));
|
||||
assert_eq!(o["options"]["show_dimensions"], false);
|
||||
assert_eq!(o["options"]["snap"], false);
|
||||
assert_eq!(o["options"]["slice"], true);
|
||||
let v = sketch(&s).view;
|
||||
assert!(v.hide_dimensions && v.no_snap && v.slice && v.three_d && !v.hide_points);
|
||||
// Another sketch starts with the defaults; the first keeps its options after finishing.
|
||||
let first = s.active_sketch.expect("sketch");
|
||||
run(&mut s, "SketchStop", json!({}));
|
||||
run(&mut s, "SketchCreate", json!({"plane": "XZ"}));
|
||||
assert_eq!(sketch(&s).view, solvecraft_sketch::SketchView::default());
|
||||
let o = run(&mut s, "sketch.options", json!({"sketch": first}));
|
||||
assert_eq!(o["options"]["show_dimensions"], false);
|
||||
// Saved with the design.
|
||||
let text = serde_json::to_string(&s.doc.sketch(first).expect("first").clone()).expect("json");
|
||||
let back: Sketch = serde_json::from_str(&text).expect("parse");
|
||||
assert!(back.view.slice);
|
||||
assert!(s.execute("sketch.options", &json!({"bogus": true})).is_err());
|
||||
// Undo restores the option.
|
||||
run(&mut s, "sketch.options", json!({"show_points": false}));
|
||||
run(&mut s, "UndoCommand", json!({}));
|
||||
assert!(!sketch(&s).view.hide_points);
|
||||
}
|
||||
@@ -72,6 +72,9 @@ pub fn grid_step(half_height: f64) -> (f64, f64) {
|
||||
pub fn sketch_lines(sk: &Sketch, plane: &Plane, active: bool, determined: &[bool]) -> Vec<(Vec<Vec3>, Rgb, bool)> {
|
||||
let mut out = Vec::new();
|
||||
for (i, c) in sk.curves.iter().enumerate() {
|
||||
if c.link.is_some() && sk.view.hide_projected && !sk.is_text_curve(i) {
|
||||
continue;
|
||||
}
|
||||
let col = if c.link.is_some() && !sk.is_text_curve(i) {
|
||||
if sk.is_lost_curve(i) { colors::SKETCH_LOST } else { colors::SKETCH_PROJECTED }
|
||||
} else if c.fixed {
|
||||
@@ -92,7 +95,7 @@ pub fn sketch_lines(sk: &Sketch, plane: &Plane, active: bool, determined: &[bool
|
||||
}
|
||||
}
|
||||
// 3D curves are in world coordinates already.
|
||||
for w in &sk.wires {
|
||||
for w in sk.wires.iter().filter(|_| !sk.view.hide_projected) {
|
||||
out.push((w.pts.clone(), colors::SKETCH_PROJECTED, false));
|
||||
}
|
||||
out
|
||||
|
||||
@@ -24,7 +24,7 @@ pub use curves::{
|
||||
};
|
||||
pub use dims::{DimFrame, DimLayout, chain_centres, default_text, dim_frame, dim_layout, encode_text};
|
||||
pub use link::{Link, LinkGeom, LinkKind, LinkSource, MAX_LINK_ENTITIES};
|
||||
pub use model::{Constraint, ConstraintKind, Curve, CurveKind, SPoint, Sketch, SketchError};
|
||||
pub use model::{Constraint, ConstraintKind, Curve, CurveKind, SPoint, Sketch, SketchError, SketchView};
|
||||
pub use profiles::{Profile, find_drawn_profiles, find_profiles, merge_regions};
|
||||
pub use shape::{Shape, intersections};
|
||||
pub use solver::{Hold, SolveReport, SolveStatus, solve, solve_holding};
|
||||
|
||||
@@ -430,6 +430,34 @@ pub struct Constraint {
|
||||
pub text: Option<Vec2>,
|
||||
}
|
||||
|
||||
/// How a sketch is shown and drawn in (the Sketch Palette options kept with the sketch). Every
|
||||
/// flag is off by default, so a sketch shows everything, with grid and snap.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct SketchView {
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub hide_profile: bool,
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub hide_points: bool,
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub hide_dimensions: bool,
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub hide_constraints: bool,
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub hide_projected: bool,
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub hide_grid: bool,
|
||||
/// Clicks don't snap to the grid.
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub no_snap: bool,
|
||||
/// Cut away the model in front of the sketch plane while editing.
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub slice: bool,
|
||||
/// 3D sketch: drawing may leave the sketch plane.
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub three_d: bool,
|
||||
}
|
||||
|
||||
/// A 2D sketch. Point 0 is always the fixed sketch origin (id `origin`).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Sketch {
|
||||
@@ -445,6 +473,13 @@ pub struct Sketch {
|
||||
/// Next number per id prefix (`l` → l1, l2…).
|
||||
#[serde(default)]
|
||||
counters: std::collections::BTreeMap<String, u64>,
|
||||
/// Display options (Sketch Palette).
|
||||
#[serde(default, skip_serializing_if = "is_default_view")]
|
||||
pub view: SketchView,
|
||||
}
|
||||
|
||||
fn is_default_view(v: &SketchView) -> bool {
|
||||
*v == SketchView::default()
|
||||
}
|
||||
|
||||
impl Default for Sketch {
|
||||
@@ -462,6 +497,7 @@ impl Sketch {
|
||||
links: Vec::new(),
|
||||
wires: Vec::new(),
|
||||
counters: Default::default(),
|
||||
view: SketchView::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -186,6 +186,10 @@ pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &P
|
||||
return;
|
||||
};
|
||||
let sk = &ss.sketch;
|
||||
if sk.view.hide_dimensions {
|
||||
DRAWN.with(|d| d.borrow_mut().clear());
|
||||
return;
|
||||
}
|
||||
let tk = Tokens::get();
|
||||
let sel = selected();
|
||||
// A selected dimension that was deleted (undo, another command) is forgotten.
|
||||
|
||||
@@ -32,6 +32,7 @@ pub mod selection;
|
||||
pub mod sketch_dims;
|
||||
#[cfg(test)]
|
||||
mod sketch_edit_tests;
|
||||
pub mod sketch_palette;
|
||||
pub mod sketch_tools;
|
||||
pub mod theme;
|
||||
pub mod timeline;
|
||||
@@ -609,6 +610,7 @@ impl SolveApp {
|
||||
viewport::show(self, ui);
|
||||
});
|
||||
dialogs::show(self, ui.ctx());
|
||||
sketch_palette::show(self, ui.ctx());
|
||||
params_dialog::show(self, ui.ctx());
|
||||
context_menu::show(self, ui.ctx());
|
||||
delete::show(self, ui.ctx());
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
//! The Sketch Palette: a panel docked at the right while a sketch is being edited, like a
|
||||
//! command dialog. Options: the line type of the selected curves (normal, construction,
|
||||
//! centerline), Look At, and the display toggles kept with the sketch (grid, snap, slice,
|
||||
//! 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`.
|
||||
|
||||
use std::cell::Cell;
|
||||
|
||||
use egui::{Color32, RichText, Stroke, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_engine::Sel;
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::theme::Tokens;
|
||||
|
||||
thread_local! {
|
||||
static FOLDED: Cell<bool> = const { Cell::new(false) };
|
||||
static OPTIONS_FOLDED: Cell<bool> = const { Cell::new(false) };
|
||||
}
|
||||
|
||||
/// Palette width (logical px).
|
||||
const WIDTH: f32 = 236.0;
|
||||
|
||||
/// The display toggles: option key and label.
|
||||
pub const TOGGLES: [(&str, &str); 9] = [
|
||||
("grid", "Sketch Grid"),
|
||||
("snap", "Snap"),
|
||||
("slice", "Slice"),
|
||||
("show_profile", "Show Profile"),
|
||||
("show_points", "Show Points"),
|
||||
("show_dimensions", "Show Dimensions"),
|
||||
("show_constraints", "Show Constraints"),
|
||||
("show_projected", "Show Projected Geometries"),
|
||||
("sketch_3d", "3D Sketch"),
|
||||
];
|
||||
|
||||
/// The degrees-of-freedom line: "Fully constrained" or "N degrees of freedom".
|
||||
pub fn dof_text(dof: usize, ok: bool) -> String {
|
||||
if !ok {
|
||||
"Constraints conflict".into()
|
||||
} else if dof == 0 {
|
||||
"Fully constrained".into()
|
||||
} else if dof == 1 {
|
||||
"1 degree of freedom".into()
|
||||
} else {
|
||||
format!("{dof} degrees of freedom")
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Set the line type of the selected curves: 0 normal, 1 construction, 2 centerline (lines).
|
||||
pub fn set_linetype(app: &mut SolveApp, kind: u8) {
|
||||
let curves = selected_curves(app);
|
||||
if curves.is_empty() {
|
||||
app.set_status("Select sketch curves first", false);
|
||||
return;
|
||||
}
|
||||
let lines: Vec<String> = {
|
||||
let st = app.session.model.state();
|
||||
let Some(ss) = app.session.active_sketch.and_then(|s| st.sketch(s)) else { return };
|
||||
curves
|
||||
.iter()
|
||||
.filter(|id| {
|
||||
ss.sketch
|
||||
.curve_index(id)
|
||||
.and_then(|i| ss.sketch.curves.get(i))
|
||||
.is_some_and(|c| matches!(c.kind, solvecraft_engine::sketch::CurveKind::Line { .. }))
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
};
|
||||
let r = match kind {
|
||||
0 => {
|
||||
if !lines.is_empty() {
|
||||
let _ = app.run("sketch.centerline", json!({ "curves": lines, "value": false }));
|
||||
}
|
||||
app.run("sketch.construction", json!({ "curves": curves, "value": false }))
|
||||
}
|
||||
1 => app.run("sketch.construction", json!({ "curves": curves, "value": true })),
|
||||
_ if lines.is_empty() => Err("a centerline must be a line".to_string()),
|
||||
_ => app.run("sketch.centerline", json!({ "curves": lines, "value": true })),
|
||||
};
|
||||
if let Err(e) = r {
|
||||
app.set_status(e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the palette (while sketching and no command dialog is open).
|
||||
pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
if app.dialog.is_some() {
|
||||
return;
|
||||
}
|
||||
let (opts, dof_line, ok) = {
|
||||
let st = app.session.model.state();
|
||||
let Some(ss) = app.session.active_sketch.and_then(|s| st.sketch(s)) else { return };
|
||||
(ss.sketch.view.clone(), dof_text(ss.report.dof, ss.report.ok()), ss.report.ok() && ss.report.dof == 0)
|
||||
};
|
||||
let t = Tokens::get();
|
||||
let vp = app.viewport.rect.unwrap_or_else(|| ctx.content_rect());
|
||||
let anchor = egui::pos2(vp.right(), vp.top() + crate::viewport::VIEW_CUBE_CLEARANCE);
|
||||
let frame = egui::Frame::window(&ctx.global_style())
|
||||
.fill(t.dialog_bg)
|
||||
.stroke(Stroke::new(1.0, t.dialog_border))
|
||||
.corner_radius(egui::CornerRadius { nw: 4, sw: 4, ne: 0, se: 0 })
|
||||
.shadow(egui::Shadow { offset: [-2, 2], blur: 8, spread: 0, color: Color32::from_black_alpha(40) });
|
||||
let shown = |k: &str| match k {
|
||||
"grid" => !opts.hide_grid,
|
||||
"snap" => !opts.no_snap,
|
||||
"slice" => opts.slice,
|
||||
"show_profile" => !opts.hide_profile,
|
||||
"show_points" => !opts.hide_points,
|
||||
"show_dimensions" => !opts.hide_dimensions,
|
||||
"show_constraints" => !opts.hide_constraints,
|
||||
"show_projected" => !opts.hide_projected,
|
||||
"sketch_3d" => opts.three_d,
|
||||
_ => false,
|
||||
};
|
||||
let mut set: Option<(String, bool)> = None;
|
||||
let mut linetype: Option<u8> = None;
|
||||
let mut look = false;
|
||||
let mut finish = false;
|
||||
let has_curves = !selected_curves(app).is_empty();
|
||||
egui::Window::new("SKETCH PALETTE")
|
||||
.id(egui::Id::new("sc_sketch_palette"))
|
||||
.title_bar(false)
|
||||
.frame(frame)
|
||||
.pivot(egui::Align2::RIGHT_TOP)
|
||||
.fixed_pos(anchor)
|
||||
.constrain(false)
|
||||
.auto_sized()
|
||||
.collapsible(false)
|
||||
.show(ctx, |ui| {
|
||||
ui.set_width(WIDTH);
|
||||
ui.horizontal(|ui| {
|
||||
let folded = FOLDED.with(Cell::get);
|
||||
if ui.add(egui::Button::new(RichText::new(if folded { "+" } else { "−" }).size(14.0)).frame(false)).clicked() {
|
||||
FOLDED.with(|f| f.set(!folded));
|
||||
}
|
||||
ui.label(RichText::new("SKETCH PALETTE").strong().size(12.5));
|
||||
});
|
||||
if FOLDED.with(Cell::get) {
|
||||
return;
|
||||
}
|
||||
let ofold = OPTIONS_FOLDED.with(Cell::get);
|
||||
if ui.add(egui::Button::new(RichText::new(format!("{} Options", if ofold { "+" } else { "−" })).size(12.0)).frame(false)).clicked() {
|
||||
OPTIONS_FOLDED.with(|f| f.set(!ofold));
|
||||
}
|
||||
if !ofold {
|
||||
egui::Grid::new("sc_palette_grid").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
|
||||
ui.label("Linetype");
|
||||
ui.horizontal(|ui| {
|
||||
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() {
|
||||
linetype = Some(i as u8);
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Look At");
|
||||
if ui.button("Look At").clicked() {
|
||||
look = true;
|
||||
}
|
||||
ui.end_row();
|
||||
for (k, label) in TOGGLES {
|
||||
ui.label(label);
|
||||
let mut on = shown(k);
|
||||
if ui.checkbox(&mut on, "").changed() {
|
||||
set = Some((k.to_string(), on));
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
ui.label(RichText::new(dof_line.as_str()).color(if ok { t.sketch_accent } else { t.text_dim }));
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(WIDTH - 100.0);
|
||||
if ui.add(egui::Button::new(RichText::new("Finish Sketch").color(Color32::WHITE)).fill(t.accent)).clicked() {
|
||||
finish = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
if let Some((k, on)) = set {
|
||||
let mut p = serde_json::Map::new();
|
||||
p.insert(k, Value::Bool(on));
|
||||
if let Err(e) = app.run("sketch.options", Value::Object(p)) {
|
||||
app.set_status(e, true);
|
||||
}
|
||||
}
|
||||
if let Some(k) = linetype {
|
||||
set_linetype(app, k);
|
||||
}
|
||||
if look {
|
||||
crate::dialogs::look_at_sketch(app);
|
||||
}
|
||||
if finish {
|
||||
app.finish_sketch();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn dof_readout() {
|
||||
assert_eq!(dof_text(0, true), "Fully constrained");
|
||||
assert_eq!(dof_text(1, true), "1 degree of freedom");
|
||||
assert_eq!(dof_text(4, true), "4 degrees of freedom");
|
||||
assert_eq!(dof_text(4, false), "Constraints conflict");
|
||||
}
|
||||
}
|
||||
@@ -710,6 +710,10 @@ fn glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
let Some(sid) = active_sketch(app) else { return };
|
||||
let st = app.session.model.state();
|
||||
let Some(ss) = st.sketch(sid) else { return };
|
||||
if ss.sketch.view.hide_constraints {
|
||||
GLYPHS.with(|g| g.borrow_mut().clear());
|
||||
return;
|
||||
}
|
||||
let tk = crate::theme::Tokens::get();
|
||||
let mut placed: Vec<Pos2> = Vec::new();
|
||||
let picked = PICKED.with(|p| p.borrow().clone());
|
||||
|
||||
@@ -195,7 +195,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
let s = &app.session;
|
||||
// Bodies where their occurrences place them.
|
||||
let st = s.world_state();
|
||||
if app.ui.show_grid {
|
||||
if app.ui.show_grid && !active_view(app).hide_grid {
|
||||
let (minor, major) = grid_step(app.cam.half_height());
|
||||
let ext = (app.cam.half_height() * 3.0 / major).ceil() * major;
|
||||
let c = Vec3::new((app.cam.target.x / major).round() * major, (app.cam.target.y / major).round() * major, 0.0);
|
||||
@@ -250,7 +250,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
let active = s.active_sketch == Some(sid);
|
||||
// Closed profiles are shaded (lifted off a face they may lie on).
|
||||
let lift = ss.plane.normal() * 0.01;
|
||||
let profiles = if app.ui.hidden_profiles.contains(&sid) { &[][..] } else { &ss.profiles[..] };
|
||||
let profiles = if app.ui.hidden_profiles.contains(&sid) || ss.sketch.view.hide_profile { &[][..] } else { &ss.profiles[..] };
|
||||
for p in profiles {
|
||||
for tri in p.region.triangulate(0.05) {
|
||||
for q in tri {
|
||||
@@ -469,6 +469,9 @@ pub fn pick(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
|
||||
let Some(ss) = st.sketch(sid) else { continue };
|
||||
let bonus = if s.active_sketch == Some(sid) { 2.0 } else { 0.0 };
|
||||
for (i, c) in ss.sketch.curves.iter().enumerate() {
|
||||
if c.link.is_some() && ss.sketch.view.hide_projected {
|
||||
continue;
|
||||
}
|
||||
let straight = matches!(c.kind, CurveKind::Line { .. });
|
||||
for seg in ss.sketch.segs(i) {
|
||||
let pts: Vec<Pos2> = seg.polyline(0.05).iter().filter_map(|q| proj.to_screen(ss.plane.to_world(*q))).collect();
|
||||
@@ -578,7 +581,9 @@ pub fn pick(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
|
||||
continue;
|
||||
}
|
||||
let lp = ss.plane.to_local(o + d * t);
|
||||
for (pi, p) in ss.profiles.iter().enumerate() {
|
||||
// Show Profile off: the sketch being edited offers no profiles.
|
||||
let profiles = if ss.sketch.view.hide_profile && s.active_sketch == Some(sid) { &[][..] } else { &ss.profiles[..] };
|
||||
for (pi, p) in profiles.iter().enumerate() {
|
||||
if p.region.contains(lp) && bestp.as_ref().is_none_or(|(_, a, _)| p.area < *a) {
|
||||
bestp = Some((t, p.area, Hit::Profile { sketch: sid, index: pi }));
|
||||
}
|
||||
@@ -606,6 +611,12 @@ pub fn pick(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
|
||||
}
|
||||
|
||||
/// The section cut shown now: the open Section Analysis dialog's plane, else the session's.
|
||||
/// The active sketch's display options (defaults when not sketching).
|
||||
pub fn active_view(app: &SolveApp) -> solvecraft_engine::sketch::SketchView {
|
||||
let st = app.session.model.state();
|
||||
app.session.active_sketch.and_then(|s| st.sketch(s)).map(|ss| ss.sketch.view.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn section_plane(app: &SolveApp) -> Option<(Vec3, Vec3)> {
|
||||
if let Some(d) = app.dialog.as_ref().filter(|d| matches!(d.kind, crate::dialogs::Kind::Section { .. })) {
|
||||
let cmds = crate::dialogs::apply_commands(app, d).ok()?;
|
||||
@@ -614,6 +625,13 @@ pub fn section_plane(app: &SolveApp) -> Option<(Vec3, Vec3)> {
|
||||
scratch.execute("FusionHalfSectionViewCommand", &p).ok()?;
|
||||
return scratch.section;
|
||||
}
|
||||
// Slice (Sketch Palette): cut the model at the plane of the sketch being edited.
|
||||
if app.session.section.is_none() {
|
||||
let st = app.session.model.state();
|
||||
if let Some(ss) = app.session.active_sketch.and_then(|s| st.sketch(s)).filter(|ss| ss.sketch.view.slice) {
|
||||
return Some((ss.plane.origin, ss.plane.normal()));
|
||||
}
|
||||
}
|
||||
app.session.section
|
||||
}
|
||||
|
||||
@@ -958,6 +976,10 @@ pub fn sketch_point_at(app: &SolveApp, proj: &Proj, pos: Pos2) -> Option<(Vec2,
|
||||
if let Some(p) = crate::sketch_tools::refine_snap(app, proj, &ss.sketch, &ss.plane, lp) {
|
||||
return Some(p);
|
||||
}
|
||||
// With Snap off (Sketch Palette) the point is where the cursor is.
|
||||
if ss.sketch.view.no_snap {
|
||||
return Some((lp, None));
|
||||
}
|
||||
Some((Vec2::new((lp.x / step).round() * step, (lp.y / step).round() * step), None))
|
||||
}
|
||||
|
||||
@@ -1401,7 +1423,8 @@ fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
if !solvecraft_engine::view::sketch_point_visible(sk, i) {
|
||||
continue;
|
||||
}
|
||||
let Some(sp) = proj.to_screen(ss.plane.to_world(p.pos)) else { continue };
|
||||
let hidden = (sk.view.hide_points && i != 0) || (sk.view.hide_projected && p.link.is_some());
|
||||
let Some(sp) = proj.to_screen(ss.plane.to_world(p.pos)).filter(|_| !hidden) else { continue };
|
||||
let det = ss.report.point_determined.get(i).copied().unwrap_or(false);
|
||||
let c = if p.link.is_none() && locked.get(i).copied().unwrap_or(false) {
|
||||
t.sketch_locked
|
||||
@@ -1416,7 +1439,7 @@ fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
painter.rect_filled(Rect::from_center_size(sp, vec2(5.0, 5.0)), 0.0, c);
|
||||
}
|
||||
}
|
||||
for c in sk.constraints.iter().filter(|c| !c.kind.is_dimension()) {
|
||||
for c in sk.constraints.iter().filter(|c| !c.kind.is_dimension() && !sk.view.hide_constraints) {
|
||||
glyph(painter, proj, ss, &c.kind);
|
||||
}
|
||||
// Sketch status.
|
||||
|
||||
Reference in new issue
Block a user