forked from 1b-agent/opencadstudio-upstream
Support Color in the Python adapter generator; map Leader.override_color
Color converts as {'kind': ByLayer|None|ByBlock|Index|Rgb, 'value': ...}.
Leader.override_color is now writable; neither DWG nor DXF stores it, which
is recorded as a fourth persistence blocker with a canary assertion.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
6b4c86fd7a
commit
22f9c20fe8
8 files changed
+73
-9
No files matched your search
@@ -778,6 +778,7 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
|
||||
| "text_height"
|
||||
| "text_width"
|
||||
| "vertices"
|
||||
| "override_color"
|
||||
| "annotation_handle"
|
||||
| "normal"
|
||||
| "horizontal_direction"
|
||||
|
||||
@@ -281,6 +281,7 @@
|
||||
"text_height",
|
||||
"text_width",
|
||||
"vertices",
|
||||
"override_color",
|
||||
"annotation_handle",
|
||||
"normal",
|
||||
"horizontal_direction",
|
||||
|
||||
@@ -1684,6 +1684,7 @@ mod tests {
|
||||
"Leader.text_width".to_owned(),
|
||||
"Leader.vertices".to_owned(),
|
||||
"Leader.annotation_handle".to_owned(),
|
||||
"Leader.override_color".to_owned(),
|
||||
"Leader.normal".to_owned(),
|
||||
"Leader.horizontal_direction".to_owned(),
|
||||
"Leader.block_offset".to_owned(),
|
||||
|
||||
@@ -28,7 +28,7 @@ Completion gate codes: **C** = create through Python document model; **R** = rea
|
||||
| AttributeDefinition | **Integration-tested / DXF blocker pending revalidation** | Host commit `5b854bd1` maps block ownership plus every stable ATTDEF field except the nested `embedded_mtext` payload, which is snapshot-readable and preserved untouched. `staged_python_attribute_definition_lifecycle_over_real_ipc` passes C/R/E/D/U/I/V/P with a real block record, Block/BlockEnd markers and linked Insert: high-level creation inside the block, readback, grouped placement/default/rotation/width edits, selection, rendered text, atomic tag/height/style/owner rejection, deletion persistence, and three-step undo/redo. DWG reopens with the full edited state. DXF reopens identity, ownership, tag, prompt, value, placement, height and rotation, and OCS no longer double-converts ATTDEF/ATTRIB rotation. **W remains open:** acadrust `568a12c` dropped optional ATTDEF fields including width factor while reading DXF. Rerun the regression against the currently merged `7ea4247` revision and update this row with the result. `embedded_mtext` mutation remains unsupported pending linked-layout validation. |
|
||||
| AttributeEntity | **Complete** | OCS commit `cf2a9235` and adapter commit `9c6b5c3` make nested ATTRIB records addressable by handle while retaining canonical storage in `Insert.attributes`. Host validation binds tag to a definition in the Insert's block, validates placement/text geometry and style references, keeps owner and definition handles read-only, and preserves embedded MTEXT. `staged_python_attribute_entity_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P with a real block, ATTDEF and Insert: high-level create/read/transaction edit/delete, parent selection, rendered text, atomic invalid tag/height/owner rejection, edited and deleted DWG/DXF reopen, and three-step undo/redo. `nested_insert_attribute_crud_uses_parent_storage_and_undo` additionally proves the child never becomes an orphan flat entity in the host document. |
|
||||
| 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`. `override_color` is writable through the new generator `Color` form (`{'kind': 'Rgb', 'value': {'r','g','b'}}`, `Index`, `ByLayer`, `ByBlock`, `None`). Unmapped but preserved on edit: 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); (4) neither writer stores `override_color`, so it reopens as `ByLayer` in DWG and DXF. Not `Complete` until the engine round-trips (1) and (2); (3) needs a decision to document or make read-only. |
|
||||
| MLine | **Complete** | Writable: `vertices` (raw vertex dicts; a script may pass only `position`), `flags` (integer, unknown bits rejected), `justification`, `normal`, `scale_factor`, `style_name`. Read-only: `start_point`, `style_handle`, `style_element_count`. Host validation requires ≥2 finite non-coincident vertices, ≥3 for a closed multiline, a finite nonzero scale and normal, an existing non-empty `MLineStyle`, and per-vertex segments that are either empty or match the style element count. Vertex `direction`/`miter`, segment layout and per-element miter offsets (justification and scale included) are host-derived by `normalize_scripted_mline` on every scripted create or edit, reusing the drawing command's `rebuild_mline_geometry`/`sync_mline_element_parameters`; dash and fill cuts survive an edit unless a vertex moves. `staged_python_mline_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P: create against the Standard style, a three-vertex/scale/justification transaction edit with untouched-field preservation, corner miter widening and element offsets checked, real `mline_lines` linework before and after DWG and DXF reopen, nine single-edit rejections plus a cross-entity atomic rollback, re-edit after reopen in both formats, deletion, and three-step undo/redo. Known limit: switching to a style with a different element count is rejected until the script also clears the vertices' `segments`; only the rejection is covered by the integration test. |
|
||||
| Dimension | **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. |
|
||||
|
||||
@@ -110,7 +110,7 @@ const BUILTIN: &[&str] = &[
|
||||
];
|
||||
/// Types with their own hand-written conversion helpers in `ocs_module.rs`
|
||||
/// (`vector3_to_py`/`py_to_vector3`, etc.) rather than generated ones.
|
||||
const SPECIAL: &[&str] = &["Vector3", "Vector2", "Handle"];
|
||||
const SPECIAL: &[&str] = &["Vector3", "Vector2", "Handle", "Color"];
|
||||
|
||||
enum Classify {
|
||||
/// A C-like enum: every variant is a unit variant. Represented in Python
|
||||
@@ -289,6 +289,7 @@ fn default_expr(type_id: &str) -> String {
|
||||
"Vector3" => "acadrust::types::Vector3::ZERO".to_string(),
|
||||
"Vector2" => "acadrust::types::Vector2::ZERO".to_string(),
|
||||
"Handle" => "acadrust::Handle::new(0)".to_string(),
|
||||
"Color" => "acadrust::types::Color::ByLayer".to_string(),
|
||||
other => format!("default_{}()", snake_case(other)),
|
||||
}
|
||||
}
|
||||
@@ -309,6 +310,7 @@ fn leaf_to_py(registry: &TypeRegistry, type_id: &str, item_expr: &str) -> String
|
||||
"Vector3" => format!("vector3_to_py_dict(vm, {item_expr})"),
|
||||
"Vector2" => format!("vector2_to_py(vm, {item_expr})"),
|
||||
"Handle" => format!("Ok(vm.new_pyobj({parenthesized}.value()))"),
|
||||
"Color" => format!("color_to_py_dict(vm, {item_expr})"),
|
||||
other => {
|
||||
let info = get(registry, other)
|
||||
.unwrap_or_else(|| panic!("type {other} missing from registry"));
|
||||
@@ -333,6 +335,7 @@ fn leaf_from_py(registry: &TypeRegistry, type_id: &str, value_expr: &str) -> Str
|
||||
"Vector3" => format!("py_to_vector3_dict({value_expr}, vm)"),
|
||||
"Vector2" => format!("py_to_vector2({value_expr}, vm)"),
|
||||
"Handle" => format!("{value_expr}.try_into_value::<u64>(vm).map(acadrust::Handle::new)"),
|
||||
"Color" => format!("py_to_color_dict({value_expr}, vm)"),
|
||||
other => {
|
||||
let info = get(registry, other)
|
||||
.unwrap_or_else(|| panic!("type {other} missing from registry"));
|
||||
|
||||
@@ -262,11 +262,7 @@
|
||||
"dwg_unknown_bit3": { "exclude": true },
|
||||
"dwg_unknown_bit4": { "exclude": true },
|
||||
"dwg_unknown_bit5": { "exclude": true },
|
||||
"dwg_unknown_short1": { "exclude": true },
|
||||
"override_color": {
|
||||
"exclude": true,
|
||||
"_comment": "Color is a multi-payload tagged enum the generator cannot express yet. It stays snapshot-readable and is preserved untouched by Python edits."
|
||||
}
|
||||
"dwg_unknown_short1": { "exclude": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3131,6 +3131,63 @@ mod ocs {
|
||||
}
|
||||
}
|
||||
|
||||
/// `Color` as `{"kind": "ByLayer" | "None" | "ByBlock"}`,
|
||||
/// `{"kind": "Index", "value": n}` or
|
||||
/// `{"kind": "Rgb", "value": {"r": .., "g": .., "b": ..}}`.
|
||||
fn color_to_py_dict(vm: &VirtualMachine, color: &acadrust::types::Color) -> PyResult<PyObjectRef> {
|
||||
use acadrust::types::Color;
|
||||
let dict = vm.ctx.new_dict();
|
||||
match color {
|
||||
Color::ByLayer => dict.set_item("kind", vm.new_pyobj("ByLayer"), vm)?,
|
||||
Color::None => dict.set_item("kind", vm.new_pyobj("None"), vm)?,
|
||||
Color::ByBlock => dict.set_item("kind", vm.new_pyobj("ByBlock"), vm)?,
|
||||
Color::Index(index) => {
|
||||
dict.set_item("kind", vm.new_pyobj("Index"), vm)?;
|
||||
dict.set_item("value", vm.new_pyobj(i64::from(*index)), vm)?;
|
||||
}
|
||||
Color::Rgb { r, g, b } => {
|
||||
dict.set_item("kind", vm.new_pyobj("Rgb"), vm)?;
|
||||
let rgb = vm.ctx.new_dict();
|
||||
rgb.set_item("r", vm.new_pyobj(i64::from(*r)), vm)?;
|
||||
rgb.set_item("g", vm.new_pyobj(i64::from(*g)), vm)?;
|
||||
rgb.set_item("b", vm.new_pyobj(i64::from(*b)), vm)?;
|
||||
dict.set_item("value", PyObjectRef::from(rgb), vm)?;
|
||||
}
|
||||
}
|
||||
Ok(dict.into())
|
||||
}
|
||||
|
||||
fn py_to_color_dict(value: PyObjectRef, vm: &VirtualMachine) -> PyResult<acadrust::types::Color> {
|
||||
use acadrust::types::Color;
|
||||
let dict = value.try_into_value::<rustpython_vm::builtins::PyDictRef>(vm)?;
|
||||
let kind = dict.get_item("kind", vm)?.try_into_value::<String>(vm)?;
|
||||
let byte = |v: PyObjectRef, what: &str| -> PyResult<u8> {
|
||||
let n = v.try_into_value::<i64>(vm)?;
|
||||
u8::try_from(n).map_err(|_| vm.new_value_error(format!("Color {what} must be 0..=255")))
|
||||
};
|
||||
Ok(match kind.as_str() {
|
||||
"ByLayer" => Color::ByLayer,
|
||||
"None" => Color::None,
|
||||
"ByBlock" => Color::ByBlock,
|
||||
"Index" => {
|
||||
let index = byte(dict.get_item("value", vm)?, "index")?;
|
||||
if index == 0 {
|
||||
return Err(vm.new_value_error("Color index must be 1..=255; use ByBlock for 0".to_owned()));
|
||||
}
|
||||
Color::Index(index)
|
||||
}
|
||||
"Rgb" => {
|
||||
let rgb = dict.get_item("value", vm)?.try_into_value::<rustpython_vm::builtins::PyDictRef>(vm)?;
|
||||
Color::Rgb {
|
||||
r: byte(rgb.get_item("r", vm)?, "r")?,
|
||||
g: byte(rgb.get_item("g", vm)?, "g")?,
|
||||
b: byte(rgb.get_item("b", vm)?, "b")?,
|
||||
}
|
||||
}
|
||||
other => return Err(vm.new_value_error(format!("ocs: unsupported Color kind: {other}"))),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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())
|
||||
|
||||
@@ -2265,6 +2265,7 @@ mod tests {
|
||||
" leader.text_height = 4.0\n",
|
||||
" leader.arrow_enabled = False\n",
|
||||
" leader.annotation_offset = (1.0, 2.0, 0.0)\n",
|
||||
" leader.override_color = {{'kind':'Rgb','value':{{'r':255,'g':0,'b':0}}}}\n",
|
||||
"doc.selection = [leader]\n"
|
||||
), leader_handle.value())).unwrap();
|
||||
dispatch(&mut host, &format!("PY_RUN {}", script.display()));
|
||||
@@ -2281,7 +2282,7 @@ mod tests {
|
||||
assert_eq!(edited.hookline_enabled, created_leader.hookline_enabled);
|
||||
assert_eq!(edited.normal, created_leader.normal);
|
||||
assert_eq!(edited.origin, created_leader.origin);
|
||||
assert_eq!(edited.override_color, created_leader.override_color);
|
||||
assert_eq!(edited.override_color, acadrust::types::Color::Rgb { r: 255, g: 0, b: 0 });
|
||||
assert_eq!(host.selection(), vec![leader_handle]);
|
||||
assert_geometry(&expected, host.document());
|
||||
|
||||
@@ -2292,7 +2293,7 @@ mod tests {
|
||||
("'vertices':[{'x':0.0,'y':0.0,'z':0.0}]", "at least 2"),
|
||||
("'text_height':0.0", "greater than zero"),
|
||||
("'dimension_style':'Missing'", "dimension style"),
|
||||
("'override_color':{}", "outside the editable schema"),
|
||||
("'override_color':{'kind':'Index','value':300}", "Color"),
|
||||
] {
|
||||
dispatch(&mut host, &format!(
|
||||
"PY_EVAL ocs.update_many('Reject leader', [{{'handle':{}, {patch}}}])",
|
||||
@@ -2325,6 +2326,10 @@ mod tests {
|
||||
assert_eq!(leader.annotation_handle, text_handle);
|
||||
}
|
||||
assert!(!leader.arrow_enabled);
|
||||
// BLOCKER: neither writer stores `override_color`, so it reopens
|
||||
// as ByLayer in both formats.
|
||||
assert_eq!(leader.override_color, acadrust::types::Color::ByLayer,
|
||||
"acadrust now persists Leader.override_color; drop the blocker");
|
||||
if is_dxf {
|
||||
// BLOCKER: the DXF writer emits group 213 but the reader's
|
||||
// coordinate mapping does not reassemble it.
|
||||
|
||||
Reference in new issue
Block a user