Revert #1682 (three-axis milling) (#1715)

Thanks @bherbruck.
This commit is contained in:
bherbruck authored and GitHub committed 2026-10-16 09:02:19 -04:00
1 parent 1ded8fdd27
commit 2c020dbcbc
85 files changed
+43 -14487

No files matched your search

+1 -14
View File
@@ -253,17 +253,6 @@ fn groups(t: &Tokens) -> Vec<Group> {
let ic = &t.icons;
let a = &t.analysis;
let mut out = vec![
group(
"manufacturing",
&[
("cam_stock", t.cam_stock),
("cam_fixture", t.cam_fixture),
("cam_cut", t.cam_cut),
("cam_rapid", t.cam_rapid),
("cam_tool", t.cam_tool),
],
&[("viewport_top", t.viewport_top), ("viewport_bottom", t.viewport_bottom)],
),
group(
"sketch lines",
&[
@@ -622,9 +611,7 @@ fn changed_fields(a: &Tokens, b: &Tokens) -> BTreeSet<String> {
#[test]
fn each_mode_changes_only_its_table() {
let set = |s: &[&str]| {
s.iter().copied().chain(["cam_stock", "cam_fixture", "cam_cut", "cam_rapid", "cam_tool"]).map(str::to_string).collect::<BTreeSet<_>>()
};
let set = |s: &[&str]| s.iter().map(|x| x.to_string()).collect::<BTreeSet<_>>();
let red_green_both = [
"triad",
"gizmo",
+7 -22
View File
@@ -342,23 +342,13 @@ pub fn items(app: &SolveApp, target: &Target) -> Vec<Item> {
Target::Workspace => {
let rendering = crate::render_workspace::active(app);
let mut v = vec![
act("ui.workspace.design", "Design", "box").with(json!({"workspace":"design"})).key(if rendering || app.ui.tab == "MILLING" {
""
} else {
"current"
}),
act("ui.workspace.design", "Design", "box").with(json!({"workspace":"design"})).key(if rendering { "" } else { "current" }),
act("ui.workspace.render", "Render", "camera")
.with(json!({"workspace":"render"}))
.key(if rendering { "current" } else { "" })
.on(app.session.active_sketch.is_none() && app.tool.is_none() && app.dialog.is_none() && !app.file_load.busy()),
];
v.push(
act("ui.workspace.manufacture", "Manufacture", "box")
.with(json!({"workspace":"manufacture"}))
.key(if app.ui.tab == "MILLING" { "current" } else { "" })
.on(app.session.active_sketch.is_none() && app.tool.is_none() && app.dialog.is_none() && !app.file_load.busy()),
);
for w in ["Animation", "Simulation", "Drawing"] {
for w in ["Animation", "Simulation", "Manufacture", "Drawing"] {
v.push(act("ui.workspace", w, "").key("coming later").on(false));
}
v
@@ -1052,16 +1042,11 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) {
"ui.toolbarPin" | "ui.toolbarRemove" | "ui.toolbarReset" | "ui.toolbarCustomize" | "ui.shortcutPin" => {
crate::toolbar_custom::run(app, &item.id, &p)
}
"ui.workspace" | "ui.workspace.design" | "ui.workspace.render" | "ui.workspace.manufacture" => {
if let Some(workspace) = p.get("workspace").and_then(Value::as_str) {
if let Err(e) = crate::render_workspace::enter(app, workspace == "render") {
app.set_status(e, true);
} else {
crate::manufacturing::cancel_editor(app);
if workspace != "render" {
app.ui.tab = if workspace == "manufacture" { "MILLING" } else { "SOLID" }.into();
}
}
"ui.workspace" | "ui.workspace.design" | "ui.workspace.render" => {
if let Some(workspace) = p.get("workspace").and_then(Value::as_str)
&& let Err(e) = crate::render_workspace::enter(app, workspace == "render")
{
app.set_status(e, true);
}
}
"ui.findJoint" => {
+5 -26
View File
@@ -176,17 +176,14 @@ pub fn request_scope(app: &SolveApp, params: &Value) -> Result<Option<vfs::PathS
/// mode, the section the viewport shows: an open Section Analysis dialog's preview, a sketch's
/// Slice), with the grid and the origin's axes as `guides` says.
pub(crate) fn view_scene(app: &SolveApp, guides: solidcraft_engine::view::Guides) -> solidcraft_engine::render::Scene {
let mut scene = solidcraft_engine::view::scene_with_palette_and_guides(
solidcraft_engine::view::scene_with_palette_and_guides(
&app.session,
&app.cam,
crate::viewport::section_plane(app),
solidcraft_engine::view::world_curve_parts,
&crate::viewport::scene_palette(app.ui.dark, app.preferences.color_vision),
guides,
);
crate::manufacturing::view::cpu(app, &mut scene);
crate::manufacturing::view::cpu_overlays(app, &mut scene);
scene
)
}
/// The grid and the origin's axes as the window shows them (the Grid and Origin switches).
@@ -227,20 +224,14 @@ fn dispatch(app: &mut SolveApp, ctx: &egui::Context, method: &str, p: &Value) ->
match method {
"ui.workspace" => {
if let Some(workspace) = s("workspace") {
if !matches!(workspace, "design" | "render" | "manufacture") {
return err("workspace must be design, render or manufacture");
if !matches!(workspace, "design" | "render") {
return err("workspace must be design or render");
}
if let Err(e) = crate::render_workspace::enter(app, workspace == "render") {
return err(e);
}
crate::manufacturing::cancel_editor(app);
if workspace != "render" {
app.ui.tab = if workspace == "manufacture" { "MILLING" } else { "SOLID" }.into();
}
}
ok(
json!({"workspace":if crate::render_workspace::active(app) {"render"} else if app.ui.tab == "MILLING" {"manufacture"} else {"design"}}),
)
ok(json!({"workspace":if crate::render_workspace::active(app) {"render"} else {"design"}}))
}
"engine.execute" | "command" => {
let Some(id) = s("command").or(s("id")) else { return err("missing `command`") };
@@ -276,18 +267,6 @@ fn dispatch(app: &mut SolveApp, ctx: &egui::Context, method: &str, p: &Value) ->
"viewport": app.viewport.rect.map(|r| json!([r.left(), r.top(), r.width(), r.height()])),
"camera": app.cam,
"tool": app.tool.as_ref().map(|t| t.cmd),
"manufacturing": {
"program": app.manufacturing.program,
"job": app.manufacturing.job,
"completed_motions": app.manufacturing.playback,
"finding": app.manufacturing.finding,
"playing": app.manufacturing.playing,
"workpiece": format!("{:?}", app.manufacturing.view.workpiece),
"fixtures": app.manufacturing.view.fixtures,
"paths": app.manufacturing.view.paths,
"stock_boundary": app.manufacturing.view.boundary,
"events_per_second": app.manufacturing.view.speed,
},
"dialog": app.dialog.as_ref().map(|d| format!("{d:?}")),
"status": app.status.as_ref().map(|(text, _, error)| json!({"text": text, "error": error})),
"hover": app.viewport.hover.as_ref().map(|h| format!("{h:?}")),
-1
View File
@@ -3130,7 +3130,6 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
/// Close the dialog without applying it (Esc, Cancel). An edit puts the timeline marker back.
pub fn cancel(app: &mut SolveApp) {
crate::manufacturing::cancel_editor(app);
app.applying = None;
if let Some(mut d) = app.dialog.take() {
crate::dialogs_motion::closed(app, &mut d, false);
-3
View File
@@ -92,7 +92,6 @@ fn swap_in(app: &mut SolveApp, slot: DocSlot) -> DocSlot {
// Whatever was going on in the design leaves with it.
crate::sketch_text_move::cancel_drag(app);
crate::dialogs::cancel(app);
app.manufacturing = Default::default();
crate::dim_view::cancel();
app.tool = None;
app.params = None;
@@ -167,7 +166,6 @@ pub fn adopt(app: &mut SolveApp, session: Session) {
if active_is_blank(app) {
let rev = app.session.revision;
crate::dialogs::cancel(app);
app.manufacturing = Default::default();
app.tool = None;
app.menu.props = None;
crate::sketch_attachment_ui::clear(app);
@@ -253,7 +251,6 @@ pub fn close(app: &mut SolveApp, i: usize, force: bool) {
crate::sketch_text_move::cancel_drag(app);
let rev = app.session.revision;
crate::dialogs::cancel(app);
app.manufacturing = Default::default();
app.tool = None;
app.session.replace_design(Session::default());
app.session.revision = rev + 1;
-108
View File
@@ -1,108 +0,0 @@
//! Native file actions must share the toolbar flow while JSON calls remain dialog-free.
#![allow(clippy::unwrap_used)]
use crate::scenario::Harness;
use serde_json::json;
use std::{cell::Cell, rc::Rc};
fn manufacturing(h: &mut Harness) {
let mut input = solidcraft_engine::cam::corpus::fixture("face").unwrap();
h.app.run("solid.box", json!({"length":20,"width":20,"height":5})).unwrap();
input.setup.models[0].body = h.app.session.model.state().bodies[0].name.clone();
h.app.run("cam.setup.put", json!({"setup":input.setup})).unwrap();
h.app.run("cam.tool.put", json!({"tool":input.tools[0]})).unwrap();
h.app.run("cam.operation.put", json!({"operation":input.operations[0]})).unwrap();
h.app.run("cam.program.put", json!({"program":{"id":1,"name":"Facing","setup":1,"operations":[1]}})).unwrap();
}
#[test]
fn interactive_save_and_save_as_preserve_cam_definitions_and_cancel_cleanly() {
let mut h = Harness::new();
manufacturing(&mut h);
let before = serde_json::to_value(&h.app.session.doc.manufacturing).unwrap();
let first = h.dir.join("first.solidcraft");
let second = h.dir.join("second.solidcraft");
let picked = first.clone();
h.app.services.pick_save = Some(Box::new(move |_, _| {
let path = picked.to_string_lossy().into_owned();
Box::pin(async move { Some(path) })
}));
// Programmatic commands must never invoke the picker.
assert!(h.app.run("file.save", json!({})).is_err());
assert!(!first.exists());
h.app.start("file.save");
h.frames(2);
assert!(first.exists());
assert!(!h.app.session.is_dirty());
h.app.run("solid.box", json!({"length":1,"width":1,"height":1})).unwrap();
h.app.services.pick_save = Some(Box::new(|_, _| Box::pin(async { None })));
h.app.start("file.save_as");
h.frames(2);
assert!(h.app.session.is_dirty());
assert_eq!(h.app.session.path.as_deref(), first.to_str());
let picked = second.clone();
h.app.services.pick_save = Some(Box::new(move |_, _| {
let path = picked.to_string_lossy().into_owned();
Box::pin(async move { Some(path) })
}));
h.app.start("file.save_as");
h.frames(2);
let reopened = solidcraft_engine::io::read_design(&std::fs::read(&second).unwrap()).unwrap();
assert_eq!(serde_json::to_value(&reopened.manufacturing).unwrap(), before);
assert_eq!(reopened.features.len(), 2);
assert!(!h.app.session.is_dirty());
}
#[test]
fn quick_save_uses_key_event_modifiers_after_same_frame_release() {
for modifiers in [egui::Modifiers { command: true, mac_cmd: true, ..Default::default() }, egui::Modifiers::CTRL] {
let mut h = Harness::new();
h.app.home.open = false;
let calls = Rc::new(Cell::new(0));
let count = calls.clone();
h.app.services.pick_save = Some(Box::new(move |_, _| {
count.set(count.get() + 1);
Box::pin(async { None })
}));
let modifiers = egui::Modifiers { command: true, ..modifiers };
let raw = egui::RawInput {
focused: true,
events: vec![
egui::Event::ModifiersChanged(modifiers),
egui::Event::Key { key: egui::Key::S, physical_key: None, pressed: true, repeat: false, modifiers },
egui::Event::ModifiersChanged(egui::Modifiers::default()),
],
..Default::default()
};
let _ = h.ctx.run_ui(raw, |ui| crate::toolbar::shortcuts(&mut h.app, ui.ctx()));
assert_eq!(calls.get(), 1, "the Save picker must open, rather than plain-S command search");
}
}
#[test]
fn interactive_open_uses_background_loader_and_keeps_current_design_on_cancel() {
let mut h = Harness::new();
manufacturing(&mut h);
let path = h.dir.join("cam.solidcraft");
h.app.run("file.save", json!({"path":path})).unwrap();
let before = serde_json::to_value(&h.app.session.doc.manufacturing).unwrap();
h.app.services.pick_open = Some(Box::new(|| Box::pin(async { None })));
h.app.start("doc.open");
h.frames(2);
assert_eq!(serde_json::to_value(&h.app.session.doc.manufacturing).unwrap(), before);
crate::documents::new_design(&mut h.app);
let picked = path.clone();
h.app.services.pick_open = Some(Box::new(move || {
let path = picked.to_string_lossy().into_owned();
Box::pin(async move { Some(path) })
}));
h.app.start("doc.open");
h.frames(2);
let started = std::time::Instant::now();
while h.app.file_picker_busy() || h.app.file_load.busy() {
h.frames(1);
assert!(started.elapsed().as_secs() < 30);
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert_eq!(serde_json::to_value(&h.app.session.doc.manufacturing).unwrap(), before);
assert_eq!(h.app.session.path.as_deref(), path.to_str());
}
+1 -37
View File
@@ -65,7 +65,6 @@ pub mod install;
pub mod issue_report;
pub mod joint_drag;
pub mod keymap;
pub mod manufacturing;
mod material_card_review;
#[cfg(test)]
mod measurement_ui_tests;
@@ -108,8 +107,6 @@ pub(crate) mod display_lod;
#[cfg(test)]
mod file_load_ui_tests;
#[cfg(test)]
mod file_ui_tests;
#[cfg(test)]
mod fillet_spline_handle_tests;
pub(crate) mod navigation;
pub mod palette;
@@ -274,7 +271,6 @@ pub struct Services {
}
pub struct SolveApp {
pub manufacturing: manufacturing::State,
pub session: Session,
pub ui: UiState,
pub cam: Camera,
@@ -424,7 +420,6 @@ impl SolveApp {
file_picker: None,
viewport: viewport::ViewportState::default(),
render_workspace,
manufacturing: Default::default(),
tool: None,
dialog: None,
params: None,
@@ -714,31 +709,6 @@ impl SolveApp {
self.set_status(format!("{label}: {why}"), true);
return;
}
match id {
"file.save" => {
self.home.open = false;
return toolbar::save(self);
}
"file.save_as" => {
self.home.open = false;
return toolbar::save_as(self);
}
"doc.open" => {
self.pick_open(|app, path| {
if let Some(path) = path {
app.open_path_async(&path);
}
});
return;
}
_ => {}
}
manufacturing::cancel_editor(self);
if id.starts_with("cam.") {
self.home.open = false;
manufacturing::start(self, id);
return;
}
if id == "material.properties.validate" {
self.material_card_review = Some(material_card_review::ReviewWindow::default());
self.home.open = false;
@@ -1251,9 +1221,6 @@ impl SolveApp {
/// Per-frame logic before layout.
pub fn logic(&mut self, ctx: &egui::Context) {
manufacturing::update_exports(self, ctx);
#[cfg(target_arch = "wasm32")]
manufacturing::advance(self, ctx);
file_picker::tick(self, ctx);
render_workspace::sync_backend(self);
render_workspace::tick(self, ctx);
@@ -1391,9 +1358,7 @@ impl SolveApp {
timeline::timeline(self, ui);
}
palette::palette_bar(self, ui);
if self.ui.tab == "MILLING" {
manufacturing::panel(self, ui);
} else if self.ui.show_browser {
if self.ui.show_browser {
if left_timeline {
browser::browser_below_timeline(self, ui);
} else {
@@ -1415,7 +1380,6 @@ impl SolveApp {
}
});
dialogs::show(self, ui.ctx());
manufacturing::editor(self, ui.ctx());
issue_report::CadInput::block(self, ui, report_input);
sketch_palette::show(self, ui.ctx());
self.resolve_fit(ui.ctx());
File diff suppressed because it is too large. Load diff
@@ -1,96 +0,0 @@
//! Clock-independent cooperative scheduling; the web host supplies its deadline.
/// Poll a round-robin sequence until all jobs settle or the host budget expires.
/// `poll` returns true for pending work. The count cap also bounds a stalled clock.
pub(super) fn tick(cursor: &mut usize, count: usize, mut poll: impl FnMut(usize) -> bool, mut has_time: impl FnMut() -> bool) -> bool {
if count == 0 {
*cursor = 0;
return false;
}
let mut idle = 0;
for _ in 0..65_536 {
if !has_time() {
return true;
}
let index = *cursor % count;
*cursor = (index + 1) % count;
if poll(index) {
idle = 0;
} else {
idle += 1;
if idle == count {
return false;
}
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn short_slices_do_not_starve_jobs_and_settled_jobs_stop_repainting() {
let mut cursor = 0;
let mut remaining = [7_usize, 1, 5];
let mut order = Vec::new();
for _ in 0..30 {
let mut budget = 1;
let pending = tick(
&mut cursor,
remaining.len(),
|i| {
order.push(i);
remaining[i] = remaining[i].saturating_sub(1usize);
remaining[i] > 0
},
|| {
let more = budget > 0;
budget -= usize::from(more);
more
},
);
assert!(pending);
}
assert!(order.as_chunks::<3>().0.iter().all(|chunk| *chunk == [0, 1, 2]));
assert_eq!(remaining, [0; 3]);
assert!(!tick(&mut cursor, 3, |_| false, || true));
// Removing completed jobs must tolerate a cursor past the new length.
cursor = 12;
assert!(!tick(
&mut cursor,
1,
|i| {
assert_eq!(i, 0);
false
},
|| true
));
assert!(!tick(&mut cursor, 0, |_| true, || true));
}
#[test]
fn clock_stall_and_empty_budget_are_bounded() {
let mut cursor = 0;
let mut polls = 0;
assert!(tick(
&mut cursor,
2,
|_| {
polls += 1;
true
},
|| false
));
assert_eq!(polls, 0);
assert!(tick(
&mut cursor,
2,
|_| {
polls += 1;
true
},
|| true
));
assert_eq!(polls, 65_536);
}
}
-385
View File
@@ -1,385 +0,0 @@
//! Manufacture display choices. Geometry comes from immutable engine jobs;
//! this module changes neither the model nor the verification result.
use super::*;
use crate::theme::Tokens;
use solidcraft_engine::{
geom::{Aabb3, Mesh},
render::{Rgb, Scene, SceneMesh},
};
use std::{
collections::BTreeSet,
hash::{Hash, Hasher},
sync::Arc,
};
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash)]
pub enum Workpiece {
#[default]
Model,
InitialStock,
RemainingStock,
}
pub struct View {
pub workpiece: Workpiece,
pub fixtures: bool,
pub paths: bool,
pub boundary: bool,
pub speed: f64,
pub loaded_job: Option<u64>,
pub fraction: f64,
}
impl Default for View {
fn default() -> Self {
Self { workpiece: Workpiece::Model, fixtures: true, paths: true, boundary: true, speed: 60., loaded_job: None, fraction: 0. }
}
}
pub fn setup(app: &SolveApp) -> Option<&cam::Setup> {
if let Some(Editor::Setup(setup)) = &app.manufacturing.editor {
return Some(setup);
}
let m = &app.session.doc.manufacturing;
let program = m.programs.iter().find(|p| p.id == app.manufacturing.program).or_else(|| m.programs.first());
program.and_then(|p| m.setups.iter().find(|s| s.id == p.setup)).or_else(|| m.setups.first())
}
pub fn corners(stock: cam::Stock) -> [Vec3; 8] {
let a = stock.min;
let b = stock.max;
[
Vec3::new(a.x, a.y, a.z),
Vec3::new(b.x, a.y, a.z),
Vec3::new(b.x, b.y, a.z),
Vec3::new(a.x, b.y, a.z),
Vec3::new(a.x, a.y, b.z),
Vec3::new(b.x, a.y, b.z),
Vec3::new(b.x, b.y, b.z),
Vec3::new(a.x, b.y, b.z),
]
}
pub fn bounds(app: &SolveApp) -> Aabb3 {
let mut bounds = Aabb3::EMPTY;
if app.ui.tab != "MILLING" {
return bounds;
}
if let Some(setup) = setup(app).filter(|s| s.validate().is_ok()) {
for p in corners(setup.stock) {
bounds.add(setup.frame.world(p));
}
for p in corners(cam::Stock { min: setup.stock.min, max: Vec3::new(setup.stock.max.x, setup.stock.max.y, setup.clearance) }) {
bounds.add(setup.frame.world(p));
}
}
bounds
}
pub fn fresh_job(app: &SolveApp) -> Option<Arc<solidcraft_engine::manufacturing::Job>> {
let job = solidcraft_engine::manufacturing::job(&app.session, app.manufacturing.job?).ok()?;
solidcraft_engine::manufacturing::current(&app.session, &job).ok()?;
Some(job)
}
pub fn stock(app: &SolveApp) -> Option<Arc<Mesh>> {
if app.ui.tab != "MILLING" || matches!(app.manufacturing.editor, Some(Editor::Setup(_))) {
return None;
}
match app.manufacturing.view.workpiece {
Workpiece::Model => None,
Workpiece::RemainingStock => fresh_job(app)?.stock_mesh().ok().flatten(),
Workpiece::InitialStock => {
let setup = setup(app).filter(|s| s.validate().is_ok())?;
let points = corners(setup.stock).map(|p| setup.frame.world(p));
let mut mesh = Mesh::default();
for face in [[0, 3, 2, 1], [4, 5, 6, 7], [0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7]] {
for indices in [[face[0], face[1], face[2]], [face[0], face[2], face[3]]] {
let t = indices.map(|i| points[i]);
let n = (t[1] - t[0]).cross(t[2] - t[0]).normalized().unwrap_or(Vec3::Z);
let base = mesh.positions.len() as u32;
mesh.positions.extend(t);
mesh.normals.extend([n; 3]);
mesh.triangles.push([base, base + 1, base + 2]);
}
}
Some(Arc::new(mesh))
}
}
}
fn names(app: &SolveApp, refs: &[cam::BodyRef]) -> BTreeSet<String> {
refs.iter()
.filter_map(|r| {
if r.occurrence.is_empty() {
Some(r.body.clone())
} else {
app.session.occurrence_placement(&r.occurrence).ok()?.bodies.into_iter().find(|b| b.source == r.body).map(|b| b.world)
}
})
.collect()
}
pub struct Bodies {
models: BTreeSet<String>,
fixtures: BTreeSet<String>,
active: bool,
show_model: bool,
show_fixtures: bool,
}
impl Bodies {
pub fn new(app: &SolveApp) -> Self {
let setup = setup(app).filter(|_| app.ui.tab == "MILLING");
Self {
models: setup.map(|s| names(app, &s.models)).unwrap_or_default(),
fixtures: setup.map(|s| names(app, &s.fixtures)).unwrap_or_default(),
active: setup.is_some() && !matches!(app.manufacturing.editor, Some(Editor::Setup(_))),
show_model: stock(app).is_none(),
show_fixtures: app.manufacturing.view.fixtures,
}
}
pub fn visible(&self, name: &str) -> bool {
!self.active || (self.fixtures.contains(name) && self.show_fixtures) || (self.models.contains(name) && self.show_model)
}
pub fn color(&self, name: &str) -> Option<Color32> {
(self.active && self.fixtures.contains(name)).then_some(Tokens::get().cam_fixture)
}
}
pub fn key(app: &SolveApp, h: &mut impl Hasher) {
let active = app.ui.tab == "MILLING";
active.hash(h);
if !active {
return;
}
matches!(app.manufacturing.editor, Some(Editor::Setup(_))).hash(h);
if let Some(Editor::Setup(setup)) = &app.manufacturing.editor {
for r in &setup.fixtures {
r.body.hash(h);
r.occurrence.hash(h);
}
}
app.manufacturing.program.hash(h);
app.manufacturing.job.hash(h);
app.manufacturing.view.workpiece.hash(h);
app.manufacturing.view.fixtures.hash(h);
fresh_job(app).and_then(|j| j.stock_mesh().ok().flatten()).map(|m| Arc::as_ptr(&m) as usize).hash(h);
}
pub fn gpu(app: &SolveApp, scene: &mut crate::gpu::GpuScene) {
let Some(mesh) = stock(app) else { return };
let color = Tokens::get().cam_stock.to_array();
for indices in &mesh.triangles {
for i in indices {
if let (Some(p), Some(n)) = (mesh.positions.get(*i as usize), mesh.normals.get(*i as usize)) {
scene.tri(p.to_f32(), n.to_f32(), color);
}
}
}
}
pub fn cpu(app: &SolveApp, scene: &mut Scene) {
if app.ui.tab != "MILLING" {
return;
}
// Replace the model mesh list with the selected machining bodies, preserving
// occurrence meshes and applying the same visibility/color choices as GPU rendering.
let bodies = Bodies::new(app);
let world = app.session.world_state();
scene.meshes.clear();
for body in &world.bodies {
if !app.session.body_visible(&body.name) || !bodies.visible(&body.name) {
continue;
}
let c = bodies.color(&body.name).unwrap_or(Tokens::get().body);
scene.meshes.push(SceneMesh { mesh: body.mesh(), color: Rgb(c.r(), c.g(), c.b()), face_colors: vec![] });
}
// CAD edge lines would reveal hidden target geometry inside the stock preview.
// CAM outlines and paths are drawn by the shared overlay afterwards.
scene.lines.clear();
if let Some(mesh) = stock(app) {
let c = Tokens::get().cam_stock;
scene.meshes.push(SceneMesh { mesh, color: Rgb(c.r(), c.g(), c.b()), face_colors: vec![] });
}
scene.radius = scene.radius.max(bounds(app).diagonal());
}
/// Complete motion blocks. Zero is the initial pose, never a sentinel for all.
pub fn advance(state: &mut State, count: usize, dt: f64) {
if state.view.loaded_job != state.job {
state.view.loaded_job = state.job;
state.playback = count;
state.playing = false;
state.view.fraction = 0.;
}
state.playback = state.playback.min(count);
if !state.playing {
return;
}
state.view.fraction += dt.clamp(0., 0.1) * state.view.speed.clamp(1., 1000.);
let steps = state.view.fraction.floor() as usize;
state.view.fraction -= steps as f64;
state.playback = state.playback.saturating_add(steps).min(count);
if state.playback == count {
state.playing = false;
state.view.fraction = 0.;
}
}
/// Draw the same motion prefix for native overlays and CPU snapshots.
pub fn segments(path: &cam::Toolpath, setup: &cam::Setup, completed: usize, mut line: impl FnMut(Vec3, Vec3, bool)) {
let mut pos = Vec3::new(0., 0., setup.clearance);
for block in path.blocks.iter().take(completed) {
match block.motion {
cam::Motion::Rapid { to } | cam::Motion::Linear { to, .. } => {
line(setup.frame.world(pos), setup.frame.world(to), matches!(block.motion, cam::Motion::Rapid { .. }));
pos = to;
}
cam::Motion::Arc { to, center, clockwise, .. } => {
let start = (pos.y - center.y).atan2(pos.x - center.x);
let end = (to.y - center.y).atan2(to.x - center.x);
let delta = (end - start).rem_euclid(std::f64::consts::TAU);
let sweep = if clockwise {
if delta < 1e-12 { -std::f64::consts::TAU } else { delta - std::f64::consts::TAU }
} else if delta < 1e-12 {
std::f64::consts::TAU
} else {
delta
};
let radius = (pos.x - center.x).hypot(pos.y - center.y);
let angle = (2. * (1. - 0.02 / radius).clamp(-1., 1.).acos()).max(0.001);
let n = (sweep.abs() / angle).ceil().clamp(1., 512.) as usize;
let mut previous = pos;
for i in 1..=n {
let theta = start + sweep * i as f64 / n as f64;
let p = if i == n { to } else { Vec3::new(center.x + radius * theta.cos(), center.y + radius * theta.sin(), to.z) };
line(setup.frame.world(previous), setup.frame.world(p), false);
previous = p;
}
pos = to;
}
_ => {}
}
}
}
pub fn severity_color(severity: cam::verify::FindingSeverity) -> Color32 {
match severity {
cam::verify::FindingSeverity::Error => Tokens::get().error,
cam::verify::FindingSeverity::Unresolved => Tokens::get().warning,
cam::verify::FindingSeverity::Information => Tokens::get().cam_stock,
}
}
pub fn finding_marker(app: &SolveApp) -> Option<(Vec3, Color32)> {
let index = app.manufacturing.finding?;
let job = fresh_job(app)?;
let JobState::Verified(_, report) = job.state().ok()? else { return None };
let finding = report.findings.get(index)?;
Some((job.input().ok()?.setup.frame.world(finding.at), severity_color(finding.severity)))
}
pub fn cpu_overlays(app: &SolveApp, scene: &mut Scene) {
if app.ui.tab != "MILLING" {
return;
}
let tokens = Tokens::get();
let mut line = |a: Vec3, b: Vec3, c: Color32| {
scene.lines.push(solidcraft_engine::render::SceneLine { points: vec![a, b], color: Rgb(c.r(), c.g(), c.b()), width: 1.2, on_top: true })
};
if app.manufacturing.view.boundary
&& let Some(setup) = setup(app)
{
let points = corners(setup.stock).map(|p| setup.frame.world(p));
for (a, b) in [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)] {
line(points[a], points[b], tokens.cam_stock);
}
}
if let Some((point, color)) = finding_marker(app) {
let size = setup(app).map_or(1., |s| (s.stock.max - s.stock.min).len() / 100.);
for delta in [Vec3::X * size, Vec3::Y * size, Vec3::Z * size] {
line(point - delta, point + delta, color);
}
}
if !app.manufacturing.view.paths {
return;
}
let Some(job) = fresh_job(app) else { return };
let Ok(input) = job.input() else { return };
let path = match job.state() {
Ok(JobState::Generated(p) | JobState::Verified(p, _)) => p,
_ => return,
};
segments(&path, &input.setup, app.manufacturing.playback, |a, b, rapid| line(a, b, if rapid { tokens.cam_rapid } else { tokens.cam_cut }));
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn playback_zero_is_initial_and_rate_is_independent_of_frames() {
let mut s = State { job: Some(1), ..Default::default() };
advance(&mut s, 100, 0.);
assert_eq!(s.playback, 100);
s.playback = 0;
advance(&mut s, 100, 0.1);
assert_eq!(s.playback, 0);
for fps in [10, 30, 60] {
s.playback = 0;
s.playing = true;
s.view.fraction = 0.;
for _ in 0..fps {
advance(&mut s, 100, 1. / fps as f64);
}
assert_eq!(s.playback, 60);
}
s.job = Some(2);
advance(&mut s, 10, 0.1);
assert_eq!(s.playback, 10);
assert!(!s.playing);
}
#[test]
fn full_circle_playback_preserves_direction_and_empty_prefix() {
let input = cam::corpus::fixture("face").unwrap();
for clockwise in [true, false] {
let path = cam::Toolpath {
fingerprint: "test".into(),
blocks: vec![
cam::Block { operation: 1, motion: cam::Motion::Rapid { to: Vec3::new(1., 0., 0.) } },
cam::Block { operation: 1, motion: cam::Motion::Arc { to: Vec3::new(1., 0., 0.), center: Vec3::ZERO, clockwise, feed: 100. } },
],
};
let mut lines = Vec::new();
segments(&path, &input.setup, 0, |a, b, _| lines.push((a, b)));
assert!(lines.is_empty());
segments(&path, &input.setup, 2, |a, b, rapid| {
if !rapid {
lines.push((a, b))
}
});
assert!(lines.len() > 8);
assert_eq!(lines.last().unwrap().1, Vec3::new(1., 0., 0.));
assert_eq!(lines[0].1.y < 0., clockwise);
let length: f64 = lines.iter().map(|(a, b)| a.dist(*b)).sum();
assert!((length - std::f64::consts::TAU).abs() < 0.1);
}
}
#[test]
fn stock_display_uses_setup_frame_and_respects_fixture_visibility() {
let mut h = crate::scenario::Harness::new();
h.app.run("solid.box", json!({"length":20,"width":20,"height":5})).unwrap();
h.app.run("solid.box", json!({"length":3,"width":3,"height":3})).unwrap();
let names: Vec<_> = h.app.session.model.state().bodies.iter().map(|b| b.name.clone()).collect();
let mut input = cam::corpus::fixture("face").unwrap();
input.setup.models[0].body = names[0].clone();
input.setup.fixtures = vec![cam::BodyRef { body: names[1].clone(), occurrence: vec![] }];
input.setup.frame = cam::Frame { origin: Vec3::new(40., 30., 2.), x: Vec3::Y, y: -Vec3::X, z: Vec3::Z };
h.app.run("cam.setup.put", json!({"setup":input.setup})).unwrap();
h.app.ui.tab = "MILLING".into();
h.app.manufacturing.view.workpiece = Workpiece::InitialStock;
let mesh = stock(&h.app).unwrap();
let b = mesh.bounds();
assert_eq!(b.min, Vec3::new(20., 30., 2.));
assert_eq!(b.max, Vec3::new(40., 50., 8.));
assert!((mesh.measure().volume - 2400.).abs() < 1e-8);
let bodies = Bodies::new(&h.app);
assert!(!bodies.visible(&names[0]));
assert!(bodies.visible(&names[1]));
assert_eq!(bodies.color(&names[1]), Some(Tokens::get().cam_fixture));
h.app.manufacturing.view.fixtures = false;
assert!(!Bodies::new(&h.app).visible(&names[1]));
let mut scene = solidcraft_engine::view::scene(&h.app.session, &h.app.cam);
cpu(&h.app, &mut scene);
assert_eq!(scene.meshes.len(), 1);
assert_eq!(scene.meshes[0].mesh.bounds().min, b.min);
h.app.manufacturing.editor = Some(Editor::Setup(input.setup));
assert!(Bodies::new(&h.app).visible(&names[1]));
assert!(stock(&h.app).is_none());
}
}
-6
View File
@@ -207,12 +207,6 @@ pub struct Tokens {
pub preview_sketch: Color32,
/// Removed material in a feature preview (translucent).
pub preview_cut: Color32,
// Manufacturing display.
pub cam_stock: Color32,
pub cam_fixture: Color32,
pub cam_cut: Color32,
pub cam_rapid: Color32,
pub cam_tool: Color32,
/// Navigation cube faces, outline and labels.
pub cube_face: Color32,
pub cube_border: Color32,
+8 -19
View File
@@ -51,7 +51,6 @@ pub fn panels(tab: &str) -> &'static [&'static str] {
return p;
}
match tab {
"MILLING" => &["SETUP", "TOOLPATHS", "VERIFY", "EXPORT"],
"SOLID" => &["CREATE", "MODIFY", "ASSEMBLE", "CONSTRUCT", "INSPECT", "INSERT", "SELECT"],
"SKETCH" => &["CREATE", "MODIFY", "PATTERN", "CONSTRAINTS", "INSPECT", "INSERT", "SELECT", "FINISH SKETCH"],
"SURFACE" => &["CREATE", "MODIFY", "ASSEMBLE", "CONSTRUCT", "INSPECT", "INSERT", "SELECT"],
@@ -546,7 +545,7 @@ pub fn save(app: &mut SolveApp) {
}
}
pub(crate) fn save_as(app: &mut SolveApp) {
fn save_as(app: &mut SolveApp) {
save_as_with_extension(app, solidcraft_engine::io::DESIGN_EXT);
}
@@ -602,13 +601,7 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
painter.text(
pos2(ws.center().x - 5.0, ws.top() + 52.0),
Align2::CENTER_CENTER,
app.tr_label(if app.ui.tab == "MILLING" {
"Manufacture"
} else if crate::render_workspace::active(app) {
"RENDER"
} else {
crate::toolbar_custom::WORKSPACE
}),
app.tr_label(if crate::render_workspace::active(app) { "RENDER" } else { crate::toolbar_custom::WORKSPACE }),
FontId::proportional(12.0),
t.text,
);
@@ -624,9 +617,7 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
if sketching {
tabs.push("SKETCH");
}
if app.ui.tab == "MILLING" {
tabs.push("MILLING");
} else if crate::render_workspace::active(app) {
if crate::render_workspace::active(app) {
tabs.push("RENDER");
} else {
tabs.extend(TABS.iter());
@@ -1301,12 +1292,10 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) {
if ctx.egui_wants_keyboard_input() || crate::dim_view::editing() || crate::dialogs_save_mesh::popup_open(ctx) {
return;
}
// A modifier released later in this frame must not turn Cmd+S into plain S.
let keys: Vec<_> = ctx.input(|i| {
i.events
.iter()
.filter_map(|e| if let egui::Event::Key { key, pressed: true, modifiers, .. } = e { Some((*key, *modifiers)) } else { None })
.collect()
let (keys, mods) = ctx.input(|i| {
let keys: Vec<egui::Key> =
i.events.iter().filter_map(|e| if let egui::Event::Key { key, pressed: true, .. } = e { Some(*key) } else { None }).collect();
(keys, i.modifiers)
});
// Home hides the design: only New, Open and Preferences work there.
let shown = design_shown(app);
@@ -1315,7 +1304,7 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) {
} else {
crate::edit_clipboard::flush(ctx);
}
for (k, mods) in keys {
for k in keys {
use egui::Key;
if shown && crate::navigation::view_key(app, ctx, k, mods) {
continue;
+14 -45
View File
@@ -280,7 +280,6 @@ pub fn view_bounds(app: &SolveApp) -> solidcraft_engine::geom::Aabb3 {
fn scene_key(app: &SolveApp) -> u64 {
let mut h = std::collections::hash_map::DefaultHasher::new();
app.session.revision.hash(&mut h);
crate::manufacturing::view::key(app, &mut h);
(std::sync::Arc::as_ptr(&app.session.world_state()) as usize).hash(&mut h);
app.ui.show_grid.hash(&mut h);
app.ui.visual_style.hash(&mut h);
@@ -618,7 +617,6 @@ fn build_scene(app: &SolveApp) -> GpuScene {
let s = &app.session;
// Bodies where their occurrences place them.
let st = s.world_state();
let cam_bodies = crate::manufacturing::view::Bodies::new(app);
let active_bodies = s.active_occurrence_path.as_ref().and_then(|p| s.occurrence_bodies(p, true).ok());
let (shaded, edges) = match app.ui.visual_style {
1 => (true, false),
@@ -635,17 +633,16 @@ fn build_scene(app: &SolveApp) -> GpuScene {
let mut run: Vec<BodyDraw> = Vec::new();
let mut run_key = std::collections::hash_map::DefaultHasher::new();
for b in &st.bodies {
if !app.session.body_visible(&b.name) || app.preview.replaced.contains(&b.name) || !cam_bodies.visible(&b.name) {
if !app.session.body_visible(&b.name) || app.preview.replaced.contains(&b.name) {
continue;
}
let faded = body_faded(app, b, &active_bodies);
let cam_color = cam_bodies.color(&b.name);
let base = cam_color.or_else(|| body_color(&s.doc, b)).unwrap_or(tk.body);
let base = body_color(&s.doc, b).unwrap_or(tk.body);
let col = if faded { c4(base.gamma_multiply(0.35)) } else { c4(base) };
// Appearances: the body's opacity, and faces with looks of their own.
let opacity = s.doc.model_body_opacity(b);
let face_looks: std::collections::HashMap<usize, solidcraft_engine::doc::appearance::Look> =
if shaded && cam_color.is_none() { s.doc.face_colors(b).into_iter().collect() } else { Default::default() };
if shaded { s.doc.face_colors(b).into_iter().collect() } else { Default::default() };
let opaque = !faded && opacity >= 0.999 && face_looks.values().all(|l| l.opacity >= 0.999);
let planned = drawn_plan(app, b, opaque);
let on_gpu = planned.is_some();
@@ -676,14 +673,13 @@ fn build_scene(app: &SolveApp) -> GpuScene {
if let Ok(mut b) = app.viewport.blocks.lock() {
*b = made;
}
crate::manufacturing::view::gpu(app, &mut sc);
if app.ui.ground_shadow && app.ui.visual_style != 2 && app.cam.pitch > 0.0 {
ground_shadow(&mut sc, app, &st);
}
// Section: the outline of each body where the plane cuts it.
if let Some((o, n)) = section_plane(app) {
let col = c4(tk.text.gamma_multiply(0.9));
for b in st.bodies.iter().filter(|b| app.session.body_visible(&b.name) && cam_bodies.visible(&b.name)) {
for b in st.bodies.iter().filter(|b| app.session.body_visible(&b.name)) {
for (a, c) in solidcraft_engine::view::section_segments(&b.mesh(), o, n) {
sc.line(a.to_f32(), c.to_f32(), col, 1.8, false);
}
@@ -791,12 +787,8 @@ fn ground_shadow(sc: &mut GpuScene, app: &SolveApp, st: &std::sync::Arc<solidcra
// The bodies' box in the camera's Z-up frame (the ground is its XY plane).
let mut b = solidcraft_engine::geom::Aabb3::EMPTY;
let boxes = body_bounds(app, st);
let bodies = crate::manufacturing::view::Bodies::new(app);
let stock = crate::manufacturing::view::stock(app);
let visible =
st.bodies.iter().zip(boxes.iter()).filter(|(x, _)| app.session.body_visible(&x.name) && bodies.visible(&x.name)).map(|(_, (_, bb))| *bb);
for bb in visible.chain(stock.iter().map(|mesh| mesh.bounds())) {
if bb.is_empty() {
for (x, (_, bb)) in st.bodies.iter().zip(boxes.iter()) {
if bb.is_empty() || !app.session.body_visible(&x.name) {
continue;
}
for i in 0..8 {
@@ -1018,9 +1010,7 @@ type BodyBounds = (std::sync::Arc<solidcraft_engine::doc::ModelState>, std::sync
/// The model's extent: bodies (from the cached boxes) and sketches.
pub fn model_bounds(app: &SolveApp) -> solidcraft_engine::geom::Aabb3 {
let st = app.session.world_state();
let bodies = crate::manufacturing::view::Bodies::new(app);
let mut b =
body_bounds(app, &st).iter().filter(|(name, _)| bodies.visible(name)).fold(crate::manufacturing::view::bounds(app), |a, (_, x)| a.union(x));
let mut b = body_bounds(app, &st).iter().fold(solidcraft_engine::geom::Aabb3::EMPTY, |a, (_, x)| a.union(x));
for ss in &st.sketches {
if let Some((lo, hi)) = ss.sketch.bounds() {
for p in [lo, hi] {
@@ -1093,7 +1083,6 @@ pub(crate) fn pick_cached(app: &mut SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit
v.to_bits().hash(&mut h);
}
app.session.revision.hash(&mut h);
crate::manufacturing::view::key(app, &mut h);
(std::sync::Arc::as_ptr(&app.session.world_state()) as usize).hash(&mut h);
app.session.active_sketch.hash(&mut h);
crate::sketch3d::drawing(app).hash(&mut h);
@@ -1190,7 +1179,6 @@ impl<T: PartialOrd + Copy> Near<T> {
}
fn pick_hits(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool, all: bool) -> Vec<Hit> {
let cam_bodies = crate::manufacturing::view::Bodies::new(app);
let s = &app.session;
let st = s.world_state();
let bounds = body_bounds(app, &st);
@@ -1260,7 +1248,7 @@ fn pick_hits(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool,
let mut bestf: Option<(f64, Hit)> = None;
let mut faces = Near::new(all);
for b in &st.bodies {
if !app.session.body_visible(&b.name) || !cam_bodies.visible(&b.name) {
if !app.session.body_visible(&b.name) {
continue;
}
// Bodies whose box the ray misses (or enters behind the best hit so far) are skipped.
@@ -1292,7 +1280,7 @@ fn pick_hits(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool,
let mut bestv = Near::new(all);
let mut beste = Near::new(all);
for b in &st.bodies {
if !app.session.body_visible(&b.name) || !cam_bodies.visible(&b.name) {
if !app.session.body_visible(&b.name) {
continue;
}
// Only bodies whose outline on screen is near the cursor can have an edge under it.
@@ -1824,7 +1812,6 @@ fn quad(sc: &mut GpuScene, q: &[Vec3; 4], fill: Color32, edge: Color32) {
fn highlight_key(app: &SolveApp) -> u64 {
let mut h = std::collections::hash_map::DefaultHasher::new();
app.session.revision.hash(&mut h);
crate::manufacturing::view::key(app, &mut h);
serde_json::to_string(&app.highlighted()).unwrap_or_default().hash(&mut h);
format!("{:?}", app.viewport.hover).hash(&mut h);
app.viewport.hover_feature.hash(&mut h);
@@ -1850,7 +1837,6 @@ fn highlight_key(app: &SolveApp) -> u64 {
/// The highlight scene: origin widget, construction planes, selection and hover.
fn build_highlight(app: &SolveApp) -> GpuScene {
let cam_bodies = crate::manufacturing::view::Bodies::new(app);
let t = Tokens::get();
let mut sc = GpuScene::default();
let st = app.session.world_state();
@@ -1861,23 +1847,12 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
let mut sel = Vec::new();
for item in app.highlighted() {
for piece in displayed_selection(app, &item) {
let body = match &piece {
Sel::Body { name } => Some(name),
Sel::Face { body, .. } | Sel::Edge { body, .. } | Sel::Vertex { body, .. } => Some(body),
_ => None,
};
if body.is_some_and(|name| !cam_bodies.visible(name)) {
continue;
}
if !sel.iter().any(|x| crate::selection::same_item(x, &piece)) {
sel.push(piece);
}
}
}
let hover = app.viewport.hover.as_ref().filter(|h| match h {
Hit::Face { body, .. } | Hit::Edge { body, .. } | Hit::Vertex { body, .. } => cam_bodies.visible(body),
_ => true,
});
let hover = app.viewport.hover.as_ref();
let size = origin_size(app.cam.half_height());
// Origin planes and construction planes.
let plane_state = |name: &str| {
@@ -2005,8 +1980,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
// Timeline modifiers light only their resulting faces; browser rows light whole bodies.
let mut hovered = app.viewport.hover_feature.map(|id| feature_geometry(app, id)).unwrap_or_default();
hovered.extend(app.viewport.hover_bodies.iter().map(|name| Sel::Body { name: name.clone() }));
for b in st.bodies.iter().filter(|b| app.session.body_visible(&b.name) && !app.preview.replaced.contains(&b.name) && cam_bodies.visible(&b.name))
{
for b in st.bodies.iter().filter(|b| app.session.body_visible(&b.name) && !app.preview.replaced.contains(&b.name)) {
let whole = hovered.iter().any(|x| matches!(x, Sel::Body { name } if name == &b.name));
let faces: Vec<_> = if whole {
(0..b.body.face_count()).collect()
@@ -2814,7 +2788,6 @@ fn box_select(app: &mut SolveApp, proj: &Proj, bx: BoxSel, add: bool) {
if app.applying.is_some() {
return;
}
let bodies = crate::manufacturing::view::Bodies::new(app);
let st = app.session.world_state();
let mut out: Vec<Sel> = Vec::new();
let to2 = |pts: &[Vec3]| -> Vec<Pos2> { pts.iter().filter_map(|p| proj.to_screen(*p)).collect() };
@@ -2878,7 +2851,7 @@ fn box_select(app: &mut SolveApp, proj: &Proj, bx: BoxSel, add: bool) {
} else {
let accept = app.dialog.as_ref().and_then(|d| d.active_input()).map(|i| i.accept).unwrap_or(BODIES);
for b in &st.bodies {
if !app.session.body_visible(&b.name) || !bodies.visible(&b.name) {
if !app.session.body_visible(&b.name) {
continue;
}
let m = b.mesh();
@@ -2965,7 +2938,6 @@ fn box_select(app: &mut SolveApp, proj: &Proj, bx: BoxSel, add: bool) {
/// Screen-space dots: the origin point, and hovered or selected body vertices.
fn points_2d(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
let t = Tokens::get();
let bodies = crate::manufacturing::view::Bodies::new(app);
if app.origin_visible()
&& !app.session.visibility.hidden_origin.iter().any(|h| h == "O")
&& let Some(o) = proj.to_screen(Vec3::ZERO)
@@ -2973,15 +2945,13 @@ fn points_2d(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
painter.circle(o, 4.5, t.origin_point, Stroke::new(1.0, Color32::from_gray(110)));
}
for x in app.highlighted() {
if let Sel::Vertex { point, body, .. } = x
&& bodies.visible(&body)
if let Sel::Vertex { point, .. } = x
&& let Some(p) = proj.to_screen(point)
{
painter.circle(p, 4.0, t.sel_vertex, Stroke::new(1.5, t.sel_edge));
}
}
if let Some(Hit::Vertex { point, body, .. }) = &app.viewport.hover
&& bodies.visible(body)
if let Some(Hit::Vertex { point, .. }) = &app.viewport.hover
&& let Some(p) = proj.to_screen(*point)
{
painter.circle(p, 4.5, t.hover_edge_halo, Stroke::new(1.5, t.hover_edge));
@@ -3016,7 +2986,6 @@ fn hover_highlight(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
/// Sketch points and the status of the active sketch (constraint glyphs: `sketch_tools`).
fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
crate::manufacturing::overlay(app, painter, proj);
let t = Tokens::get();
let st = app.session.world_state();
for item in &app.session.selection {