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 <noreply@anthropic.com>
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,
|
EntityCoverageCatalog, EntityKindCoverage, EntityScope, ModelAccess, PropertyCoverage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct SyntheticProperty {
|
||||||
|
name: String,
|
||||||
|
type_id: String,
|
||||||
|
access: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct EntityCoveragePolicy {
|
struct EntityCoveragePolicy {
|
||||||
internal_kinds: Vec<String>,
|
internal_kinds: Vec<String>,
|
||||||
@@ -28,6 +35,10 @@ struct EntityCoveragePolicy {
|
|||||||
editable: BTreeMap<String, Vec<String>>,
|
editable: BTreeMap<String, Vec<String>>,
|
||||||
readable: BTreeMap<String, Vec<String>>,
|
readable: BTreeMap<String, Vec<String>>,
|
||||||
aliases: BTreeMap<String, BTreeMap<String, 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() {
|
fn main() {
|
||||||
@@ -802,6 +813,19 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
|
|||||||
model_access: access,
|
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!(
|
assert!(
|
||||||
unresolved.is_empty(),
|
unresolved.is_empty(),
|
||||||
"{kind} has coverage keys absent from registry: {unresolved:?}"
|
"{kind} has coverage keys absent from registry: {unresolved:?}"
|
||||||
|
|||||||
@@ -1,44 +1,526 @@
|
|||||||
{
|
{
|
||||||
"internal_kinds": ["Block", "BlockEnd", "Seqend"],
|
"internal_kinds": [
|
||||||
"opaque_kinds": ["Extended", "Unknown"],
|
"Block",
|
||||||
|
"BlockEnd",
|
||||||
|
"Seqend"
|
||||||
|
],
|
||||||
|
"opaque_kinds": [
|
||||||
|
"Extended",
|
||||||
|
"Unknown"
|
||||||
|
],
|
||||||
"editable": {
|
"editable": {
|
||||||
"Point": ["layer", "location", "thickness", "normal", "x_axis_angle"],
|
"Point": [
|
||||||
"Line": ["layer", "start", "end", "thickness", "normal"],
|
"layer",
|
||||||
"Circle": ["layer", "center", "radius", "thickness", "normal"],
|
"location",
|
||||||
"Arc": ["layer", "center", "radius", "start_angle", "end_angle", "thickness", "normal"],
|
"thickness",
|
||||||
"Ellipse": ["layer", "center", "major_axis", "minor_axis_ratio", "start_parameter", "end_parameter", "normal"],
|
"normal",
|
||||||
"Polyline": ["layer", "closed", "vertices"],
|
"x_axis_angle"
|
||||||
"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"],
|
"Line": [
|
||||||
"LwPolyline": ["layer", "vertices", "is_closed", "plinegen", "constant_width", "elevation", "thickness", "normal"],
|
"layer",
|
||||||
"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"],
|
"start",
|
||||||
"Text": ["layer", "text", "insertion", "alignment_point", "height", "rotation", "width_factor", "oblique_angle", "style", "horizontal_alignment", "vertical_alignment", "normal", "thickness", "generation_flags"],
|
"end",
|
||||||
"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"],
|
"thickness",
|
||||||
"Ray": ["layer", "base_point", "direction"],
|
"normal"
|
||||||
"XLine": ["layer", "base_point", "direction"],
|
],
|
||||||
"Solid": ["layer", "first_corner", "second_corner", "third_corner", "fourth_corner", "normal", "thickness", "is_trace"],
|
"Circle": [
|
||||||
"Face3D": ["layer", "first_corner", "second_corner", "third_corner", "fourth_corner", "invisible_edges"],
|
"layer",
|
||||||
"Insert": ["layer", "insert_point", "x_scale", "y_scale", "z_scale", "rotation", "normal", "column_count", "row_count", "column_spacing", "row_spacing"],
|
"center",
|
||||||
"Tolerance": ["layer", "insertion_point", "direction", "normal", "text", "dimension_style_name", "text_height", "dimension_gap"],
|
"radius",
|
||||||
"Shape": ["layer", "insertion_point", "size", "shape_name", "shape_number", "rotation", "relative_x_scale", "oblique_angle", "normal", "thickness", "style_name"],
|
"thickness",
|
||||||
"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"],
|
"normal"
|
||||||
"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"],
|
"Arc": [
|
||||||
"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"],
|
"layer",
|
||||||
"MLine": ["layer", "flags", "justification", "normal", "scale_factor", "style_name", "vertices"]
|
"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": {
|
"readable": {
|
||||||
"Insert": ["block_name"],
|
"Insert": [
|
||||||
"Tolerance": ["dimension_style_handle"],
|
"block_name"
|
||||||
"Shape": ["style_handle"],
|
],
|
||||||
"MLine": ["start_point", "style_handle", "style_element_count"],
|
"Tolerance": [
|
||||||
"Leader": ["origin", "dimension_gap", "arrowhead_type", "arrow_size", "byblock_color"],
|
"dimension_style_handle"
|
||||||
"AttributeEntity": ["attdef_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": {
|
"aliases": {
|
||||||
"Polyline": {"closed": "flags"},
|
"Polyline": {
|
||||||
"Polyline2D": {"closed": "flags"},
|
"closed": "flags"
|
||||||
"Text": {"text": "value", "insertion": "insertion_point"},
|
},
|
||||||
"MText": {"text": "value", "insertion": "insertion_point"}
|
"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(())
|
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
|
/// 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.
|
/// host checks. Called by the Python add path before an entity enters the document.
|
||||||
#[cfg(feature = "host")]
|
#[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::Leader(value) => validate_leader(None, value)?,
|
||||||
EntityType::MLine(value) => validate_mline(None, value)?,
|
EntityType::MLine(value) => validate_mline(None, value)?,
|
||||||
|
EntityType::Dimension(value) => validate_dimension(None, value)?,
|
||||||
EntityType::AttributeDefinition(value) => {
|
EntityType::AttributeDefinition(value) => {
|
||||||
finite_vector(
|
finite_vector(
|
||||||
"AttributeDefinition.insertion_point",
|
"AttributeDefinition.insertion_point",
|
||||||
@@ -824,6 +913,7 @@ pub fn validate_entity_mutation(
|
|||||||
return Err("Shape.style_handle is read-only".into());
|
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::MLine(old), EntityType::MLine(new)) => validate_mline(Some(old), new)?,
|
||||||
(EntityType::Leader(old), EntityType::Leader(new)) => validate_leader(Some(old), new)?,
|
(EntityType::Leader(old), EntityType::Leader(new)) => validate_leader(Some(old), new)?,
|
||||||
(EntityType::AttributeDefinition(old), EntityType::AttributeDefinition(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));
|
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 {
|
if let EntityType::MLine(value) = entity {
|
||||||
let style = resolve_mline_style(document, value).ok_or_else(|| {
|
let style = resolve_mline_style(document, value).ok_or_else(|| {
|
||||||
format!("MLine style {:?} does not exist", value.style_name)
|
format!("MLine style {:?} does not exist", value.style_name)
|
||||||
@@ -1443,6 +1539,7 @@ mod tests {
|
|||||||
| "Hatch"
|
| "Hatch"
|
||||||
| "Leader"
|
| "Leader"
|
||||||
| "MLine"
|
| "MLine"
|
||||||
|
| "Dimension"
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1597,6 +1694,40 @@ mod tests {
|
|||||||
"MLine.scale_factor".to_owned(),
|
"MLine.scale_factor".to_owned(),
|
||||||
"MLine.style_name".to_owned(),
|
"MLine.style_name".to_owned(),
|
||||||
"MLine.vertices".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
|
As of 20 September 2026, the working branch is `plugin/host-model-api` at
|
||||||
`a8dd3640` plus the Leader increment, published as draft PR
|
`a8dd3640` plus the Leader increment, published as draft PR
|
||||||
[#1391](https://github.com/HakanSeven12/OpenCADStudio/pull/1391). Host API is
|
[#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
|
v7. The Python feature has explicit writes for **25 of 43** canvas kinds: 24
|
||||||
are creatable and `Insert` remains update-only. This is **not** 24 completed
|
are creatable and `Insert` remains update-only. This is **not** 25 completed
|
||||||
kinds: Tolerance, Shape, AttributeEntity, Hatch and MLine pass every completion gate.
|
kinds: Tolerance, Shape, AttributeEntity, Hatch, MLine and Dimension pass every completion gate.
|
||||||
AttributeDefinition passes its host and real IPC lifecycle but remains short of
|
AttributeDefinition passes its host and real IPC lifecycle but remains short of
|
||||||
`Complete` because the previously pinned CAD codec dropped optional ATTDEF
|
`Complete` because the previously pinned CAD codec dropped optional ATTDEF
|
||||||
fields during DXF reads; revalidate that blocker against the current
|
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
|
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.
|
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
|
## 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
|
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
|
R2010+ text-size question is decided, flip the canary assertions in
|
||||||
`staged_python_leader_lifecycle_over_real_ipc` and mark it `Complete`.
|
`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`.
|
- `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`.
|
- 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 `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
|
# 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.
|
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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| MultiLeader | Layer-only | Requires nested context and style fixture. |
|
||||||
| Table | Layer-only | Requires table style and cell fixture. |
|
| Table | Layer-only | Requires table style and cell fixture. |
|
||||||
| PolygonMesh | Layer-only | Requires consistent grid/count 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
|
catalog. All 43 canvas kinds also support nonempty `layer` changes through
|
||||||
the same undoable transaction; internal and opaque variants remain read-only
|
the same undoable transaction; internal and opaque variants remain read-only
|
||||||
through the document model. The adapter accepts only `layer` patches for
|
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
|
still report `type_conversion_only`; broader CAD range and cross-property
|
||||||
checks remain outstanding.
|
checks remain outstanding.
|
||||||
The host's `entity_snapshot` helper serializes any typed entity as a detached
|
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.
|
token from another tab returns no result and does not consume it.
|
||||||
|
|
||||||
Python `doc.entities[handle]` returns a descriptor for every entity. For kinds
|
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
|
`layer` is editable on canvas kinds. `doc.coverage()` enumerates the
|
||||||
catalog; `doc.coverage("Line")` and `line.coverage` return one entry. For
|
catalog; `doc.coverage("Line")` and `line.coverage` return one entry. For
|
||||||
covered kinds, the coverage object lists actual readable and editable
|
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
|
traced field, including fields marked `unmapped`. Query it with
|
||||||
`doc.coverage()` or `doc.coverage("Line")`; each row includes source field,
|
`doc.coverage()` or `doc.coverage("Line")`; each row includes source field,
|
||||||
type, access, and current validation status. Geometry edits remain limited to
|
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
|
For any entity, `entity.snapshot` returns a detached dictionary of its raw
|
||||||
CAD fields, including `snapshot["common"]` and kind-specific nested values.
|
CAD fields, including `snapshot["common"]` and kind-specific nested values.
|
||||||
It can inspect an unmapped kind; editing the dictionary does
|
It can inspect an unmapped kind; editing the dictionary does
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ use serde::Deserialize;
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Manifest {
|
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
|
/// Entity kinds exposed to `ocs.add`/`ocs.update`/`entity_to_dict`. The
|
||||||
/// central, easy-to-widen filter for Phase 2 (see the plan doc).
|
/// central, easy-to-widen filter for Phase 2 (see the plan doc).
|
||||||
pub type_filter: Vec<String>,
|
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();
|
let mut queue: Vec<String> = Vec::new();
|
||||||
|
|
||||||
for kind in &manifest.type_filter {
|
for kind in &manifest.type_filter {
|
||||||
|
if manifest.manual_kinds.contains(kind) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let Some(info) = get(registry, kind) else {
|
let Some(info) = get(registry, kind) else {
|
||||||
panic!("entity_manifest.json: unknown entity kind {kind}");
|
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 {
|
fn gen_entity_to_dict(manifest: &Manifest, registry: &TypeRegistry) -> String {
|
||||||
let mut arms = String::new();
|
let mut arms = String::new();
|
||||||
for kind in &manifest.type_filter {
|
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 info = get(registry, kind).unwrap();
|
||||||
let var = "value";
|
let var = "value";
|
||||||
let fields = resolve_entity_fields(manifest, registry, kind, info, var, Mode::Add);
|
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) {
|
if manifest.overrides.get(kind).is_some_and(|override_def| override_def.update_only) {
|
||||||
continue;
|
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 info = get(registry, kind).unwrap();
|
||||||
let var = "value";
|
let var = "value";
|
||||||
let fields = resolve_entity_fields(manifest, registry, kind, info, var, Mode::Add);
|
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 {
|
fn gen_apply_dict_to_entity(manifest: &Manifest, registry: &TypeRegistry) -> String {
|
||||||
let mut arms = String::new();
|
let mut arms = String::new();
|
||||||
for kind in &manifest.type_filter {
|
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 info = get(registry, kind).unwrap();
|
||||||
let var = "value";
|
let var = "value";
|
||||||
let fields = resolve_entity_fields(manifest, registry, kind, info, var, Mode::Update);
|
let fields = resolve_entity_fields(manifest, registry, kind, info, var, Mode::Update);
|
||||||
|
|||||||
@@ -23,8 +23,10 @@
|
|||||||
"AttributeEntity",
|
"AttributeEntity",
|
||||||
"Hatch",
|
"Hatch",
|
||||||
"Leader",
|
"Leader",
|
||||||
"MLine"
|
"MLine",
|
||||||
|
"Dimension"
|
||||||
],
|
],
|
||||||
|
"manual_kinds": ["Dimension"],
|
||||||
"base_fields": ["handle", "layer", "owner_handle"],
|
"base_fields": ["handle", "layer", "owner_handle"],
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"Point": {
|
"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)? {
|
match dict.get_item_opt(key, vm)? {
|
||||||
None => Ok(0.0),
|
None => Ok(0.0),
|
||||||
Some(v) if vm.is_none(&v) => 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(
|
fn get_opt_bool(
|
||||||
dict: &rustpython_vm::builtins::PyDictRef,
|
dict: &rustpython_vm::builtins::PyDictRef,
|
||||||
key: &str,
|
key: &str,
|
||||||
@@ -3186,6 +3191,7 @@ mod ocs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/entity_crud.rs"));
|
include!(concat!(env!("OUT_DIR"), "/entity_crud.rs"));
|
||||||
|
include!("dimension_model.rs.inc");
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
// Phase 1 — wiring the generic conversion functions above into `ocs`.
|
// Phase 1 — wiring the generic conversion functions above into `ocs`.
|
||||||
@@ -4640,6 +4646,23 @@ mod tests {
|
|||||||
current = None;
|
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 {
|
for entry in &catalog.entity_kinds {
|
||||||
let expected: BTreeSet<_> = entry
|
let expected: BTreeSet<_> = entry
|
||||||
.properties
|
.properties
|
||||||
@@ -4663,7 +4686,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert_eq!(emitted.len(), 24);
|
assert_eq!(emitted.len(), 25);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "experimental-host-model")]
|
#[cfg(feature = "experimental-host-model")]
|
||||||
|
|||||||
@@ -155,6 +155,9 @@ impl<'a> HostSession<'a> {
|
|||||||
old: Option<&EntityType>,
|
old: Option<&EntityType>,
|
||||||
entity: &mut EntityType,
|
entity: &mut EntityType,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
if let EntityType::Dimension(dimension) = entity {
|
||||||
|
crate::entities::dimension::normalize_scripted_dimension(dimension);
|
||||||
|
}
|
||||||
if let EntityType::MLine(new) = entity {
|
if let EntityType::MLine(new) = entity {
|
||||||
let old = match old {
|
let old = match old {
|
||||||
Some(EntityType::MLine(old)) => Some(old),
|
Some(EntityType::MLine(old)) => Some(old),
|
||||||
@@ -2572,6 +2575,209 @@ mod tests {
|
|||||||
assert!(app.tabs[0].scene.document.get_entity(handle).is_none());
|
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]
|
#[test]
|
||||||
fn staged_python_point_pick_and_cancel_over_real_ipc() {
|
fn staged_python_point_pick_and_cancel_over_real_ipc() {
|
||||||
let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else {
|
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;
|
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 {
|
fn dimension_definition_point(dim: &Dimension) -> acadrust::types::Vector3 {
|
||||||
match dim {
|
match dim {
|
||||||
Dimension::Aligned(d) => d.definition_point,
|
Dimension::Aligned(d) => d.definition_point,
|
||||||
|
|||||||
Reference in new issue
Block a user