forked from 1b-agent/opencadstudio-upstream
Complete Dimension host model coverage for all nine subtypes
Adds a hand-written flat Dimension model to the adapter (manifest manual_kinds hook) and a synthetic-property section to the host coverage policy. The host derives actual_measurement and the base definition point on scripted create/edit. staged_python_dimension_lifecycle_over_real_ipc passes the full gate for every subtype including DWG and DXF reopen. Python ints are now accepted for coordinates and angles. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
a86d5bc727
commit
6b4c86fd7a
13 files changed
+1281
-49
No files matched your search
@@ -21,6 +21,13 @@ use entity_coverage_schema::{
|
||||
EntityCoverageCatalog, EntityKindCoverage, EntityScope, ModelAccess, PropertyCoverage,
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct SyntheticProperty {
|
||||
name: String,
|
||||
type_id: String,
|
||||
access: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct EntityCoveragePolicy {
|
||||
internal_kinds: Vec<String>,
|
||||
@@ -28,6 +35,10 @@ struct EntityCoveragePolicy {
|
||||
editable: BTreeMap<String, Vec<String>>,
|
||||
readable: BTreeMap<String, Vec<String>>,
|
||||
aliases: BTreeMap<String, BTreeMap<String, String>>,
|
||||
/// Properties for kinds whose payload is a nested enum (Dimension), where
|
||||
/// the traced variant has no single field list to map.
|
||||
#[serde(default)]
|
||||
synthetic: BTreeMap<String, Vec<SyntheticProperty>>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -802,6 +813,19 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
|
||||
model_access: access,
|
||||
});
|
||||
}
|
||||
for synthetic in policy.synthetic.get(kind).into_iter().flatten() {
|
||||
let writable = synthetic.access == "read_write";
|
||||
properties.push(PropertyCoverage {
|
||||
name: synthetic.name.clone(),
|
||||
source_path: format!("<subtype>.{}", synthetic.name),
|
||||
type_id: synthetic.type_id.clone(),
|
||||
optional: false,
|
||||
is_sequence: false,
|
||||
snapshot_readable: true,
|
||||
validation: if writable { "transaction_geometry" } else { "none" }.into(),
|
||||
model_access: if writable { ModelAccess::ReadWrite } else { ModelAccess::ReadOnly },
|
||||
});
|
||||
}
|
||||
assert!(
|
||||
unresolved.is_empty(),
|
||||
"{kind} has coverage keys absent from registry: {unresolved:?}"
|
||||
|
||||
@@ -1,44 +1,526 @@
|
||||
{
|
||||
"internal_kinds": ["Block", "BlockEnd", "Seqend"],
|
||||
"opaque_kinds": ["Extended", "Unknown"],
|
||||
"internal_kinds": [
|
||||
"Block",
|
||||
"BlockEnd",
|
||||
"Seqend"
|
||||
],
|
||||
"opaque_kinds": [
|
||||
"Extended",
|
||||
"Unknown"
|
||||
],
|
||||
"editable": {
|
||||
"Point": ["layer", "location", "thickness", "normal", "x_axis_angle"],
|
||||
"Line": ["layer", "start", "end", "thickness", "normal"],
|
||||
"Circle": ["layer", "center", "radius", "thickness", "normal"],
|
||||
"Arc": ["layer", "center", "radius", "start_angle", "end_angle", "thickness", "normal"],
|
||||
"Ellipse": ["layer", "center", "major_axis", "minor_axis_ratio", "start_parameter", "end_parameter", "normal"],
|
||||
"Polyline": ["layer", "closed", "vertices"],
|
||||
"Polyline2D": ["layer", "closed", "smooth_surface", "start_width", "end_width", "thickness", "elevation", "normal", "vertices"],
|
||||
"Polyline3D": ["layer", "flags", "default_start_width", "default_end_width", "mesh_m_count", "mesh_n_count", "smooth_m_density", "smooth_n_density", "elevation", "normal", "vertices"],
|
||||
"LwPolyline": ["layer", "vertices", "is_closed", "plinegen", "constant_width", "elevation", "thickness", "normal"],
|
||||
"Spline": ["layer", "degree", "flags", "knots", "control_points", "weights", "fit_points", "normal", "knot_tolerance", "control_tolerance", "fit_tolerance", "begin_tangent", "end_tangent", "knot_parameterization", "cv_frame_visible", "dwg_flags1", "dxf_flags"],
|
||||
"Text": ["layer", "text", "insertion", "alignment_point", "height", "rotation", "width_factor", "oblique_angle", "style", "horizontal_alignment", "vertical_alignment", "normal", "thickness", "generation_flags"],
|
||||
"MText": ["layer", "text", "insertion", "height", "rectangle_width", "rectangle_height", "rotation", "dwg_x_direction", "style", "attachment_point", "drawing_direction", "line_spacing_factor", "line_spacing_style", "normal", "background_fill_flags", "background_scale", "background_transparency", "is_annotative", "column_data", "extents_width", "extents_height"],
|
||||
"Ray": ["layer", "base_point", "direction"],
|
||||
"XLine": ["layer", "base_point", "direction"],
|
||||
"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"],
|
||||
"Shape": ["layer", "insertion_point", "size", "shape_name", "shape_number", "rotation", "relative_x_scale", "oblique_angle", "normal", "thickness", "style_name"],
|
||||
"AttributeDefinition": ["layer", "tag", "prompt", "default_value", "insertion_point", "alignment_point", "height", "rotation", "width_factor", "oblique_angle", "text_style", "text_generation_flags", "horizontal_alignment", "vertical_alignment", "flags", "field_length", "normal", "mtext_flag", "is_multiline", "line_count", "lock_position"],
|
||||
"AttributeEntity": ["layer", "tag", "value", "insertion_point", "alignment_point", "height", "rotation", "width_factor", "oblique_angle", "text_style", "text_generation_flags", "horizontal_alignment", "vertical_alignment", "flags", "field_length", "normal", "mtext_flag", "is_multiline", "line_count", "lock_position"],
|
||||
"Hatch": ["layer", "elevation", "normal", "is_solid", "pattern", "pattern_angle", "pattern_scale", "pattern_type", "is_double", "style", "is_associative", "pixel_size", "paths", "seed_points"],
|
||||
"Leader": ["layer", "dimension_style", "arrow_enabled", "path_type", "creation_type", "hookline_direction", "hookline_enabled", "text_height", "text_width", "vertices", "annotation_handle", "normal", "horizontal_direction", "block_offset", "annotation_offset"],
|
||||
"MLine": ["layer", "flags", "justification", "normal", "scale_factor", "style_name", "vertices"]
|
||||
"Point": [
|
||||
"layer",
|
||||
"location",
|
||||
"thickness",
|
||||
"normal",
|
||||
"x_axis_angle"
|
||||
],
|
||||
"Line": [
|
||||
"layer",
|
||||
"start",
|
||||
"end",
|
||||
"thickness",
|
||||
"normal"
|
||||
],
|
||||
"Circle": [
|
||||
"layer",
|
||||
"center",
|
||||
"radius",
|
||||
"thickness",
|
||||
"normal"
|
||||
],
|
||||
"Arc": [
|
||||
"layer",
|
||||
"center",
|
||||
"radius",
|
||||
"start_angle",
|
||||
"end_angle",
|
||||
"thickness",
|
||||
"normal"
|
||||
],
|
||||
"Ellipse": [
|
||||
"layer",
|
||||
"center",
|
||||
"major_axis",
|
||||
"minor_axis_ratio",
|
||||
"start_parameter",
|
||||
"end_parameter",
|
||||
"normal"
|
||||
],
|
||||
"Polyline": [
|
||||
"layer",
|
||||
"closed",
|
||||
"vertices"
|
||||
],
|
||||
"Polyline2D": [
|
||||
"layer",
|
||||
"closed",
|
||||
"smooth_surface",
|
||||
"start_width",
|
||||
"end_width",
|
||||
"thickness",
|
||||
"elevation",
|
||||
"normal",
|
||||
"vertices"
|
||||
],
|
||||
"Polyline3D": [
|
||||
"layer",
|
||||
"flags",
|
||||
"default_start_width",
|
||||
"default_end_width",
|
||||
"mesh_m_count",
|
||||
"mesh_n_count",
|
||||
"smooth_m_density",
|
||||
"smooth_n_density",
|
||||
"elevation",
|
||||
"normal",
|
||||
"vertices"
|
||||
],
|
||||
"LwPolyline": [
|
||||
"layer",
|
||||
"vertices",
|
||||
"is_closed",
|
||||
"plinegen",
|
||||
"constant_width",
|
||||
"elevation",
|
||||
"thickness",
|
||||
"normal"
|
||||
],
|
||||
"Spline": [
|
||||
"layer",
|
||||
"degree",
|
||||
"flags",
|
||||
"knots",
|
||||
"control_points",
|
||||
"weights",
|
||||
"fit_points",
|
||||
"normal",
|
||||
"knot_tolerance",
|
||||
"control_tolerance",
|
||||
"fit_tolerance",
|
||||
"begin_tangent",
|
||||
"end_tangent",
|
||||
"knot_parameterization",
|
||||
"cv_frame_visible",
|
||||
"dwg_flags1",
|
||||
"dxf_flags"
|
||||
],
|
||||
"Text": [
|
||||
"layer",
|
||||
"text",
|
||||
"insertion",
|
||||
"alignment_point",
|
||||
"height",
|
||||
"rotation",
|
||||
"width_factor",
|
||||
"oblique_angle",
|
||||
"style",
|
||||
"horizontal_alignment",
|
||||
"vertical_alignment",
|
||||
"normal",
|
||||
"thickness",
|
||||
"generation_flags"
|
||||
],
|
||||
"MText": [
|
||||
"layer",
|
||||
"text",
|
||||
"insertion",
|
||||
"height",
|
||||
"rectangle_width",
|
||||
"rectangle_height",
|
||||
"rotation",
|
||||
"dwg_x_direction",
|
||||
"style",
|
||||
"attachment_point",
|
||||
"drawing_direction",
|
||||
"line_spacing_factor",
|
||||
"line_spacing_style",
|
||||
"normal",
|
||||
"background_fill_flags",
|
||||
"background_scale",
|
||||
"background_transparency",
|
||||
"is_annotative",
|
||||
"column_data",
|
||||
"extents_width",
|
||||
"extents_height"
|
||||
],
|
||||
"Ray": [
|
||||
"layer",
|
||||
"base_point",
|
||||
"direction"
|
||||
],
|
||||
"XLine": [
|
||||
"layer",
|
||||
"base_point",
|
||||
"direction"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"Shape": [
|
||||
"layer",
|
||||
"insertion_point",
|
||||
"size",
|
||||
"shape_name",
|
||||
"shape_number",
|
||||
"rotation",
|
||||
"relative_x_scale",
|
||||
"oblique_angle",
|
||||
"normal",
|
||||
"thickness",
|
||||
"style_name"
|
||||
],
|
||||
"AttributeDefinition": [
|
||||
"layer",
|
||||
"tag",
|
||||
"prompt",
|
||||
"default_value",
|
||||
"insertion_point",
|
||||
"alignment_point",
|
||||
"height",
|
||||
"rotation",
|
||||
"width_factor",
|
||||
"oblique_angle",
|
||||
"text_style",
|
||||
"text_generation_flags",
|
||||
"horizontal_alignment",
|
||||
"vertical_alignment",
|
||||
"flags",
|
||||
"field_length",
|
||||
"normal",
|
||||
"mtext_flag",
|
||||
"is_multiline",
|
||||
"line_count",
|
||||
"lock_position"
|
||||
],
|
||||
"AttributeEntity": [
|
||||
"layer",
|
||||
"tag",
|
||||
"value",
|
||||
"insertion_point",
|
||||
"alignment_point",
|
||||
"height",
|
||||
"rotation",
|
||||
"width_factor",
|
||||
"oblique_angle",
|
||||
"text_style",
|
||||
"text_generation_flags",
|
||||
"horizontal_alignment",
|
||||
"vertical_alignment",
|
||||
"flags",
|
||||
"field_length",
|
||||
"normal",
|
||||
"mtext_flag",
|
||||
"is_multiline",
|
||||
"line_count",
|
||||
"lock_position"
|
||||
],
|
||||
"Hatch": [
|
||||
"layer",
|
||||
"elevation",
|
||||
"normal",
|
||||
"is_solid",
|
||||
"pattern",
|
||||
"pattern_angle",
|
||||
"pattern_scale",
|
||||
"pattern_type",
|
||||
"is_double",
|
||||
"style",
|
||||
"is_associative",
|
||||
"pixel_size",
|
||||
"paths",
|
||||
"seed_points"
|
||||
],
|
||||
"Leader": [
|
||||
"layer",
|
||||
"dimension_style",
|
||||
"arrow_enabled",
|
||||
"path_type",
|
||||
"creation_type",
|
||||
"hookline_direction",
|
||||
"hookline_enabled",
|
||||
"text_height",
|
||||
"text_width",
|
||||
"vertices",
|
||||
"annotation_handle",
|
||||
"normal",
|
||||
"horizontal_direction",
|
||||
"block_offset",
|
||||
"annotation_offset"
|
||||
],
|
||||
"MLine": [
|
||||
"layer",
|
||||
"flags",
|
||||
"justification",
|
||||
"normal",
|
||||
"scale_factor",
|
||||
"style_name",
|
||||
"vertices"
|
||||
]
|
||||
},
|
||||
"readable": {
|
||||
"Insert": ["block_name"],
|
||||
"Tolerance": ["dimension_style_handle"],
|
||||
"Shape": ["style_handle"],
|
||||
"MLine": ["start_point", "style_handle", "style_element_count"],
|
||||
"Leader": ["origin", "dimension_gap", "arrowhead_type", "arrow_size", "byblock_color"],
|
||||
"AttributeEntity": ["attdef_handle"]
|
||||
"Insert": [
|
||||
"block_name"
|
||||
],
|
||||
"Tolerance": [
|
||||
"dimension_style_handle"
|
||||
],
|
||||
"Shape": [
|
||||
"style_handle"
|
||||
],
|
||||
"MLine": [
|
||||
"start_point",
|
||||
"style_handle",
|
||||
"style_element_count"
|
||||
],
|
||||
"Leader": [
|
||||
"origin",
|
||||
"dimension_gap",
|
||||
"arrowhead_type",
|
||||
"arrow_size",
|
||||
"byblock_color"
|
||||
],
|
||||
"AttributeEntity": [
|
||||
"attdef_handle"
|
||||
]
|
||||
},
|
||||
"aliases": {
|
||||
"Polyline": {"closed": "flags"},
|
||||
"Polyline2D": {"closed": "flags"},
|
||||
"Text": {"text": "value", "insertion": "insertion_point"},
|
||||
"MText": {"text": "value", "insertion": "insertion_point"}
|
||||
"Polyline": {
|
||||
"closed": "flags"
|
||||
},
|
||||
"Polyline2D": {
|
||||
"closed": "flags"
|
||||
},
|
||||
"Text": {
|
||||
"text": "value",
|
||||
"insertion": "insertion_point"
|
||||
},
|
||||
"MText": {
|
||||
"text": "value",
|
||||
"insertion": "insertion_point"
|
||||
}
|
||||
},
|
||||
"synthetic": {
|
||||
"Dimension": [
|
||||
{
|
||||
"name": "subtype",
|
||||
"type_id": "String",
|
||||
"access": "read_only"
|
||||
},
|
||||
{
|
||||
"name": "actual_measurement",
|
||||
"type_id": "f64",
|
||||
"access": "read_only"
|
||||
},
|
||||
{
|
||||
"name": "text",
|
||||
"type_id": "String",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "style_name",
|
||||
"type_id": "String",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "normal",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "text_middle_point",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "attachment_point",
|
||||
"type_id": "AttachmentPointType",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "text_rotation",
|
||||
"type_id": "f64",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "horizontal_direction",
|
||||
"type_id": "f64",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "flip_arrow1",
|
||||
"type_id": "bool",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "flip_arrow2",
|
||||
"type_id": "bool",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "text_user_positioned",
|
||||
"type_id": "bool",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "definition_point",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "first_point",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "second_point",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "angle_vertex",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "dimension_arc",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "feature_location",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "leader_endpoint",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "first_extension_point",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "second_extension_point",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "center_point",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "first_leader_point",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "second_leader_point",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "chord_point",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "override_center",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "jog_point",
|
||||
"type_id": "Vector3",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "rotation",
|
||||
"type_id": "f64",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "ext_line_rotation",
|
||||
"type_id": "f64",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "leader_length",
|
||||
"type_id": "f64",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "arc_start_parameter",
|
||||
"type_id": "f64",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "arc_end_parameter",
|
||||
"type_id": "f64",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "jog_angle",
|
||||
"type_id": "f64",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "is_ordinate_type_x",
|
||||
"type_id": "bool",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "is_partial",
|
||||
"type_id": "bool",
|
||||
"access": "read_write"
|
||||
},
|
||||
{
|
||||
"name": "has_leader",
|
||||
"type_id": "bool",
|
||||
"access": "read_write"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -377,6 +377,94 @@ fn validate_mline(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Field checks for every Dimension subtype. Derived fields (base definition
|
||||
/// point, `actual_measurement`) are recomputed by the host, so they are not
|
||||
/// validated as inputs; the measurement the geometry implies must be finite.
|
||||
#[cfg(feature = "host")]
|
||||
fn validate_dimension(
|
||||
old: Option<&crate::host::acadrust::entities::Dimension>,
|
||||
new: &crate::host::acadrust::entities::Dimension,
|
||||
) -> Result<(), String> {
|
||||
use crate::host::acadrust::entities::Dimension as D;
|
||||
if old == Some(new) {
|
||||
return Ok(());
|
||||
}
|
||||
let base = new.base();
|
||||
solid_normal("Dimension.normal", &base.normal)?;
|
||||
finite_vector("Dimension.text_middle_point", &base.text_middle_point)?;
|
||||
if !base.text_rotation.is_finite() || !base.horizontal_direction.is_finite() {
|
||||
return Err("Dimension.text_rotation and horizontal_direction must be finite".into());
|
||||
}
|
||||
if base.style_name.trim().is_empty() {
|
||||
return Err("Dimension.style_name is empty".into());
|
||||
}
|
||||
let (points, scalars, distinct): (Vec<(&str, &crate::host::acadrust::types::Vector3)>, Vec<(&str, f64)>, Vec<(&str, &str)>) = match new {
|
||||
D::Aligned(d) => (
|
||||
vec![("definition_point", &d.definition_point), ("first_point", &d.first_point), ("second_point", &d.second_point)],
|
||||
vec![("ext_line_rotation", d.ext_line_rotation)],
|
||||
vec![("first_point", "second_point")],
|
||||
),
|
||||
D::Linear(d) => (
|
||||
vec![("definition_point", &d.definition_point), ("first_point", &d.first_point), ("second_point", &d.second_point)],
|
||||
vec![("rotation", d.rotation), ("ext_line_rotation", d.ext_line_rotation)],
|
||||
vec![("first_point", "second_point")],
|
||||
),
|
||||
D::Radius(d) => (
|
||||
vec![("definition_point", &d.definition_point), ("angle_vertex", &d.angle_vertex)],
|
||||
vec![("leader_length", d.leader_length)],
|
||||
vec![("definition_point", "angle_vertex")],
|
||||
),
|
||||
D::Diameter(d) => (
|
||||
vec![("definition_point", &d.definition_point), ("angle_vertex", &d.angle_vertex)],
|
||||
vec![("leader_length", d.leader_length)],
|
||||
vec![("definition_point", "angle_vertex")],
|
||||
),
|
||||
D::Angular2Ln(d) => (
|
||||
vec![("definition_point", &d.definition_point), ("dimension_arc", &d.dimension_arc), ("first_point", &d.first_point), ("second_point", &d.second_point), ("angle_vertex", &d.angle_vertex)],
|
||||
vec![],
|
||||
vec![],
|
||||
),
|
||||
D::Angular3Pt(d) => (
|
||||
vec![("definition_point", &d.definition_point), ("first_point", &d.first_point), ("second_point", &d.second_point), ("angle_vertex", &d.angle_vertex)],
|
||||
vec![],
|
||||
vec![("first_point", "angle_vertex"), ("second_point", "angle_vertex")],
|
||||
),
|
||||
D::Ordinate(d) => (
|
||||
vec![("definition_point", &d.definition_point), ("feature_location", &d.feature_location), ("leader_endpoint", &d.leader_endpoint)],
|
||||
vec![],
|
||||
vec![],
|
||||
),
|
||||
D::Arc(d) => (
|
||||
vec![("definition_point", &d.definition_point), ("first_extension_point", &d.first_extension_point), ("second_extension_point", &d.second_extension_point), ("center_point", &d.center_point), ("first_leader_point", &d.first_leader_point), ("second_leader_point", &d.second_leader_point)],
|
||||
vec![("arc_start_parameter", d.arc_start_parameter), ("arc_end_parameter", d.arc_end_parameter)],
|
||||
vec![("first_extension_point", "second_extension_point")],
|
||||
),
|
||||
D::LargeRadial(d) => (
|
||||
vec![("definition_point", &d.definition_point), ("chord_point", &d.chord_point), ("override_center", &d.override_center), ("jog_point", &d.jog_point)],
|
||||
vec![("jog_angle", d.jog_angle)],
|
||||
vec![("definition_point", "chord_point")],
|
||||
),
|
||||
};
|
||||
for (name, point) in &points {
|
||||
finite_vector(&format!("Dimension.{name}"), point)?;
|
||||
}
|
||||
for (name, value) in &scalars {
|
||||
if !value.is_finite() {
|
||||
return Err(format!("Dimension.{name} must be finite"));
|
||||
}
|
||||
}
|
||||
let find = |name: &str| points.iter().find(|(n, _)| *n == name).map(|(_, p)| **p);
|
||||
for (a, b) in distinct {
|
||||
if find(a) == find(b) {
|
||||
return Err(format!("Dimension.{a} and {b} must not coincide"));
|
||||
}
|
||||
}
|
||||
if !new.measurement().is_finite() {
|
||||
return Err("Dimension geometry does not define a finite measurement".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate newly created geometry for the kinds whose mapped fields have
|
||||
/// host checks. Called by the Python add path before an entity enters the document.
|
||||
#[cfg(feature = "host")]
|
||||
@@ -470,6 +558,7 @@ pub fn validate_new_canvas_entity(entity: &crate::host::EntityType) -> Result<()
|
||||
}
|
||||
EntityType::Leader(value) => validate_leader(None, value)?,
|
||||
EntityType::MLine(value) => validate_mline(None, value)?,
|
||||
EntityType::Dimension(value) => validate_dimension(None, value)?,
|
||||
EntityType::AttributeDefinition(value) => {
|
||||
finite_vector(
|
||||
"AttributeDefinition.insertion_point",
|
||||
@@ -824,6 +913,7 @@ pub fn validate_entity_mutation(
|
||||
return Err("Shape.style_handle is read-only".into());
|
||||
}
|
||||
}
|
||||
(EntityType::Dimension(old), EntityType::Dimension(new)) => validate_dimension(Some(old), new)?,
|
||||
(EntityType::MLine(old), EntityType::MLine(new)) => validate_mline(Some(old), new)?,
|
||||
(EntityType::Leader(old), EntityType::Leader(new)) => validate_leader(Some(old), new)?,
|
||||
(EntityType::AttributeDefinition(old), EntityType::AttributeDefinition(new)) => {
|
||||
@@ -1087,6 +1177,12 @@ pub fn validate_canvas_entity_references(
|
||||
return Err(format!("Shape text style {:?} has no SHX file", style.name));
|
||||
}
|
||||
}
|
||||
if let EntityType::Dimension(value) = entity {
|
||||
let name = value.base().style_name.trim();
|
||||
if !document.dim_styles.iter().any(|style| style.name.eq_ignore_ascii_case(name)) {
|
||||
return Err(format!("Dimension style {:?} does not exist", value.base().style_name));
|
||||
}
|
||||
}
|
||||
if let EntityType::MLine(value) = entity {
|
||||
let style = resolve_mline_style(document, value).ok_or_else(|| {
|
||||
format!("MLine style {:?} does not exist", value.style_name)
|
||||
@@ -1443,6 +1539,7 @@ mod tests {
|
||||
| "Hatch"
|
||||
| "Leader"
|
||||
| "MLine"
|
||||
| "Dimension"
|
||||
)
|
||||
{
|
||||
assert!(
|
||||
@@ -1597,6 +1694,40 @@ mod tests {
|
||||
"MLine.scale_factor".to_owned(),
|
||||
"MLine.style_name".to_owned(),
|
||||
"MLine.vertices".to_owned(),
|
||||
"Dimension.text".to_owned(),
|
||||
"Dimension.style_name".to_owned(),
|
||||
"Dimension.normal".to_owned(),
|
||||
"Dimension.text_middle_point".to_owned(),
|
||||
"Dimension.attachment_point".to_owned(),
|
||||
"Dimension.text_rotation".to_owned(),
|
||||
"Dimension.horizontal_direction".to_owned(),
|
||||
"Dimension.flip_arrow1".to_owned(),
|
||||
"Dimension.flip_arrow2".to_owned(),
|
||||
"Dimension.text_user_positioned".to_owned(),
|
||||
"Dimension.definition_point".to_owned(),
|
||||
"Dimension.first_point".to_owned(),
|
||||
"Dimension.second_point".to_owned(),
|
||||
"Dimension.angle_vertex".to_owned(),
|
||||
"Dimension.dimension_arc".to_owned(),
|
||||
"Dimension.feature_location".to_owned(),
|
||||
"Dimension.leader_endpoint".to_owned(),
|
||||
"Dimension.first_extension_point".to_owned(),
|
||||
"Dimension.second_extension_point".to_owned(),
|
||||
"Dimension.center_point".to_owned(),
|
||||
"Dimension.first_leader_point".to_owned(),
|
||||
"Dimension.second_leader_point".to_owned(),
|
||||
"Dimension.chord_point".to_owned(),
|
||||
"Dimension.override_center".to_owned(),
|
||||
"Dimension.jog_point".to_owned(),
|
||||
"Dimension.rotation".to_owned(),
|
||||
"Dimension.ext_line_rotation".to_owned(),
|
||||
"Dimension.leader_length".to_owned(),
|
||||
"Dimension.arc_start_parameter".to_owned(),
|
||||
"Dimension.arc_end_parameter".to_owned(),
|
||||
"Dimension.jog_angle".to_owned(),
|
||||
"Dimension.is_ordinate_type_x".to_owned(),
|
||||
"Dimension.is_partial".to_owned(),
|
||||
"Dimension.has_leader".to_owned(),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ Build a general Python document model over the OCS host API for all **43 canvas
|
||||
As of 20 September 2026, the working branch is `plugin/host-model-api` at
|
||||
`a8dd3640` plus the Leader increment, published as draft PR
|
||||
[#1391](https://github.com/HakanSeven12/OpenCADStudio/pull/1391). Host API is
|
||||
v7. The Python feature has explicit writes for **24 of 43** canvas kinds: 23
|
||||
are creatable and `Insert` remains update-only. This is **not** 24 completed
|
||||
kinds: Tolerance, Shape, AttributeEntity, Hatch and MLine pass every completion gate.
|
||||
v7. The Python feature has explicit writes for **25 of 43** canvas kinds: 24
|
||||
are creatable and `Insert` remains update-only. This is **not** 25 completed
|
||||
kinds: Tolerance, Shape, AttributeEntity, Hatch, MLine and Dimension pass every completion gate.
|
||||
AttributeDefinition passes its host and real IPC lifecycle but remains short of
|
||||
`Complete` because the previously pinned CAD codec dropped optional ATTDEF
|
||||
fields during DXF reads; revalidate that blocker against the current
|
||||
`acadrust` revision `7ea4247`. Leader is mapped and integration-tested but blocked at W by three acadrust persistence gaps (see the ledger). The other 19 canvas kinds have only `layer`
|
||||
`acadrust` revision `7ea4247`. Leader is mapped and integration-tested but blocked at W by three acadrust persistence gaps (see the ledger). The other 18 canvas kinds have only `layer`
|
||||
writes through the document model. All 43 have raw typed snapshots. There are
|
||||
three internal records and two opaque fallbacks outside the 43 canvas kinds.
|
||||
|
||||
@@ -123,7 +123,7 @@ The last five may require new engine capabilities rather than only Python conver
|
||||
|
||||
## Handoff start point
|
||||
|
||||
Continue with **Dimension** (queue item 3; track each subtype separately). Leader is done except for its W
|
||||
Continue with **MultiLeader** (queue item 4). Leader is done except for its W
|
||||
gate; when the engine writes DXF group 340, reads group 213 and the DWG
|
||||
R2010+ text-size question is decided, flip the canary assertions in
|
||||
`staged_python_leader_lifecycle_over_real_ipc` and mark it `Complete`.
|
||||
@@ -159,3 +159,4 @@ exact queue above and update statuses with evidence after each increment.
|
||||
- `staged_python_hatch_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P with closed line-loop fixtures: solid creation, conversion to a stored-line pattern, boundary replacement, selection and live render-cache checks, atomic invalid scale/association/open-loop/unmapped-payload rejection, edited/deleted DWG and DXF reopen, post-reopen edits, and three-step undo/redo. Hatch is **Complete**; the queue now continues with `Leader`.
|
||||
- Mapped `Leader` (host validation and reference binding in `entity_coverage.rs`, policy/build.rs/manifest entries, adapter count 23). `staged_python_leader_lifecycle_over_real_ipc` passes C/R/E/D/U/I/V/P through the real runner. W is blocked by acadrust: the DXF writer omits the annotation handle (340), DXF reads lose `annotation_offset` (213), and DWG R2010+ does not store text size or hookline flag. Each gap is pinned by a canary assertion so it fails loudly once fixed. The queue now continues with `MLine`.
|
||||
- Completed `MLine`. Host validation and style binding live in `ocs_plugin_api`; derived vertex geometry and element parameters are recomputed by the host (`normalize_scripted_mline`) on scripted create/edit, so scripts supply positions only. The adapter maps `flags` as an integer through a manifest override because the generator cannot classify acadrust's serde-newtype `MLineFlags`. `staged_python_mline_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P with DWG and DXF reopen. The queue now continues with `Dimension`.
|
||||
- Completed `Dimension` for all nine subtypes. Because the payload is an enum, the adapter generator gained a `manual_kinds` hook (hand-written `<kind>_to_dict/_from_dict/_apply`) and the host catalog a `synthetic` policy section. The host derives `actual_measurement` and the base definition point on every scripted create/edit. Python ints are now accepted wherever a coordinate or angle is expected. `staged_python_dimension_lifecycle_over_real_ipc` records each subtype separately and all nine pass C/R/E/D/U/I/W/V/P. The queue now continues with `MultiLeader`.
|
||||
@@ -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 20 September 2026: **24 of 43 canvas kinds have explicit geometry writes** (23 creatable, plus update-only `Insert`), **5 kinds are complete**, and 19 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 20 September 2026: **25 of 43 canvas kinds have explicit geometry writes** (24 creatable, plus update-only `Insert`), **6 kinds are complete**, and 18 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** = build and load the in-tree bundled plugin with a manifest matching the host toolchain and acadrust source. 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.
|
||||
|
||||
@@ -30,7 +30,7 @@ Completion gate codes: **C** = create through Python document model; **R** = rea
|
||||
| Hatch | **Complete** | OCS commit `98b56ef6` and adapter commit `a9b5de8` expose solid and patterned Hatches with elevation/normal, complete pattern definitions, pattern controls, style, association state, boundary paths and seed points. Nested tagged boundary edges use the reusable `{kind, value}` adapter form. Host validation requires closed finite loops, validates line/arc/ellipse/polyline/spline payloads and stored pattern lines, resolves associative boundary handles, and preserves gradient and MPOLYGON payloads as explicitly unsupported. `staged_python_hatch_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P: Python solid creation, patterned boundary edit, live render-model pattern/boundary checks, selection, atomic invalid scale/association/open-loop/unmapped-property rejection, edited and deleted DWG/DXF reopen, post-reopen edits, and three-step undo/redo. |
|
||||
| Leader | **Integration-tested / blocked at W by acadrust** | Fields mapped writable: `vertices` (≥2 finite points, not all coincident), `annotation_handle`, `creation_type`, `path_type`, `dimension_style`, `arrow_enabled`, `hookline_enabled`, `hookline_direction`, `text_height`, `text_width`, `normal`, `horizontal_direction`, `block_offset`, `annotation_offset`. Read-only: `origin`, `dimension_gap`, `arrowhead_type`, `arrow_size`, `byblock_color`. Unmapped but preserved on edit: `override_color` (multi-payload `Color` enum the generator cannot express) and the `dwg_unknown_*` fields. Host reference binding validates that `annotation_handle` exists, is not the leader itself, matches `creation_type` (Text/MText, Tolerance, Insert; `NoAnnotation` forbids a handle) and that `dimension_style` exists; nothing is silently defaulted. `staged_python_leader_lifecycle_over_real_ipc` passes C/R/E/D/U/I/V/P against acadrust `7ea4247`: linked Text annotation, path/height/arrow/offset transaction edit with untouched-field preservation, GUI pick grouping (scripted selection stays exact), seven single-edit rejections plus a cross-entity atomic rollback, four-step undo/redo, real render linework, DWG and DXF reopen and re-edit. Erasing a leader also erases its annotation (scene leader grouping); undo restores both. **W blockers (engine, recorded as canary assertions in the test):** (1) the DXF writer never emits group 340, so a DXF save unlinks the annotation; (2) DXF reads drop `annotation_offset` (group 213 is written but not reassembled); (3) DWG R2010+ does not store `text_height`, `text_width` or `hookline_enabled` (acadrust writes them only for R13–R2007 / R13–R14). Not `Complete` until the engine round-trips (1) and (2); (3) needs a decision to document or make read-only. |
|
||||
| MLine | **Complete** | Writable: `vertices` (raw vertex dicts; a script may pass only `position`), `flags` (integer, unknown bits rejected), `justification`, `normal`, `scale_factor`, `style_name`. Read-only: `start_point`, `style_handle`, `style_element_count`. Host validation requires ≥2 finite non-coincident vertices, ≥3 for a closed multiline, a finite nonzero scale and normal, an existing non-empty `MLineStyle`, and per-vertex segments that are either empty or match the style element count. Vertex `direction`/`miter`, segment layout and per-element miter offsets (justification and scale included) are host-derived by `normalize_scripted_mline` on every scripted create or edit, reusing the drawing command's `rebuild_mline_geometry`/`sync_mline_element_parameters`; dash and fill cuts survive an edit unless a vertex moves. `staged_python_mline_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P: create against the Standard style, a three-vertex/scale/justification transaction edit with untouched-field preservation, corner miter widening and element offsets checked, real `mline_lines` linework before and after DWG and DXF reopen, nine single-edit rejections plus a cross-entity atomic rollback, re-edit after reopen in both formats, deletion, and three-step undo/redo. Known limit: switching to a style with a different element count is rejected until the script also clears the vertices' `segments`; only the rejection is covered by the integration test. |
|
||||
| Dimension | Layer-only | Track individual dimension subtypes. |
|
||||
| Dimension | **Complete** (all 9 subtypes) | `EntityType::Dimension` wraps a nine-variant enum, so the adapter uses a hand-written flat model (`dimension_model.rs.inc`, wired in through the manifest's `manual_kinds`) and the coverage catalog lists its properties through the policy's `synthetic` section. Python sees `subtype` (fixed at creation), the shared base fields (`text`, `style_name`, `normal`, `text_middle_point`, `attachment_point`, `text_rotation`, `horizontal_direction`, `flip_arrow1/2`, `text_user_positioned`) and only the fields of that subtype; a field from another subtype is rejected, not ignored. `actual_measurement` and the base definition point are host-derived (`normalize_scripted_dimension`, the same recompute grip edits use). Unmapped but preserved: `user_text`, `insertion_point`, `block_name`, version and DWG flags, style overrides in XDATA. Per-subtype status, all verified by `staged_python_dimension_lifecycle_over_real_ipc` (C/R/E/D/U/I/W/V/P; measurement and canvas wires checked for each, DWG and DXF reopen recorded per subtype): Linear (10), Aligned (5), Radius (5), Diameter (10), Angular3Pt (90°), Angular2Ln (90°), Ordinate (5), Arc (5·π/2), LargeRadial (6) — every one creates, reads, derives the right measurement, renders wires, and keeps its measurement through DWG and DXF. Edits (Linear second point/text/attachment, Radius definition point) update the measurement, nine rejection cases plus a cross-entity atomic rollback and three create-time rejections leave the document untouched, and post-reopen re-edit passes in both formats. Not covered: text formatting overrides, arrowheads and other DIMSTYLE overrides stored in XDATA, the anonymous `*D` block. The subtype-specific field meanings for the angular subtypes follow the DXF group-code layout rather than the acadrust convenience constructors. |
|
||||
| MultiLeader | Layer-only | Requires nested context and style fixture. |
|
||||
| Table | Layer-only | Requires table style and cell fixture. |
|
||||
| PolygonMesh | Layer-only | Requires consistent grid/count fixture. |
|
||||
|
||||
@@ -35,7 +35,7 @@ attached attribute and sequence records. Those fields report `transaction_geomet
|
||||
catalog. All 43 canvas kinds also support nonempty `layer` changes through
|
||||
the same undoable transaction; internal and opaque variants remain read-only
|
||||
through the document model. The adapter accepts only `layer` patches for
|
||||
canvas kinds outside its 24 geometry converters. Other writable properties
|
||||
canvas kinds outside its 25 geometry converters. Other writable properties
|
||||
still report `type_conversion_only`; broader CAD range and cross-property
|
||||
checks remain outstanding.
|
||||
The host's `entity_snapshot` helper serializes any typed entity as a detached
|
||||
@@ -91,7 +91,7 @@ Input tokens are bound to the drawing tab that requested the pick. Polling a
|
||||
token from another tab returns no result and does not consume it.
|
||||
|
||||
Python `doc.entities[handle]` returns a descriptor for every entity. For kinds
|
||||
outside the 24-kind generated schema it contains only handle, kind, and layer;
|
||||
outside the 25-kind generated schema it contains only handle, kind, and layer;
|
||||
`layer` is editable on canvas kinds. `doc.coverage()` enumerates the
|
||||
catalog; `doc.coverage("Line")` and `line.coverage` return one entry. For
|
||||
covered kinds, the coverage object lists actual readable and editable
|
||||
|
||||
@@ -94,7 +94,7 @@ The host-owned catalog classifies all 48 entity variants and tracks every
|
||||
traced field, including fields marked `unmapped`. Query it with
|
||||
`doc.coverage()` or `doc.coverage("Line")`; each row includes source field,
|
||||
type, access, and current validation status. Geometry edits remain limited to
|
||||
the 24 mapped kinds.
|
||||
the 25 mapped kinds.
|
||||
For any entity, `entity.snapshot` returns a detached dictionary of its raw
|
||||
CAD fields, including `snapshot["common"]` and kind-specific nested values.
|
||||
It can inspect an unmapped kind; editing the dictionary does
|
||||
|
||||
@@ -25,6 +25,10 @@ use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Manifest {
|
||||
/// Kinds whose payload is a nested enum: converted by hand-written
|
||||
/// `<snake>_to_dict`/`<snake>_from_dict`/`<snake>_apply` functions.
|
||||
#[serde(default)]
|
||||
pub manual_kinds: Vec<String>,
|
||||
/// Entity kinds exposed to `ocs.add`/`ocs.update`/`entity_to_dict`. The
|
||||
/// central, easy-to-widen filter for Phase 2 (see the plan doc).
|
||||
pub type_filter: Vec<String>,
|
||||
@@ -164,6 +168,9 @@ fn helper_closure(manifest: &Manifest, registry: &TypeRegistry) -> Vec<String> {
|
||||
let mut queue: Vec<String> = Vec::new();
|
||||
|
||||
for kind in &manifest.type_filter {
|
||||
if manifest.manual_kinds.contains(kind) {
|
||||
continue;
|
||||
}
|
||||
let Some(info) = get(registry, kind) else {
|
||||
panic!("entity_manifest.json: unknown entity kind {kind}");
|
||||
};
|
||||
@@ -683,6 +690,13 @@ fn resolve_entity_fields(
|
||||
fn gen_entity_to_dict(manifest: &Manifest, registry: &TypeRegistry) -> String {
|
||||
let mut arms = String::new();
|
||||
for kind in &manifest.type_filter {
|
||||
if manifest.manual_kinds.contains(kind) {
|
||||
arms.push_str(&format!(
|
||||
" acadrust::EntityType::{kind}(value) => {}_to_dict(vm, value),\n",
|
||||
snake_case(kind)
|
||||
));
|
||||
continue;
|
||||
}
|
||||
let info = get(registry, kind).unwrap();
|
||||
let var = "value";
|
||||
let fields = resolve_entity_fields(manifest, registry, kind, info, var, Mode::Add);
|
||||
@@ -734,6 +748,13 @@ fn gen_dict_to_entity(manifest: &Manifest, registry: &TypeRegistry) -> String {
|
||||
if manifest.overrides.get(kind).is_some_and(|override_def| override_def.update_only) {
|
||||
continue;
|
||||
}
|
||||
if manifest.manual_kinds.contains(kind) {
|
||||
arms.push_str(&format!(
|
||||
" \"{kind}\" => acadrust::EntityType::{kind}({}_from_dict(dict, vm)?),\n",
|
||||
snake_case(kind)
|
||||
));
|
||||
continue;
|
||||
}
|
||||
let info = get(registry, kind).unwrap();
|
||||
let var = "value";
|
||||
let fields = resolve_entity_fields(manifest, registry, kind, info, var, Mode::Add);
|
||||
@@ -792,6 +813,13 @@ pub(crate) fn dict_to_entity(dict: &rustpython_vm::builtins::PyDictRef, vm: &Vir
|
||||
fn gen_apply_dict_to_entity(manifest: &Manifest, registry: &TypeRegistry) -> String {
|
||||
let mut arms = String::new();
|
||||
for kind in &manifest.type_filter {
|
||||
if manifest.manual_kinds.contains(kind) {
|
||||
arms.push_str(&format!(
|
||||
" acadrust::EntityType::{kind}(existing_value) => acadrust::EntityType::{kind}({}_apply(existing_value, dict, vm)?),\n",
|
||||
snake_case(kind)
|
||||
));
|
||||
continue;
|
||||
}
|
||||
let info = get(registry, kind).unwrap();
|
||||
let var = "value";
|
||||
let fields = resolve_entity_fields(manifest, registry, kind, info, var, Mode::Update);
|
||||
|
||||
@@ -23,8 +23,10 @@
|
||||
"AttributeEntity",
|
||||
"Hatch",
|
||||
"Leader",
|
||||
"MLine"
|
||||
"MLine",
|
||||
"Dimension"
|
||||
],
|
||||
"manual_kinds": ["Dimension"],
|
||||
"base_fields": ["handle", "layer", "owner_handle"],
|
||||
"overrides": {
|
||||
"Point": {
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
// Hand-written Dimension model, `include!`d next to the generated CRUD code.
|
||||
//
|
||||
// `EntityType::Dimension` wraps a nine-variant enum, so the generator cannot
|
||||
// map it. Python sees one flat dict: `subtype` (fixed at creation), the
|
||||
// shared base fields, and the fields of that subtype only. Fields that belong
|
||||
// to another subtype are rejected rather than ignored. `actual_measurement`
|
||||
// and the base `definition_point` are host-derived, never scripted.
|
||||
|
||||
enum DimField<'a> {
|
||||
V(&'a mut Vector3),
|
||||
F(&'a mut f64),
|
||||
B(&'a mut bool),
|
||||
}
|
||||
|
||||
const DIM_SUBTYPES: &[&str] = &[
|
||||
"Aligned", "Linear", "Radius", "Diameter", "Angular2Ln", "Angular3Pt", "Ordinate", "Arc",
|
||||
"LargeRadial",
|
||||
];
|
||||
const DIM_BASE_KEYS: &[&str] = &[
|
||||
"text", "style_name", "normal", "text_middle_point", "attachment_point", "text_rotation",
|
||||
"horizontal_direction", "flip_arrow1", "flip_arrow2", "text_user_positioned",
|
||||
];
|
||||
const DIM_READ_ONLY_KEYS: &[&str] = &["subtype", "actual_measurement"];
|
||||
|
||||
fn dimension_subtype_name(dim: &acadrust::entities::Dimension) -> &'static str {
|
||||
use acadrust::entities::Dimension as D;
|
||||
match dim {
|
||||
D::Aligned(_) => "Aligned",
|
||||
D::Linear(_) => "Linear",
|
||||
D::Radius(_) => "Radius",
|
||||
D::Diameter(_) => "Diameter",
|
||||
D::Angular2Ln(_) => "Angular2Ln",
|
||||
D::Angular3Pt(_) => "Angular3Pt",
|
||||
D::Ordinate(_) => "Ordinate",
|
||||
D::Arc(_) => "Arc",
|
||||
D::LargeRadial(_) => "LargeRadial",
|
||||
}
|
||||
}
|
||||
|
||||
/// Subtype-specific fields in stable order.
|
||||
fn dimension_fields(dim: &mut acadrust::entities::Dimension) -> Vec<(&'static str, DimField<'_>)> {
|
||||
use acadrust::entities::Dimension as D;
|
||||
use DimField::{B, F, V};
|
||||
match dim {
|
||||
D::Aligned(d) => vec![
|
||||
("definition_point", V(&mut d.definition_point)),
|
||||
("first_point", V(&mut d.first_point)),
|
||||
("second_point", V(&mut d.second_point)),
|
||||
("ext_line_rotation", F(&mut d.ext_line_rotation)),
|
||||
],
|
||||
D::Linear(d) => vec![
|
||||
("definition_point", V(&mut d.definition_point)),
|
||||
("first_point", V(&mut d.first_point)),
|
||||
("second_point", V(&mut d.second_point)),
|
||||
("rotation", F(&mut d.rotation)),
|
||||
("ext_line_rotation", F(&mut d.ext_line_rotation)),
|
||||
],
|
||||
D::Radius(d) => vec![
|
||||
("definition_point", V(&mut d.definition_point)),
|
||||
("angle_vertex", V(&mut d.angle_vertex)),
|
||||
("leader_length", F(&mut d.leader_length)),
|
||||
],
|
||||
D::Diameter(d) => vec![
|
||||
("definition_point", V(&mut d.definition_point)),
|
||||
("angle_vertex", V(&mut d.angle_vertex)),
|
||||
("leader_length", F(&mut d.leader_length)),
|
||||
],
|
||||
D::Angular2Ln(d) => vec![
|
||||
("definition_point", V(&mut d.definition_point)),
|
||||
("dimension_arc", V(&mut d.dimension_arc)),
|
||||
("first_point", V(&mut d.first_point)),
|
||||
("second_point", V(&mut d.second_point)),
|
||||
("angle_vertex", V(&mut d.angle_vertex)),
|
||||
],
|
||||
D::Angular3Pt(d) => vec![
|
||||
("definition_point", V(&mut d.definition_point)),
|
||||
("first_point", V(&mut d.first_point)),
|
||||
("second_point", V(&mut d.second_point)),
|
||||
("angle_vertex", V(&mut d.angle_vertex)),
|
||||
],
|
||||
D::Ordinate(d) => vec![
|
||||
("definition_point", V(&mut d.definition_point)),
|
||||
("feature_location", V(&mut d.feature_location)),
|
||||
("leader_endpoint", V(&mut d.leader_endpoint)),
|
||||
("is_ordinate_type_x", B(&mut d.is_ordinate_type_x)),
|
||||
],
|
||||
D::Arc(d) => vec![
|
||||
("definition_point", V(&mut d.definition_point)),
|
||||
("first_extension_point", V(&mut d.first_extension_point)),
|
||||
("second_extension_point", V(&mut d.second_extension_point)),
|
||||
("center_point", V(&mut d.center_point)),
|
||||
("is_partial", B(&mut d.is_partial)),
|
||||
("arc_start_parameter", F(&mut d.arc_start_parameter)),
|
||||
("arc_end_parameter", F(&mut d.arc_end_parameter)),
|
||||
("has_leader", B(&mut d.has_leader)),
|
||||
("first_leader_point", V(&mut d.first_leader_point)),
|
||||
("second_leader_point", V(&mut d.second_leader_point)),
|
||||
],
|
||||
D::LargeRadial(d) => vec![
|
||||
("definition_point", V(&mut d.definition_point)),
|
||||
("chord_point", V(&mut d.chord_point)),
|
||||
("jog_angle", F(&mut d.jog_angle)),
|
||||
("override_center", V(&mut d.override_center)),
|
||||
("jog_point", V(&mut d.jog_point)),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Fields `ocs.add()` must receive (the geometry that defines the dimension).
|
||||
fn dimension_required(subtype: &str) -> &'static [&'static str] {
|
||||
match subtype {
|
||||
"Aligned" | "Linear" => &["definition_point", "first_point", "second_point"],
|
||||
"Radius" | "Diameter" => &["definition_point", "angle_vertex"],
|
||||
"Angular2Ln" => &["definition_point", "dimension_arc", "first_point", "second_point", "angle_vertex"],
|
||||
"Angular3Pt" => &["definition_point", "first_point", "second_point", "angle_vertex"],
|
||||
"Ordinate" => &["feature_location", "leader_endpoint"],
|
||||
"Arc" => &["definition_point", "first_extension_point", "second_extension_point", "center_point"],
|
||||
"LargeRadial" => &["definition_point", "chord_point"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
fn dimension_base_mut(dim: &mut acadrust::entities::Dimension) -> &mut acadrust::entities::DimensionBase {
|
||||
dim.base_mut()
|
||||
}
|
||||
|
||||
fn dim_attachment_point_name(value: acadrust::entities::AttachmentPointType) -> &'static str {
|
||||
use acadrust::entities::AttachmentPointType as A;
|
||||
match value {
|
||||
A::TopLeft => "TopLeft",
|
||||
A::TopCenter => "TopCenter",
|
||||
A::TopRight => "TopRight",
|
||||
A::MiddleLeft => "MiddleLeft",
|
||||
A::MiddleCenter => "MiddleCenter",
|
||||
A::MiddleRight => "MiddleRight",
|
||||
A::BottomLeft => "BottomLeft",
|
||||
A::BottomCenter => "BottomCenter",
|
||||
A::BottomRight => "BottomRight",
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_dim_attachment_point(
|
||||
value: &str,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<acadrust::entities::AttachmentPointType> {
|
||||
use acadrust::entities::AttachmentPointType as A;
|
||||
Ok(match value {
|
||||
"TopLeft" => A::TopLeft,
|
||||
"TopCenter" => A::TopCenter,
|
||||
"TopRight" => A::TopRight,
|
||||
"MiddleLeft" => A::MiddleLeft,
|
||||
"MiddleCenter" => A::MiddleCenter,
|
||||
"MiddleRight" => A::MiddleRight,
|
||||
"BottomLeft" => A::BottomLeft,
|
||||
"BottomCenter" => A::BottomCenter,
|
||||
"BottomRight" => A::BottomRight,
|
||||
other => {
|
||||
return Err(vm.new_value_error(format!("ocs: unsupported AttachmentPointType value: {other}")))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn dimension_to_dict(
|
||||
vm: &VirtualMachine,
|
||||
dimension: &acadrust::entities::Dimension,
|
||||
) -> PyResult<PyObjectRef> {
|
||||
let mut dim = dimension.clone();
|
||||
let dict = vm.ctx.new_dict();
|
||||
let base = dimension.base();
|
||||
dict.set_item("kind", vm.new_pyobj("Dimension"), vm)?;
|
||||
dict.set_item("handle", vm.new_pyobj(base.common.handle.value()), vm)?;
|
||||
dict.set_item("layer", vm.new_pyobj(base.common.layer.clone()), vm)?;
|
||||
dict.set_item("owner_handle", vm.new_pyobj(base.common.owner_handle.value()), vm)?;
|
||||
dict.set_item("subtype", vm.new_pyobj(dimension_subtype_name(dimension)), vm)?;
|
||||
dict.set_item("actual_measurement", vm.new_pyobj(base.actual_measurement), vm)?;
|
||||
dict.set_item("text", vm.new_pyobj(base.text.clone()), vm)?;
|
||||
dict.set_item("style_name", vm.new_pyobj(base.style_name.clone()), vm)?;
|
||||
dict.set_item("normal", vector3_to_py_dict(vm, &base.normal)?, vm)?;
|
||||
dict.set_item("text_middle_point", vector3_to_py_dict(vm, &base.text_middle_point)?, vm)?;
|
||||
dict.set_item("attachment_point", vm.new_pyobj(dim_attachment_point_name(base.attachment_point)), vm)?;
|
||||
dict.set_item("text_rotation", vm.new_pyobj(base.text_rotation), vm)?;
|
||||
dict.set_item("horizontal_direction", vm.new_pyobj(base.horizontal_direction), vm)?;
|
||||
dict.set_item("flip_arrow1", vm.new_pyobj(base.flip_arrow1), vm)?;
|
||||
dict.set_item("flip_arrow2", vm.new_pyobj(base.flip_arrow2), vm)?;
|
||||
dict.set_item("text_user_positioned", vm.new_pyobj(base.text_user_positioned), vm)?;
|
||||
for (name, field) in dimension_fields(&mut dim) {
|
||||
let value: PyObjectRef = match field {
|
||||
DimField::V(v) => vector3_to_py_dict(vm, v)?,
|
||||
DimField::F(v) => vm.new_pyobj(*v),
|
||||
DimField::B(v) => vm.new_pyobj(*v),
|
||||
};
|
||||
dict.set_item(name, value, vm)?;
|
||||
}
|
||||
Ok(dict.into())
|
||||
}
|
||||
|
||||
/// Merge `dict` onto `dim`. With `creating`, geometry the subtype needs must
|
||||
/// be present so a script cannot silently default a point to the origin.
|
||||
fn dimension_merge(
|
||||
dim: &mut acadrust::entities::Dimension,
|
||||
dict: &rustpython_vm::builtins::PyDictRef,
|
||||
creating: bool,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
let subtype = dimension_subtype_name(dim);
|
||||
let applicable: Vec<&'static str> =
|
||||
dimension_fields(&mut dim.clone()).into_iter().map(|(name, _)| name).collect();
|
||||
for key in dict.keys_vec() {
|
||||
let key = key.try_into_value::<String>(vm)?;
|
||||
let key = key.as_str();
|
||||
if matches!(key, "kind" | "handle" | "layer" | "owner_handle") || DIM_BASE_KEYS.contains(&key) {
|
||||
continue;
|
||||
}
|
||||
if key == "subtype" {
|
||||
let value = dict.get_item("subtype", vm)?.try_into_value::<String>(vm)?;
|
||||
if value != subtype {
|
||||
return Err(vm.new_value_error("Dimension.subtype is read-only".to_owned()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if key == "actual_measurement" {
|
||||
return Err(vm.new_value_error("Dimension.actual_measurement is read-only".to_owned()));
|
||||
}
|
||||
if !applicable.contains(&key) {
|
||||
let known = [
|
||||
"definition_point", "first_point", "second_point", "angle_vertex", "dimension_arc",
|
||||
"feature_location", "leader_endpoint", "first_extension_point", "second_extension_point",
|
||||
"center_point", "first_leader_point", "second_leader_point", "chord_point",
|
||||
"override_center", "jog_point", "rotation", "ext_line_rotation", "leader_length",
|
||||
"arc_start_parameter", "arc_end_parameter", "jog_angle", "is_ordinate_type_x",
|
||||
"is_partial", "has_leader",
|
||||
];
|
||||
return Err(vm.new_value_error(if known.contains(&key) {
|
||||
format!("Dimension.{key} does not apply to the {subtype} subtype")
|
||||
} else {
|
||||
format!("ocs: Dimension property {key:?} is outside the editable schema")
|
||||
}));
|
||||
}
|
||||
}
|
||||
if creating {
|
||||
for name in dimension_required(subtype) {
|
||||
if dict.get_item_opt(*name, vm)?.is_none() {
|
||||
return Err(vm.new_value_error(format!("ocs.add: Dimension {subtype} requires {name}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (name, field) in dimension_fields(dim) {
|
||||
let Some(value) = dict.get_item_opt(name, vm)? else { continue };
|
||||
match field {
|
||||
DimField::V(target) => *target = py_to_vector3_dict(value, vm)?,
|
||||
DimField::F(target) => *target = py_number_to_f64(value, vm)?,
|
||||
DimField::B(target) => *target = value.try_into_value::<bool>(vm)?,
|
||||
}
|
||||
}
|
||||
let base = dimension_base_mut(dim);
|
||||
if let Some(v) = dict.get_item_opt("text", vm)? { base.text = v.try_into_value::<String>(vm)?; }
|
||||
if let Some(v) = dict.get_item_opt("style_name", vm)? { base.style_name = v.try_into_value::<String>(vm)?; }
|
||||
if let Some(v) = dict.get_item_opt("normal", vm)? { base.normal = py_to_vector3_dict(v, vm)?; }
|
||||
if let Some(v) = dict.get_item_opt("text_middle_point", vm)? { base.text_middle_point = py_to_vector3_dict(v, vm)?; }
|
||||
if let Some(v) = dict.get_item_opt("attachment_point", vm)? {
|
||||
base.attachment_point = parse_dim_attachment_point(&v.try_into_value::<String>(vm)?, vm)?;
|
||||
}
|
||||
if let Some(v) = dict.get_item_opt("text_rotation", vm)? { base.text_rotation = py_number_to_f64(v, vm)?; }
|
||||
if let Some(v) = dict.get_item_opt("horizontal_direction", vm)? { base.horizontal_direction = py_number_to_f64(v, vm)?; }
|
||||
if let Some(v) = dict.get_item_opt("flip_arrow1", vm)? { base.flip_arrow1 = v.try_into_value::<bool>(vm)?; }
|
||||
if let Some(v) = dict.get_item_opt("flip_arrow2", vm)? { base.flip_arrow2 = v.try_into_value::<bool>(vm)?; }
|
||||
if let Some(v) = dict.get_item_opt("text_user_positioned", vm)? { base.text_user_positioned = v.try_into_value::<bool>(vm)?; }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dimension_from_dict(
|
||||
dict: &rustpython_vm::builtins::PyDictRef,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<acadrust::entities::Dimension> {
|
||||
use acadrust::entities::*;
|
||||
let subtype = match dict.get_item_opt("subtype", vm)? {
|
||||
Some(v) => v.try_into_value::<String>(vm)?,
|
||||
None => {
|
||||
return Err(vm.new_value_error(format!(
|
||||
"ocs.add: Dimension requires subtype, one of {DIM_SUBTYPES:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
let mut dim = match subtype.as_str() {
|
||||
"Aligned" => Dimension::Aligned(DimensionAligned::default()),
|
||||
"Linear" => Dimension::Linear(DimensionLinear::default()),
|
||||
"Radius" => Dimension::Radius(DimensionRadius::default()),
|
||||
"Diameter" => Dimension::Diameter(DimensionDiameter::default()),
|
||||
"Angular2Ln" => Dimension::Angular2Ln(DimensionAngular2Ln::default()),
|
||||
"Angular3Pt" => Dimension::Angular3Pt(DimensionAngular3Pt::default()),
|
||||
"Ordinate" => Dimension::Ordinate(DimensionOrdinate::default()),
|
||||
"Arc" => Dimension::Arc(DimensionArc::default()),
|
||||
"LargeRadial" => Dimension::LargeRadial(DimensionLargeRadial::default()),
|
||||
other => {
|
||||
return Err(vm.new_value_error(format!(
|
||||
"ocs.add: unsupported Dimension subtype {other:?}, expected one of {DIM_SUBTYPES:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
dimension_merge(&mut dim, dict, true, vm)?;
|
||||
let base = dimension_base_mut(&mut dim);
|
||||
base.common.handle = acadrust::Handle::new(get_opt_u64(dict, "handle", vm)?);
|
||||
base.common.layer = get_opt_string(dict, "layer", "0", vm)?;
|
||||
base.common.owner_handle = acadrust::Handle::new(get_opt_u64(dict, "owner_handle", vm)?);
|
||||
Ok(dim)
|
||||
}
|
||||
|
||||
fn dimension_apply(
|
||||
existing: &acadrust::entities::Dimension,
|
||||
dict: &rustpython_vm::builtins::PyDictRef,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<acadrust::entities::Dimension> {
|
||||
let mut dim = existing.clone();
|
||||
dimension_merge(&mut dim, dict, false, vm)?;
|
||||
let base = dimension_base_mut(&mut dim);
|
||||
if let Some(v) = dict.get_item_opt("layer", vm)? {
|
||||
if !vm.is_none(&v) {
|
||||
base.common.layer = v.try_into_value::<String>(vm)?;
|
||||
}
|
||||
}
|
||||
if let Some(owner) = dict.get_item_opt("owner_handle", vm)? {
|
||||
if owner.try_into_value::<u64>(vm)? != base.common.owner_handle.value() {
|
||||
return Err(vm.new_value_error("owner_handle is read-only after creation".to_owned()));
|
||||
}
|
||||
}
|
||||
Ok(dim)
|
||||
}
|
||||
@@ -3127,10 +3127,15 @@ mod ocs {
|
||||
match dict.get_item_opt(key, vm)? {
|
||||
None => Ok(0.0),
|
||||
Some(v) if vm.is_none(&v) => Ok(0.0),
|
||||
Some(v) => v.try_into_value::<f64>(vm),
|
||||
Some(v) => py_number_to_f64(v, vm),
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept a Python int or float; `try_into_value::<f64>` rejects ints.
|
||||
fn py_number_to_f64(value: PyObjectRef, vm: &VirtualMachine) -> PyResult<f64> {
|
||||
Ok(value.try_float(vm)?.to_f64())
|
||||
}
|
||||
|
||||
fn get_opt_bool(
|
||||
dict: &rustpython_vm::builtins::PyDictRef,
|
||||
key: &str,
|
||||
@@ -3186,6 +3191,7 @@ mod ocs {
|
||||
}
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/entity_crud.rs"));
|
||||
include!("dimension_model.rs.inc");
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// Phase 1 — wiring the generic conversion functions above into `ocs`.
|
||||
@@ -4640,6 +4646,23 @@ mod tests {
|
||||
current = None;
|
||||
}
|
||||
}
|
||||
// Hand-written kinds: keys come from the include file's dict writes
|
||||
// and per-subtype field table.
|
||||
let manual = include_str!("dimension_model.rs.inc");
|
||||
let dimension_keys = emitted.entry("Dimension".to_owned()).or_default();
|
||||
for line in manual.lines() {
|
||||
let line = line.trim();
|
||||
let key = line
|
||||
.strip_prefix("dict.set_item(\"")
|
||||
.or_else(|| {
|
||||
(line.contains("\", V(") || line.contains("\", F(") || line.contains("\", B("))
|
||||
.then(|| line.strip_prefix("(\""))
|
||||
.flatten()
|
||||
});
|
||||
if let Some(key) = key {
|
||||
dimension_keys.insert(key.split('"').next().unwrap().to_owned());
|
||||
}
|
||||
}
|
||||
for entry in &catalog.entity_kinds {
|
||||
let expected: BTreeSet<_> = entry
|
||||
.properties
|
||||
@@ -4663,7 +4686,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(emitted.len(), 24);
|
||||
assert_eq!(emitted.len(), 25);
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-host-model")]
|
||||
|
||||
@@ -155,6 +155,9 @@ impl<'a> HostSession<'a> {
|
||||
old: Option<&EntityType>,
|
||||
entity: &mut EntityType,
|
||||
) -> Result<(), String> {
|
||||
if let EntityType::Dimension(dimension) = entity {
|
||||
crate::entities::dimension::normalize_scripted_dimension(dimension);
|
||||
}
|
||||
if let EntityType::MLine(new) = entity {
|
||||
let old = match old {
|
||||
Some(EntityType::MLine(old)) => Some(old),
|
||||
@@ -2572,6 +2575,209 @@ mod tests {
|
||||
assert!(app.tabs[0].scene.document.get_entity(handle).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_python_dimension_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 mut app = OpenCADStudio::new_for_test();
|
||||
app.tabs[0].is_start = false;
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
let process = ocs_plugin_api::process::PluginProcess::spawn(
|
||||
std::path::Path::new(&plugin_path),
|
||||
&mut host,
|
||||
crate::plugin::v4_support::notification_handler(),
|
||||
)
|
||||
.unwrap();
|
||||
let dispatch = |host: &mut HostSession<'_>, command: &str| {
|
||||
assert!(process.dispatch(host, command, &mut |_| {}).expect("Python dispatch"));
|
||||
};
|
||||
let last_output = |host: &HostSession<'_>| host.app.command_line.history.last().unwrap().text.clone();
|
||||
|
||||
// (subtype, python fields, expected measurement)
|
||||
let fixtures: [(&str, &str, f64); 9] = [
|
||||
("Linear", "definition_point=P(10,5), first_point=P(0,0), second_point=P(10,0)", 10.0),
|
||||
("Aligned", "definition_point=P(1,5.5), first_point=P(0,0), second_point=P(3,4)", 5.0),
|
||||
("Radius", "definition_point=P(5,0), angle_vertex=P(0,0)", 5.0),
|
||||
("Diameter", "definition_point=P(10,0), angle_vertex=P(0,0)", 10.0),
|
||||
("Angular3Pt", "definition_point=P(3,3), first_point=P(5,0), second_point=P(0,5), angle_vertex=P(0,0)", 90.0),
|
||||
("Angular2Ln", "definition_point=P(0,5), dimension_arc=P(3,3), first_point=P(0,0), second_point=P(5,0), angle_vertex=P(0,0)", 90.0),
|
||||
("Ordinate", "feature_location=P(5,3), leader_endpoint=P(5,8), is_ordinate_type_x=True", 5.0),
|
||||
("Arc", "definition_point=P(3.5,3.5), first_extension_point=P(5,0), second_extension_point=P(0,5), center_point=P(0,0), arc_start_parameter=0.0, arc_end_parameter=1.5707963267948966", 5.0 * std::f64::consts::FRAC_PI_2),
|
||||
("LargeRadial", "definition_point=P(10,0), chord_point=P(4,0)", 6.0),
|
||||
];
|
||||
let mut script = String::from("def P(x, y): return {'x': x, 'y': y, 'z': 0.0}\ndoc = ocs.active_document\n");
|
||||
for (subtype, fields, _) in &fixtures {
|
||||
script.push_str(&format!("doc.create_entity('Dimension', subtype='{subtype}', {fields})\n"));
|
||||
}
|
||||
let path = std::env::temp_dir().join(format!("ocs_dimension_create_{}.py", std::process::id()));
|
||||
std::fs::write(&path, script).unwrap();
|
||||
dispatch(&mut host, &format!("PY_RUN {}", path.display()));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let subtype_of = |dimension: &acadrust::entities::Dimension| -> &'static str {
|
||||
use acadrust::entities::Dimension as D;
|
||||
match dimension {
|
||||
D::Aligned(_) => "Aligned", D::Linear(_) => "Linear", D::Radius(_) => "Radius",
|
||||
D::Diameter(_) => "Diameter", D::Angular2Ln(_) => "Angular2Ln",
|
||||
D::Angular3Pt(_) => "Angular3Pt", D::Ordinate(_) => "Ordinate",
|
||||
D::Arc(_) => "Arc", D::LargeRadial(_) => "LargeRadial",
|
||||
}
|
||||
};
|
||||
let handles_by_subtype = |document: &CadDocument| {
|
||||
let mut map = std::collections::BTreeMap::new();
|
||||
for entity in document.entities() {
|
||||
if let EntityType::Dimension(dimension) = entity {
|
||||
map.insert(subtype_of(dimension), dimension.base().common.handle);
|
||||
}
|
||||
}
|
||||
map
|
||||
};
|
||||
let handles = handles_by_subtype(host.document());
|
||||
assert_eq!(handles.len(), 9, "created: {handles:?}; {}", last_output(&host));
|
||||
let measured = |document: &CadDocument, handle: Handle| -> f64 {
|
||||
let Some(EntityType::Dimension(dimension)) = document.get_entity(handle) else {
|
||||
panic!("Dimension missing");
|
||||
};
|
||||
dimension.base().actual_measurement
|
||||
};
|
||||
for (subtype, _, expected) in &fixtures {
|
||||
let value = measured(host.document(), handles[subtype]);
|
||||
assert!((value - expected).abs() < 1e-6, "{subtype}: measured {value}, expected {expected}");
|
||||
let Some(EntityType::Dimension(dimension)) = host.document().get_entity(handles[subtype]) else { unreachable!() };
|
||||
assert_eq!(dimension.measurement(), value, "{subtype}: stored measurement is derived");
|
||||
let wires = host.app.tabs[0].scene.wire_models_for(&[handles[subtype]]);
|
||||
assert!(!wires.is_empty(), "{subtype}: dimension produced no canvas geometry");
|
||||
}
|
||||
let linear = handles["Linear"];
|
||||
let created_linear = host.document().get_entity(linear).unwrap().clone();
|
||||
|
||||
// Edit: move a defining point (the measurement follows), change text
|
||||
// and style-independent placement; untouched fields survive.
|
||||
let script = std::env::temp_dir().join(format!("ocs_dimension_edit_{}.py", std::process::id()));
|
||||
std::fs::write(&script, format!(concat!(
|
||||
"doc = ocs.active_document\n",
|
||||
"linear = doc.entities[{}]\n",
|
||||
"radius = doc.entities[{}]\n",
|
||||
"with doc.transaction('Edit dimensions'):\n",
|
||||
" linear.second_point = (20.0, 0.0, 0.0)\n",
|
||||
" linear.text = '<> mm'\n",
|
||||
" linear.attachment_point = 'TopCenter'\n",
|
||||
" radius.definition_point = (8.0, 0.0, 0.0)\n",
|
||||
"doc.selection = [linear]\n"
|
||||
), linear.value(), handles["Radius"].value())).unwrap();
|
||||
dispatch(&mut host, &format!("PY_RUN {}", script.display()));
|
||||
let _ = std::fs::remove_file(&script);
|
||||
let expected_linear = host.document().get_entity(linear).unwrap().clone();
|
||||
let EntityType::Dimension(edited) = &expected_linear else { unreachable!() };
|
||||
let EntityType::Dimension(before) = &created_linear else { unreachable!() };
|
||||
assert!((edited.base().actual_measurement - 20.0).abs() < 1e-9, "edit failed: {}", last_output(&host));
|
||||
assert!((measured(host.document(), handles["Radius"]) - 8.0).abs() < 1e-9);
|
||||
assert_eq!(edited.base().text, "<> mm");
|
||||
assert_eq!(edited.base().attachment_point, acadrust::entities::AttachmentPointType::TopCenter);
|
||||
assert_eq!(edited.base().common, before.base().common);
|
||||
assert_eq!(edited.base().style_name, before.base().style_name);
|
||||
assert_eq!(edited.base().normal, before.base().normal);
|
||||
assert_eq!(host.selection(), vec![linear]);
|
||||
let expected_radius = host.document().get_entity(handles["Radius"]).unwrap().clone();
|
||||
assert!(!host.app.tabs[0].scene.wire_models_for(&[linear]).is_empty());
|
||||
|
||||
let reject_cases: [(&str, &str, &str); 9] = [
|
||||
("Linear", "'leader_length':2.0", "does not apply to the Linear subtype"),
|
||||
("Linear", "'subtype':'Radius'", "read-only"),
|
||||
("Linear", "'actual_measurement':5.0", "read-only"),
|
||||
("Linear", "'style_name':'Missing'", "does not exist"),
|
||||
("Linear", "'first_point':{'x':20.0,'y':0.0,'z':0.0}", "must not coincide"),
|
||||
("Linear", "'first_point':{'x':float('nan'),'y':0.0,'z':0.0}", "finite"),
|
||||
("Linear", "'normal':{'x':0.0,'y':0.0,'z':0.0}", "nonzero"),
|
||||
("Radius", "'angle_vertex':{'x':8.0,'y':0.0,'z':0.0}", "must not coincide"),
|
||||
("Linear", "'attachment_point':'Nowhere'", "unsupported AttachmentPointType"),
|
||||
];
|
||||
for (subtype, patch, message) in reject_cases {
|
||||
dispatch(&mut host, &format!(
|
||||
"PY_EVAL ocs.update_many('Reject dimension', [{{'handle':{}, {patch}}}])",
|
||||
handles[subtype].value()));
|
||||
assert_eq!(host.document().get_entity(linear), Some(&expected_linear), "{patch}");
|
||||
assert_eq!(host.document().get_entity(handles["Radius"]), Some(&expected_radius), "{patch}");
|
||||
assert!(last_output(&host).contains(message), "{patch}: {}", last_output(&host));
|
||||
}
|
||||
dispatch(&mut host, &format!(concat!(
|
||||
"PY_EVAL ocs.update_many('Atomic', [{{'handle':{},'text':'changed'}},",
|
||||
"{{'handle':{},'style_name':'Missing'}}])"
|
||||
), linear.value(), handles["Radius"].value()));
|
||||
assert_eq!(host.document().get_entity(linear), Some(&expected_linear));
|
||||
for (patch, message) in [
|
||||
("ocs.active_document.create_entity('Dimension', definition_point=P(0,0))", "requires subtype"),
|
||||
("ocs.active_document.create_entity('Dimension', subtype='Cone')", "unsupported Dimension subtype"),
|
||||
("ocs.active_document.create_entity('Dimension', subtype='Linear', first_point=P(0,0), second_point=P(1,0))", "requires definition_point"),
|
||||
] {
|
||||
dispatch(&mut host, &format!("PY_EVAL (lambda P: {patch})(lambda x, y: {{'x': x, 'y': y, 'z': 0.0}})"));
|
||||
assert_eq!(handles_by_subtype(host.document()).len(), 9, "{patch}");
|
||||
assert!(last_output(&host).contains(message), "{patch}: {}", last_output(&host));
|
||||
}
|
||||
|
||||
// DWG and DXF: every subtype is recorded separately.
|
||||
let mut failures = Vec::new();
|
||||
let dwg = crate::io::load_bytes("dimension.dwg", acadrust::DwgWriter::write_to_vec(host.document()).unwrap()).unwrap();
|
||||
let dxf = crate::io::load_bytes("dimension.dxf", acadrust::DxfWriter::new(host.document()).write_to_vec().unwrap()).unwrap();
|
||||
for (format, document) in [("DWG", &dwg), ("DXF", &dxf)] {
|
||||
let reopened = handles_by_subtype(document);
|
||||
for (subtype, _, expected) in &fixtures {
|
||||
let expected = match *subtype { "Linear" => 20.0, "Radius" => 8.0, _ => *expected };
|
||||
match reopened.get(subtype) {
|
||||
None => failures.push(format!("{format} {subtype}: missing")),
|
||||
Some(handle) => {
|
||||
let value = measured(document, *handle);
|
||||
if (value - expected).abs() > 1e-6 {
|
||||
failures.push(format!("{format} {subtype}: measurement {value} != {expected}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some(EntityType::Dimension(dimension)) = document.get_entity(linear) else {
|
||||
failures.push(format!("{format} Linear handle lost"));
|
||||
continue;
|
||||
};
|
||||
if dimension.base().text != "<> mm" { failures.push(format!("{format} Linear text {:?}", dimension.base().text)); }
|
||||
}
|
||||
assert!(failures.is_empty(), "persistence gaps: {failures:#?}");
|
||||
for (name, mut document) in [("dimension-reedit.dwg", dwg), ("dimension-reedit.dxf", dxf)] {
|
||||
let before = document.get_entity(linear).unwrap().clone();
|
||||
let mut after = before.clone();
|
||||
let EntityType::Dimension(dimension) = &mut after else { unreachable!() };
|
||||
if let acadrust::entities::Dimension::Linear(value) = dimension { value.second_point.x = 30.0; }
|
||||
ocs_plugin_api::entity_coverage::validate_entity_mutation(&before, &after).unwrap();
|
||||
ocs_plugin_api::entity_coverage::validate_canvas_entity_references(&document, &after).unwrap();
|
||||
let EntityType::Dimension(dimension) = &mut after else { unreachable!() };
|
||||
crate::entities::dimension::normalize_scripted_dimension(dimension);
|
||||
*document.get_entity_mut(linear).unwrap() = after;
|
||||
let bytes = if name.ends_with("dwg") {
|
||||
acadrust::DwgWriter::write_to_vec(&document).unwrap()
|
||||
} else {
|
||||
acadrust::DxfWriter::new(&document).write_to_vec().unwrap()
|
||||
};
|
||||
let reopened = crate::io::load_bytes(name, bytes).unwrap();
|
||||
assert!((measured(&reopened, linear) - 30.0).abs() < 1e-6, "{name}");
|
||||
}
|
||||
|
||||
dispatch(&mut host, &format!("PY_EVAL ocs.active_document.delete_entity({})", linear.value()));
|
||||
assert!(host.document().get_entity(linear).is_none());
|
||||
drop(process);
|
||||
drop(host);
|
||||
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(linear), Some(&expected_linear));
|
||||
app.undo_steps(1);
|
||||
assert_eq!(app.tabs[0].scene.document.get_entity(linear), Some(&created_linear));
|
||||
app.undo_steps(1);
|
||||
assert!(app.tabs[0].scene.document.get_entity(linear).is_none());
|
||||
app.redo_steps(3);
|
||||
assert!(app.tabs[0].scene.document.get_entity(linear).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 {
|
||||
|
||||
@@ -25,6 +25,14 @@ pub(crate) fn set_dimension_text_override(base: &mut DimensionBase, text: Option
|
||||
base.user_text = text;
|
||||
}
|
||||
|
||||
/// Recompute the fields a script cannot set: the base definition point and
|
||||
/// the stored measurement, exactly as grip edits do.
|
||||
pub(crate) fn normalize_scripted_dimension(dim: &mut Dimension) {
|
||||
let definition_point = dimension_definition_point(dim);
|
||||
dim.base_mut().definition_point = definition_point;
|
||||
dim.base_mut().actual_measurement = dim.measurement();
|
||||
}
|
||||
|
||||
fn dimension_definition_point(dim: &Dimension) -> acadrust::types::Vector3 {
|
||||
match dim {
|
||||
Dimension::Aligned(d) => d.definition_point,
|
||||
|
||||
Reference in new issue
Block a user