feat: node graph overlay; move to opencadcodec/opencadkernel/opencadgraph

The codec and kernel repositories and crates are renamed opencadcodec and
opencadkernel. Inside OCS they are depended on under short keys, so code
reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node
graph engine). File-format and protocol spellings stay as they were: the
ACIS product id `acadrust`, the ACADRUST_* switches and the plugin
version keys `acadrust_version` / `acadrust_source`.

A button left of MCP on the command line opens a translucent node graph
over the viewport, with a palette of object, input, math, logic, list,
text and geometry nodes added by click or drag. An object node owns
drawing entities and its rows are the Properties panel's own rows for
them: the single-object page is extracted from refresh_properties as
entity_property_sections, values use the control protocol's
property_json, and writes go through set_property_value with a property
target override, so the panel, MCP and the graph share one read/write
path. A list reaching an object row makes one entity per element. Each
graph action is one undo step; the canvas pans and zooms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-09-25 07:34:23 +03:00
1 parent 56c04408d6
commit f537aff75c
430 files changed
+9925 -8540

No files matched your search

@@ -22,8 +22,8 @@ const DIM_BASE_KEYS: &[&str] = &[
];
const DIM_READ_ONLY_KEYS: &[&str] = &["subtype", "actual_measurement"];
fn dimension_subtype_name(dim: &acadrust::entities::Dimension) -> &'static str {
use acadrust::entities::Dimension as D;
fn dimension_subtype_name(dim: &codec::entities::Dimension) -> &'static str {
use codec::entities::Dimension as D;
match dim {
D::Aligned(_) => "Aligned",
D::Linear(_) => "Linear",
@@ -38,8 +38,8 @@ fn dimension_subtype_name(dim: &acadrust::entities::Dimension) -> &'static str {
}
/// Subtype-specific fields in stable order.
fn dimension_fields(dim: &mut acadrust::entities::Dimension) -> Vec<(&'static str, DimField<'_>)> {
use acadrust::entities::Dimension as D;
fn dimension_fields(dim: &mut codec::entities::Dimension) -> Vec<(&'static str, DimField<'_>)> {
use codec::entities::Dimension as D;
use DimField::{B, F, V};
match dim {
D::Aligned(d) => vec![
@@ -120,12 +120,12 @@ fn dimension_required(subtype: &str) -> &'static [&'static str] {
}
}
fn dimension_base_mut(dim: &mut acadrust::entities::Dimension) -> &mut acadrust::entities::DimensionBase {
fn dimension_base_mut(dim: &mut codec::entities::Dimension) -> &mut codec::entities::DimensionBase {
dim.base_mut()
}
fn dim_attachment_point_name(value: acadrust::entities::AttachmentPointType) -> &'static str {
use acadrust::entities::AttachmentPointType as A;
fn dim_attachment_point_name(value: codec::entities::AttachmentPointType) -> &'static str {
use codec::entities::AttachmentPointType as A;
match value {
A::TopLeft => "TopLeft",
A::TopCenter => "TopCenter",
@@ -142,8 +142,8 @@ fn dim_attachment_point_name(value: acadrust::entities::AttachmentPointType) ->
fn parse_dim_attachment_point(
value: &str,
vm: &VirtualMachine,
) -> PyResult<acadrust::entities::AttachmentPointType> {
use acadrust::entities::AttachmentPointType as A;
) -> PyResult<codec::entities::AttachmentPointType> {
use codec::entities::AttachmentPointType as A;
Ok(match value {
"TopLeft" => A::TopLeft,
"TopCenter" => A::TopCenter,
@@ -162,7 +162,7 @@ fn parse_dim_attachment_point(
fn dimension_to_dict(
vm: &VirtualMachine,
dimension: &acadrust::entities::Dimension,
dimension: &codec::entities::Dimension,
) -> PyResult<PyObjectRef> {
let mut dim = dimension.clone();
let dict = vm.ctx.new_dict();
@@ -197,7 +197,7 @@ fn dimension_to_dict(
/// Merge `dict` onto `dim`. With `creating`, geometry the subtype needs must
/// be present so a script cannot silently default a point to the origin.
fn dimension_merge(
dim: &mut acadrust::entities::Dimension,
dim: &mut codec::entities::Dimension,
dict: &rustpython_vm::builtins::PyDictRef,
creating: bool,
vm: &VirtualMachine,
@@ -271,8 +271,8 @@ fn dimension_merge(
fn dimension_from_dict(
dict: &rustpython_vm::builtins::PyDictRef,
vm: &VirtualMachine,
) -> PyResult<acadrust::entities::Dimension> {
use acadrust::entities::*;
) -> PyResult<codec::entities::Dimension> {
use codec::entities::*;
let subtype = match dict.get_item_opt("subtype", vm)? {
Some(v) => v.try_into_value::<String>(vm)?,
None => {
@@ -299,17 +299,17 @@ fn dimension_from_dict(
};
dimension_merge(&mut dim, dict, true, vm)?;
let base = dimension_base_mut(&mut dim);
base.common.handle = acadrust::Handle::new(get_opt_u64(dict, "handle", vm)?);
base.common.handle = codec::Handle::new(get_opt_u64(dict, "handle", vm)?);
base.common.layer = get_opt_string(dict, "layer", "0", vm)?;
base.common.owner_handle = acadrust::Handle::new(get_opt_u64(dict, "owner_handle", vm)?);
base.common.owner_handle = codec::Handle::new(get_opt_u64(dict, "owner_handle", vm)?);
Ok(dim)
}
fn dimension_apply(
existing: &acadrust::entities::Dimension,
existing: &codec::entities::Dimension,
dict: &rustpython_vm::builtins::PyDictRef,
vm: &VirtualMachine,
) -> PyResult<acadrust::entities::Dimension> {
) -> PyResult<codec::entities::Dimension> {
let mut dim = existing.clone();
dimension_merge(&mut dim, dict, false, vm)?;
let base = dimension_base_mut(&mut dim);
+44 -44
View File
@@ -13,14 +13,14 @@ mod ocs {
use rustpython_vm::convert::TryFromObject;
use rustpython_vm::{function::ArgIntoFloat, PyObjectRef, PyResult, VirtualMachine};
use acadrust::entities::{
use codec::entities::{
Arc, LwPolyline, LwVertex, MText, TableBuilder, Text, TextHorizontalAlignment,
TextVerticalAlignment,
};
use acadrust::objects::ObjectType;
use acadrust::types::{Transform, Vector2, Vector3};
use codec::objects::ObjectType;
use codec::types::{Transform, Vector2, Vector3};
use ocs_plugin_api::host::{
acadrust, EntityType, ExtendedDataRecord, Handle, ReaderEntityKind, XDataValue,
codec, EntityType, ExtendedDataRecord, Handle, ReaderEntityKind, XDataValue,
};
use crate::host_ctx;
@@ -51,7 +51,7 @@ mod ocs {
Ok(())
}
fn top_level_blocks(document: &acadrust::CadDocument) -> std::collections::HashSet<Handle> {
fn top_level_blocks(document: &codec::CadDocument) -> std::collections::HashSet<Handle> {
let mut blocks: std::collections::HashSet<Handle> = document
.objects
.values()
@@ -958,7 +958,7 @@ mod ocs {
options: &rustpython_vm::builtins::PyDictRef,
vm: &VirtualMachine,
) -> PyResult<ocs_plugin_api::host::LayerConfig> {
use acadrust::types::{Color, LineWeight, Transparency};
use codec::types::{Color, LineWeight, Transparency};
ensure_known_entity_keys(
options,
"layer",
@@ -1342,7 +1342,7 @@ mod ocs {
let document = host.document();
let mut inserts = std::collections::HashMap::<String, usize>::new();
for entity in document.entities() {
if let acadrust::entities::EntityType::Insert(insert) = entity {
if let codec::entities::EntityType::Insert(insert) = entity {
*inserts.entry(insert.block_name.to_uppercase()).or_default() += 1;
}
}
@@ -1546,7 +1546,7 @@ mod ocs {
#[cfg(feature = "experimental-host-model")]
#[pyfunction]
fn layout_records(vm: &VirtualMachine) -> PyResult<PyObjectRef> {
use acadrust::objects::ObjectType;
use codec::objects::ObjectType;
let rows = host_ctx::with_host(|host| {
let current = match host.system_variable("CTAB") {
Some(ocs_plugin_api::host::HostSettingValue::Text(name)) => name,
@@ -1558,7 +1558,7 @@ mod ocs {
for entity in document.entities() {
let owner = entity.common().owner_handle.value();
*owned.entry(owner).or_default() += 1;
if matches!(entity, acadrust::entities::EntityType::Viewport(_)) {
if matches!(entity, codec::entities::EntityType::Viewport(_)) {
*viewports.entry(owner).or_default() += 1;
}
}
@@ -1664,7 +1664,7 @@ mod ocs {
#[cfg(feature = "experimental-host-model")]
#[pyfunction]
fn layer_records(vm: &VirtualMachine) -> PyResult<PyObjectRef> {
use acadrust::types::{LineWeight, Transparency};
use codec::types::{LineWeight, Transparency};
let rows = host_ctx::with_host(|host| {
let document = host.document();
let mut counts = std::collections::HashMap::<String, usize>::new();
@@ -1858,7 +1858,7 @@ mod ocs {
"ocs.export_layer_dwg: cannot create {output_path:?}: {error}"
))
})?;
if let Err(error) = acadrust::DwgWriter::write_to_writer(&mut file, &snapshot) {
if let Err(error) = codec::DwgWriter::write_to_writer(&mut file, &snapshot) {
drop(file);
let _ = std::fs::remove_file(path);
return Err(
@@ -3720,7 +3720,7 @@ mod ocs {
y2: ArgIntoFloat,
vm: &VirtualMachine,
) -> PyResult<u64> {
let line = acadrust::Line::from_coords(
let line = codec::Line::from_coords(
x1.into_float(),
y1.into_float(),
0.0,
@@ -3746,7 +3746,7 @@ mod ocs {
vm: &VirtualMachine,
) -> PyResult<u64> {
let circle =
acadrust::Circle::from_coords(x.into_float(), y.into_float(), 0.0, radius.into_float());
codec::Circle::from_coords(x.into_float(), y.into_float(), 0.0, radius.into_float());
host_ctx::with_host(|host| {
host_ctx::ensure_undo_started(host);
host.add_entity(EntityType::Circle(circle)).value()
@@ -3768,8 +3768,8 @@ mod ocs {
end_deg: ArgIntoFloat,
vm: &VirtualMachine,
) -> PyResult<u64> {
let center = acadrust::Vector3::new(x.into_float(), y.into_float(), 0.0);
let arc = acadrust::Arc::from_center_radius_angles(
let center = codec::Vector3::new(x.into_float(), y.into_float(), 0.0);
let arc = codec::Arc::from_center_radius_angles(
center,
radius.into_float(),
start_deg.into_float().to_radians(),
@@ -3915,7 +3915,7 @@ mod ocs {
"ocs.add_points: each point must be [finite x, y, z]".to_owned(),
));
}
entities.push(EntityType::Point(acadrust::Point::from_coords(
entities.push(EntityType::Point(codec::Point::from_coords(
point[0], point[1], point[2],
)));
}
@@ -4123,8 +4123,8 @@ 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;
fn color_to_py_dict(vm: &VirtualMachine, color: &codec::types::Color) -> PyResult<PyObjectRef> {
use codec::types::Color;
let dict = vm.ctx.new_dict();
match color {
Color::ByLayer => dict.set_item("kind", vm.new_pyobj("ByLayer"), vm)?,
@@ -4146,8 +4146,8 @@ mod ocs {
Ok(dict.into())
}
fn py_to_color_dict(value: PyObjectRef, vm: &VirtualMachine) -> PyResult<acadrust::types::Color> {
use acadrust::types::Color;
fn py_to_color_dict(value: PyObjectRef, vm: &VirtualMachine) -> PyResult<codec::types::Color> {
use codec::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> {
@@ -4682,7 +4682,7 @@ mod ocs {
#[test]
fn unmapped_canvas_kind_accepts_only_layer_patch() {
with_vm(|vm| {
let entity = EntityType::MLine(acadrust::entities::MLine::default());
let entity = EntityType::MLine(codec::entities::MLine::default());
let patch = vm.ctx.new_dict();
patch.set_item("handle", vm.new_pyobj(1_u64), vm).unwrap();
patch
@@ -4868,7 +4868,7 @@ mod ocs {
fn insert_is_created_by_block_name_and_preserves_block_identity() {
with_vm(|vm| {
let mut insert =
acadrust::entities::Insert::new("DOOR", Vector3::new(1.0, 2.0, 0.0));
codec::entities::Insert::new("DOOR", Vector3::new(1.0, 2.0, 0.0));
insert.set_x_scale(2.0);
let original = EntityType::Insert(insert);
let rendered = entity_to_dict(vm, &original)
@@ -5050,7 +5050,7 @@ mod ocs {
assert_eq!(definition.tag, "PART_NO");
assert_eq!(definition.text_style, "Standard");
let mut embedded = acadrust::entities::MText::default();
let mut embedded = codec::entities::MText::default();
embedded.value = "first\\Psecond".into();
definition.embedded_mtext = Some(Box::new(embedded));
let entity = EntityType::AttributeDefinition(definition);
@@ -5123,7 +5123,7 @@ mod ocs {
};
assert_eq!(attribute.common.owner_handle, Handle::new(50));
attribute.attdef_handle = Handle::new(42);
let mut embedded = acadrust::entities::MText::default();
let mut embedded = codec::entities::MText::default();
embedded.value = "first\\Psecond".into();
attribute.embedded_mtext = Some(Box::new(embedded));
let entity = EntityType::AttributeEntity(attribute);
@@ -5274,7 +5274,7 @@ mod ocs {
#[test]
fn phase_seven_kinds_survive_dwg_round_trip() {
with_vm(|vm| {
let mut doc = acadrust::CadDocument::new();
let mut doc = codec::CadDocument::new();
let mut handles = Vec::new();
for kind in ["Ray", "XLine", "Solid", "Face3D"] {
let input = vm.ctx.new_dict();
@@ -5298,8 +5298,8 @@ mod ocs {
}
let path = std::env::temp_dir()
.join(format!("ocs_phase_seven_{}.dwg", std::process::id()));
acadrust::DwgWriter::write_to_file(&path, &doc).unwrap();
let reopened = acadrust::DwgReader::from_file(&path)
codec::DwgWriter::write_to_file(&path, &doc).unwrap();
let reopened = codec::DwgReader::from_file(&path)
.unwrap()
.read()
.unwrap();
@@ -5325,7 +5325,7 @@ mod ocs {
#[test]
fn text_is_in_editable_schema_and_keeps_unmentioned_fields() {
with_vm(|vm| {
let mut text = acadrust::entities::Text::new();
let mut text = codec::entities::Text::new();
text.value = "Before".into();
text.height = 3.5;
text.insertion_point = Vector3::new(1.0, 2.0, 0.0);
@@ -5363,7 +5363,7 @@ mod ocs {
dict.set_item("radius", vm.new_pyobj(5.0), vm).unwrap();
let entity = dict_to_entity(&dict, vm).expect("dict_to_entity");
let acadrust::EntityType::Circle(circle) = &entity else {
let codec::EntityType::Circle(circle) = &entity else {
panic!("expected Circle, got {entity:?}");
};
assert_eq!(circle.common.layer, "Walls");
@@ -5462,7 +5462,7 @@ mod ocs {
.unwrap();
let entity = dict_to_entity(&dict, vm).expect("dict_to_entity");
let acadrust::EntityType::Polyline2D(polyline) = &entity else {
let codec::EntityType::Polyline2D(polyline) = &entity else {
panic!("expected Polyline2D, got {entity:?}");
};
assert!(polyline.flags.is_closed());
@@ -5491,13 +5491,13 @@ mod ocs {
.unwrap();
let entity = dict_to_entity(&dict, vm).expect("dict_to_entity");
let acadrust::EntityType::MText(text) = &entity else {
let codec::EntityType::MText(text) = &entity else {
panic!("expected MText, got {entity:?}");
};
assert_eq!(text.value, "hello");
assert_eq!(
text.attachment_point,
acadrust::entities::AttachmentPoint::MiddleCenter
codec::entities::AttachmentPoint::MiddleCenter
);
let round_tripped = entity_to_dict(vm, &entity).expect("entity_to_dict");
@@ -5539,7 +5539,7 @@ mod ocs {
let updated = apply_dict_to_entity(&existing, &update_dict, vm)
.expect("apply_dict_to_entity");
let acadrust::EntityType::Line(line) = &updated else {
let codec::EntityType::Line(line) = &updated else {
panic!("expected Line, got {updated:?}");
};
assert_eq!(line.common.layer, "Geometry", "layer must be preserved");
@@ -5562,7 +5562,7 @@ mod ocs {
.unwrap();
let updated =
apply_dict_to_entity(&updated, &update_dict, vm).expect("apply_dict_to_entity");
let acadrust::EntityType::Line(line) = &updated else {
let codec::EntityType::Line(line) = &updated else {
panic!("expected Line, got {updated:?}");
};
assert_eq!(line.common.layer, "Renamed");
@@ -5574,7 +5574,7 @@ mod ocs {
/// The concrete Phase 1 acceptance test: create, read,
/// edit, delete, save, and reopen a Line and an Ellipse, with common
/// properties preserved — exercised against a real
/// `acadrust::CadDocument` and a real DWG round trip (not a fake
/// `codec::CadDocument` and a real DWG round trip (not a fake
/// `HostApi`; `dict_to_entity`/`apply_dict_to_entity`/`entity_to_dict`
/// are exactly what a live `HostApi`-backed `ocs.add`/`ocs.update`/
/// `ocs.get_entity` call into, so this covers the same logic without
@@ -5582,14 +5582,14 @@ mod ocs {
#[test]
fn line_and_ellipse_survive_create_read_edit_delete_save_reopen() {
with_vm(|vm| {
let mut doc = acadrust::CadDocument::new();
let mut doc = codec::CadDocument::new();
// A layer must exist in the document's layer table before an
// entity can reference it — same requirement `set_entity_layer`
// above already enforces for a live document. Without this,
// the DWG writer silently falls back the entity to layer "0"
// instead of erroring, which this test caught the first time
// around (it used "Geometry" without registering it first).
doc.layers.add(acadrust::Layer::new("Geometry")).unwrap();
doc.layers.add(codec::Layer::new("Geometry")).unwrap();
// --- CREATE ---
let add_line = vm.ctx.new_dict();
@@ -5643,7 +5643,7 @@ mod ocs {
.expect("apply_dict_to_entity Line");
*doc.get_entity_mut(line_handle).unwrap() = updated;
let acadrust::EntityType::Line(line) = doc.get_entity(line_handle).unwrap() else {
let codec::EntityType::Line(line) = doc.get_entity(line_handle).unwrap() else {
panic!("expected Line");
};
assert_eq!(
@@ -5670,14 +5670,14 @@ mod ocs {
"ocs_entity_crud_acceptance_test_{}.dwg",
std::process::id()
));
acadrust::DwgWriter::write_to_file(&path, &doc).expect("write dwg");
let reopened = acadrust::DwgReader::from_file(&path)
codec::DwgWriter::write_to_file(&path, &doc).expect("write dwg");
let reopened = codec::DwgReader::from_file(&path)
.expect("open dwg")
.read()
.expect("read dwg");
let _ = std::fs::remove_file(&path);
let acadrust::EntityType::Line(reopened_line) = reopened
let codec::EntityType::Line(reopened_line) = reopened
.get_entity(line_handle)
.expect("line survives save/reopen")
else {
@@ -5740,7 +5740,7 @@ mod tests {
let mut current = None;
for line in converter.lines() {
let line = line.trim();
if let Some(arm) = line.strip_prefix("acadrust::EntityType::") {
if let Some(arm) = line.strip_prefix("codec::EntityType::") {
if let Some((kind, _)) = arm.split_once("(value) => {") {
current = Some(kind.to_owned());
emitted.entry(kind.to_owned()).or_default();
@@ -5800,8 +5800,8 @@ mod tests {
#[cfg(feature = "experimental-host-model")]
#[test]
fn entity_snapshot_uses_catalog_variant_name() {
use ocs_plugin_api::host::acadrust;
let hatch = acadrust::EntityType::Hatch(acadrust::entities::Hatch::default());
use ocs_plugin_api::host::codec;
let hatch = codec::EntityType::Hatch(codec::entities::Hatch::default());
let value = serde_json::to_value(hatch).unwrap();
let kind = value.as_object().unwrap().keys().next().unwrap();
assert_eq!(kind, "Hatch");
@@ -3,10 +3,10 @@
/// Apply `patch` (a dict of context fields) onto `existing`, so a script can
/// change one context field without resetting the rest. Unknown keys error.
fn merge_multileader_context(
existing: &acadrust::entities::MultiLeaderAnnotContext,
existing: &codec::entities::MultiLeaderAnnotContext,
patch: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<acadrust::entities::MultiLeaderAnnotContext> {
) -> PyResult<codec::entities::MultiLeaderAnnotContext> {
let patch = patch.try_into_value::<rustpython_vm::builtins::PyDictRef>(vm)?;
let merged = multi_leader_annot_context_to_dict(vm, existing)?
.try_into_value::<rustpython_vm::builtins::PyDictRef>(vm)?;