Investigate the cadkernel path for ACIS kinds; add spikes and findings

OCS already creates, transforms and combines bodies through cadkernel, and
solid_to_sat / kernel_acis_body verify each result lifts back losslessly.
Two ignored spike tests measure volumes and DWG/DXF round trips. The
blocker is an operation channel, not geometry; the note recommends an
additive solid_operation host method and phases the work.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-20 17:10:51 +02:00
1 parent e583da83db
commit 07fe739571
4 files changed
+251 -4

No files matched your search

+137
View File
@@ -0,0 +1,137 @@
# Scripting ACIS bodies through cadkernel: findings and plan
Investigation of the blocker on **Solid3D, Body, Region and Surface**
(`docs/plugin-host-model-coverage-ledger.md`). The blocker said a script can
neither produce a valid ACIS payload nor edit one, and that the handover
forbids rewriting an opaque payload without a proven lossless path. This note
records what OCS's own kernel path can already do, measured with two spike tests
(`spike_kernel_body_round_trip`, `spike_kernel_timings` in
`src/app/plugin_host.rs`, both `#[ignore]` because they are slow in a debug
build). Measured on cadcodec `5b682ed` / cadkernel `6f046af`, debug build.
**Conclusion:** the blocker was overstated. OCS already owns a kernel-backed
create/transform/boolean pipeline whose output is verified lossless before it is
accepted. The missing piece is not geometry but an *operation channel* from a
script to that pipeline. Solid3D and Body are then straightforward; Region and
Surface need a little more; the one real cost is boolean speed.
## What exists
| Capability | Where | Notes |
|---|---|---|
| Primitives: box, wedge, cylinder, cone/frustum, elliptical cylinder, sphere, torus, pyramid, pyramid frustum | `scene/model/solid_model.rs` (`box_solid`, ...) | return a kernel `Body` |
| Transforms: place, turn about an axis, mirror, column-major 4x4 | `placed`, `turned`, `mirrored`, `by_matrix` | analytic surfaces move with their frames, no point rewriting |
| Booleans: union, subtract, intersect | `boolean_result` | refuses with a reason instead of returning a broken solid |
| Body to entity payload | `acis_export::solid_to_sat` | **self-verifying**: it lifts its own SAT text and SAB binary back and returns `None` unless both are lossless (`loss.is_empty()`) and validate |
| Entity payload to body | `solid3d_tess::kernel_body` / `kernel_region_body` / `kernel_surface_body` / `kernel_acis_body` | returns `None` when the lift is lossy or has more than one body |
| Regenerating derived data | `app/model_ops.rs::entity_with_boolean_body` | rewrites `acis_data`, `wires`, clears `silhouettes` and `history_handle` for Solid3D, Region and Surface |
| Volume, extent, edge wires | `volume`, `extent`, `edge_wires` | used as oracles below |
`kernel_acis_body` is exactly the "proven lossless path" the handover asked for:
a payload is edited only if it lifts with no loss, otherwise it is left alone.
## Spike results
Everything below passed, with the kernel's own volume as the oracle.
| Test | Result |
|---|---|
| Box 10x6x4 to Solid3D | volume 240, 12 edge wires, lifts back |
| DWG save/reopen | lifts back, volume 240, 12 wires kept. `acis_data` changes from SAT text to **SAB binary** (same solid, different bytes) |
| DXF save/reopen | lifts back, volume 240. Cached `wires` are dropped (0) and are regenerated on demand; silhouettes were already empty |
| Translate by (5,5,5) | volume 240, extent shifted exactly, lifts back |
| Rotate 45 degrees about Z | volume 240, extent -5.657..5.657, lifts back |
| Mirror in X | volume 240, lifts back |
| Box union cylinder | volume 315.277, 16 wires, lifts back |
| Box subtract cylinder | volume 189.815, 14 wires, lifts back |
| Box intersect cylinder | volume 50.185, 2 wires, lifts back |
| Sphere, torus, wedge, pyramid | volumes 112.696, 98.415, 12.000, 28.532; all lift back |
| Same payload in a `Body` entity | lifts back; still lifts back after DWG and after DXF |
Timings (debug build): primitive, transform, `solid_to_sat` and `edge_wires` are
effectively instant (under 0.05 s). **Each boolean took about 23 s**, most likely
a debug-build artifact for a kernel doing heavy numeric work, but it has not been
measured in release and must be before booleans are exposed.
Two things to understand about "lossless":
- DWG rewrites SAT text as SAB binary. The solid is identical (same volume,
topology, faces) but the bytes are not, so the honest oracle is "lifts back with
no loss and the same volume", not byte equality. An untouched body that a
script never edits is written exactly as OCS writes any other body.
- DXF does not carry the cached wires; they are derived data.
## Design
The obstacle is the channel. The generic entity path moves an acadrust
`EntityType` from the plugin to the host; the plugin has no kernel and must never
see or rewrite ACIS bytes. Three ways to give a script the operations:
**A. Host-side normalization with a request carrier (no ABI change).** A
hand-written converter (as for Dimension) turns `create_entity('Solid3D',
primitive='box', ...)` into a Solid3D that carries the request in an otherwise
unused field, and the host's `normalize_scripted_entity` replaces it with the real
payload, exactly as it already does for MLine geometry, Helix splines and image
definitions. It works inside today's v7 protocol, but the carrier field is a hack.
**B. New host operation (recommended).** Add one additive operation to `HostApi`
and the v4 IPC protocol, for example `solid_operation(op) -> Result<Handle, String>`,
with `op` one of: create primitive, transform, boolean, and (later) region from
curves. The host runs it through the pipeline above inside an undo step and
returns the new or updated handle. Because it is a new method with a default
"unsupported" implementation it is backward compatible; bump the minor/negotiate
by capability rather than the major version. Python side:
```python
box = doc.solids.box(center=(0, 0, 0), size=(10, 6, 4))
cyl = doc.solids.cylinder(base=(0, 0, -3), radius=2, height=10)
hole = box.subtract(cyl) # new Solid3D; operands untouched
box.transform(translate=(5, 5, 5)) # in-place, transactional, undoable
```
**C. Do nothing but layer edits** (the current state).
Recommended: **B**, with `solid_operation` covering create, transform and boolean.
Every operation goes through `solid_to_sat` (self-verifying) and the kernel's own
refusal path, so a failure is an error before any mutation, and an untouched
payload is never rewritten. In-place edits of an existing entity are gated by
`kernel_acis_body` returning `Some`: a payload the kernel cannot lift losslessly
is refused with a clear message instead of being rewritten.
## Per-kind feasibility
| Kind | Create | Edit | Notes |
|---|---|---|---|
| **Solid3D** | 9 primitives, boolean of two solids | transform, boolean | Straightforward. Regenerate `wires`; clear `silhouettes`; `history_handle` is cleared by `entity_with_boolean_body`, so a history-carrying solid needs a decision (refuse, or accept losing parametric history). |
| **Body** | same payload container | transform | Same pipeline, `kernel_acis_body` works on a `Body` payload (spike). A true non-solid (sheet or wire) body may not lift; those stay layer-only. |
| **Region** | from closed curve entities (the REGION command's `add_region_model`) | transform | Needs a "region from these curve handles" operation and planar-face construction. |
| **Surface** | plane, extruded, revolved from a profile | transform | The most involved: surface kinds carry `surface_data` besides `acis_data`, and only `Plane` keeps its kind on regeneration (`SurfaceKind::Generic` otherwise). |
## Risks and open questions
1. **Boolean speed.** About 23 s each in a debug build. Time them in release
before deciding whether they can run on the host thread (the GUI BOOLEAN
command has the same cost) or need a background task. Everything else is
instant.
2. **Interoperability cannot be verified here.** `solid_to_sat` is verified by
OCS's own lifter. Whether AutoCAD accepts the SAT/SAB that OCS writes is not
testable without AutoCAD, so completion should be scoped as "OCS round trip",
as for RasterImage.
3. **Parametric history.** Regenerating a payload clears `history_handle`.
4. **Frames.** Some solids carry a body-to-world transform in the SAT
(`body_transform` in `kernel_acis_body`); the lift already applies it, and the
spike's translated/rotated bodies confirm it, but a foreign DWG with a
scaled or sheared body transform should be added as a fixture.
5. **API surface.** `solid_operation` is a protocol addition; decide whether it is
one generic operation enum or separate methods before starting.
## Suggested phases
1. `solid_operation` protocol and host executor for **create primitive** and
**transform** on Solid3D, with the full lifecycle test through the real runner
(volume oracle, DWG and DXF reopen, undo, refusal of a lossy payload).
2. **Booleans** (after release-build timing) and the **Body** container.
3. **Region** from curve handles.
4. **Surface** (plane first).
Phase 1 alone would move Solid3D from blocked to complete-in-OCS.
@@ -176,3 +176,4 @@ exact queue above and update statuses with evidence after each increment.
- Decided the two open audit questions. **Insert is creatable** by naming an existing block (existence, ordinary-block and no-cycle checks; attribute records stay with the AttributeEntity flow), which completes the kind. **Legacy Polyline is update-only** with a hint pointing scripts at Polyline2D and Polyline3D, because both formats convert it on save. The generator gained an `update_only_message` for such kinds, integer coordinates are accepted in Insert scales, and the driver gained `MAKEPOLYLINE` and `MAKEBLOCK` fixtures.
- Wrote up the cadcodec DXF reader gaps as draft issues in [cadcodec-reader-gaps.md](cadcodec-reader-gaps.md) (nothing filed): ATTDEF, boolean groups read through `as_i16()`, UNDERLAY rotation units, LEADER, DRAWINGVIEW/SECTIONLINE dispatch and TABLE merged ranges. Verifying them corrected an earlier attribution: the Spline "DXF drops weights" finding was an OCS bug (the writer emits weights only for a rational spline), now fixed by setting `flags.rational` from the weights, so Spline is complete.
- Mapped `Ole2Frame` as update-only (queue item 21, previously blocked). OCS already builds real embedded frames (`build_embedded_ole`), which supplied the fixture the blocker was waiting for. Frame corners and the aspect lock are scriptable; the opaque storage and envelope are excluded and refused on edit. The audit proves the embedded picture survives DWG and DXF unchanged. Creation from Python remains a follow-up. The remaining blocked kinds are SectionSymbol, Region, Body, Solid3D and Surface.
- Investigated the cadkernel path for the ACIS kinds ([cadkernel-body-path.md](cadkernel-body-path.md)). The earlier "blocked" reasoning was overstated: OCS already builds primitives, transforms bodies and runs booleans through cadkernel, and `solid_to_sat` plus `kernel_acis_body` verify each result lifts back with no loss. Two `#[ignore]` spike tests measured exact volumes and DWG and DXF round trips for a box, moved, rotated and mirrored copies, three booleans, four other primitives and a `Body` entity. The real gap is an operation channel from a script to that pipeline; the note recommends an additive `solid_operation` host method and a phased plan (Solid3D create and transform, then booleans and Body, then Region, then Surface). Booleans took about 23 s each in a debug build and need release timing first. Region, Body, Solid3D and Surface move from blocked to unblocked-by-design; SectionSymbol stays blocked.
+4 -4
View File
@@ -44,10 +44,10 @@ Completion gate codes: **C** = create through Python document model; **R** = rea
| 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. |
| 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 | **Blocked** | Not mapped. The entity is an opaque ACIS payload (`acis_data` SAT/SAB text or binary) plus derived `wires`, `silhouettes` and, for solids, a history record. A script can neither produce a valid body nor edit one, because every geometric change needs the CAD kernel to rewrite the payload, and the handover forbids deserializing and rewriting opaque ACIS data without a proven lossless path. The only lossless edit today is the layer change the document model already supports. Adding this kind needs a host-side, kernel-backed API (create a primitive, or apply a transform, then regenerate `acis_data` with its wires and silhouettes) with a DWG and DXF round-trip that preserves the payload and history byte-for-byte for untouched bodies. |
| Body | **Blocked** | Not mapped. The entity is an opaque ACIS payload (`acis_data` SAT/SAB text or binary) plus derived `wires`, `silhouettes` and, for solids, a history record. A script can neither produce a valid body nor edit one, because every geometric change needs the CAD kernel to rewrite the payload, and the handover forbids deserializing and rewriting opaque ACIS data without a proven lossless path. The only lossless edit today is the layer change the document model already supports. Adding this kind needs a host-side, kernel-backed API (create a primitive, or apply a transform, then regenerate `acis_data` with its wires and silhouettes) with a DWG and DXF round-trip that preserves the payload and history byte-for-byte for untouched bodies. |
| Solid3D | **Blocked** | Not mapped. The entity is an opaque ACIS payload (`acis_data` SAT/SAB text or binary) plus derived `wires`, `silhouettes` and, for solids, a history record. A script can neither produce a valid body nor edit one, because every geometric change needs the CAD kernel to rewrite the payload, and the handover forbids deserializing and rewriting opaque ACIS data without a proven lossless path. The only lossless edit today is the layer change the document model already supports. Adding this kind needs a host-side, kernel-backed API (create a primitive, or apply a transform, then regenerate `acis_data` with its wires and silhouettes) with a DWG and DXF round-trip that preserves the payload and history byte-for-byte for untouched bodies. |
| Surface | **Blocked** | Not mapped. The entity is an opaque ACIS payload (`acis_data` SAT/SAB text or binary) plus derived `wires`, `silhouettes` and, for solids, a history record. A script can neither produce a valid body nor edit one, because every geometric change needs the CAD kernel to rewrite the payload, and the handover forbids deserializing and rewriting opaque ACIS data without a proven lossless path. The only lossless edit today is the layer change the document model already supports. Adding this kind needs a host-side, kernel-backed API (create a primitive, or apply a transform, then regenerate `acis_data` with its wires and silhouettes) with a DWG and DXF round-trip that preserves the payload and history byte-for-byte for untouched bodies. |
| Region | **Blocked, unblocked by design** | Not mapped yet. Transform uses the same pipeline; creation needs a "region from these curve handles" operation built on the REGION command's `add_region_model`. See [cadkernel-body-path.md](cadkernel-body-path.md). |
| Body | **Blocked, unblocked by design** | Not mapped yet. Same pipeline as Solid3D: `kernel_acis_body` lifts a `Body` payload and the spike round-tripped it through DWG and DXF. A genuinely non-solid (sheet or wire) body may not lift and would stay layer-only. See [cadkernel-body-path.md](cadkernel-body-path.md). |
| Solid3D | **Blocked, unblocked by design** | Not mapped yet. The 2026-09-20 kernel investigation ([cadkernel-body-path.md](cadkernel-body-path.md)) shows OCS already creates the payload (nine primitives), transforms it and combines bodies through cadkernel, and that `solid_to_sat` and `kernel_acis_body` verify every result lifts back with no loss; spikes confirmed exact volumes and DWG and DXF round trips. What is missing is a script-to-host operation channel (recommended: an additive `solid_operation` host method covering create-primitive, transform and boolean), not geometry. Open items: boolean speed (about 23 s in a debug build, unmeasured in release), history-carrying solids, and AutoCAD acceptance, which cannot be tested here. |
| Surface | **Blocked, unblocked by design** | Not mapped yet, and the most involved: a surface carries `surface_data` beside `acis_data`, and only a plane keeps its kind when regenerated. Plan: planes first, then extruded and revolved from a profile. See [cadkernel-body-path.md](cadkernel-body-path.md). |
| Ole2Frame | **Update-only (all applicable gates pass)** | Writable: `upper_left_corner`, `lower_right_corner`, `lock_aspect`, `is_paper_space`. Read-only: `version`, `source_application`, `ole_object_type`, `dwg_mode`. The embedded object `storage` and the frame `envelope` are opaque, excluded from the Python form, refused by the mutation check (`Ole2Frame embedded storage cannot change`) and preserved untouched. Host validation requires finite corners spanning a nonzero width and height and `lock_aspect` of 0 or 1. Creation from a script is refused with an explanatory message, because a valid frame needs a real embedded object; the host's own `build_embedded_ole` builds one from an image. `audit_python_ole2frame_lifecycle_over_real_ipc` (shared driver; the fixture embeds a real 4x3 PNG through `add_embedded_image`) passes E/D/U/I/W/V/P: a corner and aspect-lock transaction, canvas geometry, rejection of a zero-size frame, a bad aspect lock, non-finite corners, a read-only `version` and an attempt to write `storage`, an atomic rollback, **DWG and DXF reopen with the 119-byte embedded picture byte-for-byte intact (extracted through `extract_presentation`)**, re-edit, and two-step undo/redo. Not `Complete` because Python cannot create a frame. A create path (`file_path` of an image, built with `build_embedded_ole`) would need a manual-kind converter like Dimension and is the natural follow-up. |
After each increment, update the row with test names, fixture paths, exact
+109
View File
@@ -4449,6 +4449,115 @@ mod tests {
});
}
/// Evidence for docs/cadkernel-body-path.md: primitive, transform and
/// boolean bodies become Solid3D/Body payloads that lift back losslessly
/// and survive DWG and DXF. Slow in a debug build (booleans take ~23 s).
#[test]
#[ignore = "kernel spike; slow in debug builds"]
fn spike_kernel_body_round_trip() {
use crate::scene::model::solid_model as sm;
use crate::scene::convert::solid3d_tess::{kernel_acis_body, kernel_body};
let make = |body: &cadkernel::brep::Body| -> acadrust::entities::Solid3D {
let sat = crate::scene::convert::acis_export::solid_to_sat(body).expect("solid_to_sat");
let mut solid = acadrust::entities::Solid3D::new();
solid.wires = sm::edge_wires(body);
solid.set_sat_document(&sat);
solid
};
let report = |label: &str, solid: &acadrust::entities::Solid3D| {
let body = kernel_body(solid);
eprintln!("SPIKE {label}: lift={} volume={:?} wires={} sat_bytes={} binary={}",
body.is_some(), body.as_ref().map(sm::volume), solid.wires.len(),
solid.acis_data.sat_data.len(), solid.acis_data.is_binary);
body
};
let boxed = sm::box_solid([0.0, 0.0, 0.0], 10.0, 6.0, 4.0).unwrap();
let mut doc = acadrust::CadDocument::new();
let solid = make(&boxed);
report("created box", &solid);
let handle = doc.add_entity(EntityType::Solid3D(solid.clone())).unwrap();
// Round trip both formats and compare payloads.
let dwg = crate::io::load_bytes("s.dwg", acadrust::DwgWriter::write_to_vec(&doc).unwrap()).unwrap();
let dxf = crate::io::load_bytes("s.dxf", acadrust::DxfWriter::new(&doc).write_to_vec().unwrap()).unwrap();
for (label, reopened) in [("DWG", &dwg), ("DXF", &dxf)] {
let Some(EntityType::Solid3D(back)) = reopened.get_entity(handle) else { panic!("{label} lost the solid") };
report(&format!("{label} reopen"), back);
eprintln!("SPIKE {label}: acis_data equal={} wires equal={} silhouettes {}", back.acis_data == solid.acis_data,
back.wires.len() == solid.wires.len(), back.silhouettes.len());
}
// Transforms: translate, rotate, mirror, matrix.
let moved = sm::by_matrix(&boxed, [1.,0.,0.,0., 0.,1.,0.,0., 0.,0.,1.,0., 5.,5.,5.,1.]).unwrap();
let turned = sm::turned(&boxed, 2, std::f64::consts::FRAC_PI_4, [0.0, 0.0, 0.0]).unwrap();
let mirrored = sm::mirrored(&boxed, 0, [0.0, 0.0, 0.0]).unwrap();
for (label, body) in [("moved", &moved), ("turned", &turned), ("mirrored", &mirrored)] {
let solid = make(body);
let extent = sm::extent(body);
eprintln!("SPIKE {label}: volume={:.3} extent={:?} lifts_back={}", sm::volume(body), extent, kernel_body(&solid).is_some());
}
// Booleans and primitives.
let cyl = sm::cylinder_solid([0.0, 0.0, -3.0], 2.0, 10.0).unwrap();
for (label, op) in [("union", sm::Bool::Union), ("subtract", sm::Bool::Subtract), ("intersect", sm::Bool::Intersect)] {
match sm::boolean_result(op, &boxed, &cyl) {
Ok(result) => { let solid = make(&result); eprintln!("SPIKE box {label} cylinder: volume={:.3} faces_wires={} lifts_back={}", sm::volume(&result), solid.wires.len(), kernel_body(&solid).is_some()); }
Err(snag) => eprintln!("SPIKE box {label} cylinder: refused {snag:?}"),
}
}
for (label, body) in [
("sphere", sm::sphere_solid([0.0; 3], 3.0)), ("torus", sm::torus_solid([0.0; 3], 5.0, 1.0)),
("wedge", sm::wedge_solid([0.0; 3], 4.0, 3.0, 2.0)), ("pyramid", sm::pyramid_solid([0.0; 3], 3.0, 4.0, 5)),
] {
match body {
Some(body) => { let solid = make(&body); eprintln!("SPIKE {label}: volume={:.3} lifts_back={}", sm::volume(&body), kernel_body(&solid).is_some()); }
None => eprintln!("SPIKE {label}: kernel returned None"),
}
}
// The same payload inside a Body entity and a Region-like container.
let mut body_entity = acadrust::entities::Body::new();
body_entity.set_sat_document(&crate::scene::convert::acis_export::solid_to_sat(&boxed).unwrap());
let lifted = kernel_acis_body(&body_entity.acis_data);
eprintln!("SPIKE Body entity: lift={} volume={:?}", lifted.is_some(), lifted.as_ref().map(sm::volume));
let bh = doc.add_entity(EntityType::Body(body_entity)).unwrap();
let dwg = crate::io::load_bytes("b.dwg", acadrust::DwgWriter::write_to_vec(&doc).unwrap()).unwrap();
let dxf = crate::io::load_bytes("b.dxf", acadrust::DxfWriter::new(&doc).write_to_vec().unwrap()).unwrap();
for (label, reopened) in [("DWG", &dwg), ("DXF", &dxf)] {
match reopened.get_entity(bh) {
Some(EntityType::Body(back)) => eprintln!("SPIKE Body {label}: lift={}", kernel_acis_body(&back.acis_data).is_some()),
other => eprintln!("SPIKE Body {label}: came back as {:?}", other.map(|e| format!("{e:?}").chars().take(30).collect::<String>())),
}
}
}
/// Per-operation kernel timings for docs/cadkernel-body-path.md.
#[test]
#[ignore = "kernel spike; slow in debug builds"]
fn spike_kernel_timings() {
use crate::scene::model::solid_model as sm;
use std::time::Instant;
let time = |label: &str, f: &mut dyn FnMut() -> String| {
let start = Instant::now();
let note = f();
eprintln!("TIMING {label}: {:.2}s {note}", start.elapsed().as_secs_f64());
};
let boxed = sm::box_solid([0.0; 3], 10.0, 6.0, 4.0).unwrap();
let cyl = sm::cylinder_solid([0.0, 0.0, -3.0], 2.0, 10.0).unwrap();
time("box primitive", &mut || format!("{}", sm::box_solid([0.0; 3], 10.0, 6.0, 4.0).is_some()));
time("sphere primitive", &mut || format!("{}", sm::sphere_solid([0.0; 3], 3.0).is_some()));
time("solid_to_sat(box)", &mut || format!("{}", crate::scene::convert::acis_export::solid_to_sat(&boxed).is_some()));
time("edge_wires(box)", &mut || format!("{}", sm::edge_wires(&boxed).len()));
time("transform (matrix)", &mut || format!("{}", sm::by_matrix(&boxed, [1.,0.,0.,0., 0.,1.,0.,0., 0.,0.,1.,0., 5.,5.,5.,1.]).is_some()));
time("union", &mut || format!("{}", sm::boolean_result(sm::Bool::Union, &boxed, &cyl).is_ok()));
time("subtract", &mut || format!("{}", sm::boolean_result(sm::Bool::Subtract, &boxed, &cyl).is_ok()));
time("intersect", &mut || format!("{}", sm::boolean_result(sm::Bool::Intersect, &boxed, &cyl).is_ok()));
let result = sm::boolean_result(sm::Bool::Union, &boxed, &cyl).unwrap();
time("solid_to_sat(union)", &mut || format!("{}", crate::scene::convert::acis_export::solid_to_sat(&result).is_some()));
time("edge_wires(union)", &mut || format!("{}", sm::edge_wires(&result).len()));
time("volume(union)", &mut || format!("{:.1}", sm::volume(&result)));
}
#[test]
fn staged_python_point_pick_and_cancel_over_real_ipc() {
let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else {