forked from 1b-agent/opencadstudio-upstream
Merge branch 'main' into feature/parametric-constraint-system
# Conflicts: # src/app/automation.rs # src/app/mod.rs # src/ui/ribbon/widgets.rs
This commit is contained in:
commit
e2d39b049d
114 files changed
+13641
-1685
No files matched your search
+272
-78
@@ -18,7 +18,7 @@
|
||||
use std::io::{BufRead, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::OpenCADStudio;
|
||||
|
||||
@@ -138,6 +138,14 @@ fn v3(v: acadrust::types::Vector3) -> Value {
|
||||
json!([v.x, v.y, v.z])
|
||||
}
|
||||
|
||||
fn entity_type_matches(entity: &acadrust::EntityType, requested: &str) -> bool {
|
||||
if crate::entities::names::ui_name(entity).eq_ignore_ascii_case(requested) {
|
||||
return true;
|
||||
}
|
||||
let record_name = crate::entities::names::dxf_name(entity);
|
||||
record_name != "ENTITY" && record_name.eq_ignore_ascii_case(requested)
|
||||
}
|
||||
|
||||
/// One entity as JSON. Summary mode carries identity only, geometry adds the
|
||||
/// entity's defining values, and full also includes its world bounds.
|
||||
fn entity_json(e: &acadrust::EntityType, detail: &str) -> Value {
|
||||
@@ -194,15 +202,30 @@ fn entity_json(e: &acadrust::EntityType, detail: &str) -> Value {
|
||||
}
|
||||
E::Insert(ins) => {
|
||||
map.insert("block".into(), json!(ins.block_name));
|
||||
map.insert("position".into(), v3(ins.insert_point));
|
||||
let attributes: serde_json::Map<String, Value> = ins
|
||||
.attributes
|
||||
.iter()
|
||||
.map(|attribute| (attribute.tag.clone(), json!(attribute.value)))
|
||||
.collect();
|
||||
map.insert("attributes".into(), Value::Object(attributes));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if detail == "full" {
|
||||
let bounds = e.as_entity().bounding_box();
|
||||
map.insert("bounds".into(), json!({
|
||||
"min":[bounds.min.x,bounds.min.y,bounds.min.z],
|
||||
"max":[bounds.max.x,bounds.max.y,bounds.max.z]
|
||||
}));
|
||||
map.insert(
|
||||
"bounds".into(),
|
||||
json!({
|
||||
"min":[bounds.min.x,bounds.min.y,bounds.min.z],
|
||||
"max":[bounds.max.x,bounds.max.y,bounds.max.z]
|
||||
}),
|
||||
);
|
||||
if let Ok(Value::Object(wrapper)) = serde_json::to_value(e) {
|
||||
if let Some((_, properties)) = wrapper.into_iter().next() {
|
||||
map.insert("properties".into(), properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
obj
|
||||
}
|
||||
@@ -218,16 +241,24 @@ fn request_point(req: &Value, key: &str) -> Option<[f64; 2]> {
|
||||
}
|
||||
|
||||
fn request_handle(value: &Value) -> Option<acadrust::Handle> {
|
||||
value.as_str()
|
||||
.and_then(|value| u64::from_str_radix(value.trim_start_matches("0x"), 16).ok())
|
||||
value
|
||||
.as_str()
|
||||
.and_then(|value| {
|
||||
let value = value
|
||||
.strip_prefix("0x")
|
||||
.or_else(|| value.strip_prefix("0X"))
|
||||
.unwrap_or(value);
|
||||
u64::from_str_radix(value, 16).ok()
|
||||
})
|
||||
.map(acadrust::Handle::new)
|
||||
}
|
||||
|
||||
fn projected_fields(mut entity: Value, fields: Option<&Vec<Value>>) -> Value {
|
||||
let Some(fields) = fields else { return entity };
|
||||
let Some(source) = entity.as_object_mut() else { return entity };
|
||||
let keep: std::collections::HashSet<&str> =
|
||||
fields.iter().filter_map(Value::as_str).collect();
|
||||
let Some(source) = entity.as_object_mut() else {
|
||||
return entity;
|
||||
};
|
||||
let keep: std::collections::HashSet<&str> = fields.iter().filter_map(Value::as_str).collect();
|
||||
source.retain(|key, _| key == "handle" || keep.contains(key.as_str()));
|
||||
entity
|
||||
}
|
||||
@@ -244,7 +275,9 @@ impl OpenCADStudio {
|
||||
return err(error);
|
||||
}
|
||||
if matches!(response["status"].as_str(), Some("accepted" | "running")) {
|
||||
return self.control_request(json!({"op":"operation","request_id":id})).0;
|
||||
return self
|
||||
.control_request(json!({"op":"operation","request_id":id}))
|
||||
.0;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
@@ -327,7 +360,9 @@ impl OpenCADStudio {
|
||||
let i = self.active_tab;
|
||||
let before = self.tabs[i].scene.document.entities().count();
|
||||
let error_revision = self.command_line.error_revision;
|
||||
if let Err(error) = self.run_headless(&cmd) { return err(error); }
|
||||
if let Err(error) = self.run_headless(&cmd) {
|
||||
return err(error);
|
||||
}
|
||||
if self.command_line.error_revision != error_revision {
|
||||
return err(self.command_line.last_error.clone().unwrap_or_default());
|
||||
}
|
||||
@@ -342,13 +377,15 @@ impl OpenCADStudio {
|
||||
}
|
||||
"entities" => self.entity_summary(),
|
||||
"query" => self.entity_query(&req),
|
||||
"records" => self.record_query(&req),
|
||||
"record_schema" => self.record_schema(&req),
|
||||
"capabilities" => self.record_capabilities(),
|
||||
"layers" => {
|
||||
let i = self.active_tab;
|
||||
let offset = req["offset"].as_u64().unwrap_or(0) as usize;
|
||||
let limit = req["limit"].as_u64().unwrap_or(1000).min(10_000) as usize;
|
||||
let count = self.tabs[i].scene.document.layers.iter().count();
|
||||
let layers: Vec<Value> = self
|
||||
.tabs[i]
|
||||
let layers: Vec<Value> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
@@ -408,8 +445,14 @@ impl OpenCADStudio {
|
||||
// By explicit handles (hex, as returned by `query`).
|
||||
if let Some(arr) = req["handles"].as_array() {
|
||||
for h in arr.iter().filter_map(|h| h.as_str()) {
|
||||
if let Ok(v) = u64::from_str_radix(h.trim_start_matches("0x"), 16) {
|
||||
self.tabs[i].scene.select_entity(acadrust::Handle::new(v), false);
|
||||
let h = h
|
||||
.strip_prefix("0x")
|
||||
.or_else(|| h.strip_prefix("0X"))
|
||||
.unwrap_or(h);
|
||||
if let Ok(v) = u64::from_str_radix(h, 16) {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.select_entity(acadrust::Handle::new(v), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,11 +464,7 @@ impl OpenCADStudio {
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter(|e| {
|
||||
type_filter.is_none_or(|t| {
|
||||
crate::entities::names::ui_name(e).eq_ignore_ascii_case(t)
|
||||
})
|
||||
})
|
||||
.filter(|e| type_filter.is_none_or(|t| entity_type_matches(e, t)))
|
||||
.filter(|e| layer_filter.is_none_or(|l| e.common().layer == l))
|
||||
.map(|e| e.common().handle)
|
||||
.collect();
|
||||
@@ -446,8 +485,7 @@ impl OpenCADStudio {
|
||||
return err("save: no \"path\" and the document has none");
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let result =
|
||||
self.save_tab_synchronously_protected(i, path.clone(), true);
|
||||
let result = self.save_tab_synchronously_protected(i, path.clone(), true);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let result = crate::io::save(&self.tabs[i].scene.document, &path)
|
||||
.map_err(crate::io::SaveFailure::other);
|
||||
@@ -472,19 +510,30 @@ impl OpenCADStudio {
|
||||
self.drive_headless_task(task)
|
||||
}
|
||||
|
||||
pub(super) fn drive_headless_task(&mut self, task: iced::Task<super::Message>) -> Result<(), String> {
|
||||
pub(super) fn drive_headless_task(
|
||||
&mut self,
|
||||
task: iced::Task<super::Message>,
|
||||
) -> Result<(), String> {
|
||||
use iced::futures::StreamExt;
|
||||
let mut streams = Vec::new();
|
||||
if let Some(stream) = iced_runtime::task::into_stream(task) { streams.push(stream); }
|
||||
if let Some(stream) = iced_runtime::task::into_stream(task) {
|
||||
streams.push(stream);
|
||||
}
|
||||
while let Some(stream) = streams.last_mut() {
|
||||
match iced::futures::executor::block_on(stream.next()) {
|
||||
Some(iced_runtime::Action::Output(message)) => {
|
||||
let next = self.update(message);
|
||||
if let Some(stream) = iced_runtime::task::into_stream(next) { streams.push(stream); }
|
||||
if let Some(stream) = iced_runtime::task::into_stream(next) {
|
||||
streams.push(stream);
|
||||
}
|
||||
}
|
||||
Some(iced_runtime::Action::Widget(_)) | Some(iced_runtime::Action::Tick) | Some(iced_runtime::Action::Reload) => {},
|
||||
Some(iced_runtime::Action::Widget(_))
|
||||
| Some(iced_runtime::Action::Tick)
|
||||
| Some(iced_runtime::Action::Reload) => {}
|
||||
Some(action) => return Err(format!("GUI runtime required for {action:?}")),
|
||||
None => { streams.pop(); }
|
||||
None => {
|
||||
streams.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.finish_all_pending_history();
|
||||
@@ -540,20 +589,36 @@ impl OpenCADStudio {
|
||||
let layer_filter = req["layer"].as_str();
|
||||
let handles: Option<std::collections::HashSet<u64>> = req["handles"]
|
||||
.as_array()
|
||||
.map(|values| values.iter().filter_map(request_handle).map(|h| h.value()).collect())
|
||||
.or_else(|| request_handle(&req["handle"])
|
||||
.map(|handle| std::iter::once(handle.value()).collect()));
|
||||
.map(|values| {
|
||||
values
|
||||
.iter()
|
||||
.filter_map(request_handle)
|
||||
.map(|h| h.value())
|
||||
.collect()
|
||||
})
|
||||
.or_else(|| {
|
||||
request_handle(&req["handle"])
|
||||
.map(|handle| std::iter::once(handle.value()).collect())
|
||||
});
|
||||
let near = request_point(req, "near");
|
||||
let contains = request_point(req, "contains_point");
|
||||
let bounds = req["bounds"].as_array().and_then(|values| {
|
||||
(values.len() == 4).then(|| Some([
|
||||
values[0].as_f64()?, values[1].as_f64()?,
|
||||
values[2].as_f64()?, values[3].as_f64()?,
|
||||
])).flatten()
|
||||
(values.len() == 4)
|
||||
.then(|| {
|
||||
Some([
|
||||
values[0].as_f64()?,
|
||||
values[1].as_f64()?,
|
||||
values[2].as_f64()?,
|
||||
values[3].as_f64()?,
|
||||
])
|
||||
})
|
||||
.flatten()
|
||||
});
|
||||
if req.get("handles").is_some() && handles.as_ref().is_some_and(|parsed| {
|
||||
parsed.len() != req["handles"].as_array().map_or(0, Vec::len)
|
||||
}) {
|
||||
if req.get("handles").is_some()
|
||||
&& handles
|
||||
.as_ref()
|
||||
.is_some_and(|parsed| parsed.len() != req["handles"].as_array().map_or(0, Vec::len))
|
||||
{
|
||||
return err("query handles contains an invalid hexadecimal handle");
|
||||
}
|
||||
if req.get("handle").is_some() && request_handle(&req["handle"]).is_none() {
|
||||
@@ -565,10 +630,13 @@ impl OpenCADStudio {
|
||||
if req.get("contains_point").is_some() && contains.is_none() {
|
||||
return err("query contains_point expects two or three finite coordinates");
|
||||
}
|
||||
if req.get("bounds").is_some() && bounds.is_none_or(|bounds| {
|
||||
!bounds.iter().all(|value| value.is_finite())
|
||||
|| bounds[0] > bounds[2] || bounds[1] > bounds[3]
|
||||
}) {
|
||||
if req.get("bounds").is_some()
|
||||
&& bounds.is_none_or(|bounds| {
|
||||
!bounds.iter().all(|value| value.is_finite())
|
||||
|| bounds[0] > bounds[2]
|
||||
|| bounds[1] > bounds[3]
|
||||
})
|
||||
{
|
||||
return err("query bounds expects finite [min_x,min_y,max_x,max_y]");
|
||||
}
|
||||
let detail = req["detail"].as_str().unwrap_or("geometry");
|
||||
@@ -580,39 +648,47 @@ impl OpenCADStudio {
|
||||
|
||||
let mut matched = Vec::new();
|
||||
for e in tab.scene.document.entities() {
|
||||
if handles.as_ref().is_some_and(|handles| {
|
||||
!handles.contains(&e.common().handle.value())
|
||||
}) {
|
||||
if handles
|
||||
.as_ref()
|
||||
.is_some_and(|handles| !handles.contains(&e.common().handle.value()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if type_filter.is_some_and(|value| {
|
||||
!crate::entities::names::ui_name(e).eq_ignore_ascii_case(value)
|
||||
}) || layer_filter.is_some_and(|value| e.common().layer != value) {
|
||||
if type_filter.is_some_and(|value| !entity_type_matches(e, value))
|
||||
|| layer_filter.is_some_and(|value| e.common().layer != value)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(bounds) = bounds {
|
||||
let entity_bounds = e.as_entity().bounding_box();
|
||||
if entity_bounds.max.x < bounds[0] || entity_bounds.max.y < bounds[1]
|
||||
|| entity_bounds.min.x > bounds[2] || entity_bounds.min.y > bounds[3]
|
||||
if entity_bounds.max.x < bounds[0]
|
||||
|| entity_bounds.max.y < bounds[1]
|
||||
|| entity_bounds.min.x > bounds[2]
|
||||
|| entity_bounds.min.y > bounds[3]
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let curve = (near.is_some() || contains.is_some())
|
||||
.then(|| crate::entities::curve::entity_curve_xy(e)).flatten();
|
||||
.then(|| crate::entities::curve::entity_curve_xy(e))
|
||||
.flatten();
|
||||
if let Some(point) = contains {
|
||||
let Some(curve) = curve.as_ref().filter(|curve| curve.is_closed()) else {
|
||||
continue;
|
||||
};
|
||||
if !cadkernel::geom2d::contains(
|
||||
std::slice::from_ref(curve), point,
|
||||
std::slice::from_ref(curve),
|
||||
point,
|
||||
cadkernel::geom2d::Tolerance::default(),
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let nearest = near.and_then(|point| curve.as_ref()
|
||||
.map(|curve| cadkernel::geom2d::closest_point(curve, point)));
|
||||
let nearest = near.and_then(|point| {
|
||||
curve
|
||||
.as_ref()
|
||||
.map(|curve| cadkernel::geom2d::closest_point(curve, point))
|
||||
});
|
||||
if near.is_some() && nearest.is_none() {
|
||||
continue;
|
||||
}
|
||||
@@ -620,20 +696,29 @@ impl OpenCADStudio {
|
||||
if let Some(nearest) = nearest {
|
||||
let object = entity.as_object_mut().expect("entity JSON object");
|
||||
object.insert("distance".into(), json!(nearest.distance));
|
||||
object.insert("closest_point".into(),
|
||||
json!([nearest.point[0], nearest.point[1]]));
|
||||
object.insert(
|
||||
"closest_point".into(),
|
||||
json!([nearest.point[0], nearest.point[1]]),
|
||||
);
|
||||
object.insert("parameter".into(), json!(nearest.t));
|
||||
}
|
||||
matched.push((nearest.map(|nearest| nearest.distance), entity));
|
||||
}
|
||||
if near.is_some() {
|
||||
matched.sort_by(|left, right| left.0.partial_cmp(&right.0)
|
||||
.unwrap_or(std::cmp::Ordering::Equal));
|
||||
matched.sort_by(|left, right| {
|
||||
left.0
|
||||
.partial_cmp(&right.0)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
}
|
||||
let count = matched.len();
|
||||
let fields = req["fields"].as_array();
|
||||
let entities: Vec<Value> = matched.into_iter().skip(offset).take(limit)
|
||||
.map(|(_, entity)| projected_fields(entity, fields)).collect();
|
||||
let entities: Vec<Value> = matched
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.map(|(_, entity)| projected_fields(entity, fields))
|
||||
.collect();
|
||||
json!({
|
||||
"ok": true,
|
||||
"document_id":tab.id,
|
||||
@@ -668,7 +753,10 @@ mod tests {
|
||||
fn layout_notice_skips_grid_camera_and_scene_builds() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
assert_eq!(app.automation_op(r#"{"op":"new"}"#)["ok"], true);
|
||||
assert_eq!(app.automation_op(r#"{"op":"run","cmd":"LINE 0,0 10,10"}"#)["ok"], true);
|
||||
assert_eq!(
|
||||
app.automation_op(r#"{"op":"run","cmd":"LINE 0,0 10,10"}"#)["ok"],
|
||||
true
|
||||
);
|
||||
let i = app.active_tab;
|
||||
let scene = &mut app.tabs[i].scene;
|
||||
scene.document.add_layout("Review").unwrap();
|
||||
@@ -749,6 +837,44 @@ mod tests {
|
||||
assert_eq!(app.automation_op(r#"{"op":"run"}"#)["ok"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_reference_query_exposes_instance_attributes() {
|
||||
use acadrust::entities::{AttributeEntity, EntityType, Insert};
|
||||
use acadrust::types::Vector3;
|
||||
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
let i = app.active_tab;
|
||||
let mut insert = Insert::new("A_CPT", Vector3::new(12.0, 34.0, 5.0));
|
||||
insert.common.layer = "Equipment".to_string();
|
||||
insert
|
||||
.attributes
|
||||
.push(AttributeEntity::simple("COMPANY", "BPH"));
|
||||
insert
|
||||
.attributes
|
||||
.push(AttributeEntity::simple("STATUS", "ACTIVE"));
|
||||
app.tabs[i].scene.add_entity(EntityType::Insert(insert));
|
||||
|
||||
let result = app
|
||||
.automation_op(r#"{"op":"query","type":"Insert","layer":"Equipment","detail":"full"}"#);
|
||||
assert_eq!(result["count"], 1);
|
||||
assert_eq!(result["entities"][0]["type"], "Block Reference");
|
||||
assert_eq!(result["entities"][0]["block"], "A_CPT");
|
||||
assert_eq!(
|
||||
result["entities"][0]["position"],
|
||||
serde_json::json!([12.0, 34.0, 5.0])
|
||||
);
|
||||
assert_eq!(result["entities"][0]["attributes"]["COMPANY"], "BPH");
|
||||
assert_eq!(result["entities"][0]["attributes"]["STATUS"], "ACTIVE");
|
||||
assert_eq!(
|
||||
result["entities"][0]["properties"]["attributes"][0]["value"],
|
||||
"BPH"
|
||||
);
|
||||
|
||||
let selected = app.automation_op(r#"{"op":"select","type":"Insert"}"#);
|
||||
assert_eq!(selected["selected"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ucs_interactive_inline_args() {
|
||||
// `UCS Z 90` must drive the interactive UCS command step-by-step (option
|
||||
@@ -800,9 +926,7 @@ mod tests {
|
||||
fn tilted_ucs_places_planar_entities_with_the_plane_normal() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
app.automation_op(
|
||||
r#"{"op":"run","cmd":"UCS 3POINT 0,0,0 1,0,0 0,0,1"}"#,
|
||||
);
|
||||
app.automation_op(r#"{"op":"run","cmd":"UCS 3POINT 0,0,0 1,0,0 0,0,1"}"#);
|
||||
app.automation_op(r#"{"op":"run","cmd":"CIRCLE 2,3 1"}"#);
|
||||
|
||||
let circle = app.tabs[app.active_tab]
|
||||
@@ -842,7 +966,10 @@ mod tests {
|
||||
h.linetype_scale
|
||||
);
|
||||
// No command should be left dangling.
|
||||
assert!(app.tabs[i].active_cmd.is_none(), "command must have finished");
|
||||
assert!(
|
||||
app.tabs[i].active_cmd.is_none(),
|
||||
"command must have finished"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1004,7 +1131,9 @@ mod tests {
|
||||
(20.0, 300.0),
|
||||
]
|
||||
};
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(path[0].0, path[0].1)));
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(
|
||||
path[0].0, path[0].1,
|
||||
)));
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
std::thread::sleep(std::time::Duration::from_millis(180));
|
||||
for &(x, y) in &path {
|
||||
@@ -1047,7 +1176,10 @@ mod tests {
|
||||
// Two-step: bare command then the value, like typing 1 + Enter
|
||||
// (feed_active_cmd is the same path the GUI submit offers first).
|
||||
let _ = app.run_command_line("PICKDRAG");
|
||||
assert!(app.tabs[app.active_tab].active_cmd.is_some(), "prompt must open");
|
||||
assert!(
|
||||
app.tabs[app.active_tab].active_cmd.is_some(),
|
||||
"prompt must open"
|
||||
);
|
||||
let _ = app.feed_active_cmd("1");
|
||||
assert!(app.pick_drag_rect, "PICKDRAG 1 via the prompt must switch");
|
||||
}
|
||||
@@ -1085,10 +1217,7 @@ mod tests {
|
||||
let _ = app.run_command_line("MATCHPROP");
|
||||
assert!(app.tabs[i].active_cmd.is_some(), "MATCHPROP must start");
|
||||
let _ = app.feed_command(StepInput::EntityPick(src_h, glam::DVec3::ZERO));
|
||||
let _ = app.feed_command(StepInput::SelectionComplete(vec![
|
||||
dst_text_h,
|
||||
dst_mtext_h,
|
||||
]));
|
||||
let _ = app.feed_command(StepInput::SelectionComplete(vec![dst_text_h, dst_mtext_h]));
|
||||
|
||||
let doc = &app.tabs[i].scene.document;
|
||||
match doc.get_entity(dst_text_h) {
|
||||
@@ -1140,7 +1269,10 @@ mod tests {
|
||||
app.opening.is_none(),
|
||||
"an already-open drawing must not start a load"
|
||||
);
|
||||
assert!(app.pending_opens.is_empty(), "and must not queue one either");
|
||||
assert!(
|
||||
app.pending_opens.is_empty(),
|
||||
"and must not queue one either"
|
||||
);
|
||||
|
||||
// The same file spelled differently (a `..` hop) is still the same file.
|
||||
let indirect = canon.parent().unwrap().join("..").join(
|
||||
@@ -1234,8 +1366,7 @@ mod tests {
|
||||
assert_eq!(saved["ok"], true, "{label}: {}", saved["error"]);
|
||||
let saved_again = app.automation_op(r#"{"op":"save"}"#);
|
||||
assert_eq!(
|
||||
saved_again["ok"],
|
||||
true,
|
||||
saved_again["ok"], true,
|
||||
"normal save: {}",
|
||||
saved_again["error"]
|
||||
);
|
||||
@@ -1253,10 +1384,8 @@ mod tests {
|
||||
#[test]
|
||||
fn save_then_open_round_trips() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"ocs_automation_test_{}.dxf",
|
||||
std::process::id()
|
||||
));
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("ocs_automation_test_{}.dxf", std::process::id()));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let p = path.to_string_lossy().replace('\\', "\\\\");
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
@@ -1321,4 +1450,69 @@ mod tests {
|
||||
drop(app);
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pline_line_then_arc() {
|
||||
use crate::app::Message;
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
{
|
||||
app.tabs[0].scene.selection.borrow_mut().vp_size = (1920.0, 1080.0);
|
||||
app.tabs[0].scene.sync_tiles_from_panes(1920.0, 1080.0);
|
||||
}
|
||||
|
||||
// Start PLINE
|
||||
let _ = app.update(Message::CommandInput("PLINE".to_string()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
|
||||
// Click first point (100, 100)
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(100.0, 100.0)));
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
|
||||
// Move to (200, 100) and click second point (draw line)
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 100.0)));
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
|
||||
let wid = app.main_window.unwrap_or_else(iced::window::Id::unique);
|
||||
|
||||
// Switch to arc: Option 1 - CommandOptionPick("A")
|
||||
let _ = app.update(Message::CommandOptionPick("A".to_string()));
|
||||
let _ = app.view(wid);
|
||||
println!(
|
||||
"ENTITIES: {}",
|
||||
app.tabs[0].scene.document.entities().count()
|
||||
);
|
||||
println!(
|
||||
"CMD: {:?}",
|
||||
app.tabs[0].active_cmd.as_ref().map(|c| c.name())
|
||||
);
|
||||
|
||||
// Move mouse!
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 100.0)));
|
||||
let _ = app.view(wid);
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(201.0, 100.0)));
|
||||
let _ = app.view(wid);
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 150.0)));
|
||||
let _ = app.view(wid);
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(150.0, 150.0)));
|
||||
let _ = app.view(wid);
|
||||
|
||||
// Click arc point
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
|
||||
// Move mouse again
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(150.0, 160.0)));
|
||||
|
||||
// Switch to line
|
||||
let _ = app.update(Message::CommandOptionPick("L".to_string()));
|
||||
|
||||
// Move mouse again
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(100.0, 150.0)));
|
||||
|
||||
// Finish
|
||||
let _ = app.update(Message::CommandOptionPick(String::new()));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user