1 parent
1ded8fdd27
commit
2c020dbcbc
85 files changed
+43
-14487
No files matched your search
@@ -1,603 +0,0 @@
|
||||
//! Manufacturing commands shared by UI, CLI and MCP.
|
||||
use super::CommandSpec;
|
||||
use crate::{EngineError, Result, Session, cam, manufacturing as jobs};
|
||||
use serde_json::{Value, json};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("cam.holes.from_selection", "Use Selected Holes", from_holes)
|
||||
.noundo()
|
||||
.params("setup: ID, selections: [{kind:face,body,index,occurrence?:[IDs]}|{kind:profile,sketch,index,occurrence?:[IDs]}]; returns ordered holes, diameters and durable sources; face body is a world label unless occurrence is explicit"),
|
||||
CommandSpec::new("cam.boundary.from_profile", "Use Sketch Profile", from_profile)
|
||||
.noundo()
|
||||
.params("setup: ID, sketch: feature ID, profile: index, occurrence?: [IDs]; returns boundary and durable source reference"),
|
||||
CommandSpec::new("cam.setup.put", "Milling Setup", setup)
|
||||
.at("MILLING", "SETUP")
|
||||
.icon("box")
|
||||
.params("setup: complete Setup object (create or replace by ID)"),
|
||||
CommandSpec::new("cam.tool.put", "Tool Library", tool).at("MILLING", "SETUP").icon("hole").params("tool: complete Tool object"),
|
||||
CommandSpec::new("cam.operation.put", "Milling Operation", operation)
|
||||
.at("MILLING", "TOOLPATHS")
|
||||
.icon("cam_path")
|
||||
.params("operation: complete Operation object; strategy face, contour, pocket or drill"),
|
||||
CommandSpec::new("cam.program.put", "NC Program", program)
|
||||
.at("MILLING", "SETUP")
|
||||
.icon("save")
|
||||
.params("program: {id,name,setup,operations:[ordered IDs]}"),
|
||||
CommandSpec::new("cam.operation.order", "Reorder Operations", order).params("program: ID, operations: permutation of existing operation IDs"),
|
||||
CommandSpec::new("cam.delete", "Delete Manufacturing Item", delete)
|
||||
.params("kind: setup|tool|operation|program, id; referenced items cannot be deleted"),
|
||||
CommandSpec::new("cam.inspect", "Inspect Manufacturing", inspect)
|
||||
.noundo()
|
||||
.params("job?: ID; include_path?: bool; include_stock?: bool (world-space display mesh); motion?: completed block count; finding?: zero-based verification finding index"),
|
||||
CommandSpec::new("cam.generate", "Generate Toolpaths", generate)
|
||||
.at("MILLING", "TOOLPATHS")
|
||||
.icon("cam_generate")
|
||||
.noundo()
|
||||
.params("program: ID, background?: bool (default false); returns job ID"),
|
||||
CommandSpec::new("cam.job.status", "CAM Job Status", status).noundo().params("job: ID"),
|
||||
CommandSpec::new("cam.job.cancel", "Cancel CAM Job", cancel).noundo().params("job: ID"),
|
||||
CommandSpec::new("cam.simulate", "Simulate Toolpaths", simulate)
|
||||
.at("MILLING", "VERIFY")
|
||||
.icon("cam_verify")
|
||||
.noundo()
|
||||
.params("job: ID, background?: bool"),
|
||||
CommandSpec::new("cam.post", "Post Reference G-code", post)
|
||||
.at("MILLING", "EXPORT")
|
||||
.icon("save")
|
||||
.noundo()
|
||||
.params("job: ID, path?: string, background?: bool; requires current passing verification; background output is published by cam.export.result"),
|
||||
CommandSpec::new("cam.export", "Export CAM Artifact", export)
|
||||
.noundo()
|
||||
.params("job: ID, format: toolpath|setup_sheet|stock|validation, path?: string, background?: bool; background output is published by cam.export.result"),
|
||||
CommandSpec::new("cam.export.result", "Collect CAM Export", export_result).noundo().params("job: ID; consumes completed output after checking freshness; writes the requested path or returns content"),
|
||||
];
|
||||
fn from_holes(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let setup = id(p, "setup")?;
|
||||
let setup = s.doc.manufacturing.setups.iter().find(|x| x.id == setup).ok_or_else(|| EngineError::Other("setup not found".into()))?;
|
||||
let (sources, selected) = jobs::holes::select(s, setup, decode(p, "selections")?)?;
|
||||
Ok(
|
||||
json!({"sources":sources,"holes":selected.iter().map(|h|h.center).collect::<Vec<_>>(),"diameters":selected.iter().map(|h|h.diameter).collect::<Vec<_>>()}),
|
||||
)
|
||||
}
|
||||
fn from_profile(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let setup = id(p, "setup")?;
|
||||
let sketch = id(p, "sketch")?;
|
||||
let profile = id(p, "profile")? as usize;
|
||||
let state = s.model.state();
|
||||
let profile = state
|
||||
.sketches
|
||||
.iter()
|
||||
.find(|x| x.feature == sketch)
|
||||
.and_then(|s| s.profiles.get(profile))
|
||||
.ok_or_else(|| EngineError::Other("selected profile not found".into()))?;
|
||||
let source = cam::ProfileRef {
|
||||
sketch,
|
||||
curves: profile.curve_ids(),
|
||||
occurrence: p.get("occurrence").map(|_| decode(p, "occurrence")).transpose()?.unwrap_or_default(),
|
||||
};
|
||||
let setup = s.doc.manufacturing.setups.iter().find(|x| x.id == setup).ok_or_else(|| EngineError::Other("setup not found".into()))?;
|
||||
let boundary = jobs::profile_boundary(s, setup, &source, cam::TOLERANCE)?;
|
||||
Ok(json!({"boundary":boundary,"source":source}))
|
||||
}
|
||||
fn decode<T: serde::de::DeserializeOwned>(p: &Value, key: &str) -> Result<T> {
|
||||
serde_json::from_value(p.get(key).cloned().ok_or_else(|| EngineError::Other(format!("missing {key}")))?)
|
||||
.map_err(|e| EngineError::Other(e.to_string()))
|
||||
}
|
||||
fn id(p: &Value, key: &str) -> Result<u64> {
|
||||
p.get(key).and_then(Value::as_u64).ok_or_else(|| EngineError::Other(format!("{key} must be an ID")))
|
||||
}
|
||||
macro_rules! put {($fn:ident,$key:literal,$ty:ty,$field:ident)=>{fn $fn(s:&mut Session,p:&Value)->Result<Value>{
|
||||
let item:$ty=decode(p,$key)?;let id=item.id;let mut m=s.doc.manufacturing.clone();
|
||||
if let Some(old)=m.$field.iter_mut().find(|x|x.id==id){*old=item;}else{m.$field.push(item);}
|
||||
m.validate().map_err(jobs::error)?;s.doc_mut().manufacturing=m;Ok(json!({"id":id}))
|
||||
}};}
|
||||
put!(setup, "setup", cam::Setup, setups);
|
||||
put!(tool, "tool", cam::Tool, tools);
|
||||
put!(operation, "operation", cam::Operation, operations);
|
||||
put!(program, "program", cam::Program, programs);
|
||||
fn order(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let program = id(p, "program")?;
|
||||
let ids: Vec<u64> = decode(p, "operations")?;
|
||||
let mut m = s.doc.manufacturing.clone();
|
||||
let target = m.programs.iter_mut().find(|x| x.id == program).ok_or_else(|| EngineError::Other("program not found".into()))?;
|
||||
let mut a = target.operations.clone();
|
||||
let mut b = ids.clone();
|
||||
a.sort();
|
||||
b.sort();
|
||||
if a != b {
|
||||
return Err(EngineError::Other("order must be a permutation of the program operations".into()));
|
||||
}
|
||||
target.operations = ids;
|
||||
m.validate().map_err(jobs::error)?;
|
||||
s.doc_mut().manufacturing = m;
|
||||
Ok(json!({"program":program}))
|
||||
}
|
||||
fn delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = id(p, "id")?;
|
||||
let mut m = s.doc.manufacturing.clone();
|
||||
let removed = match p.get("kind").and_then(Value::as_str) {
|
||||
Some("setup") => {
|
||||
let n = m.setups.len();
|
||||
m.setups.retain(|x| x.id != id);
|
||||
n != m.setups.len()
|
||||
}
|
||||
Some("tool") => {
|
||||
let n = m.tools.len();
|
||||
m.tools.retain(|x| x.id != id);
|
||||
n != m.tools.len()
|
||||
}
|
||||
Some("operation") => {
|
||||
let n = m.operations.len();
|
||||
m.operations.retain(|x| x.id != id);
|
||||
n != m.operations.len()
|
||||
}
|
||||
Some("program") => {
|
||||
let n = m.programs.len();
|
||||
m.programs.retain(|x| x.id != id);
|
||||
n != m.programs.len()
|
||||
}
|
||||
_ => return Err(EngineError::Other("unknown CAM item kind".into())),
|
||||
};
|
||||
if !removed {
|
||||
return Err(EngineError::Other("CAM item not found".into()));
|
||||
}
|
||||
m.validate().map_err(jobs::error)?;
|
||||
s.doc_mut().manufacturing = m;
|
||||
Ok(json!({"deleted":id}))
|
||||
}
|
||||
fn inspect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if p.get("job").is_none() {
|
||||
if p.get("finding").is_some()
|
||||
|| p.get("motion").is_some()
|
||||
|| ["include_path", "include_stock"].iter().any(|key| p.get(key).and_then(Value::as_bool) == Some(true))
|
||||
{
|
||||
return Err(EngineError::Other("job is required for result inspection".into()));
|
||||
}
|
||||
return Ok(json!({"manufacturing":s.doc.manufacturing,"jobs":s.cam_jobs.jobs.keys().collect::<Vec<_>>()}));
|
||||
}
|
||||
let job = jobs::job(s, id(p, "job")?)?;
|
||||
let mut result = status(s, p)?;
|
||||
match job.state()? {
|
||||
jobs::JobState::Generated(path) | jobs::JobState::Verified(path, _) if p.get("include_path").and_then(Value::as_bool) == Some(true) => {
|
||||
result["toolpath"] = json!(path);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(motion) = p.get("motion") {
|
||||
jobs::current(s, &job)?;
|
||||
let motion = motion
|
||||
.as_u64()
|
||||
.and_then(|n| usize::try_from(n).ok())
|
||||
.ok_or_else(|| EngineError::Other("motion must be a nonnegative block count".into()))?;
|
||||
let path = match job.state()? {
|
||||
jobs::JobState::Generated(path) | jobs::JobState::Verified(path, _) => path,
|
||||
_ => return Err(EngineError::Other("generate before inspecting playback".into())),
|
||||
};
|
||||
let state = cam::playback::at(&path, &job.input()?.setup, motion).map_err(jobs::error)?;
|
||||
result["playback"] = json!(state);
|
||||
let operation = job.input()?.operations.iter().find(|o| Some(o.id) == state.operation);
|
||||
result["source"] = json!(operation.and_then(|o| o.source.as_ref()));
|
||||
result["hole_sources"] = json!(operation.map(|o| &o.hole_sources));
|
||||
}
|
||||
if let Some(index) = p.get("finding") {
|
||||
jobs::current(s, &job)?;
|
||||
let index =
|
||||
index.as_u64().and_then(|i| usize::try_from(i).ok()).ok_or_else(|| EngineError::Other("finding must be a nonnegative index".into()))?;
|
||||
let jobs::JobState::Verified(path, report) = job.state()? else {
|
||||
return Err(EngineError::Other("simulate before inspecting findings".into()));
|
||||
};
|
||||
let finding = report.findings.get(index).ok_or_else(|| EngineError::Other("finding index out of range".into()))?;
|
||||
let input = job.input()?;
|
||||
let operation = finding.source.and_then(|source| input.operations.iter().find(|o| o.id == source.operation));
|
||||
let code = cam::nc::post(&path, &input.setup).map_err(jobs::error)?;
|
||||
let line = finding.line.and_then(|line| line.checked_sub(1)).and_then(|index| code.lines().nth(index));
|
||||
result["diagnostic"] = json!({"index":index,"finding":finding,"world":input.setup.frame.world(finding.at),"operation":operation,
|
||||
"source":operation.and_then(|o|o.source.as_ref()),"hole_sources":operation.map(|o|&o.hole_sources),"gcode_line":line,
|
||||
"playback":finding.source.map(|source|cam::playback::at(&path,&input.setup,source.block)).transpose().map_err(jobs::error)?});
|
||||
}
|
||||
if p.get("include_stock").and_then(Value::as_bool) == Some(true) {
|
||||
jobs::current(s, &job)?;
|
||||
result["stock_mesh"] = serde_json::to_value(job.stock_mesh()?.as_deref()).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
fn generate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = jobs::start(s, id(p, "program")?, p.get("background").and_then(Value::as_bool).unwrap_or(false))?;
|
||||
status(s, &json!({"job":id}))
|
||||
}
|
||||
fn status(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = id(p, "job")?;
|
||||
let job = jobs::job(s, id)?;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
jobs::advance(&job);
|
||||
let stale = jobs::current(s, &job).is_err();
|
||||
let mut v = json!({"job":id,"stale":stale,"progress":job.progress.load(Ordering::Relaxed),"fingerprint":job.input().ok().map(|input| &input.fingerprint)});
|
||||
match job.state()? {
|
||||
jobs::JobState::Verifying(_) => v["state"] = json!("simulating"),
|
||||
jobs::JobState::Preparing => v["state"] = json!("preparing"),
|
||||
jobs::JobState::Running => v["state"] = json!("running"),
|
||||
jobs::JobState::Cancelled => v["state"] = json!("cancelled"),
|
||||
jobs::JobState::Failed(e) => {
|
||||
v["state"] = json!("failed");
|
||||
v["error"] = json!(e);
|
||||
}
|
||||
jobs::JobState::Generated(path) => {
|
||||
v["state"] = json!("generated");
|
||||
v["blocks"] = json!(path.blocks.len());
|
||||
}
|
||||
jobs::JobState::Verified(path, r) => {
|
||||
v["state"] = json!("verified");
|
||||
v["blocks"] = json!(path.blocks.len());
|
||||
v["stock_preview"] = match job.stock_mesh() {
|
||||
Ok(Some(mesh)) => json!({"available":true,"triangles":mesh.triangles.len()}),
|
||||
Ok(None) => json!({"available":false}),
|
||||
Err(e) => json!({"available":false,"error":e.to_string()}),
|
||||
};
|
||||
v["verification"] = json!({"status":r.status,"finished_dimensions":r.finished_dimensions,"stock_complete":r.stock_complete,
|
||||
"findings":r.findings,"cells":r.cells.len(),"work":r.work});
|
||||
}
|
||||
}
|
||||
v["export"] = jobs::exports::status(&job)?;
|
||||
Ok(v)
|
||||
}
|
||||
fn cancel(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let job = jobs::job(s, id(p, "job")?)?;
|
||||
job.cancelled.store(true, Ordering::Relaxed);
|
||||
job.set(jobs::JobState::Cancelled);
|
||||
status(s, p)
|
||||
}
|
||||
fn simulate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let job = jobs::job(s, id(p, "job")?)?;
|
||||
jobs::current(s, &job)?;
|
||||
if jobs::exports::busy(&job)? {
|
||||
return Err(EngineError::Other("finish or cancel the current export first".into()));
|
||||
}
|
||||
let path = match job.state()? {
|
||||
jobs::JobState::Generated(p) | jobs::JobState::Verified(p, _) => p,
|
||||
_ => return Err(EngineError::Other("generate a path first; another job may still be running".into())),
|
||||
};
|
||||
job.set(jobs::JobState::Verifying(path));
|
||||
jobs::dispatch(job, p.get("background").and_then(Value::as_bool).unwrap_or(false), true)?;
|
||||
status(s, p)
|
||||
}
|
||||
fn output(p: &Value, content: String) -> Result<Value> {
|
||||
if let Some(path) = p.get("path").and_then(Value::as_str) {
|
||||
crate::io::write_atomic(std::path::Path::new(path), content.as_bytes(), false).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
Ok(json!({"path":path,"bytes":content.len()}))
|
||||
} else {
|
||||
Ok(json!({"content":content}))
|
||||
}
|
||||
}
|
||||
fn post(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let job = jobs::job(s, id(p, "job")?)?;
|
||||
jobs::current(s, &job)?;
|
||||
let jobs::JobState::Verified(path, report) = job.state()? else {
|
||||
return Err(EngineError::Other("simulate before posting".into()));
|
||||
};
|
||||
if report.status != cam::verify::Status::Pass || report.finished_dimensions != cam::verify::Status::Pass || !report.stock_complete {
|
||||
return Err(EngineError::Other("posting blocked by failed or inconclusive verification".into()));
|
||||
}
|
||||
if p.get("background").and_then(Value::as_bool).unwrap_or(false) {
|
||||
jobs::exports::start(job, "gcode".into(), p.get("path").and_then(Value::as_str).map(str::to_owned))?;
|
||||
return status(s, p);
|
||||
}
|
||||
cam::nc::round_trip(&path, &job.input()?.setup, &job.input()?.tools).map_err(jobs::error)?;
|
||||
output(p, cam::nc::post(&path, &job.input()?.setup).map_err(jobs::error)?)
|
||||
}
|
||||
fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let job = jobs::job(s, id(p, "job")?)?;
|
||||
jobs::current(s, &job)?;
|
||||
let (path, report) = match job.state()? {
|
||||
jobs::JobState::Generated(p) => (p, None),
|
||||
jobs::JobState::Verified(p, r) => (p, Some(r)),
|
||||
_ => return Err(EngineError::Other("no generated result".into())),
|
||||
};
|
||||
let format = p.get("format").and_then(Value::as_str).unwrap_or("validation");
|
||||
if p.get("background").and_then(Value::as_bool).unwrap_or(false) {
|
||||
jobs::exports::start(job, format.into(), p.get("path").and_then(Value::as_str).map(str::to_owned))?;
|
||||
return status(s, p);
|
||||
}
|
||||
output(p, crate::io::cam::artifact(job.input()?, &path, report.as_deref(), format)?)
|
||||
}
|
||||
|
||||
fn export_result(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let job = jobs::job(s, id(p, "job")?)?;
|
||||
jobs::current(s, &job)?;
|
||||
let (content, destination) = jobs::exports::take(&job)?;
|
||||
output(&destination.map_or_else(|| json!({}), |path| json!({"path":path})), content)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn wait_export(s: &mut Session, job: u64) -> Value {
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
|
||||
loop {
|
||||
let status = s.execute("cam.job.status", &json!({"job":job})).unwrap();
|
||||
if status["export"]["state"] != "running" {
|
||||
return status;
|
||||
}
|
||||
assert!(std::time::Instant::now() < deadline, "export worker timeout");
|
||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn background_exports_only_publish_complete_current_results_once() {
|
||||
let mut s = configured();
|
||||
let job = s.execute("cam.generate", &json!({"program":1})).unwrap()["job"].as_u64().unwrap();
|
||||
s.execute("cam.simulate", &json!({"job":job})).unwrap();
|
||||
for format in ["toolpath", "validation", "setup_sheet", "stock", "gcode"] {
|
||||
let cmd = if format == "gcode" { "cam.post" } else { "cam.export" };
|
||||
let expected = s.execute(cmd, &json!({"job":job,"format":format})).unwrap()["content"].as_str().unwrap().to_owned();
|
||||
s.execute(cmd, &json!({"job":job,"format":format,"background":true})).unwrap();
|
||||
assert!(s.execute(cmd, &json!({"job":job,"format":format,"background":true})).is_err());
|
||||
let status = wait_export(&mut s, job);
|
||||
assert_eq!(status["export"]["state"], "ready", "{status}");
|
||||
let actual = s.execute("cam.export.result", &json!({"job":job})).unwrap()["content"].as_str().unwrap().to_owned();
|
||||
match format {
|
||||
"toolpath" | "validation" => {
|
||||
assert_eq!(serde_json::from_str::<Value>(&actual).unwrap(), serde_json::from_str::<Value>(&expected).unwrap())
|
||||
}
|
||||
"setup_sheet" => assert!(actual.contains("<h2>Operation settings and selected geometry</h2>")),
|
||||
_ => assert_eq!(actual, expected),
|
||||
}
|
||||
assert!(s.execute("cam.export.result", &json!({"job":job})).is_err());
|
||||
}
|
||||
let path = std::env::temp_dir().join(format!("solidcraft-cam-export-{}.nc", std::process::id()));
|
||||
s.execute("cam.post", &json!({"job":job,"background":true,"path":path})).unwrap();
|
||||
assert_eq!(wait_export(&mut s, job)["export"]["state"], "ready");
|
||||
assert!(!path.exists());
|
||||
s.execute("cam.export.result", &json!({"job":job})).unwrap();
|
||||
assert!(std::fs::read_to_string(&path).unwrap().contains("G21"));
|
||||
std::fs::remove_file(&path).unwrap();
|
||||
s.execute("cam.post", &json!({"job":job,"background":true,"path":path})).unwrap();
|
||||
wait_export(&mut s, job);
|
||||
let mut tool = s.doc.manufacturing.tools[0].clone();
|
||||
tool.diameter += 0.1;
|
||||
s.execute("cam.tool.put", &json!({"tool":tool})).unwrap();
|
||||
assert!(s.execute("cam.export.result", &json!({"job":job})).unwrap_err().to_string().contains("stale"));
|
||||
assert!(!path.exists());
|
||||
}
|
||||
#[test]
|
||||
fn cancelling_a_background_export_cannot_publish_or_restart_it() {
|
||||
let mut s = configured();
|
||||
let job = s.execute("cam.generate", &json!({"program":1})).unwrap()["job"].as_u64().unwrap();
|
||||
s.execute("cam.export", &json!({"job":job,"format":"toolpath","background":true})).unwrap();
|
||||
s.execute("cam.job.cancel", &json!({"job":job})).unwrap();
|
||||
assert_eq!(wait_export(&mut s, job)["export"]["state"], "cancelled");
|
||||
assert!(s.execute("cam.export.result", &json!({"job":job})).is_err());
|
||||
assert!(s.execute("cam.export", &json!({"job":job,"format":"toolpath","background":true})).is_err());
|
||||
}
|
||||
fn configured() -> Session {
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"length":20,"width":20,"height":4})).unwrap();
|
||||
let mut i = cam::corpus::fixture("face").unwrap();
|
||||
i.setup.models[0].body = s.model.state().bodies[0].name.clone();
|
||||
let bounds = s.model.state().bodies[0].mesh().bounds();
|
||||
i.setup.stock.min = bounds.min;
|
||||
i.setup.stock.max = bounds.max + crate::geom::Vec3::Z;
|
||||
i.operations[0].top = bounds.max.z + 1.0;
|
||||
i.operations[0].bottom = bounds.max.z;
|
||||
s.execute("cam.setup.put", &json!({"setup":i.setup})).unwrap();
|
||||
s.execute("cam.tool.put", &json!({"tool":i.tools[0]})).unwrap();
|
||||
s.execute("cam.operation.put", &json!({"operation":i.operations[0]})).unwrap();
|
||||
s.execute("cam.program.put", &json!({"program":{"id":1,"name":"Plate","setup":1,"operations":[1]}})).unwrap();
|
||||
s
|
||||
}
|
||||
#[test]
|
||||
fn save_undo_and_stale_generation() {
|
||||
let mut s = configured();
|
||||
let saved = crate::io::write_design(&s.doc);
|
||||
assert_eq!(crate::io::read_design(&saved).unwrap().manufacturing, s.doc.manufacturing);
|
||||
let r = s.execute("cam.generate", &json!({"program":1})).unwrap();
|
||||
assert_eq!(r["state"], "generated");
|
||||
let id = r["job"].clone();
|
||||
s.execute("cam.operation.order", &json!({"program":1,"operations":[1]})).unwrap();
|
||||
s.execute("solid.box", &json!({"length":2,"width":2,"height":2})).unwrap();
|
||||
assert!(s.execute("cam.post", &json!({"job":id})).is_err());
|
||||
assert_eq!(s.execute("cam.job.status", &json!({"job":id})).unwrap()["stale"], true);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.execute("cam.job.status", &json!({"job":id})).unwrap()["stale"], false);
|
||||
}
|
||||
#[test]
|
||||
fn operation_order_survives_reopen_undo_redo_and_controls_generated_sequence() {
|
||||
let mut s = configured();
|
||||
let mut second = s.doc.manufacturing.operations[0].clone();
|
||||
second.id = 2;
|
||||
second.name = "Second facing pass".into();
|
||||
second.feed += 25.;
|
||||
s.execute("cam.operation.put", &json!({"operation":second})).unwrap();
|
||||
s.execute("cam.program.put", &json!({"program":{"id":1,"name":"Ordered milling","setup":1,"operations":[1,2]}})).unwrap();
|
||||
let job = s.execute("cam.generate", &json!({"program":1})).unwrap()["job"].clone();
|
||||
for invalid in [json!([1, 1]), json!([2]), json!([1, 3])] {
|
||||
let before = s.doc.clone();
|
||||
assert!(s.execute("cam.operation.order", &json!({"program":1,"operations":invalid})).is_err());
|
||||
assert_eq!(s.doc, before);
|
||||
}
|
||||
s.execute("cam.operation.order", &json!({"program":1,"operations":[2,1]})).unwrap();
|
||||
assert_eq!(s.execute("cam.job.status", &json!({"job":job})).unwrap()["stale"], true);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc.manufacturing.programs[0].operations, vec![1, 2]);
|
||||
assert_eq!(s.execute("cam.job.status", &json!({"job":job})).unwrap()["stale"], false);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc.manufacturing.programs[0].operations, vec![2, 1]);
|
||||
assert_eq!(s.execute("cam.job.status", &json!({"job":job})).unwrap()["stale"], true);
|
||||
let doc = crate::io::read_design(&crate::io::write_design(&s.doc)).unwrap();
|
||||
let mut reopened = Session::new(doc);
|
||||
let id = reopened.execute("cam.generate", &json!({"program":1})).unwrap()["job"].as_u64().unwrap();
|
||||
let state = jobs::job(&reopened, id).unwrap().state().unwrap();
|
||||
let path = match state {
|
||||
jobs::JobState::Generated(path) => Some(path),
|
||||
_ => None,
|
||||
}
|
||||
.unwrap();
|
||||
let mut sequence = path.blocks.iter().map(|b| b.operation).collect::<Vec<_>>();
|
||||
sequence.dedup();
|
||||
assert_eq!(sequence, vec![2, 1]);
|
||||
assert_eq!(reopened.doc.manufacturing.programs[0].operations, vec![2, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn referenced_tool_delete_rolls_back() {
|
||||
let mut s = configured();
|
||||
let d = s.doc.clone();
|
||||
assert!(s.execute("cam.delete", &json!({"kind":"tool","id":1})).is_err());
|
||||
assert_eq!(s.doc, d);
|
||||
}
|
||||
#[test]
|
||||
fn missing_body_is_not_guessed() {
|
||||
let mut s = configured();
|
||||
s.doc_mut().manufacturing.setups[0].models[0].body = "missing".into();
|
||||
assert!(s.execute("cam.generate", &json!({"program":1})).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn generate_simulate_post_and_reopen() {
|
||||
let mut s = configured();
|
||||
let r = s.execute("cam.generate", &json!({"program":1})).unwrap();
|
||||
let id = r["job"].clone();
|
||||
let v = s.execute("cam.simulate", &json!({"job":id})).unwrap();
|
||||
assert_eq!(v["verification"]["status"], "pass", "{v}");
|
||||
let posted = s.execute("cam.post", &json!({"job":id})).unwrap();
|
||||
assert!(posted["content"].as_str().unwrap().contains("G21 G90 G17 G91.1 G94 G54 G40 G49 G80\nM5\nM9\n"));
|
||||
for format in ["toolpath", "setup_sheet", "validation", "stock"] {
|
||||
assert!(!s.execute("cam.export", &json!({"job":id,"format":format})).unwrap()["content"].as_str().unwrap().is_empty());
|
||||
}
|
||||
let doc = crate::io::read_design(&crate::io::write_design(&s.doc)).unwrap();
|
||||
let mut reopened = Session::new(doc);
|
||||
assert!(reopened.execute("cam.post", &json!({"job":id})).is_err());
|
||||
assert_eq!(reopened.execute("cam.generate", &json!({"program":1})).unwrap()["state"], "generated");
|
||||
s.execute("cam.job.cancel", &json!({"job":id})).unwrap();
|
||||
assert!(s.execute("cam.post", &json!({"job":id})).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn legacy_document_has_no_manufacturing() {
|
||||
let mut value: Value = serde_json::from_slice(&crate::io::write_design(&solidcraft_doc::Document::default())).unwrap();
|
||||
value["format"] = json!("solidcraft/11");
|
||||
value.as_object_mut().unwrap().remove("manufacturing");
|
||||
let migrated = crate::io::read_design(value.to_string().as_bytes()).unwrap();
|
||||
assert!(migrated.manufacturing.is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn inconclusive_dimensions_cannot_be_posted() {
|
||||
let mut s = configured();
|
||||
let r = s.execute("cam.generate", &json!({"program":1})).unwrap();
|
||||
let id = r["job"].as_u64().unwrap();
|
||||
s.execute("cam.simulate", &json!({"job":id})).unwrap();
|
||||
let job = jobs::job(&s, id).unwrap();
|
||||
if let jobs::JobState::Verified(path, report) = job.state().unwrap() {
|
||||
let mut report = (*report).clone();
|
||||
report.finished_dimensions = cam::verify::Status::Inconclusive;
|
||||
job.set(jobs::JobState::Verified(path, std::sync::Arc::new(report)));
|
||||
}
|
||||
assert!(s.execute("cam.post", &json!({"job":id})).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn drilled_cad_body_generates_verifies_posts_and_reopens() {
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"length":20,"width":20,"height":6})).unwrap();
|
||||
s.execute("solid.hole", &json!({"position":[10,10,6],"direction":[0,0,-1],"diameter":1,"depth":1,"type":"drilled","tip_angle":118})).unwrap();
|
||||
let mut input = cam::corpus::fixture("drill").unwrap();
|
||||
input.setup.models[0].body = s.model.state().bodies[0].name.clone();
|
||||
input.operations[0].geometry = cam::Strategy::Drill { holes: vec![[10., 10.]], peck: Some(0.3), full_diameter: true };
|
||||
s.execute("cam.setup.put", &json!({"setup":input.setup})).unwrap();
|
||||
let body = s.model.state().bodies[0].clone();
|
||||
let face = (0..body.body.face_count()).find(|i| crate::kernel::hole_axis(&body.body, *i, 0.0005).is_ok()).unwrap();
|
||||
let selected =
|
||||
s.execute("cam.holes.from_selection", &json!({"setup":1,"selections":[{"kind":"face","body":body.name,"index":face}]})).unwrap();
|
||||
input.operations[0].hole_sources = serde_json::from_value(selected["sources"].clone()).unwrap();
|
||||
assert!((selected["diameters"][0].as_f64().unwrap() - 1.).abs() < 1e-8);
|
||||
let center: Vec<[f64; 2]> = serde_json::from_value(selected["holes"].clone()).unwrap();
|
||||
assert!((center[0][0] - 10.).abs() < 1e-8 && (center[0][1] - 10.).abs() < 1e-8);
|
||||
s.execute("cam.tool.put", &json!({"tool":input.tools[0]})).unwrap();
|
||||
s.execute("cam.operation.put", &json!({"operation":input.operations[0]})).unwrap();
|
||||
s.execute("cam.program.put", &json!({"program":{"id":1,"name":"Drilled plate","setup":1,"operations":[1]}})).unwrap();
|
||||
let generated = s.execute("cam.generate", &json!({"program":1})).unwrap();
|
||||
let id = generated["job"].clone();
|
||||
let verified = s.execute("cam.simulate", &json!({"job":id})).unwrap();
|
||||
assert_eq!(verified["verification"]["status"], "pass", "{verified}");
|
||||
assert_eq!(verified["verification"]["finished_dimensions"], "pass", "{verified}");
|
||||
assert!(s.execute("cam.post", &json!({"job":id})).unwrap()["content"].as_str().unwrap().contains("M30"));
|
||||
let saved = crate::io::write_design(&s.doc);
|
||||
let mut reopened = Session::new(crate::io::read_design(&saved).unwrap());
|
||||
let generated = reopened.execute("cam.generate", &json!({"program":1})).unwrap();
|
||||
let verified = reopened.execute("cam.simulate", &json!({"job":generated["job"]})).unwrap();
|
||||
assert_eq!(verified["verification"]["finished_dimensions"], "pass");
|
||||
}
|
||||
#[test]
|
||||
fn milled_cad_pocket_and_contour_generate_verify_post_and_reopen() {
|
||||
let mut model = Session::default();
|
||||
model.execute("solid.box", &json!({"length":20,"width":20,"height":6})).unwrap();
|
||||
model.execute("solid.hole", &json!({"position":[10,10,6],"direction":[0,0,-1],"diameter":16,"depth":1,"type":"simple"})).unwrap();
|
||||
model.execute("sketch.create", &json!({"plane":"XY","name":"Pocket boundary"})).unwrap();
|
||||
model.execute("sketch.circle.center", &json!({"center":[10,10],"radius":8,"id":"wall"})).unwrap();
|
||||
model.execute("sketch.finish", &json!({})).unwrap();
|
||||
let sketch = model.model.state().sketches.last().unwrap().feature;
|
||||
for name in ["contour", "pocket"] {
|
||||
let mut s = Session::new((*model.doc).clone());
|
||||
let mut input = cam::corpus::fixture(name).unwrap();
|
||||
input.setup.models[0].body = s.model.state().bodies[0].name.clone();
|
||||
input.operations[0].ramp_angle = Some(5_f64.to_radians());
|
||||
s.execute("cam.setup.put", &json!({"setup":input.setup})).unwrap();
|
||||
let selected = s.execute("cam.boundary.from_profile", &json!({"setup":1,"sketch":sketch,"profile":0})).unwrap();
|
||||
input.operations[0].source = Some(serde_json::from_value(selected["source"].clone()).unwrap());
|
||||
match &mut input.operations[0].geometry {
|
||||
cam::Strategy::Contour { boundary, .. } | cam::Strategy::Pocket { boundary } => {
|
||||
*boundary = serde_json::from_value(selected["boundary"].clone()).unwrap()
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
s.execute("cam.tool.put", &json!({"tool":input.tools[0]})).unwrap();
|
||||
s.execute("cam.operation.put", &json!({"operation":input.operations[0]})).unwrap();
|
||||
s.execute("cam.program.put", &json!({"program":{"id":1,"name":name,"setup":1,"operations":[1]}})).unwrap();
|
||||
let generated = s.execute("cam.generate", &json!({"program":1})).unwrap();
|
||||
let id = generated["job"].clone();
|
||||
let verified = s.execute("cam.simulate", &json!({"job":id})).unwrap();
|
||||
assert_eq!(verified["verification"]["status"], "pass", "{name}: {verified}");
|
||||
assert_eq!(verified["verification"]["finished_dimensions"], "pass", "{name}: {verified}");
|
||||
assert!(s.execute("cam.post", &json!({"job":id})).unwrap()["content"].as_str().unwrap().contains("M30"));
|
||||
let mut reopened = Session::new(crate::io::read_design(&crate::io::write_design(&s.doc)).unwrap());
|
||||
let generated = reopened.execute("cam.generate", &json!({"program":1})).unwrap();
|
||||
let verified = reopened.execute("cam.simulate", &json!({"job":generated["job"]})).unwrap();
|
||||
assert_eq!(verified["verification"]["finished_dimensions"], "pass", "{name}: {verified}");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn diagnostic_inspection_identifies_posted_operation_and_rejects_stale_results() {
|
||||
let mut s = configured();
|
||||
assert!(s.execute("cam.inspect", &json!({"finding":0})).is_err());
|
||||
let origin = crate::geom::Vec3::new(100., 200., 0.);
|
||||
let mut setup = s.doc.manufacturing.setups[0].clone();
|
||||
setup.frame.origin = origin;
|
||||
setup.stock.min -= origin;
|
||||
setup.stock.max -= origin;
|
||||
s.execute("cam.setup.put", &json!({"setup":setup})).unwrap();
|
||||
let mut operation = s.doc.manufacturing.operations[0].clone();
|
||||
operation.bottom = 3.;
|
||||
s.execute("cam.operation.put", &json!({"operation":operation})).unwrap();
|
||||
let id = s.execute("cam.generate", &json!({"program":1})).unwrap()["job"].clone();
|
||||
assert!(s.execute("cam.inspect", &json!({"job":id,"finding":0})).is_err());
|
||||
let report = s.execute("cam.simulate", &json!({"job":id})).unwrap();
|
||||
assert_eq!(report["verification"]["status"], "fail");
|
||||
let index = report["verification"]["findings"].as_array().unwrap().iter().position(|f| f["source"].is_object()).unwrap();
|
||||
let inspected = s.execute("cam.inspect", &json!({"job":id,"finding":index})).unwrap();
|
||||
let diagnostic = &inspected["diagnostic"];
|
||||
assert_eq!(diagnostic["operation"]["id"], 1);
|
||||
assert_eq!(diagnostic["playback"]["completed"], diagnostic["finding"]["source"]["block"]);
|
||||
assert!(diagnostic["gcode_line"].as_str().unwrap().starts_with("G1"));
|
||||
let at: crate::geom::Vec3 = serde_json::from_value(diagnostic["finding"]["at"].clone()).unwrap();
|
||||
assert_eq!(diagnostic["world"], json!(at + origin));
|
||||
assert!(s.execute("cam.inspect", &json!({"job":id,"finding":-1})).is_err());
|
||||
assert!(s.execute("cam.inspect", &json!({"job":id,"finding":999999})).is_err());
|
||||
assert!(s.execute("cam.post", &json!({"job":id})).is_err());
|
||||
let exported = s.execute("cam.export", &json!({"job":id,"format":"validation"})).unwrap();
|
||||
assert!(exported["content"].as_str().unwrap().contains("source"));
|
||||
s.execute("solid.box", &json!({"length":1,"width":1,"height":1})).unwrap();
|
||||
assert!(s.execute("cam.inspect", &json!({"job":id,"finding":index})).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "cam_holes_tests.rs"]
|
||||
mod holes_tests;
|
||||
@@ -1,201 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use super::*;
|
||||
use crate::geom::Vec3;
|
||||
fn run(s: &mut Session, cmd: &str, value: Value) -> Value {
|
||||
s.execute(cmd, &value).unwrap()
|
||||
}
|
||||
fn linked_profiles() -> (Session, u64) {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length":20,"width":20,"height":6}));
|
||||
run(&mut s, "sketch.create", json!({"plane":"XY"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center":[4,5],"radius":0.5,"id":"a"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center":[12,15],"radius":0.5,"id":"b"}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let state = s.model.state();
|
||||
let sketch = state.sketches.last().unwrap();
|
||||
let id = sketch.feature;
|
||||
let a = sketch.profiles.iter().position(|p| p.curve_ids().contains(&"a".to_string())).unwrap();
|
||||
let b = sketch.profiles.iter().position(|p| p.curve_ids().contains(&"b".to_string())).unwrap();
|
||||
let mut input = cam::corpus::fixture("drill").unwrap();
|
||||
input.setup.models[0].body = state.bodies[0].name.clone();
|
||||
input.setup.frame.origin = Vec3::new(100., -50., 0.);
|
||||
input.setup.stock.min -= input.setup.frame.origin;
|
||||
input.setup.stock.max -= input.setup.frame.origin;
|
||||
run(&mut s, "cam.setup.put", json!({"setup":input.setup}));
|
||||
let selected = run(
|
||||
&mut s,
|
||||
"cam.holes.from_selection",
|
||||
json!({"setup":1,"selections":[
|
||||
{"kind":"profile","sketch":id,"index":b},{"kind":"profile","sketch":id,"index":a}
|
||||
]}),
|
||||
);
|
||||
let holes: Vec<[f64; 2]> = serde_json::from_value(selected["holes"].clone()).unwrap();
|
||||
assert_eq!(holes, vec![[-88., 65.], [-96., 55.]]);
|
||||
input.operations[0].hole_sources = serde_json::from_value(selected["sources"].clone()).unwrap();
|
||||
input.operations[0].geometry = cam::Strategy::Drill { holes, peck: Some(0.3), full_diameter: false };
|
||||
run(&mut s, "cam.tool.put", json!({"tool":input.tools[0]}));
|
||||
run(&mut s, "cam.operation.put", json!({"operation":input.operations[0]}));
|
||||
run(&mut s, "cam.program.put", json!({"program":{"id":1,"name":"Selected holes","setup":1,"operations":[1]}}));
|
||||
(s, id)
|
||||
}
|
||||
fn centers(s: &mut Session) -> (u64, Vec<[f64; 2]>) {
|
||||
let generated = run(s, "cam.generate", json!({"program":1}));
|
||||
assert_eq!(generated["state"], "generated", "{generated}");
|
||||
let id = generated["job"].as_u64().unwrap();
|
||||
let job = jobs::job(s, id).unwrap();
|
||||
let op = &job.input().unwrap().operations[0];
|
||||
let holes = match &op.geometry {
|
||||
cam::Strategy::Drill { holes, .. } => holes.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
(id, holes)
|
||||
}
|
||||
#[test]
|
||||
fn hole_selection_follows_profile_edits_frames_undo_and_reopen() {
|
||||
let (mut s, sketch) = linked_profiles();
|
||||
let (old, before) = centers(&mut s);
|
||||
let saved = crate::io::write_design(&s.doc);
|
||||
let mut reopened = Session::new(crate::io::read_design(&saved).unwrap());
|
||||
assert_eq!(centers(&mut reopened).1, before);
|
||||
run(&mut s, "sketch.edit", json!({"sketch":sketch}));
|
||||
run(&mut s, "sketch.move", json!({"entities":["a"],"translate":[2,3]}));
|
||||
assert_eq!(run(&mut s, "cam.job.status", json!({"job":old}))["stale"], true);
|
||||
assert!(s.execute("cam.post", &json!({"job":old})).is_err());
|
||||
assert_eq!(centers(&mut s).1, vec![[-88., 65.], [-94., 58.]]);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(centers(&mut s).1, before);
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
assert_eq!(centers(&mut s).1, vec![[-88., 65.], [-94., 58.]]);
|
||||
// Unassociated drilling preserves manually authored coordinates on reopen.
|
||||
let mut legacy: Value = serde_json::from_slice(&crate::io::write_design(&s.doc)).unwrap();
|
||||
legacy["manufacturing"]["operations"][0].as_object_mut().unwrap().remove("hole_sources");
|
||||
let doc = crate::io::read_design(legacy.to_string().as_bytes()).unwrap();
|
||||
assert!(doc.manufacturing.operations[0].hole_sources.is_empty());
|
||||
assert_eq!(doc.manufacturing.operations[0].geometry, s.doc.manufacturing.operations[0].geometry);
|
||||
}
|
||||
#[test]
|
||||
fn hole_selection_rejects_missing_duplicate_tilted_and_wrong_size_sources() {
|
||||
let (mut s, sketch) = linked_profiles();
|
||||
let pick = json!({"kind":"profile","sketch":sketch,"index":0});
|
||||
for picks in [json!([]), json!([pick.clone(), pick.clone()]), json!([{"kind":"profile","sketch":sketch,"index":999}])] {
|
||||
assert!(s.execute("cam.holes.from_selection", &json!({"setup":1,"selections":picks})).is_err());
|
||||
}
|
||||
let excessive = vec![pick.clone(); cam::MAX_ITEMS + 1];
|
||||
assert!(s.execute("cam.holes.from_selection", &json!({"setup":1,"selections":excessive})).is_err());
|
||||
let mut invalid = s.doc.manufacturing.operations[0].clone();
|
||||
invalid.hole_sources.pop();
|
||||
assert!(s.execute("cam.operation.put", &json!({"operation":invalid})).is_err());
|
||||
let mut invalid = s.doc.manufacturing.operations[0].clone();
|
||||
invalid.hole_sources[1] = invalid.hole_sources[0].clone();
|
||||
assert!(s.execute("cam.operation.put", &json!({"operation":invalid})).is_err());
|
||||
let mut setup = s.doc.manufacturing.setups[0].clone();
|
||||
setup.frame = cam::Frame { origin: Vec3::ZERO, x: Vec3::X, y: Vec3::Z, z: -Vec3::Y };
|
||||
run(&mut s, "cam.setup.put", json!({"setup":setup}));
|
||||
assert!(s.execute("cam.holes.from_selection", &json!({"setup":1,"selections":[pick]})).is_err());
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
let mut tool = s.doc.manufacturing.tools[0].clone();
|
||||
tool.diameter = 0.8;
|
||||
run(&mut s, "cam.tool.put", json!({"tool":tool}));
|
||||
let failed = run(&mut s, "cam.generate", json!({"program":1}));
|
||||
assert_eq!(failed["state"], "failed");
|
||||
assert!(failed["error"].as_str().unwrap().contains("diameter"));
|
||||
assert!(s.execute("cam.post", &json!({"job":failed["job"]})).is_err());
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
let mut op = s.doc.manufacturing.operations[0].clone();
|
||||
if let cam::HoleSource::Profile { profile } = &mut op.hole_sources[0] {
|
||||
profile.curves = vec!["missing".into()];
|
||||
}
|
||||
run(&mut s, "cam.operation.put", json!({"operation":op}));
|
||||
assert!(s.execute("cam.generate", &json!({"program":1})).unwrap_err().to_string().contains("reselect"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hole_selection_keeps_exact_occurrence_and_rejects_lost_faces() {
|
||||
let mut s = Session::default();
|
||||
let component = run(&mut s, "component.create", json!({"name":"Drilled part"}));
|
||||
run(&mut s, "solid.box", json!({"length":20,"width":20,"height":6}));
|
||||
run(&mut s, "solid.hole", json!({"position":[10,10,6],"direction":[0,0,-1],"diameter":1,"depth":1,"type":"drilled","tip_angle":118}));
|
||||
run(&mut s, "component.activate", json!({"component":0}));
|
||||
let second = run(&mut s, "occurrence.copy", json!({"component":component["component"],"translate":[30,0,0]}))["occurrence"].as_u64().unwrap();
|
||||
let body = s.model.state().bodies[0].clone();
|
||||
let face = (0..body.body.face_count()).find(|i| crate::kernel::hole_axis(&body.body, *i, 0.0005).is_ok()).unwrap();
|
||||
let mut input = cam::corpus::fixture("drill").unwrap();
|
||||
input.setup.models = vec![cam::BodyRef { body: body.name.clone(), occurrence: vec![second] }];
|
||||
input.setup.frame.origin = Vec3::new(20., 0., 0.);
|
||||
input.setup.stock.min.x += 10.;
|
||||
input.setup.stock.max.x += 20.;
|
||||
run(&mut s, "cam.setup.put", json!({"setup":input.setup}));
|
||||
let selected = run(
|
||||
&mut s,
|
||||
"cam.holes.from_selection",
|
||||
json!({"setup":1,"selections":[{"kind":"face","body":body.name,"index":face,"occurrence":[second]}]}),
|
||||
);
|
||||
// Viewport labels resolve through the same setup occurrence as explicit command picks.
|
||||
let state = s.model.state();
|
||||
let placed = s.placed_document();
|
||||
let label = placed.world_body_name(&state, &body.name, placed.model_body_component(&body), &[second]);
|
||||
let from_viewport = run(&mut s, "cam.holes.from_selection", json!({"setup":1,"selections":[{"kind":"face","body":label,"index":face}]}));
|
||||
assert_eq!(from_viewport, selected);
|
||||
assert!(crate::kernel::hole_axis(&body.body, face, f64::NAN).is_err());
|
||||
assert!(crate::kernel::hole_axis(&body.body, usize::MAX, 0.0005).is_err());
|
||||
let plane = (0..body.body.face_count())
|
||||
.find(|i| matches!(crate::kernel::face_surface(&body.body, *i), Some(crate::kernel::FaceSurface::Plane { .. })))
|
||||
.unwrap();
|
||||
assert!(
|
||||
s.execute(
|
||||
"cam.holes.from_selection",
|
||||
&json!({"setup":1,"selections":[{"kind":"face","body":body.name,"index":plane,"occurrence":[second]}]})
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
input.operations[0].hole_sources = serde_json::from_value(selected["sources"].clone()).unwrap();
|
||||
run(&mut s, "cam.tool.put", json!({"tool":input.tools[0]}));
|
||||
run(&mut s, "cam.operation.put", json!({"operation":input.operations[0]}));
|
||||
run(&mut s, "cam.program.put", json!({"program":{"id":1,"name":"Placed holes","setup":1,"operations":[1]}}));
|
||||
let (job, before) = centers(&mut s);
|
||||
assert!((before[0][0] - 20.).abs() < 1e-8 && (before[0][1] - 10.).abs() < 1e-8);
|
||||
run(&mut s, "occurrence.move", json!({"occurrence":second,"translate":[5,0,0],"capture":false}));
|
||||
assert_eq!(run(&mut s, "cam.job.status", json!({"job":job}))["stale"], true);
|
||||
let moved = centers(&mut s).1;
|
||||
assert!((moved[0][0] - 25.).abs() < 1e-8);
|
||||
let mut op = s.doc.manufacturing.operations[0].clone();
|
||||
if let cam::HoleSource::Face { name, .. } = &mut op.hole_sources[0] {
|
||||
*name = "missing:face".into();
|
||||
}
|
||||
run(&mut s, "cam.operation.put", json!({"operation":op}));
|
||||
let failed = run(&mut s, "cam.generate", json!({"program":1}));
|
||||
assert_eq!(failed["state"], "failed");
|
||||
assert!(failed["error"].as_str().unwrap().contains("reselect"));
|
||||
assert!(s.execute("cam.post", &json!({"job":failed["job"]})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hole_selection_combines_seam_faces_but_rejects_exterior_cylinders() {
|
||||
let mut s = Session::default();
|
||||
s.run_script(&crate::sample::script()).unwrap();
|
||||
let body = s.model.state().bodies[0].clone();
|
||||
let mut setup = cam::corpus::fixture("drill").unwrap().setup;
|
||||
setup.models[0].body = body.name.clone();
|
||||
run(&mut s, "cam.setup.put", json!({"setup":setup}));
|
||||
let mut picks = vec![];
|
||||
for index in 0..body.body.face_count() {
|
||||
if let Ok((origin, _, _)) = crate::kernel::hole_axis(&body.body, index, 0.0005) {
|
||||
if (origin.x - 68.).abs() < 1e-8 {
|
||||
picks.push(json!({"kind":"face","body":body.name,"index":index}));
|
||||
}
|
||||
} else if matches!(crate::kernel::face_surface(&body.body, index), Some(crate::kernel::FaceSurface::Cylinder { .. })) {
|
||||
assert!(
|
||||
s.execute("cam.holes.from_selection", &json!({"setup":1,"selections":[{"kind":"face","body":body.name,"index":index}]})).is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(picks.len() >= 2, "the sample must exercise multiple seam faces on one drilled wall");
|
||||
let mut duplicate = picks.clone();
|
||||
duplicate.push(picks[1].clone());
|
||||
assert!(s.execute("cam.holes.from_selection", &json!({"setup":1,"selections":duplicate})).is_err());
|
||||
let result = run(&mut s, "cam.holes.from_selection", json!({"setup":1,"selections":picks}));
|
||||
let centers: Vec<[f64; 2]> = serde_json::from_value(result["holes"].clone()).unwrap();
|
||||
assert_eq!(centers.len(), 1);
|
||||
assert!((centers[0][0] - 68.).abs() < 1e-8 && (centers[0][1] - 25.).abs() < 1e-8);
|
||||
assert_eq!(result["sources"].as_array().unwrap().len(), 1);
|
||||
}
|
||||
@@ -21,7 +21,6 @@ macro_rules! planned {
|
||||
}
|
||||
|
||||
mod browser;
|
||||
mod cam;
|
||||
mod clipboard;
|
||||
mod compare;
|
||||
mod component;
|
||||
@@ -253,7 +252,6 @@ pub fn command_specs() -> Vec<&'static CommandSpec> {
|
||||
v.extend(component_link::COMMANDS.iter());
|
||||
v.extend(joints::COMMANDS.iter());
|
||||
v.extend(motion::COMMANDS.iter());
|
||||
v.extend(cam::COMMANDS.iter());
|
||||
v.extend(config::COMMANDS.iter());
|
||||
v.extend(parts::COMMANDS.iter());
|
||||
v.extend(browser::COMMANDS.iter());
|
||||
|
||||
Reference in new issue
Block a user