forked from 1b-agent/opencadstudio-upstream
Map SectionSymbol, the last unmapped kind
points is canonical; the host derives the point counts and refreshes the end/tick/label projections with sync_display_fields when it changes, and validates that style and view-representation references are existing class objects. The lifecycle test passes every gate in DWG; DXF reopens the symbol as an unknown entity because cadcodec dispatches SECTIONLINE only inside blocks (pinned by a canary and already in the cadcodec write-up). Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
5a39052397
commit
942222c381
11 files changed
+242
-10
No files matched your search
@@ -793,6 +793,7 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
|
||||
| ("Helix", _)
|
||||
| ("RasterImage", _)
|
||||
| ("Wipeout", _)
|
||||
| ("SectionSymbol", _)
|
||||
| ("Surface", "u_isolines" | "v_isolines")
|
||||
| ("Ole2Frame", _)
|
||||
| ("Underlay", _)
|
||||
|
||||
@@ -507,6 +507,13 @@
|
||||
"layer",
|
||||
"u_isolines",
|
||||
"v_isolines"
|
||||
],
|
||||
"SectionSymbol": [
|
||||
"layer",
|
||||
"symbol_scale",
|
||||
"points",
|
||||
"style_handle",
|
||||
"view_rep_handle"
|
||||
]
|
||||
},
|
||||
"readable": {
|
||||
@@ -638,6 +645,19 @@
|
||||
"modeler_format_version",
|
||||
"point_of_reference",
|
||||
"history_handle"
|
||||
],
|
||||
"SectionSymbol": [
|
||||
"view_symbol_version",
|
||||
"raw_view_symbol_70",
|
||||
"version",
|
||||
"raw_point_count_90",
|
||||
"raw_flags_90",
|
||||
"raw_point_record_count",
|
||||
"end_a",
|
||||
"end_b",
|
||||
"tick_a",
|
||||
"tick_b",
|
||||
"label"
|
||||
]
|
||||
},
|
||||
"aliases": {
|
||||
|
||||
@@ -1263,6 +1263,33 @@ fn validate_surface(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// SectionSymbol: the cut mark drawn on a Model-Documentation view. `points`
|
||||
/// is the canonical geometry; the counts and the end/tick/label projections are
|
||||
/// derived by the host, so only the inputs are validated.
|
||||
#[cfg(feature = "host")]
|
||||
fn validate_section_symbol(
|
||||
old: Option<&crate::host::acadrust::entities::SectionSymbol>,
|
||||
new: &crate::host::acadrust::entities::SectionSymbol,
|
||||
) -> Result<(), String> {
|
||||
if old.is_some_and(|old| old.points == new.points && old.symbol_scale.to_bits() == new.symbol_scale.to_bits()) {
|
||||
return Ok(());
|
||||
}
|
||||
if !new.symbol_scale.is_finite() || new.symbol_scale <= 0.0 {
|
||||
return Err("SectionSymbol.symbol_scale must be finite and greater than zero".into());
|
||||
}
|
||||
if new.points.len() < 2 {
|
||||
return Err("SectionSymbol.points requires at least 2 points".into());
|
||||
}
|
||||
for (index, point) in new.points.iter().enumerate() {
|
||||
finite_vector(&format!("SectionSymbol.points[{index}].point"), &point.point)?;
|
||||
finite_vector(&format!("SectionSymbol.points[{index}].label_offset"), &point.label_offset)?;
|
||||
if !point.bulge.is_finite() {
|
||||
return Err(format!("SectionSymbol.points[{index}].bulge must be finite"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate newly created geometry for the kinds whose mapped fields have
|
||||
/// host checks. Called by the Python add path before an entity enters the document.
|
||||
#[cfg(feature = "host")]
|
||||
@@ -1390,6 +1417,7 @@ pub fn validate_new_canvas_entity(entity: &crate::host::EntityType) -> Result<()
|
||||
}
|
||||
EntityType::Ole2Frame(value) => validate_ole_frame(None, value)?,
|
||||
EntityType::Surface(value) => validate_surface(None, value)?,
|
||||
EntityType::SectionSymbol(value) => validate_section_symbol(None, value)?,
|
||||
EntityType::AttributeDefinition(value) => {
|
||||
finite_vector(
|
||||
"AttributeDefinition.insertion_point",
|
||||
@@ -1765,6 +1793,7 @@ pub fn validate_entity_mutation(
|
||||
}
|
||||
}
|
||||
(EntityType::Surface(old), EntityType::Surface(new)) => validate_surface(Some(old), new)?,
|
||||
(EntityType::SectionSymbol(old), EntityType::SectionSymbol(new)) => validate_section_symbol(Some(old), new)?,
|
||||
(EntityType::MLine(old), EntityType::MLine(new)) => validate_mline(Some(old), new)?,
|
||||
(EntityType::Leader(old), EntityType::Leader(new)) => validate_leader(Some(old), new)?,
|
||||
(EntityType::AttributeDefinition(old), EntityType::AttributeDefinition(new)) => {
|
||||
@@ -2075,6 +2104,28 @@ pub fn validate_canvas_entity_references(
|
||||
}
|
||||
}
|
||||
}
|
||||
if let EntityType::SectionSymbol(value) = entity {
|
||||
use crate::host::acadrust::objects::{ClassObjectData, ObjectType};
|
||||
for (name, handle, wanted) in [
|
||||
("style", value.style_handle, "SectionViewStyle"),
|
||||
("view representation", value.view_rep_handle, "ViewRep"),
|
||||
] {
|
||||
if handle.is_null() {
|
||||
continue;
|
||||
}
|
||||
let matches = match document.objects.get(&handle) {
|
||||
Some(ObjectType::ClassObject(object)) => match (&object.data, wanted) {
|
||||
(ClassObjectData::SectionViewStyle(_), "SectionViewStyle") => true,
|
||||
(ClassObjectData::ViewRep(_), "ViewRep") => true,
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
};
|
||||
if !matches {
|
||||
return Err(format!("SectionSymbol {name} {handle:?} is not an existing {wanted} object"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let EntityType::ViewBorder(value) = entity {
|
||||
if !matches!(document.get_entity(value.active_viewport), Some(EntityType::Viewport(_))) {
|
||||
return Err(format!("ViewBorder viewport {:?} does not exist", value.active_viewport));
|
||||
@@ -2570,6 +2621,7 @@ mod tests {
|
||||
| "Body"
|
||||
| "Region"
|
||||
| "Surface"
|
||||
| "SectionSymbol"
|
||||
)
|
||||
{
|
||||
assert!(
|
||||
@@ -2861,6 +2913,10 @@ mod tests {
|
||||
"Ole2Frame.is_paper_space".to_owned(),
|
||||
"Surface.u_isolines".to_owned(),
|
||||
"Surface.v_isolines".to_owned(),
|
||||
"SectionSymbol.symbol_scale".to_owned(),
|
||||
"SectionSymbol.points".to_owned(),
|
||||
"SectionSymbol.style_handle".to_owned(),
|
||||
"SectionSymbol.view_rep_handle".to_owned(),
|
||||
"Underlay.underlay_type".to_owned(),
|
||||
"Underlay.definition_handle".to_owned(),
|
||||
"Underlay.insertion_point".to_owned(),
|
||||
|
||||
@@ -151,7 +151,8 @@ reopens as a different, opaque entity kind instead of `ViewBorder`. DWG restores
|
||||
the typed record. The same applies to `SectionSymbol`.
|
||||
|
||||
**Evidence:** `audit_python_view_border_lifecycle_over_real_ipc` (canary
|
||||
`expect_edited_dxf: "wrong kind"`).
|
||||
`expect_edited_dxf: "wrong kind"`) and `audit_python_section_symbol_lifecycle_over_real_ipc`
|
||||
(canary `wrong kind Unknown`).
|
||||
|
||||
**Suggested fix:** add the two arms to the ENTITIES dispatch, calling
|
||||
`read_view_border_dxf` and `read_section_symbol_dxf`.
|
||||
|
||||
@@ -7,13 +7,13 @@ Build a general Python document model over the OCS host API for all **43 canvas
|
||||
As of 20 September 2026, the working branch is `plugin/host-model-api` at
|
||||
`a8dd3640` plus the Leader increment, published as draft PR
|
||||
[#1391](https://github.com/HakanSeven12/OpenCADStudio/pull/1391). Host API is
|
||||
v7. The Python feature has explicit writes for **42 of 43** canvas kinds: 39
|
||||
v7. The Python feature has explicit writes for **43 of 43** canvas kinds: 40
|
||||
are creatable (the legacy `Polyline`, `Ole2Frame` and `Body` are update-only). This is **not** 41 completed
|
||||
kinds: Tolerance, Shape, AttributeEntity, Hatch, MLine, Dimension, MultiLeader, Table, PolygonMesh, PolyfaceMesh, Mesh, Wipeout, Point, Line, Circle, Arc, Ellipse, LwPolyline, Spline, Text, MText, Ray, XLine, Solid, Face3D, Solid3D, Region and Surface pass every completion gate; Insert now passes every gate too; the audited Polyline (update-only by decision), Polyline2D and Polyline3D pass every gate that their format allows.
|
||||
AttributeDefinition passes its host and real IPC lifecycle but remains short of
|
||||
`Complete` because the previously pinned CAD codec dropped optional ATTDEF
|
||||
fields during DXF reads; revalidate that blocker against the current
|
||||
`acadrust` revision `7ea4247`. Leader is mapped and integration-tested but blocked at W by three acadrust persistence gaps (see the ledger). Leader, Helix, Underlay, ViewBorder and Light are mapped and integration-tested but blocked at W by acadrust persistence gaps, and RasterImage and Viewport are held short of `Complete` by unverifiable or missing oracles (see the ledger). RasterImage is mapped and integration-tested, held short of `Complete` by unverifiable image-dictionary linkage. The remaining canvas kind, SectionSymbol, has only `layer` and is recorded as blocked
|
||||
`acadrust` revision `7ea4247`. Leader is mapped and integration-tested but blocked at W by three acadrust persistence gaps (see the ledger). Leader, Helix, Underlay, ViewBorder, Light and SectionSymbol are mapped and integration-tested but blocked at W by acadrust persistence gaps, and RasterImage and Viewport are held short of `Complete` by unverifiable or missing oracles (see the ledger). RasterImage is mapped and integration-tested, held short of `Complete` by unverifiable image-dictionary linkage. Every canvas kind now has explicit writes; SectionSymbol is mapped and integration-tested but blocked at DXF persistence by cadcodec
|
||||
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.
|
||||
|
||||
@@ -182,3 +182,4 @@ exact queue above and update statuses with evidence after each increment.
|
||||
- Completed `Surface` for planes, extruded open profiles, isolines and rigid moves. `SurfaceFromProfile` and `Extrude` join the kernel operations (`doc.solids.surface` and `doc.solids.extrude`); `Extrude` also gives Solid3D extrusions from closed profiles, checked against analytic volumes. The test found and fixed a real bug: Surface's `kind` field silently overwrote the entity-type `kind` key in Python dicts, so it is exposed as `surface_kind`, and a new uniqueness test guards the whole catalog. Transform now accepts surfaces (a plane stays a plane; other kinds turn generic). Left: booleans (release timing first), regions with holes, and lofted, revolved, swept and NURBS surfaces. SectionSymbol is the only unmapped kind.
|
||||
- Timed booleans in a release build (details in [cadkernel-body-path.md](cadkernel-body-path.md)). A curved box-and-cylinder boolean takes about 1 to 2.2 s, overlapping spheres 0.2 s and planar operands 1 to 2 ms, against 23 s in a debug build. The kernel refuses many curved cases quickly with a reason (`Coincident`, `CutRefused`, `NoClosedForm`), including a second cut in an already drilled box. Recommendation: add a synchronous `SolidOperation::Boolean` that surfaces the refusal and leaves the operands untouched, test it with planar operands plus one ignored curved case, and note the 30 s per-script budget. No code changed in this step.
|
||||
- Implemented `SolidOperation::Boolean` and `doc.solids.union`, `subtract` and `intersect`, as recommended by the timing study. It runs synchronously, surfaces the kernel's refusal (`Coincident`, `CutRefused`, `NoClosedForm`) with a plain-language reason and the words "nothing was changed", consumes the operands unless `keep_operands`, and records one undo step. The lifecycle test uses planar boxes (millisecond cost, exact volumes, debug-safe) and an `#[ignore]` test documents a curved refusal. With this the kernel operations in the cadkernel note are all built. SectionSymbol is the only unmapped kind.
|
||||
- Mapped `SectionSymbol`, the last unmapped kind, so all 43 canvas kinds now have explicit writes. The blocker recorded earlier was overstated: cadcodec documents `points` as canonical, says the counts equal the point count in every verified export, and provides `sync_display_fields()`, which OCS's own edits already call; OCS also renders a symbol with no style or view link. The host derives the counts and projections when `points` changes and validates that references are existing `SectionViewStyle` and `ViewRep` class objects. Every gate passes except DXF, where cadcodec reopens a top-level `SECTIONLINE` as an unknown entity (issue 5 in the cadcodec write-up, pinned by a canary). Still unverifiable here: the meaning of `raw_flags_90` and AutoCAD's acceptance of a symbol with no parent view.
|
||||
@@ -1,6 +1,6 @@
|
||||
# OCS canvas kind coverage ledger
|
||||
|
||||
Use with [the completion plan](plugin-host-model-completion-plan.md). This ledger is current as of 20 September 2026: **42 of 43 canvas kinds have explicit geometry writes** (39 creatable, plus update-only legacy `Polyline`, `Ole2Frame` and `Body`), **29 kinds are complete**, and 1 remains layer-only (SectionSymbol, blocked). `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: **43 of 43 canvas kinds have explicit geometry writes** (40 creatable, plus update-only legacy `Polyline`, `Ole2Frame` and `Body`), **29 kinds are complete**, and none remains 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.
|
||||
|
||||
@@ -42,7 +42,7 @@ Completion gate codes: **C** = create through Python document model; **R** = rea
|
||||
| Underlay | **Integration-tested / blocked at W by acadrust (DXF rotation units)** | Writable: `underlay_type` and `definition_handle` (both fixed by the definition: creation names an existing `UnderlayDefinition`, and a mismatched type or missing definition is rejected), `insertion_point`, `x_scale`, `y_scale`, `z_scale`, `rotation`, `normal`, `flags`, `contrast`, `fade`, `clip_boundary_vertices`, `clip_inverted`. The file path and page live on the definition object, which scripts cannot create or list yet; they reference one the drawing already owns. Host validation requires finite nonzero scales, a finite rotation, a nonzero normal, contrast/fade within 0..=100, known flags, finite clip vertices (none, or at least 2) and an existing definition of the same type. `staged_python_underlay_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle` with a fixture PDF definition) passes C/R/E/D/U/I/V/P and DWG W: creation, a placement/scale/rotation/contrast/fade/clip transaction, canvas wires, eight rejection cases plus an atomic rollback, DWG reopen and re-edit, and three-step undo/redo. **W blocker (engine, pinned by canary assertions):** DXF stores the rotation in degrees but the reader returns it unconverted, so 0.5 rad reopens as 28.6 and each further DXF save-reopen compounds it (1641.4 after a second), silently corrupting the rotation. Not `Complete` until the reader converts degrees back to radians. Not covered: creating definitions from a file path, DWF/DGN fixtures. |
|
||||
| Viewport | **Integration-tested** | Writable: `center`, `width`, `height`, `status`, `view_center`, `view_direction`, `view_target`, `lens_length`, `front_clip_z`, `back_clip_z`, `view_height`, `twist_angle`, `custom_scale`, `frozen_layers` (layer handles), `render_mode`, `circle_sides`, `ucs_icon_visible`. Read-only: `id`, snap/grid settings, UCS fields, `clip_boundary_handle`, `style_sheet`, shade-plot, background, visual-style and sun handles, lighting, `ambient_color`, `elevation`. Reference rules: the owner must be a paper-space layout block (a model-space viewport is rejected) and every frozen-layer handle must be an existing layer. Host validation requires finite centers, positive width, height, view height, lens length and scale, a nonzero view direction, finite clip planes and twist, and at least 3 circle sides. `staged_python_viewport_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`, paper-space owner supplied by the driver) passes C/R/E/D/U/V/W/P: creation in the paper-space block, a move/resize/view-height/twist/frozen-layer transaction, seven rejection cases plus an atomic rollback, DWG and DXF reopen with re-edit, and three-step undo/redo. **Why not `Complete`:** the I gate lacks a canvas oracle because a paper-space viewport only draws on its layout tab, which the driver does not activate; viewport `id` uniqueness within a layout, clip boundary entities and view-to-scale synchronization are not handled by the host. |
|
||||
| ViewBorder | **Integration-tested / blocked at W by acadrust (DXF kind)** | Writable: `min`, `max`, `center` (2-D pairs), `scale`, `rotation_angle`, `active_viewport` and `scale_handle` (both required at creation). `version` is read-only. Reference rules: `active_viewport` must be an existing Viewport entity and `scale_handle` an existing `Scale` object. Host validation requires finite bounds with `min` below `max` on both axes, a positive scale and a finite rotation. `staged_python_view_border_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`, driver-made viewport and scale fixtures) passes C/R/E/D/U/I/V/P and DWG W: creation, a resize/recenter/scale/rotation transaction, canvas wires, seven rejection cases plus an atomic rollback, DWG reopen and re-edit, and three-step undo/redo. **W blocker (engine, pinned by a canary):** a DXF `DRAWINGVIEW` reopens as a different, opaque entity kind rather than as a ViewBorder, so only DWG restores the typed record. Not `Complete` until DXF loading restores the typed entity. Not covered: the linked view representation and drawing-view definition records. |
|
||||
| SectionSymbol | **Blocked** | Not mapped. A valid section mark needs a `SectionViewStyle` and a `ViewRep` view-representation object that a script cannot create through the host API, and the entity stores raw point counts and flag words (`raw_point_count_90`, `raw_flags_90`, `raw_point_record_count`, `raw_view_symbol_70`) whose relationship to `points` is undocumented in acadrust and is written back verbatim by both writers, so the host cannot keep them consistent after a scripted edit without guessing. Adding the kind needs (1) host-side creation or lookup of a section style and view representation, and (2) a confirmed derivation of the raw counts from a real DWG produced by another CAD application. The kind keeps raw snapshot access and layer edits. |
|
||||
| SectionSymbol | **Integration-tested / blocked at W by cadcodec (DXF SECTIONLINE)** | The earlier blocker was overstated, and reading cadcodec and OCS showed why. cadcodec documents `points` as the canonical geometry, says the point-count fields equal the point count in every verified ODA export, and ships `sync_display_fields()` to refresh the `end_a`/`end_b`/`tick_a`/`tick_b`/`label` projections; OCS's own grip and transform code already calls it. OCS also renders a symbol with no style and no view link. So the kind is mapped: writable `points` (each `{point, bulge, label, label_offset, raw_flag_280}`), `symbol_scale`, `style_handle` and `view_rep_handle`; read-only `view_symbol_version`, `raw_view_symbol_70`, `version`, `raw_flags_90`, the two derived counts and the five projections. The host sets `raw_point_count_90` and `raw_point_record_count` to the point count and calls `sync_display_fields()` whenever `points` is created or changed, and leaves a symbol whose points did not change exactly as read. Host validation requires at least two finite points with finite bulges and label offsets, a positive scale, and that each non-null reference is an existing `SectionViewStyle` (style) or `ViewRep` (view representation) class object; the wrong object kind is refused. Creating one without references gives a standalone symbol. `audit_python_section_symbol_lifecycle_over_real_ipc` (shared driver) passes C/R/E/D/U/I/V/P and DWG W: a two-point symbol, a three-point edit with a new label, bulge and scale in one transaction (counts and projections follow), canvas wires, seven rejection cases plus an atomic rollback, DWG reopen and re-edit, three-step undo/redo. `audit_python_section_symbol_references_over_real_ipc` covers valid references, a standalone symbol and four wrong-object refusals with host-made class objects. **W blocker (cadcodec, pinned by a canary; issue 5 in [cadcodec-reader-gaps.md](cadcodec-reader-gaps.md)):** the DXF reader dispatches `SECTIONLINE` only inside blocks, so a symbol in the drawing's entity list reopens as an `Unknown` entity. Open items that cannot be checked here: `raw_flags_90` has unknown semantics (0 for a new symbol, preserved on read), and whether AutoCAD accepts a symbol with no parent view. Scripts cannot create the style or view-representation objects. |
|
||||
| Light | **Integration-tested / blocked at W by acadrust (DXF `cast_shadows`)** | Writable: `name`, `light_type` (1 distant, 2 point, 3 spot), `position`, `target`, `status`, `light_color`, `plot_glyph`, `intensity`, the attenuation type and limits, `hotspot_angle`, `falloff_angle`, `cast_shadows`, `shadow_type`, `shadow_map_size`, `shadow_map_softness`, `photometric_mode`, `photometric_data`; `class_version` is read-only. Host validation requires a non-empty name, a known light type, finite placement, a spot or distant target that differs from the position, non-negative intensity, angles and limits, an attenuation start not above its end, a spot hotspot not above its falloff, and a non-negative shadow map size. `staged_python_light_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`) passes C/R/E/D/U/I/V/P and DWG W: a spot light with an RGB colour, an intensity/target/position/shadow transaction that preserves the colour and untouched fields, the canvas glyph, eight rejection cases plus an atomic rollback, DWG reopen and re-edit, and three-step undo/redo. **W blocker (engine, pinned by a canary):** the DXF reader does not restore `cast_shadows`, which reopens false; colour, placement and intensity persist. Not covered: photometric web data edits and sun/skylight objects. |
|
||||
| Region | **Complete (create from a profile + rigid move)** | New `SolidOperation::RegionFromProfile` and `doc.solids.region(profile, layer=None, delete_source=False)`. The host builds the region exactly as the REGION command does (`presspull_model::profile_geometry` then `cadkernel::brep::planar_region`), verifies it with `solid_to_sat`, puts it on `layer` or on the profile's layer, and erases the profile only when `delete_source` is set. One closed planar profile per call (a circle, ellipse, closed polyline, spline and so on); an open profile, a ray, a region, a missing entity or an empty layer is refused with a message before anything changes. Rigid moves go through the same `doc.solids.transform` channel as Solid3D. `audit_python_region_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P through the real runner: regions from a circle (source kept), a closed rectangle (source deleted) and a second circle on its own layer with exact extents, canvas geometry, six refusals that change nothing and record no undo step, a move with the exact shifted extent, DWG and DXF reopen with the payload lifting and the same extent, a reopened region moving again, and undo restoring the region exactly. Not covered: regions with holes and merging open curves into a boundary (the REGION command's second path), and AutoCAD acceptance of the payload. |
|
||||
| Body | **Integration-tested (rigid move only)** | Mapped read-only like Solid3D (same payload container). `audit_python_body_transform_over_real_ipc` moves a Body carrying a kernel box through `doc.solids.translate`, checks the exact extent, DWG reopen with a losslessly lifting payload and the exact volume, and the explanatory refusal of `create_entity('Body')`. Scripts cannot create a Body, and a non-solid (sheet or wire) body that does not lift is refused untouched. |
|
||||
|
||||
@@ -35,7 +35,7 @@ attached attribute and sequence records. Those fields report `transaction_geomet
|
||||
catalog. All 43 canvas kinds also support nonempty `layer` changes through
|
||||
the same undoable transaction; internal and opaque variants remain read-only
|
||||
through the document model. The adapter accepts only `layer` patches for
|
||||
canvas kinds outside its 42 geometry converters. Other writable properties
|
||||
canvas kinds outside its 43 geometry converters. Other writable properties
|
||||
still report `type_conversion_only`; broader CAD range and cross-property
|
||||
checks remain outstanding.
|
||||
The host's `entity_snapshot` helper serializes any typed entity as a detached
|
||||
@@ -92,7 +92,7 @@ Input tokens are bound to the drawing tab that requested the pick. Polling a
|
||||
token from another tab returns no result and does not consume it.
|
||||
|
||||
Python `doc.entities[handle]` returns a descriptor for every entity. For kinds
|
||||
outside the 42-kind generated schema it contains only handle, kind, and layer;
|
||||
outside the 43-kind generated schema it contains only handle, kind, and layer;
|
||||
`layer` is editable on canvas kinds. `doc.coverage()` enumerates the
|
||||
catalog; `doc.coverage("Line")` and `line.coverage` return one entry. For
|
||||
covered kinds, the coverage object lists actual readable and editable
|
||||
|
||||
@@ -94,7 +94,7 @@ The host-owned catalog classifies all 48 entity variants and tracks every
|
||||
traced field, including fields marked `unmapped`. Query it with
|
||||
`doc.coverage()` or `doc.coverage("Line")`; each row includes source field,
|
||||
type, access, and current validation status. Geometry edits remain limited to
|
||||
the 42 mapped kinds.
|
||||
the 43 mapped kinds.
|
||||
For any entity, `entity.snapshot` returns a detached dictionary of its raw
|
||||
CAD fields, including `snapshot["common"]` and kind-specific nested values.
|
||||
It can inspect an unmapped kind; editing the dictionary does
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
"Solid3D",
|
||||
"Body",
|
||||
"Region",
|
||||
"Surface"
|
||||
"Surface",
|
||||
"SectionSymbol"
|
||||
],
|
||||
"manual_kinds": [
|
||||
"Dimension"
|
||||
@@ -926,6 +927,47 @@
|
||||
"read_only": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"SectionSymbol": {
|
||||
"fields": {
|
||||
"points": {
|
||||
"required": true
|
||||
},
|
||||
"view_symbol_version": {
|
||||
"read_only": true
|
||||
},
|
||||
"raw_view_symbol_70": {
|
||||
"read_only": true
|
||||
},
|
||||
"version": {
|
||||
"read_only": true
|
||||
},
|
||||
"raw_point_count_90": {
|
||||
"read_only": true
|
||||
},
|
||||
"raw_flags_90": {
|
||||
"read_only": true,
|
||||
"_comment": "Semantics unknown in cadcodec; preserved as read, 0 for a new symbol."
|
||||
},
|
||||
"raw_point_record_count": {
|
||||
"read_only": true
|
||||
},
|
||||
"end_a": {
|
||||
"read_only": true
|
||||
},
|
||||
"end_b": {
|
||||
"read_only": true
|
||||
},
|
||||
"tick_a": {
|
||||
"read_only": true
|
||||
},
|
||||
"tick_b": {
|
||||
"read_only": true
|
||||
},
|
||||
"label": {
|
||||
"read_only": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4983,7 +4983,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(emitted.len(), 42);
|
||||
assert_eq!(emitted.len(), 43);
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-host-model")]
|
||||
|
||||
@@ -158,6 +158,19 @@ impl<'a> HostSession<'a> {
|
||||
if let EntityType::Dimension(dimension) = entity {
|
||||
crate::entities::dimension::normalize_scripted_dimension(dimension);
|
||||
}
|
||||
if let EntityType::SectionSymbol(symbol) = entity {
|
||||
// `points` is the canonical geometry: the point counts follow it
|
||||
// (they equal the point count in every verified export) and the
|
||||
// end, tick and label projections are refreshed from it. A symbol
|
||||
// whose points did not change is left exactly as it was read.
|
||||
let unchanged = matches!(old, Some(EntityType::SectionSymbol(previous)) if previous.points == symbol.points);
|
||||
if !unchanged {
|
||||
let count = i32::try_from(symbol.points.len()).unwrap_or(i32::MAX);
|
||||
symbol.raw_point_count_90 = count;
|
||||
symbol.raw_point_record_count = count;
|
||||
symbol.sync_display_fields();
|
||||
}
|
||||
}
|
||||
if let EntityType::Spline(spline) = entity {
|
||||
// The DXF writer emits weights only for a rational spline, so the
|
||||
// flag must follow the weights a script sets.
|
||||
@@ -5561,6 +5574,104 @@ mod tests {
|
||||
assert!(message.contains("Coincident") && message.contains("nothing was changed"), "{message}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audit_python_section_symbol_lifecycle_over_real_ipc() {
|
||||
run_mesh_lifecycle(&MeshCase {
|
||||
create: concat!(
|
||||
"doc.create_entity('SectionSymbol', symbol_scale=1.0, points=[\n",
|
||||
" {'point': P(0.0, 0.0, 0.0), 'label': 'A', 'label_offset': P(0.0, 2.0, 0.0)},\n",
|
||||
" {'point': P(40.0, 0.0, 0.0), 'label': 'A', 'label_offset': P(0.0, 2.0, 0.0)}])\n"),
|
||||
edit: concat!(
|
||||
"e = doc.entities[HANDLE]\n",
|
||||
"with doc.transaction('Edit'):\n",
|
||||
" e.points = [{'point': P(0.0, 0.0, 0.0), 'label': 'B', 'label_offset': P(0.0, 3.0, 0.0)},\n",
|
||||
" {'point': P(20.0, 10.0, 0.0), 'bulge': 0.0}, {'point': P(40.0, 0.0, 0.0), 'label': 'B', 'label_offset': P(0.0, -3.0, 0.0)}]\n",
|
||||
" e.symbol_scale = 2.0\n",
|
||||
"doc.selection = [e]\n"),
|
||||
rejects: &[
|
||||
("'points':[{'point':{'x':0.0,'y':0.0,'z':0.0}}]", "at least 2 points"),
|
||||
("'points':[{'point':{'x':float('nan'),'y':0.0,'z':0.0}},{'point':{'x':1.0,'y':0.0,'z':0.0}}]", "finite"),
|
||||
("'symbol_scale':0.0", "greater than zero"),
|
||||
("'style_handle':999999", "not an existing SectionViewStyle"),
|
||||
("'view_rep_handle':999999", "not an existing ViewRep"),
|
||||
("'raw_point_count_90':9", "read-only"),
|
||||
("'end_a':[1.0, 1.0]", "read-only"),
|
||||
],
|
||||
is_kind: |entity| matches!(entity, EntityType::SectionSymbol(_)),
|
||||
digest: |entity| match entity {
|
||||
EntityType::SectionSymbol(v) => format!("n{} ends {:.1},{:.1}->{:.1},{:.1} label{} tick{:.1}/{:.1} scale{:.1} counts{}/{}",
|
||||
v.points.len(), v.end_a[0], v.end_a[1], v.end_b[0], v.end_b[1], v.label, v.tick_a, v.tick_b,
|
||||
v.symbol_scale, v.raw_point_count_90, v.raw_point_record_count),
|
||||
other => format!("wrong kind {}", format!("{other:?}").split('(').next().unwrap()),
|
||||
},
|
||||
reedit: |entity| if let EntityType::SectionSymbol(v) = entity { v.symbol_scale = 3.0; },
|
||||
expect_created: "n2 ends 0.0,0.0->40.0,0.0 labelA tick2.0/2.0 scale1.0 counts2/2",
|
||||
expect_edited: "n3 ends 0.0,0.0->40.0,0.0 labelB tick3.0/-3.0 scale2.0 counts3/3",
|
||||
expect_reedited: "n3 ends 0.0,0.0->40.0,0.0 labelB tick3.0/-3.0 scale3.0 counts3/3",
|
||||
// BLOCKER (cadcodec, see docs/cadcodec-reader-gaps.md issue 5): the
|
||||
// DXF reader dispatches SECTIONLINE only inside blocks, so a symbol
|
||||
// in the entity list reopens as an unknown entity.
|
||||
expect_edited_dxf: "wrong kind Unknown",
|
||||
expect_reedited_dxf: "wrong kind Unknown",
|
||||
expect_edited_dwg: "",
|
||||
expect_reedited_dwg: "",
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audit_python_section_symbol_references_over_real_ipc() {
|
||||
use acadrust::objects::{ClassObject, ClassObjectData, ObjectType};
|
||||
let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else {
|
||||
return;
|
||||
};
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.tabs[0].is_start = false;
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
let mut object = |host: &mut HostSession<'_>, data: ClassObjectData| {
|
||||
let handle = host.document_mut().allocate_handle();
|
||||
let mut class_object = ClassObject::new(data);
|
||||
class_object.handle = handle;
|
||||
host.document_mut().objects.insert(handle, ObjectType::ClassObject(class_object));
|
||||
handle
|
||||
};
|
||||
let style = object(&mut host, ClassObjectData::SectionViewStyle(Default::default()));
|
||||
let view_rep = object(&mut host, ClassObjectData::ViewRep(Default::default()));
|
||||
let scale = host.document_mut().allocate_handle();
|
||||
let mut scale_object = acadrust::objects::Scale::new("1:1", 1.0, 1.0);
|
||||
scale_object.handle = scale;
|
||||
host.document_mut().objects.insert(scale, ObjectType::Scale(scale_object));
|
||||
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 = |host: &HostSession<'_>| host.app.command_line.history.last().unwrap().text.clone();
|
||||
let count = |host: &HostSession<'_>| host.document().entities().filter(|e| matches!(e, EntityType::SectionSymbol(_))).count();
|
||||
let create = |extra: &str| format!(
|
||||
"PY_EVAL ocs.active_document.create_entity('SectionSymbol', points=[{{'point': {{'x': 0.0, 'y': 0.0, 'z': 0.0}}}}, {{'point': {{'x': 10.0, 'y': 0.0, 'z': 0.0}}}}]{extra}).handle");
|
||||
// A symbol may reference the drawing's own style and view representation.
|
||||
dispatch(&mut host, &create(&format!(", style_handle={}, view_rep_handle={}", style.value(), view_rep.value())));
|
||||
assert_eq!(count(&host), 1, "{}", last(&host));
|
||||
let Some(EntityType::SectionSymbol(symbol)) = host.document().entities().find(|e| matches!(e, EntityType::SectionSymbol(_))) else { unreachable!() };
|
||||
assert_eq!((symbol.style_handle, symbol.view_rep_handle), (style, view_rep));
|
||||
// A standalone symbol needs neither.
|
||||
dispatch(&mut host, &create(""));
|
||||
assert_eq!(count(&host), 2, "{}", last(&host));
|
||||
// Wrong object kinds are refused: a style slot cannot hold a view rep, a
|
||||
// scale object is neither, and a swapped pair fails.
|
||||
for (extra, message) in [
|
||||
(format!(", style_handle={}", view_rep.value()), "not an existing SectionViewStyle"),
|
||||
(format!(", style_handle={}", scale.value()), "not an existing SectionViewStyle"),
|
||||
(format!(", view_rep_handle={}", style.value()), "not an existing ViewRep"),
|
||||
(", view_rep_handle=424242".to_string(), "not an existing ViewRep"),
|
||||
] {
|
||||
dispatch(&mut host, &create(&extra));
|
||||
assert_eq!(count(&host), 2, "{extra}");
|
||||
assert!(last(&host).contains(message), "{extra}: {}", last(&host));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_python_point_pick_and_cancel_over_real_ipc() {
|
||||
let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else {
|
||||
|
||||
Reference in new issue
Block a user