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

+24 -24
View File
@@ -2,29 +2,6 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "acadrust"
version = "0.5.5"
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=1c3be0c#1c3be0ca296c99f2f126ba1a03c2be1998d7c64e"
dependencies = [
"ahash 0.8.12",
"anyhow",
"bitflags",
"byteorder",
"encoding_rs",
"flate2",
"indexmap",
"itoa",
"memmap2",
"nom",
"once_cell",
"rayon",
"ryu",
"serde",
"thiserror 1.0.69",
"web-time",
]
[[package]]
name = "adler2"
version = "2.0.1"
@@ -1254,13 +1231,13 @@ dependencies = [
name = "ocs_plugin_api"
version = "0.2.1"
dependencies = [
"acadrust",
"bincode",
"cargo-lock",
"getrandom 0.2.17",
"interprocess",
"libloading 0.8.9",
"memmap2",
"opencadcodec",
"rkyv",
"serde",
"serde-reflection",
@@ -1285,6 +1262,29 @@ dependencies = [
"serde_json",
]
[[package]]
name = "opencadcodec"
version = "0.5.5"
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=43a82f9#43a82f9b09ab5250fbe6a03e49c1d5a73a4152f3"
dependencies = [
"ahash 0.8.12",
"anyhow",
"bitflags",
"byteorder",
"encoding_rs",
"flate2",
"indexmap",
"itoa",
"memmap2",
"nom",
"once_cell",
"rayon",
"ryu",
"serde",
"thiserror 1.0.69",
"web-time",
]
[[package]]
name = "optional"
version = "0.5.0"
+2 -2
View File
@@ -181,7 +181,7 @@ positive, and `dimtxsty` must name an existing text style. `Standard` is never
renamed or deleted; a style that is current or still referenced is never deleted;
a case-only rename is refused. Style operations act on the active document.
Known persistence limits (cadcodec, see `docs/cadcodec-reader-gaps.md`; the first two are fixed in
Known persistence limits (opencadcodec, see `docs/cadcodec-reader-gaps.md`; the first two are fixed in
an open upstream pull request but OCS has not yet adopted it): a DXF
save drops the backward/upside-down flags of a text style and does not restore a
dimension style's `dimtxsty` name (the handle link survives); DWG keeps both.
@@ -211,7 +211,7 @@ anonymous or externally referenced blocks, and a case-only rename. Deleting also
removes the definition's contents. Records list user blocks only (`handle`,
`name`, `description`, `explodable`, `scale_uniformly`, `base_point`,
`insert_count`, `entities` as `{handle, kind}`). A DXF save drops the block
description (cadcodec); DWG keeps it.
description (opencadcodec); DWG keeps it.
**Block contents.** Add to a definition with `create_entity(kind, block="Part",
...)`. It is validated exactly like a model-space entity and the host sets the
+27 -27
View File
@@ -133,7 +133,7 @@ enum Classify {
/// `{ "kind": "Variant", "value": ... }` so nested geometry such as
/// hatch boundary edges remains explicit and round-trippable.
TaggedEnum,
/// A struct whose only field is `bits` (an acadrust bitflags newtype,
/// A struct whose only field is `bits` (an opencadcodec bitflags newtype,
/// e.g. `PolylineFlags`/`VertexFlags`). Represented in Python as a plain
/// integer via `.bits()`/`::from_bits(..)` — the named boolean accessors
/// some of these types have (`is_closed`/`set_closed`, ...) are exposed
@@ -163,7 +163,7 @@ fn classify<'a>(name: &str, info: &'a TypeInfo) -> Classify {
Classify::UnitEnum
}
}
// acadrust `bitflags!` types serialize as a newtype over their integer
// opencadcodec `bitflags!` types serialize as a newtype over their integer
// bits; they convert exactly like the hand-written `bits` structs.
TypeKind::Newtype => Classify::BitFlags,
other => panic!("entity_manifest.json: {name} has unsupported registry kind {other:?}"),
@@ -301,12 +301,12 @@ fn native_rust_type(type_id: &str) -> &'static str {
}
}
/// Rust path of a traced type; most live in `acadrust::entities`.
/// Rust path of a traced type; most live in `codec::entities`.
fn type_path(name: &str) -> String {
match name {
"LineWeight" => "acadrust::types::LineWeight".to_owned(),
"LeaderLineBreakInfo" => "acadrust::entities::multileader::LeaderLineBreakInfo".to_owned(),
_ => format!("acadrust::entities::{name}"),
"LineWeight" => "codec::types::LineWeight".to_owned(),
"LeaderLineBreakInfo" => "codec::entities::multileader::LeaderLineBreakInfo".to_owned(),
_ => format!("codec::entities::{name}"),
}
}
@@ -356,10 +356,10 @@ fn default_expr(type_id: &str) -> String {
"i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" => "0".to_string(),
"bool" => "false".to_string(),
"String" | "str" => "String::new()".to_string(),
"Vector3" => "acadrust::types::Vector3::ZERO".to_string(),
"Vector2" => "acadrust::types::Vector2::ZERO".to_string(),
"Handle" => "acadrust::Handle::new(0)".to_string(),
"Color" => "acadrust::types::Color::ByLayer".to_string(),
"Vector3" => "codec::types::Vector3::ZERO".to_string(),
"Vector2" => "codec::types::Vector2::ZERO".to_string(),
"Handle" => "codec::Handle::new(0)".to_string(),
"Color" => "codec::types::Color::ByLayer".to_string(),
other => format!("default_{}()", snake_case(other)),
}
}
@@ -440,7 +440,7 @@ fn leaf_from_py(registry: &TypeRegistry, type_id: &str, value_expr: &str) -> Str
match type_id {
"Vector3" => format!("py_to_vector3_dict({value_expr}, vm)"),
"Vector2" => format!("py_to_vector2({value_expr}, vm)"),
"Handle" => format!("{value_expr}.try_into_value::<u64>(vm).map(acadrust::Handle::new)"),
"Handle" => format!("{value_expr}.try_into_value::<u64>(vm).map(codec::Handle::new)"),
"Color" => format!("py_to_color_dict({value_expr}, vm)"),
other => {
let info = get(registry, other)
@@ -697,7 +697,7 @@ fn gen_struct(name: &str, info: &TypeInfo, registry: &TypeRegistry, required: &[
if f.type_id.as_str() == "EntityCommon" {
// A sub-record's own common data (layer, XDATA, handles) is not part
// of the scripted model; it defaults on the way in.
let default = "acadrust::entities::EntityCommon::default()";
let default = "codec::entities::EntityCommon::default()";
from_dict_fields.push_str(&format!(" {}: {default},\n", f.name));
default_fields.push_str(&format!(" {}: {default},\n", f.name));
continue;
@@ -861,7 +861,7 @@ fn gen_entity_to_dict(manifest: &Manifest, registry: &TypeRegistry) -> String {
for kind in &manifest.type_filter {
if manifest.manual_kinds.contains(kind) {
arms.push_str(&format!(
" acadrust::EntityType::{kind}(value) => {}_to_dict(vm, value),\n",
" codec::EntityType::{kind}(value) => {}_to_dict(vm, value),\n",
snake_case(kind)
));
continue;
@@ -891,7 +891,7 @@ fn gen_entity_to_dict(manifest: &Manifest, registry: &TypeRegistry) -> String {
}
body.push_str(" Ok(dict.into())\n");
arms.push_str(&format!(
" acadrust::EntityType::{kind}({var}) => {{\n{body} }}\n"
" codec::EntityType::{kind}({var}) => {{\n{body} }}\n"
));
}
format!(
@@ -899,7 +899,7 @@ fn gen_entity_to_dict(manifest: &Manifest, registry: &TypeRegistry) -> String {
/// `"kind"` key (matching the convention every other `ocs.*` dict already
/// uses, e.g. XDATA's `{{"kind": ..., "value": ...}}`), plus `"handle"` and
/// `"layer"` and every entity-specific field the type registry knows about.
pub(crate) fn entity_to_dict(vm: &VirtualMachine, entity: &acadrust::EntityType) -> PyResult<PyObjectRef> {{
pub(crate) fn entity_to_dict(vm: &VirtualMachine, entity: &codec::EntityType) -> PyResult<PyObjectRef> {{
match entity {{
{arms} _ => Err(vm.new_value_error(
"ocs: this entity kind is not supported by the generic add/update API yet".to_owned(),
@@ -925,7 +925,7 @@ fn gen_dict_to_entity(manifest: &Manifest, registry: &TypeRegistry) -> String {
}
if manifest.manual_kinds.contains(kind) {
arms.push_str(&format!(
" \"{kind}\" => acadrust::EntityType::{kind}({}_from_dict(dict, vm)?),\n",
" \"{kind}\" => codec::EntityType::{kind}({}_from_dict(dict, vm)?),\n",
snake_case(kind)
));
continue;
@@ -946,23 +946,23 @@ fn gen_dict_to_entity(manifest: &Manifest, registry: &TypeRegistry) -> String {
let constructor = manifest.overrides.get(kind)
.and_then(|override_def| override_def.rust_constructor.as_deref())
.map(str::to_owned)
.unwrap_or_else(|| format!("acadrust::entities::{kind}::default()"));
.unwrap_or_else(|| format!("codec::entities::{kind}::default()"));
body.push_str(&format!(" let mut {var} = {constructor};\n"));
body.push_str(&format!(
" {var}.common.handle = acadrust::Handle::new(get_opt_u64(dict, \"handle\", vm)?);\n"
" {var}.common.handle = codec::Handle::new(get_opt_u64(dict, \"handle\", vm)?);\n"
));
body.push_str(&format!(
" {var}.common.layer = get_opt_string(dict, \"layer\", \"0\", vm)?;\n"
));
if manifest.base_fields.iter().any(|field| field == "owner_handle") {
body.push_str(&format!(
" {var}.common.owner_handle = acadrust::Handle::new(get_opt_u64(dict, \"owner_handle\", vm)?);\n"
" {var}.common.owner_handle = codec::Handle::new(get_opt_u64(dict, \"owner_handle\", vm)?);\n"
));
}
for f in &fields {
body.push_str(&format!(" {}\n", f.setter));
}
body.push_str(&format!(" acadrust::EntityType::{kind}({var})\n"));
body.push_str(&format!(" codec::EntityType::{kind}({var})\n"));
arms.push_str(&format!(" \"{kind}\" => {{\n{body} }}\n"));
}
format!(
@@ -971,8 +971,8 @@ fn gen_dict_to_entity(manifest: &Manifest, registry: &TypeRegistry) -> String {
/// `"required"` (the geometry that actually defines the shape — a line's
/// endpoints, a circle's center/radius, ...) error if missing rather than
/// silently defaulting to zero; every other field defaults sensibly (what a
/// fresh `acadrust::entities::X::default()` would already hold).
pub(crate) fn dict_to_entity(dict: &rustpython_vm::builtins::PyDictRef, vm: &VirtualMachine) -> PyResult<acadrust::EntityType> {{
/// fresh `codec::entities::X::default()` would already hold).
pub(crate) fn dict_to_entity(dict: &rustpython_vm::builtins::PyDictRef, vm: &VirtualMachine) -> PyResult<codec::EntityType> {{
let kind = dict.get_item("kind", vm)?.try_into_value::<String>(vm)?;
Ok(match kind.as_str() {{
{arms} other => {{
@@ -990,7 +990,7 @@ fn gen_apply_dict_to_entity(manifest: &Manifest, registry: &TypeRegistry) -> Str
for kind in &manifest.type_filter {
if manifest.manual_kinds.contains(kind) {
arms.push_str(&format!(
" acadrust::EntityType::{kind}(existing_value) => acadrust::EntityType::{kind}({}_apply(existing_value, dict, vm)?),\n",
" codec::EntityType::{kind}(existing_value) => codec::EntityType::{kind}({}_apply(existing_value, dict, vm)?),\n",
snake_case(kind)
));
continue;
@@ -1032,9 +1032,9 @@ fn gen_apply_dict_to_entity(manifest: &Manifest, registry: &TypeRegistry) -> Str
for f in &fields {
body.push_str(&format!(" {}\n", f.setter));
}
body.push_str(&format!(" acadrust::EntityType::{kind}({var})\n"));
body.push_str(&format!(" codec::EntityType::{kind}({var})\n"));
arms.push_str(&format!(
" acadrust::EntityType::{kind}(existing_value) => {{\n{body} }}\n"
" codec::EntityType::{kind}(existing_value) => {{\n{body} }}\n"
));
}
format!(
@@ -1048,10 +1048,10 @@ fn gen_apply_dict_to_entity(manifest: &Manifest, registry: &TypeRegistry) -> Str
/// `"kind"` (if present at all — round-tripped from a prior `get_entity()`)
/// is never consulted, since an update can't change an entity's shape.
pub(crate) fn apply_dict_to_entity(
existing: &acadrust::EntityType,
existing: &codec::EntityType,
dict: &rustpython_vm::builtins::PyDictRef,
vm: &VirtualMachine,
) -> PyResult<acadrust::EntityType> {{
) -> PyResult<codec::EntityType> {{
Ok(match existing {{
{arms} _ => {{
return Err(vm.new_value_error(
+7 -7
View File
@@ -142,7 +142,7 @@
},
"smooth_type": {
"exclude": true,
"_comment": "acadrust defines two distinct SmoothSurfaceType enums with the same name (polyline.rs, used by Polyline2D's smooth_surface \u2014 and a separate one private to polyline3d.rs, used here); the type registry's tracer keys purely by name and collapses them, so the generically generated conversion resolves to the wrong one. Needs a hand-written override with the fully qualified acadrust::entities::polyline3d::SmoothSurfaceType path to support, not just a rename."
"_comment": "opencadcodec defines two distinct SmoothSurfaceType enums with the same name (polyline.rs, used by Polyline2D's smooth_surface \u2014 and a separate one private to polyline3d.rs, used here); the type registry's tracer keys purely by name and collapses them, so the generically generated conversion resolves to the wrong one. Needs a hand-written override with the fully qualified opencadcodec::entities::polyline3d::SmoothSurfaceType path to support, not just a rename."
}
}
},
@@ -209,7 +209,7 @@
}
},
"Solid": {
"rust_constructor": "acadrust::entities::Solid::new(acadrust::types::Vector3::ZERO, acadrust::types::Vector3::ZERO, acadrust::types::Vector3::ZERO, acadrust::types::Vector3::ZERO)",
"rust_constructor": "codec::entities::Solid::new(codec::types::Vector3::ZERO, codec::types::Vector3::ZERO, codec::types::Vector3::ZERO, codec::types::Vector3::ZERO)",
"fields": {
"first_corner": {
"required": true
@@ -226,7 +226,7 @@
}
},
"Face3D": {
"rust_constructor": "acadrust::entities::Face3D::new(acadrust::types::Vector3::ZERO, acadrust::types::Vector3::ZERO, acadrust::types::Vector3::ZERO, acadrust::types::Vector3::ZERO)",
"rust_constructor": "codec::entities::Face3D::new(codec::types::Vector3::ZERO, codec::types::Vector3::ZERO, codec::types::Vector3::ZERO, codec::types::Vector3::ZERO)",
"fields": {
"first_corner": {
"required": true
@@ -275,7 +275,7 @@
"required": true
}
},
"rust_constructor": "acadrust::entities::Insert::new(String::new(), acadrust::types::Vector3::ZERO)"
"rust_constructor": "codec::entities::Insert::new(String::new(), codec::types::Vector3::ZERO)"
},
"Tolerance": {
"fields": {
@@ -391,7 +391,7 @@
},
"flags": {
"rust_getter": "(VAR.flags.bits() as i64)",
"rust_setter": "if let Some(v) = dict.get_item_opt(\"flags\", vm)? { let bits = v.try_into_value::<i64>(vm)?; VAR.flags = i16::try_from(bits).ok().and_then(acadrust::entities::MLineFlags::from_bits).ok_or_else(|| vm.new_value_error(\"MLine.flags has unknown bits\".to_owned()))?; }"
"rust_setter": "if let Some(v) = dict.get_item_opt(\"flags\", vm)? { let bits = v.try_into_value::<i64>(vm)?; VAR.flags = i16::try_from(bits).ok().and_then(codec::entities::MLineFlags::from_bits).ok_or_else(|| vm.new_value_error(\"MLine.flags has unknown bits\".to_owned()))?; }"
},
"start_point": {
"read_only": true
@@ -559,7 +559,7 @@
}
},
"PolygonMesh": {
"rust_constructor": "acadrust::entities::PolygonMeshEntity::default()",
"rust_constructor": "codec::entities::PolygonMeshEntity::default()",
"fields": {
"vertices": {
"required": true
@@ -951,7 +951,7 @@
},
"raw_flags_90": {
"read_only": true,
"_comment": "Semantics unknown in cadcodec; preserved as read, 0 for a new symbol."
"_comment": "Semantics unknown in opencadcodec; preserved as read, 0 for a new symbol."
},
"raw_point_record_count": {
"read_only": true
@@ -3,7 +3,7 @@
use std::io::Write;
use ocs_plugin_api::host::acadrust::{
use ocs_plugin_api::host::codec::{
entities::{Line, Text, Viewport},
CadDocument, DwgWriter, EntityType, Vector3,
};
+1 -1
View File
@@ -10,7 +10,7 @@ repository = "HakanSeven12/OpenCADStudio"
[opencad]
api_version = 7
# Both filled by release.yml at build time from the actual compiler and
# Cargo.lock's resolved acadrust source. Declare acadrust_source even though
# Cargo.lock's resolved opencadcodec source. Declare acadrust_source even though
# it's optional: the host's compatibility check only runs at all if this
# field is present, so an omitted field is silently treated as compatible
# rather than rejected (see OpenCADStudio's plugin-architecture.md,
@@ -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)?;
@@ -55,12 +55,12 @@ def acadrust_source(path):
packages = re.findall(r"(?ms)^\[\[package\]\]\n(.*?)(?=^\[\[package\]\]|\Z)", Path(path).read_text())
sources = []
for package in packages:
if re.search(r'^name = "acadrust"$', package, re.M):
if re.search(r'^name = "opencadcodec"$', package, re.M):
match = re.search(r'^source = "([^"]+)"$', package, re.M)
if match:
sources.append(match.group(1))
if len(sources) != 1:
raise SystemExit(f"expected one acadrust source in {path}, found {len(sources)}")
raise SystemExit(f"expected one opencadcodec source in {path}, found {len(sources)}")
return sources[0]
manifest = Path("plugin.toml").read_text()