1 parent
1ded8fdd27
commit
2c020dbcbc
85 files changed
+43
-14487
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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" => {
|
||||
|
||||
@@ -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:?}")),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user