feat(python): PEDIT polyline wrappers and path/3-D arrays

polyline_close/open/width/reverse/join (a line or arc is turned into a polyline
first), array_path and array_3d, written against observed prompts and checked
for exact geometry in the real-IPC audit. Polylines default their pick point to
the first vertex. The audit runs on a large-stack thread because PEDIT's
line-to-polyline step needs more than a test thread's 2 MiB in a debug build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-21 17:57:58 +02:00
1 parent 81251d9cf3
commit b180dce1a1
5 files changed
+175 -57

No files matched your search

+12
View File
@@ -297,3 +297,15 @@ The generator now (1) refuses an unknown key in every nested record, naming the
`audit_python_nested_input_is_validated_over_real_ipc` (eight refusals with the right messages for LwPolyline, Polyline2D and `audit_python_nested_input_is_validated_over_real_ipc` (eight refusals with the right messages for LwPolyline, Polyline2D and
PolygonMesh; nothing invalid reaches the drawing; the valid forms still create), and every existing per-kind lifecycle audit still passes. PolygonMesh; nothing invalid reaches the drawing; the valid forms still create), and every existing per-kind lifecycle audit still passes.
### 2026-09-21: PEDIT and the remaining arrays
`doc.modify` gained `array_path`, `array_3d`, `polyline_close`, `polyline_open`, `polyline_width`, `polyline_reverse` and `polyline_join`,
written against the prompts observed in `spike_command_runner_prompts` (PEDIT offers to turn a line or arc into a polyline and the wrapper
accepts; the join option takes a selection, and its Enter completes the whole command). The default pick point for a polyline is its first
vertex. `audit_python_modify_wrappers_over_real_ipc` checks: closed and opened flags, the widened polyline, the reversed vertex order, the
joined polyline's three vertices with its line consumed, a line turned into a widened polyline (and the line gone), all five path-array
positions (2600 to 2650 in 12.5 steps), and all eight members of the 3-D array (x 2700/2720, y 0/10, z 0/30). Finding: PEDIT's line-to-polyline
step needs between 2 and 4 MiB of stack in a debug build, more than a test thread's 2 MiB, so that audit runs on a 64 MiB thread; the
application's main thread has 8 MiB, and a release build uses far less. Not wrapped: PEDIT fit, spline, decurve, linetype generation and
vertex editing.
+9 -2
View File
@@ -288,6 +288,11 @@ M.explode([polyline]); M.join([line1, line2])
M.break_entity(line, first_point, second_point) M.break_entity(line, first_point, second_point)
M.stretch(corner1, corner2, base, target) # crossing window, then the displacement M.stretch(corner1, corner2, base, target) # crossing window, then the displacement
M.lengthen(line, 5, at=near_the_end) # delta; negative shortens M.lengthen(line, 5, at=near_the_end) # delta; negative shortens
M.array_path([a], path_curve, count=5) # evenly along a curve
M.array_3d([a], rows=2, columns=2, levels=2, row_spacing=10, column_spacing=20, level_spacing=30)
M.polyline_close(pl); M.polyline_open(pl); M.polyline_reverse(pl)
M.polyline_width(line_or_pl, 0.5) # a line or arc is turned into a polyline first
M.polyline_join(pl, [line1, arc1]) # returns the merged polyline
``` ```
Each step returns an outcome dict: `status` (`completed` or `waiting_input`), Each step returns an outcome dict: `status` (`completed` or `waiting_input`),
@@ -302,7 +307,7 @@ the line leaves a prompt open.
Selection-based tools (`move`, `copy`, `rotate`, `scale`, `mirror`, `erase`, the arrays, Selection-based tools (`move`, `copy`, `rotate`, `scale`, `mirror`, `erase`, the arrays,
`explode` and `join`) select `explode` and `join`) select
the entities you pass first, so the command skips its own selection prompt. Pick the entities you pass first, so the command skips its own selection prompt. Pick
points default to the middle of a line, arc or circle; give `at=` for anything else. points default to the middle of a line, arc or circle and the first vertex of a polyline; give `at=` for anything else.
`trim` and `extend` cut or extend against every entity in the drawing. Each command is `trim` and `extend` cut or extend against every entity in the drawing. Each command is
undone as the editor would undo it. undone as the editor would undo it.
@@ -312,7 +317,9 @@ line names a command that could end the session or re-enter Python: `QUIT`,
`RECOVER`, `SCRIPT`, `RUNSCRIPT` and any `PY_*`. Commands run on the active `RECOVER`, `SCRIPT`, `RUNSCRIPT` and any `PY_*`. Commands run on the active
document only. Commands that need an interactive surface a script cannot fill document only. Commands that need an interactive surface a script cannot fill
(the rich-text editor, a file dialog) report it in `blocked_by` and are cancelled. (the rich-text editor, a file dialog) report it in `blocked_by` and are cancelled.
Hatching has no wrapper: create it with `create_entity("Hatch", ...)`, because the PEDIT's fit, spline, decurve and vertex-edit options are not wrapped (reach them with
`start_command("PEDIT")`); `polyline_*` return the polyline entity, which is a new
entity when the command replaced the original. Hatching has no wrapper: create it with `create_entity("Hatch", ...)`, because the
HATCH command needs an interactive boundary pick. `ocs.command_step(kind, options)` HATCH command needs an interactive boundary pick. `ocs.command_step(kind, options)`
is the raw single step behind all of this. is the raw single step behind all of this.
@@ -678,6 +678,14 @@ class _Modify:
return _pt(at) return _pt(at)
samples = ocs.curve_samples(_handle(entity), 4) samples = ocs.curve_samples(_handle(entity), 4)
if samples is None: if samples is None:
# A polyline: its first vertex lies on it.
data = ocs.entity_descriptor(_handle(entity)) or {}
vertices = data.get("vertices")
if vertices:
first = vertices[0]
where = first.get("location") or first.get("position") if isinstance(first, dict) else None
if where:
return [float(where["x"]), float(where["y"]), float(where.get("z", 0.0))]
raise ValueError("pass at=(x, y, z): the middle of this kind of entity is not known") raise ValueError("pass at=(x, y, z): the middle of this kind of entity is not known")
middle = samples["vertices"][len(samples["vertices"]) // 2] middle = samples["vertices"][len(samples["vertices"]) // 2]
return [middle[0], middle[1], samples["elevation"]] return [middle[0], middle[1], samples["elevation"]]
@@ -821,6 +829,73 @@ class _Modify:
lambda c: c.enter(), lambda c: c.enter(),
]) ])
def array_path(self, entities, path, count, at=None):
"""Array the entities along the curve `path`: `count` items counting the
original. Returns the copies."""
return self._run("ARRAYPATH", [
lambda c: c.entity(path, self._at(path, at)),
lambda c: c.text(count),
], entities)
def array_3d(self, entities, rows, columns, levels, row_spacing, column_spacing, level_spacing):
"""Three-dimensional rectangular array (rows along Y, columns along X, levels
along Z); counts include the original. Returns the copies."""
return self._run("ARRAY3D", [
lambda c: c.text(rows),
lambda c: c.text(columns),
lambda c: c.text(levels),
lambda c: c.text(row_spacing),
lambda c: c.text(column_spacing),
lambda c: c.text(level_spacing),
], entities)
def _pedit(self, entity, steps, at=None):
"""Run PEDIT on one entity: pick it, accept the offer to turn a line or arc
into a polyline, apply `steps`, then exit. Returns the polyline entity."""
handle = _handle(entity)
before = list(ocs.entity_handles())
with _Command("PEDIT") as command:
command.entity(handle, self._at(entity, at))
if "Turn it into one" in command.outcome["prompt"]:
command.token("Y")
for step in steps:
step(command)
if command.waiting:
command.token("X")
_finished(command, "PEDIT")
if handle in set(ocs.entity_handles()):
return self._document.entities[handle]
added = self._added(before)
if not added:
raise RuntimeError("PEDIT left no polyline")
return added[0]
def polyline_close(self, entity, at=None):
"""Close an open polyline (a line or arc is first turned into one)."""
return self._pedit(entity, [lambda c: c.token("C")], at)
def polyline_open(self, entity, at=None):
"""Open a closed polyline."""
return self._pedit(entity, [lambda c: c.token("O")], at)
def polyline_width(self, entity, width, at=None):
"""Give every segment of a polyline the same width."""
return self._pedit(entity, [lambda c: c.token("W"), lambda c: c.text(width)], at)
def polyline_reverse(self, entity, at=None):
"""Reverse a polyline's direction."""
return self._pedit(entity, [lambda c: c.token("R")], at)
def polyline_join(self, entity, others, at=None):
"""Join lines, arcs and polylines that meet the polyline's ends into it."""
def join(c):
c.token("J")
self._select(others)
c.selection()
if c.waiting and "Join" in c.outcome["prompt"]:
c.enter()
return self._pedit(entity, [join], at)
def erase(self, entities): def erase(self, entities):
before = len(list(ocs.entity_handles())) before = len(list(ocs.entity_handles()))
self._select(entities) self._select(entities)
@@ -58,6 +58,8 @@ class DocumentModelTests(unittest.TestCase):
outcome = {"status": "completed", "blocked_by": None, "command": "", "prompt": "", "accepts": [], outcome = {"status": "completed", "blocked_by": None, "command": "", "prompt": "", "accepts": [],
"options": [], "entities": 0, "added": 0, "unconsumed": [], "error": None} "options": [], "entities": 0, "added": 0, "unconsumed": [], "error": None}
outcome.update(self.step_outcome.get(kind, {})) outcome.update(self.step_outcome.get(kind, {}))
if kind == "token" and options.get("text") == "X":
outcome["status"] = "completed"
if kind in ("enter", "entity"): if kind in ("enter", "entity"):
new = max(self.records) + 1 new = max(self.records) + 1
self.records[new] = {"handle": new, "kind": "Line", "layer": "0", "_editable": True} self.records[new] = {"handle": new, "kind": "Line", "layer": "0", "_editable": True}
@@ -114,6 +116,20 @@ class DocumentModelTests(unittest.TestCase):
self.doc.modify.stretch((0, 0), (1, 1), (0, 0), (5, 0)) self.doc.modify.stretch((0, 0), (1, 1), (0, 0), (5, 0))
self.assertEqual([c[1] for c in self.calls if c[0] == "step"], ["start", "point", "point", "enter", "point", "point"]) self.assertEqual([c[1] for c in self.calls if c[0] == "step"], ["start", "point", "point", "enter", "point", "point"])
def test_pedit_and_array_wrappers(self):
self.step_outcome = {"start": {"status": "waiting_input"}, "entity": {"status": "waiting_input", "prompt": "Turn it into one? [Yes/No]"},
"token": {"status": "waiting_input"}}
self.doc.modify.polyline_close(1)
tokens = [c[2]["text"] for c in self.calls if c[0] == "step" and c[1] == "token"]
self.assertEqual(tokens, ["Y", "C", "X"])
self.calls.clear()
self.step_outcome = {}
self.doc.modify.array_3d([1], 2, 2, 2, 10, 20, 30)
self.assertEqual([c[2]["text"] for c in self.calls if c[0] == "step" and c[1] == "text"], ["2", "2", "2", "10", "20", "30"])
self.calls.clear()
self.doc.modify.array_path([1], 2, 5, at=(0, 0, 0))
self.assertEqual([c[1] for c in self.calls if c[0] == "step"], ["start", "entity", "text"])
def test_batch_edit_and_rollback(self): def test_batch_edit_and_rollback(self):
line = self.doc.entities[1] line = self.doc.entities[1]
self.assertEqual(line.start, (1, 2, 3)) self.assertEqual(line.start, (1, 2, 3))
+63 -55
View File
@@ -8009,6 +8009,17 @@ LT.delete('Temp')
/// the real OCS command and the resulting geometry is checked exactly. /// the real OCS command and the resulting geometry is checked exactly.
#[test] #[test]
fn audit_python_modify_wrappers_over_real_ipc() { fn audit_python_modify_wrappers_over_real_ipc() {
// PEDIT's line-to-polyline conversion runs deep inside a nested command step; a debug
// build needs more than the 2 MiB a test thread gets (the application's main thread has 8 MiB).
std::thread::Builder::new()
.stack_size(64 * 1024 * 1024)
.spawn(modify_wrappers_audit_body)
.unwrap()
.join()
.unwrap();
}
fn modify_wrappers_audit_body() {
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 {
return; return;
}; };
@@ -8030,9 +8041,9 @@ LT.delete('Temp')
" else:\n failed.append('ACCEPTED_' + tag)\n", " else:\n failed.append('ACCEPTED_' + tag)\n",
"def step(tag, fn):\n", "def step(tag, fn):\n",
" try:\n return fn()\n except Exception as error:\n", " try:\n return fn()\n except Exception as error:\n",
" failed.append(tag + '_' + ''.join(c if c.isalnum() else '_' for c in str(error))[:70])\n", " failed.append(tag + '_' + ''.join(c if c.isalnum() else '_' for c in str(error))[:45])\n",
"{body}\n", "{body}\n",
"L.create('REPORT ' + str(len(refused)) + ' ~ ' + ' '.join(failed))\n", "L.create(('REPORT ' + str(len(refused)) + ' ~ ' + ' '.join(failed))[:250])\n",
), body = body)).unwrap(); ), body = body)).unwrap();
assert!(process.dispatch(host, &format!("PY_RUN {}", script.display()), &mut |_| {}).unwrap()); assert!(process.dispatch(host, &format!("PY_RUN {}", script.display()), &mut |_| {}).unwrap());
let _ = std::fs::remove_file(&script); let _ = std::fs::remove_file(&script);
@@ -8083,6 +8094,23 @@ st = line((1900, 0), (1910, 0))
step('stretch', lambda: M.stretch((1908, -2, 0), (1912, 2, 0), (1910, 0, 0), (1915, 0, 0))) step('stretch', lambda: M.stretch((1908, -2, 0), (1912, 2, 0), (1910, 0, 0), (1915, 0, 0)))
ln = line((2000, 0), (2010, 0)) ln = line((2000, 0), (2010, 0))
step('lengthen', lambda: M.lengthen(ln, 5, (2009, 0, 0))) step('lengthen', lambda: M.lengthen(ln, 5, (2009, 0, 0)))
def lwp(pts, closed=False): return doc.create_entity('LwPolyline', is_closed=closed, vertices=[{'location': P(*p)} for p in pts])
pc = lwp([(2100, 0), (2110, 0), (2110, 10)])
step('polyline_close', lambda: M.polyline_close(pc))
po = lwp([(2150, 0), (2160, 0), (2160, 10)], True)
step('polyline_open', lambda: M.polyline_open(po))
pw = lwp([(2200, 0), (2210, 0), (2210, 10)])
step('polyline_width', lambda: M.polyline_width(pw, 0.5))
pr = lwp([(2300, 0), (2310, 0), (2310, 10)])
step('polyline_reverse', lambda: M.polyline_reverse(pr))
pj = lwp([(2400, 0), (2410, 0)]); pjl = line((2410, 0), (2420, 0))
step('polyline_join', lambda: M.polyline_join(pj, [pjl]))
pline = line((2500, 0), (2510, 0))
step('polyline_from_line', lambda: M.polyline_width(pline, 1))
pb = line((2600, 0), (2601, 0)); pp = line((2600, 50), (2650, 50))
step('array_path', lambda: M.array_path([pb], pp, 5))
a3 = line((2700, 0), (2701, 0))
step('array_3d', lambda: M.array_3d([a3], 2, 2, 2, 10, 20, 30))
step('run_line', lambda: doc.command('LINE 1100,0 1110,10')) step('run_line', lambda: doc.command('LINE 1100,0 1110,10'))
def interactive(): def interactive():
with doc.start_command('CIRCLE') as c: with doc.start_command('CIRCLE') as c:
@@ -8140,6 +8168,29 @@ check('bad_point', lambda: ocs.command_step('point', {'point': [0, 0, 0]}))
assert!(has_line(1800.0, 0.0, 1802.0, 0.0) && has_line(1805.0, 0.0, 1810.0, 0.0) && !has_line(1800.0, 0.0, 1810.0, 0.0), "break: {:?}", describe()); assert!(has_line(1800.0, 0.0, 1802.0, 0.0) && has_line(1805.0, 0.0, 1810.0, 0.0) && !has_line(1800.0, 0.0, 1810.0, 0.0), "break: {:?}", describe());
assert!(has_line(1900.0, 0.0, 1915.0, 0.0), "stretch: {:?}", describe()); assert!(has_line(1900.0, 0.0, 1915.0, 0.0), "stretch: {:?}", describe());
assert!(has_line(2000.0, 0.0, 2015.0, 0.0), "lengthen: {:?}", describe()); assert!(has_line(2000.0, 0.0, 2015.0, 0.0), "lengthen: {:?}", describe());
let polylines: Vec<acadrust::entities::LwPolyline> = host.document().entities().filter_map(|e| match e { EntityType::LwPolyline(p) => Some(p.clone()), _ => None }).collect();
let at = |x: f64| polylines.iter().find(|p| p.vertices.iter().any(|v| near(v.location.x, x)));
let pc = at(2100.0).expect("closed polyline");
assert!(pc.is_closed, "polyline_close");
assert!(!at(2150.0).expect("opened polyline").is_closed, "polyline_open");
let pw = at(2200.0).expect("widened polyline");
assert!(pw.constant_width == 0.5 || pw.vertices.iter().all(|v| v.start_width == 0.5 && v.end_width == 0.5), "polyline_width: {pw:?}");
let pr = at(2300.0).expect("reversed polyline");
assert_eq!((pr.vertices[0].location.x, pr.vertices[0].location.y), (2310.0, 10.0), "polyline_reverse: {pr:?}");
let pj = polylines.iter().find(|p| p.vertices.iter().any(|v| near(v.location.x, 2420.0))).expect("joined polyline");
assert_eq!(pj.vertices.len(), 3, "polyline_join: {pj:?}");
assert!(!has_line(2410.0, 0.0, 2420.0, 0.0), "polyline_join consumed the line: {:?}", describe());
let from_line = at(2500.0).expect("a line turned into a polyline");
assert!(from_line.constant_width == 1.0 || from_line.vertices.iter().all(|v| v.start_width == 1.0), "polyline_from_line: {from_line:?}");
assert!(!has_line(2500.0, 0.0, 2510.0, 0.0), "the line became the polyline: {:?}", describe());
let path_copies: Vec<(f64, f64)> = lines.iter().filter(|l| l.start.x > 2599.0 && l.start.x < 2660.0).map(|l| (l.start.x, l.start.y)).collect();
for x in [2600.0, 2612.5, 2625.0, 2637.5, 2650.0] {
assert!(path_copies.iter().any(|p| near(p.0, x) && near(p.1, 0.0)), "array_path item at x={x} (five evenly spaced along the 50-long path): {path_copies:?}");
}
let three_d: Vec<(f64, f64, f64)> = lines.iter().filter(|l| l.start.x > 2699.0 && l.start.x < 2725.0).map(|l| (l.start.x, l.start.y, l.start.z)).collect();
for x in [2700.0, 2720.0] { for y in [0.0, 10.0] { for z in [0.0, 30.0] {
assert!(three_d.iter().any(|p| near(p.0, x) && near(p.1, y) && near(p.2, z)), "array_3d member ({x},{y},{z}): {three_d:?}");
} } }
assert!(circles.iter().any(|c| near(c.center.x, 1200.0) && near(c.radius, 3.0)), "start_command session: {circles:?}"); assert!(circles.iter().any(|c| near(c.center.x, 1200.0) && near(c.radius, 3.0)), "start_command session: {circles:?}");
assert!(host.app.tabs[0].active_cmd.is_none(), "no command is left running"); assert!(host.app.tabs[0].active_cmd.is_none(), "no command is left running");
@@ -8234,64 +8285,21 @@ step('undo_move', lambda: M.move([u], (0, 0, 0), (7, 0, 0)))
for l in lines { host.run_command(R::Run { line: (*l).into() }).unwrap(); } for l in lines { host.run_command(R::Run { line: (*l).into() }).unwrap(); }
host.document().entities().map(|e| e.common().handle).collect() host.document().entities().map(|e| e.common().handle).collect()
}; };
let cancel = |host: &mut HostSession<'_>| { let _ = host.run_command(R::Cancel); }; let hs = mk(&mut host, &["PLINE 2400,0 2410,0", "LINE 2410,0 2420,0"]);
let hs = mk(&mut host, &["LINE 0,0 10,0", "LINE 0,0 0,10", "PLINE 50,0 60,0 60,10", "LINE 100,0 105,0", "LINE 105,0 110,5", "CIRCLE 200,0 5", "LINE 300,0 310,0"]);
// CHAMFER
run(&mut host, "CHAMFER start", R::Start { name: "CHAMFER".into() });
run(&mut host, "CHAMFER D", R::Token { text: "D".into() });
run(&mut host, "CHAMFER d1", R::Text { text: "2".into() });
run(&mut host, "CHAMFER d2", R::Text { text: "3".into() });
run(&mut host, "CHAMFER first", R::Entity { handle: hs[0], point: [8.0, 0.0, 0.0] });
run(&mut host, "CHAMFER second", R::Entity { handle: hs[1], point: [0.0, 8.0, 0.0] });
cancel(&mut host);
// ARRAYRECT
host.app.tabs[0].scene.replace_selection_exact(&[hs[6]]);
run(&mut host, "ARRAYRECT start", R::Start { name: "ARRAYRECT".into() });
run(&mut host, "ARRAYRECT rows", R::Text { text: "2".into() });
run(&mut host, "ARRAYRECT cols", R::Text { text: "3".into() });
run(&mut host, "ARRAYRECT rowdist", R::Text { text: "10".into() });
run(&mut host, "ARRAYRECT coldist", R::Text { text: "20".into() });
run(&mut host, "ARRAYRECT enter", R::Enter);
cancel(&mut host);
// ARRAYPOLAR
host.app.tabs[0].scene.replace_selection_exact(&[hs[6]]);
run(&mut host, "ARRAYPOLAR start", R::Start { name: "ARRAYPOLAR".into() });
run(&mut host, "ARRAYPOLAR center", R::Point { point: [300.0, 20.0, 0.0] });
run(&mut host, "ARRAYPOLAR count", R::Text { text: "4".into() });
run(&mut host, "ARRAYPOLAR angle", R::Text { text: "360".into() });
run(&mut host, "ARRAYPOLAR enter", R::Enter);
cancel(&mut host);
// EXPLODE
run(&mut host, "EXPLODE start", R::Start { name: "EXPLODE".into() });
run(&mut host, "EXPLODE entity", R::Entity { handle: hs[2], point: [55.0, 0.0, 0.0] });
cancel(&mut host);
host.app.tabs[0].scene.replace_selection_exact(&[hs[2]]);
run(&mut host, "EXPLODE presel", R::Start { name: "EXPLODE".into() });
cancel(&mut host);
// JOIN
host.app.tabs[0].scene.replace_selection_exact(&[hs[3], hs[4]]);
run(&mut host, "JOIN presel", R::Start { name: "JOIN".into() });
run(&mut host, "JOIN enter", R::Enter);
cancel(&mut host);
// BREAK
run(&mut host, "BREAK start", R::Start { name: "BREAK".into() });
run(&mut host, "BREAK entity", R::Entity { handle: hs[6], point: [302.0, 0.0, 0.0] });
run(&mut host, "BREAK p2", R::Point { point: [305.0, 0.0, 0.0] });
cancel(&mut host);
// HATCH
run(&mut host, "HATCH start", R::Start { name: "HATCH".into() });
cancel(&mut host);
// STRETCH, LENGTHEN, JOIN, PEDIT
run(&mut host, "LENGTHEN start", R::Start { name: "LENGTHEN".into() });
cancel(&mut host);
run(&mut host, "PEDIT start", R::Start { name: "PEDIT".into() }); run(&mut host, "PEDIT start", R::Start { name: "PEDIT".into() });
cancel(&mut host); run(&mut host, "pick", R::Entity { handle: hs[0], point: [2400.0, 0.0, 0.0] });
run(&mut host, "STRETCH start", R::Start { name: "STRETCH".into() }); run(&mut host, "J", R::Token { text: "J".into() });
cancel(&mut host); host.app.tabs[0].scene.replace_selection_exact(&[hs[1]]);
run(&mut host, "selection", R::Selection);
run(&mut host, "enter", R::Enter);
run(&mut host, "X", R::Token { text: "X".into() });
eprintln!("SPIKE final entity count {}", host.document().entities().count()); eprintln!("SPIKE final entity count {}", host.document().entities().count());
} }
#[test] #[test]
fn audit_python_raster_image_definition_linkage_over_real_ipc() { fn audit_python_raster_image_definition_linkage_over_real_ipc() {
use acadrust::objects::ObjectType; use acadrust::objects::ObjectType;