forked from 1b-agent/opencadstudio-upstream
Complete Shape host model coverage
This commit is contained in:
1 parent
3387031038
commit
54322e00e3
7 files changed
+271
-23
No files matched your search
@@ -585,7 +585,10 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
|
||||
("Insert", "insert_point" | "x_scale" | "y_scale" | "z_scale" | "rotation" | "normal" |
|
||||
"column_count" | "row_count" | "column_spacing" | "row_spacing") |
|
||||
("Tolerance", "insertion_point" | "direction" | "normal" | "text" |
|
||||
"dimension_style_name" | "text_height" | "dimension_gap")) {
|
||||
"dimension_style_name" | "text_height" | "dimension_gap") |
|
||||
("Shape", "insertion_point" | "size" | "shape_name" | "shape_number" |
|
||||
"rotation" | "relative_x_scale" | "oblique_angle" | "normal" |
|
||||
"thickness" | "style_name")) {
|
||||
"transaction_geometry"
|
||||
} else { "type_conversion_only" };
|
||||
properties.push(PropertyCoverage {
|
||||
|
||||
@@ -19,11 +19,13 @@
|
||||
"Solid": ["layer", "first_corner", "second_corner", "third_corner", "fourth_corner", "normal", "thickness", "is_trace"],
|
||||
"Face3D": ["layer", "first_corner", "second_corner", "third_corner", "fourth_corner", "invisible_edges"],
|
||||
"Insert": ["layer", "insert_point", "x_scale", "y_scale", "z_scale", "rotation", "normal", "column_count", "row_count", "column_spacing", "row_spacing"],
|
||||
"Tolerance": ["layer", "insertion_point", "direction", "normal", "text", "dimension_style_name", "text_height", "dimension_gap"]
|
||||
"Tolerance": ["layer", "insertion_point", "direction", "normal", "text", "dimension_style_name", "text_height", "dimension_gap"],
|
||||
"Shape": ["layer", "insertion_point", "size", "shape_name", "shape_number", "rotation", "relative_x_scale", "oblique_angle", "normal", "thickness", "style_name"]
|
||||
},
|
||||
"readable": {
|
||||
"Insert": ["block_name"],
|
||||
"Tolerance": ["dimension_style_handle"]
|
||||
"Tolerance": ["dimension_style_handle"],
|
||||
"Shape": ["style_handle"]
|
||||
},
|
||||
"aliases": {
|
||||
"Polyline": {"closed": "flags"},
|
||||
|
||||
@@ -90,6 +90,22 @@ pub fn validate_new_canvas_entity(entity: &crate::host::EntityType) -> Result<()
|
||||
}
|
||||
if !value.dimension_gap.is_finite() { return Err("Tolerance.dimension_gap must be finite".into()); }
|
||||
}
|
||||
EntityType::Shape(value) => {
|
||||
finite_vector("Shape.insertion_point", &value.insertion_point)?;
|
||||
solid_normal("Shape.normal", &value.normal)?;
|
||||
if !value.size.is_finite() || value.size <= 0.0 { return Err("Shape.size must be finite and greater than zero".into()); }
|
||||
if value.shape_name.trim().is_empty() { return Err("Shape.shape_name is empty".into()); }
|
||||
if !(1..=i16::MAX as i32).contains(&value.shape_number) { return Err("Shape.shape_number must be between 1 and 32767".into()); }
|
||||
if !value.rotation.is_finite() { return Err("Shape.rotation must be finite".into()); }
|
||||
if !value.relative_x_scale.is_finite() || value.relative_x_scale.abs() < 1e-12 {
|
||||
return Err("Shape.relative_x_scale must be finite and nonzero".into());
|
||||
}
|
||||
if !value.oblique_angle.is_finite() || value.oblique_angle.abs() >= std::f64::consts::FRAC_PI_2 {
|
||||
return Err("Shape.oblique_angle must be finite and between -PI/2 and PI/2".into());
|
||||
}
|
||||
if !value.thickness.is_finite() { return Err("Shape.thickness must be finite".into()); }
|
||||
if value.style_name.trim().is_empty() { return Err("Shape.style_name is empty".into()); }
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
@@ -228,10 +244,38 @@ pub fn validate_entity_mutation(
|
||||
return Err("Tolerance.dimension_gap must be finite".into());
|
||||
}
|
||||
if old.dimension_style_handle != new.dimension_style_handle
|
||||
&& !(old.dimension_style_name != new.dimension_style_name && new.dimension_style_handle.is_none()) {
|
||||
&& old.dimension_style_name == new.dimension_style_name {
|
||||
return Err("Tolerance.dimension_style_handle is read-only".into());
|
||||
}
|
||||
}
|
||||
(EntityType::Shape(old), EntityType::Shape(new)) => {
|
||||
changed3("Shape.insertion_point", &old.insertion_point, &new.insertion_point)?;
|
||||
if !same3(&old.normal, &new.normal) { solid_normal("Shape.normal", &new.normal)?; }
|
||||
if old.size.to_bits() != new.size.to_bits() && (!new.size.is_finite() || new.size <= 0.0) {
|
||||
return Err("Shape.size must be finite and greater than zero".into());
|
||||
}
|
||||
if old.shape_name != new.shape_name && new.shape_name.trim().is_empty() { return Err("Shape.shape_name is empty".into()); }
|
||||
if old.shape_number != new.shape_number && !(1..=i16::MAX as i32).contains(&new.shape_number) {
|
||||
return Err("Shape.shape_number must be between 1 and 32767".into());
|
||||
}
|
||||
if old.rotation.to_bits() != new.rotation.to_bits() && !new.rotation.is_finite() { return Err("Shape.rotation must be finite".into()); }
|
||||
if old.relative_x_scale.to_bits() != new.relative_x_scale.to_bits()
|
||||
&& (!new.relative_x_scale.is_finite() || new.relative_x_scale.abs() < 1e-12) {
|
||||
return Err("Shape.relative_x_scale must be finite and nonzero".into());
|
||||
}
|
||||
if old.oblique_angle.to_bits() != new.oblique_angle.to_bits()
|
||||
&& (!new.oblique_angle.is_finite() || new.oblique_angle.abs() >= std::f64::consts::FRAC_PI_2) {
|
||||
return Err("Shape.oblique_angle must be finite and between -PI/2 and PI/2".into());
|
||||
}
|
||||
if old.thickness.to_bits() != new.thickness.to_bits() && !new.thickness.is_finite() {
|
||||
return Err("Shape.thickness must be finite".into());
|
||||
}
|
||||
if old.style_name != new.style_name && new.style_name.trim().is_empty() { return Err("Shape.style_name is empty".into()); }
|
||||
if old.style_handle != new.style_handle
|
||||
&& !(old.style_name != new.style_name && new.style_handle.is_some()) {
|
||||
return Err("Shape.style_handle is read-only".into());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
@@ -252,9 +296,42 @@ pub fn validate_canvas_entity_references(
|
||||
);
|
||||
if !found { return Err(format!("Tolerance dimension style {:?} does not exist", value.dimension_style_name)); }
|
||||
}
|
||||
if let EntityType::Shape(value) = entity {
|
||||
let style = value.style_handle.filter(|handle| !handle.is_null())
|
||||
.and_then(|handle| document.text_styles.iter().find(|style| style.handle == handle))
|
||||
.or_else(|| document.text_styles.iter().find(|style| style.name.eq_ignore_ascii_case(value.style_name.trim())))
|
||||
.ok_or_else(|| format!("Shape text style {:?} does not exist", value.style_name))?;
|
||||
if !style.is_shape_file { return Err(format!("Shape text style {:?} is not a shape-file style", style.name)); }
|
||||
if style.font_file.trim().is_empty() { return Err(format!("Shape text style {:?} has no SHX file", style.name)); }
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fill name-based table references with stable handles before a new entity or
|
||||
/// replacement is committed. Existing non-null handles remain authoritative
|
||||
/// for legacy DWG entities whose fallback name can be stale.
|
||||
#[cfg(feature = "host")]
|
||||
pub fn bind_canvas_entity_references(
|
||||
document: &crate::host::CadDocument,
|
||||
entity: &mut crate::host::EntityType,
|
||||
) -> Result<(), String> {
|
||||
use crate::host::EntityType;
|
||||
match entity {
|
||||
EntityType::Tolerance(value) if value.dimension_style_handle.filter(|h| !h.is_null()).is_none() => {
|
||||
value.dimension_style_handle = document.dim_styles.iter()
|
||||
.find(|style| style.name.eq_ignore_ascii_case(value.dimension_style_name.trim()))
|
||||
.map(|style| style.handle);
|
||||
}
|
||||
EntityType::Shape(value) if value.style_handle.filter(|h| !h.is_null()).is_none() => {
|
||||
value.style_handle = document.text_styles.iter()
|
||||
.find(|style| style.name.eq_ignore_ascii_case(value.style_name.trim()))
|
||||
.map(|style| style.handle);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
validate_canvas_entity_references(document, entity)
|
||||
}
|
||||
|
||||
/// Clone a canvas entity with a new layer, preserving its other snapshot
|
||||
/// fields. Internal records and opaque fallbacks are excluded.
|
||||
#[cfg(feature = "host")]
|
||||
@@ -305,7 +382,7 @@ mod tests {
|
||||
if entry.scope != EntityScope::Canvas || !matches!(entry.kind.as_str(),
|
||||
"Point" | "Line" | "Circle" | "Arc" | "Ellipse" | "Polyline" |
|
||||
"Polyline2D" | "Polyline3D" | "LwPolyline" | "Spline" | "Text" | "MText" |
|
||||
"Ray" | "XLine" | "Solid" | "Face3D" | "Insert" | "Tolerance") {
|
||||
"Ray" | "XLine" | "Solid" | "Face3D" | "Insert" | "Tolerance" | "Shape") {
|
||||
assert!(entry.properties.iter().all(|p| p.model_access != ModelAccess::ReadWrite
|
||||
|| (entry.scope == EntityScope::Canvas && p.name == "layer")), "{}", entry.kind);
|
||||
}
|
||||
@@ -341,6 +418,11 @@ mod tests {
|
||||
"Tolerance.normal".to_owned(), "Tolerance.text".to_owned(),
|
||||
"Tolerance.dimension_style_name".to_owned(), "Tolerance.text_height".to_owned(),
|
||||
"Tolerance.dimension_gap".to_owned(),
|
||||
"Shape.insertion_point".to_owned(), "Shape.size".to_owned(),
|
||||
"Shape.shape_name".to_owned(), "Shape.shape_number".to_owned(),
|
||||
"Shape.rotation".to_owned(), "Shape.relative_x_scale".to_owned(),
|
||||
"Shape.oblique_angle".to_owned(), "Shape.normal".to_owned(),
|
||||
"Shape.thickness".to_owned(), "Shape.style_name".to_owned(),
|
||||
]));
|
||||
}
|
||||
|
||||
@@ -364,6 +446,31 @@ mod tests {
|
||||
.unwrap_err().contains("does not exist"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[test]
|
||||
fn shape_geometry_and_shape_file_style_are_validated_and_bound() {
|
||||
use crate::host::acadrust::{self, tables::TableEntry, types::Vector3};
|
||||
let mut document = acadrust::CadDocument::new();
|
||||
let mut style = acadrust::tables::TextStyle::new("Symbols");
|
||||
style.set_handle(document.allocate_handle());
|
||||
style.is_shape_file = true;
|
||||
style.font_file = "symbols.shx".into();
|
||||
let style_handle = style.handle;
|
||||
document.text_styles.add(style).unwrap();
|
||||
let mut shape = acadrust::entities::Shape::with_style(
|
||||
Vector3::new(1.0, 2.0, 0.0), "ARROW", "Symbols", 2.0, 0.5);
|
||||
shape.shape_number = 1;
|
||||
let mut entity = acadrust::EntityType::Shape(shape);
|
||||
validate_new_canvas_entity(&entity).unwrap();
|
||||
bind_canvas_entity_references(&document, &mut entity).unwrap();
|
||||
assert!(matches!(entity, acadrust::EntityType::Shape(ref value)
|
||||
if value.style_handle == Some(style_handle)));
|
||||
let acadrust::EntityType::Shape(mut bad) = entity else { unreachable!() };
|
||||
bad.size = 0.0;
|
||||
assert!(validate_new_canvas_entity(&acadrust::EntityType::Shape(bad))
|
||||
.unwrap_err().contains("greater than zero"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_write_coverage_matches_canvas_scope() {
|
||||
let catalog: EntityCoverageCatalog = serde_json::from_str(get_embedded_entity_coverage_json()).unwrap();
|
||||
|
||||
@@ -107,3 +107,5 @@ Start with the **Phase 8 integration harness and coverage ledger**. Re-evaluate
|
||||
- Added `doc.create_entity(kind, **properties)` and `doc.delete_entity(entity_or_handle)` to the high-level Python document model. Unit tests cover forwarding and managed identity fields. The real staged-plugin Line lifecycle now uses these methods rather than low-level `ocs.add`/`remove_entity`.
|
||||
- Closed the first new-kind increment for `Tolerance`. The host coverage catalog now maps insertion point, direction, normal, text, dimension style name, text height, and gap; validates geometry and document style references; keeps the style handle read-only; and leaves the undocumented DWG short unmapped. The thin adapter adds generated conversion plus stale style-handle clearing when the style name changes.
|
||||
- `staged_python_tolerance_lifecycle_over_real_ipc` passes the full C/R/E/D/U/I/W/V/P gate with the actual runner: high-level create/read/transaction edit/delete, live canvas selection, undo/redo, invalid direction and missing style rollback, untouched-field preservation, and DWG/DXF reopen. Tolerance is recorded as **Complete** in the ledger. The queue now continues with `Shape`.
|
||||
- Closed `Shape` with a generated SHX fixture and a real shape-file `TextStyle`. Host-owned binding resolves the Python style name to the stable table handle required by DWG; validation rejects missing/non-shape styles and invalid geometry. The adapter maps all stable shape fields and keeps the bound style handle read-only.
|
||||
- `staged_python_shape_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P through the actual runner, checks the real SHX linework before and after DWG/DXF reopen, and proves that invalid size/style edits remain atomic. The DXF renderer fallback now searches named `is_shape_file` styles as well as legacy unnamed styles. Shape is **Complete**; the queue now continues with `AttributeDefinition`.
|
||||
@@ -1,6 +1,6 @@
|
||||
# OCS canvas kind coverage ledger
|
||||
|
||||
Use with [the completion plan](plugin-host-model-completion-plan.md). This ledger is current as of 19 September 2026: **18 of 43 canvas kinds have explicit geometry writes** (17 creatable, plus update-only `Insert`), **1 kind is complete**, and 25 remain layer-only. `Mapped` means at least one non-layer field is exposed for Python writes; it does **not** mean the kind passes the completion gate. `Update-only` means Python creation is unavailable. `Layer-only` means raw inspection and canvas layer edits exist, but geometry is not mapped. Record actual evidence or a blocking issue before changing any status to `Complete`.
|
||||
Use with [the completion plan](plugin-host-model-completion-plan.md). This ledger is current as of 19 September 2026: **19 of 43 canvas kinds have explicit geometry writes** (18 creatable, plus update-only `Insert`), **2 kinds are complete**, and 24 remain layer-only. `Mapped` means at least one non-layer field is exposed for Python writes; it does **not** mean the kind passes the completion gate. `Update-only` means Python creation is unavailable. `Layer-only` means raw inspection and canvas layer edits exist, but geometry is not mapped. Record actual evidence or a blocking issue before changing any status to `Complete`.
|
||||
|
||||
Completion gate codes: **C** = create through Python document model; **R** = read; **E** = edit defining geometry and another property; **D** = delete; **U** = undo/redo and atomic failure; **I** = real IPC plus GUI/canvas pick or observable behavior; **W** = DWG and DXF save/reopen; **V** = invalid-input checks and untouched-field/reference preservation; **P** = portable H7 v5 build. A kind is `Complete` only when **C/R/E/D/U/I/W/V/P** all pass and its property catalog matches implementation. `Blocked` requires a specific dependency or failing fixture.
|
||||
|
||||
@@ -24,7 +24,7 @@ Completion gate codes: **C** = create through Python document model; **R** = rea
|
||||
| Face3D | Mapped | Focused DWG round trip; full gate pending. |
|
||||
| Insert | Update-only | Transform transaction and undo tested; Python creation, deletion, real IPC/GUI and DWG/DXF gates pending. |
|
||||
| Tolerance | **Complete** | `staged_python_tolerance_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P with the staged API v7 plugin: high-level create/read/transaction edit/delete; insertion, direction, and text edits; live canvas selection; grouped undo/redo; invalid direction and missing style atomic rollback; exact untouched common/style/private-field preservation; and edited DWG plus DXF reopen. Host-owned coverage exposes placement, direction, normal, text, style name, height and gap; style handle is read-only and the undocumented DWG short remains unmapped. `tolerance_geometry_and_style_references_are_validated` and the existing Tolerance tessellation tests cover reference and rendered-frame behavior. |
|
||||
| Shape | Layer-only | Requires valid shape/style fixture. |
|
||||
| Shape | **Complete** | `staged_python_shape_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P with a generated parser-valid SHX file and a real shape-file `TextStyle`: high-level create/read/transaction edit/delete; insertion, size, rotation and width-factor edits; live canvas selection; grouped undo/redo; invalid size and missing style rollback; exact identity/style/common-field preservation; and DWG plus DXF reopen. Each live and reopened document renders the three-point SHX glyph rather than the placeholder. Host-owned reference binding writes the stable style handle needed by DWG. The DXF fallback now searches all `is_shape_file` styles, preserving named-style glyph resolution after reopen. |
|
||||
| AttributeDefinition | Layer-only | Requires block-definition fixture. |
|
||||
| AttributeEntity | Layer-only | Requires linked Insert and attribute-sequence fixture. |
|
||||
| Hatch | Layer-only | Requires valid boundary and pattern fixtures. |
|
||||
|
||||
+149
-15
@@ -132,13 +132,13 @@ impl<'a> HostSession<'a> {
|
||||
pub fn update_entities_transaction(
|
||||
&mut self,
|
||||
label: &str,
|
||||
entities: Vec<EntityType>,
|
||||
mut entities: Vec<EntityType>,
|
||||
) -> Result<(), String> {
|
||||
use std::collections::HashSet;
|
||||
if label.trim().is_empty() { return Err("transaction label is empty".into()); }
|
||||
if entities.is_empty() { return Ok(()); }
|
||||
let mut seen = HashSet::new();
|
||||
for entity in &entities {
|
||||
for entity in &mut entities {
|
||||
let handle = entity.common().handle;
|
||||
if handle.is_null() || !seen.insert(handle) {
|
||||
return Err(format!("null or duplicate entity handle: {handle:?}"));
|
||||
@@ -152,11 +152,11 @@ impl<'a> HostSession<'a> {
|
||||
if existing.common().owner_handle != entity.common().owner_handle {
|
||||
return Err(format!("entity {handle:?} changes owner"));
|
||||
}
|
||||
ocs_plugin_api::entity_coverage::validate_entity_mutation(existing, entity)
|
||||
.map_err(|error| format!("entity {handle:?}: {error}"))?;
|
||||
ocs_plugin_api::entity_coverage::validate_canvas_entity_references(
|
||||
ocs_plugin_api::entity_coverage::bind_canvas_entity_references(
|
||||
self.document(), entity,
|
||||
).map_err(|error| format!("entity {handle:?}: {error}"))?;
|
||||
ocs_plugin_api::entity_coverage::validate_entity_mutation(existing, entity)
|
||||
.map_err(|error| format!("entity {handle:?}: {error}"))?;
|
||||
if self.app.tabs[self.tab].scene.is_layer_locked(handle) {
|
||||
return Err(format!("entity {handle:?} is on a locked layer"));
|
||||
}
|
||||
@@ -644,6 +644,7 @@ fn plugin_step_to_result(step: ocs_plugin_api::host::CommandStep) -> crate::comm
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app::OpenCADStudio;
|
||||
use crate::entities::traits::RenderConvertible;
|
||||
use acadrust::entities::{Line, Point};
|
||||
use acadrust::xdata::XDataValue;
|
||||
use ocs_plugin_api::host::DocumentReader;
|
||||
@@ -891,6 +892,133 @@ mod tests {
|
||||
assert!(app.tabs[0].scene.document.get_entity(handle).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_python_shape_lifecycle_over_real_ipc() {
|
||||
let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else { return; };
|
||||
let runner_path = std::env::var_os("OCS_PLUGIN_RUNNER_EXE")
|
||||
.expect("set OCS_PLUGIN_RUNNER_EXE to the built OpenCADStudio executable");
|
||||
assert!(std::path::Path::new(&runner_path).is_file());
|
||||
|
||||
let shx_path = std::env::temp_dir().join(format!("ocs_host_shape_{}.shx", std::process::id()));
|
||||
let mut shx = b"AutoCAD-86 shapes 1.0\r\n\x1A".to_vec();
|
||||
for value in [1_u16, 1, 1, 1, 9] { shx.extend_from_slice(&value.to_le_bytes()); }
|
||||
shx.extend_from_slice(b"ARROW\0\x10\x14\0");
|
||||
std::fs::write(&shx_path, shx).unwrap();
|
||||
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.tabs[0].is_start = false;
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
let style_handle = host.document_mut().allocate_handle();
|
||||
let mut style = acadrust::tables::TextStyle::new("TestShapes");
|
||||
style.handle = style_handle;
|
||||
style.is_shape_file = true;
|
||||
style.font_file = shx_path.to_string_lossy().into_owned();
|
||||
host.document_mut().text_styles.add(style).unwrap();
|
||||
|
||||
let process = ocs_plugin_api::process::PluginProcess::spawn(
|
||||
std::path::Path::new(&plugin_path), &mut host,
|
||||
crate::plugin::v4_support::notification_handler(),
|
||||
).expect("spawn staged Python plugin");
|
||||
let dispatch = |host: &mut HostSession<'_>, command: &str| {
|
||||
assert!(process.dispatch(host, command, &mut |_| {}).expect("Python dispatch"));
|
||||
};
|
||||
dispatch(&mut host, concat!(
|
||||
"PY_EVAL ocs.active_document.create_entity('Shape',",
|
||||
"insertion_point={'x':1.0,'y':2.0,'z':0.0},size=2.0,",
|
||||
"shape_name='ARROW',shape_number=1,style_name='TestShapes').handle"
|
||||
));
|
||||
let handle = host.document().entities().next().expect("Python created a Shape")
|
||||
.common().handle;
|
||||
let Some(EntityType::Shape(created)) = host.document().get_entity(handle) else {
|
||||
panic!("expected Shape");
|
||||
};
|
||||
assert_eq!(created.style_handle, Some(style_handle));
|
||||
let created = created.clone();
|
||||
dispatch(&mut host, &format!(
|
||||
"PY_EVAL ocs.active_document.entities[{}].shape_name", handle.value()));
|
||||
assert!(host.app.command_line.history.last().unwrap().text.contains("ARROW"));
|
||||
|
||||
let script_path = std::env::temp_dir().join(format!(
|
||||
"ocs_shape_document_model_{}.py", std::process::id()));
|
||||
std::fs::write(&script_path, format!(concat!(
|
||||
"doc = ocs.active_document\n",
|
||||
"shape = doc.entities[{}]\n",
|
||||
"with doc.transaction('Edit shape'):\n",
|
||||
" shape.insertion_point = (4.0, 5.0, 0.0)\n",
|
||||
" shape.size = 3.0\n",
|
||||
" shape.rotation = 0.5\n",
|
||||
" shape.relative_x_scale = 1.5\n",
|
||||
"doc.selection = [shape]\n"
|
||||
), handle.value())).unwrap();
|
||||
dispatch(&mut host, &format!("PY_RUN {}", script_path.display()));
|
||||
let _ = std::fs::remove_file(&script_path);
|
||||
let expected = host.document().get_entity(handle).unwrap().clone();
|
||||
let EntityType::Shape(edited) = &expected else { unreachable!() };
|
||||
assert_eq!(edited.insertion_point, acadrust::types::Vector3::new(4.0, 5.0, 0.0));
|
||||
assert_eq!(edited.size, 3.0);
|
||||
assert_eq!(edited.rotation, 0.5);
|
||||
assert_eq!(edited.relative_x_scale, 1.5);
|
||||
assert_eq!(edited.common, created.common);
|
||||
assert_eq!(edited.shape_name, created.shape_name);
|
||||
assert_eq!(edited.shape_number, created.shape_number);
|
||||
assert_eq!(edited.style_name, created.style_name);
|
||||
assert_eq!(edited.style_handle, created.style_handle);
|
||||
assert_eq!(edited.normal, created.normal);
|
||||
assert_eq!(edited.thickness, created.thickness);
|
||||
assert_eq!(host.selection(), vec![handle]);
|
||||
|
||||
let assert_real_glyph = |entity: &EntityType, document: &CadDocument| {
|
||||
let EntityType::Shape(shape) = entity else { panic!("expected Shape"); };
|
||||
let render = shape.to_render(document).expect("shape render");
|
||||
let crate::scene::convert::acad_to_render::RenderObject::Lines(points) = render.object else {
|
||||
panic!("expected shape linework");
|
||||
};
|
||||
assert_eq!(points.len(), 3, "expected SHX glyph rather than diamond placeholder");
|
||||
assert!(points.iter().flatten().all(|value| value.is_finite()));
|
||||
};
|
||||
assert_real_glyph(&expected, host.document());
|
||||
|
||||
dispatch(&mut host, &format!(
|
||||
"PY_EVAL ocs.update_many('Reject size', [{{'handle':{},'size':0.0}}])",
|
||||
handle.value()));
|
||||
assert_eq!(host.document().get_entity(handle), Some(&expected));
|
||||
dispatch(&mut host, &format!(
|
||||
"PY_EVAL ocs.update_many('Reject style', [{{'handle':{},'style_name':'Missing'}}])",
|
||||
handle.value()));
|
||||
assert_eq!(host.document().get_entity(handle), Some(&expected));
|
||||
assert!(host.app.command_line.history.last().unwrap().text.contains("does not exist"));
|
||||
|
||||
let dwg_bytes = acadrust::DwgWriter::write_to_vec(host.document()).unwrap();
|
||||
let dwg_doc = crate::io::load_bytes("shape.dwg", dwg_bytes).unwrap();
|
||||
let dwg_entity = dwg_doc.get_entity(handle).expect("Shape survives DWG");
|
||||
assert!(matches!(dwg_entity, EntityType::Shape(value)
|
||||
if value.shape_number == 1 && value.size == 3.0 && value.rotation == 0.5));
|
||||
assert_real_glyph(dwg_entity, &dwg_doc);
|
||||
let dxf_bytes = acadrust::DxfWriter::new(host.document()).write_to_vec().unwrap();
|
||||
let dxf_doc = crate::io::load_bytes("shape.dxf", dxf_bytes).unwrap();
|
||||
let dxf_entity = dxf_doc.get_entity(handle).expect("Shape survives DXF");
|
||||
assert!(matches!(dxf_entity, EntityType::Shape(value)
|
||||
if value.shape_name == "ARROW" && value.size == 3.0 && (value.rotation - 0.5).abs() < 1e-12));
|
||||
assert_real_glyph(dxf_entity, &dxf_doc);
|
||||
|
||||
dispatch(&mut host, &format!("PY_EVAL ocs.active_document.delete_entity({})", handle.value()));
|
||||
assert!(host.document().get_entity(handle).is_none());
|
||||
drop(process);
|
||||
drop(host);
|
||||
let _ = std::fs::remove_file(&shx_path);
|
||||
app.finish_pending_history(0);
|
||||
assert_eq!(app.tabs[0].history.undo_stack.len(), 3);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(app.tabs[0].scene.document.get_entity(handle), Some(&expected));
|
||||
app.undo_steps(1);
|
||||
assert!(matches!(app.tabs[0].scene.document.get_entity(handle), Some(EntityType::Shape(value))
|
||||
if value.insertion_point.x == 1.0 && value.size == 2.0));
|
||||
app.undo_steps(1);
|
||||
assert!(app.tabs[0].scene.document.get_entity(handle).is_none());
|
||||
app.redo_steps(3);
|
||||
assert!(app.tabs[0].scene.document.get_entity(handle).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_python_point_pick_and_cancel_over_real_ipc() {
|
||||
let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else { return; };
|
||||
@@ -1019,17 +1147,23 @@ mod tests {
|
||||
// Notifications are best-effort and consumed by the runner's reader
|
||||
// thread before its next Dispatch. Give that thread a bounded handoff.
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
{
|
||||
let mut host = HostSession::new(&mut app, 1);
|
||||
assert!(process.dispatch(&mut host, "PY_EVAL ocs.active_document.poll_events()", &mut |_| {}).unwrap());
|
||||
}
|
||||
let other_events = &app.command_line.history.last().unwrap().text;
|
||||
let poll_events_until = |app: &mut OpenCADStudio, tab: usize, epoch: &str| {
|
||||
let mut output = String::new();
|
||||
for _ in 0..10 {
|
||||
{
|
||||
let mut host = HostSession::new(app, tab);
|
||||
assert!(process.dispatch(&mut host,
|
||||
"PY_EVAL ocs.active_document.poll_events()", &mut |_| {}).unwrap());
|
||||
}
|
||||
output = app.command_line.history.last().unwrap().text.clone();
|
||||
if output.contains(epoch) { break; }
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
output
|
||||
};
|
||||
let other_events = poll_events_until(&mut app, 1, "202");
|
||||
assert!(other_events.contains("202") && !other_events.contains("101"), "{other_events}");
|
||||
{
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
assert!(process.dispatch(&mut host, "PY_EVAL ocs.active_document.poll_events()", &mut |_| {}).unwrap());
|
||||
}
|
||||
let first_events = &app.command_line.history.last().unwrap().text;
|
||||
let first_events = poll_events_until(&mut app, 0, "101");
|
||||
assert!(first_events.contains("101") && !first_events.contains("202"), "{first_events}");
|
||||
|
||||
for epoch in 0..257 {
|
||||
|
||||
@@ -119,7 +119,7 @@ fn shx_polylines(
|
||||
// glyph named like this shape.
|
||||
if !name.is_empty() {
|
||||
for style in document.text_styles.iter() {
|
||||
if style.name.is_empty() && !style.font_file.trim().is_empty() {
|
||||
if style.is_shape_file && !style.font_file.trim().is_empty() {
|
||||
if let Some(p) = try_style(style) {
|
||||
return Some(p);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user