forked from 1b-agent/opencadstudio-upstream
Add kernel solid operations and complete Solid3D (create and rigid move)
An additive HostApi::solid_operation builds cadkernel primitives and applies rigid transforms, verifying each result lifts back losslessly and refusing a payload it cannot lift. Exposed as doc.solids in Python; Solid3D, Body and Region are mapped read-only with the opaque payload preserved. The lifecycle test passes through the real runner with analytic volumes and exact extents as oracles, DWG and DXF reopen with re-edit, and undo restores exactly. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
07fe739571
commit
04cd9e462d
18 files changed
+717
-13
No files matched your search
@@ -94,7 +94,7 @@ The host-owned catalog classifies all 48 entity variants and tracks every
|
||||
traced field, including fields marked `unmapped`. Query it with
|
||||
`doc.coverage()` or `doc.coverage("Line")`; each row includes source field,
|
||||
type, access, and current validation status. Geometry edits remain limited to
|
||||
the 38 mapped kinds.
|
||||
the 41 mapped kinds.
|
||||
For any entity, `entity.snapshot` returns a detached dictionary of its raw
|
||||
CAD fields, including `snapshot["common"]` and kind-specific nested values.
|
||||
It can inspect an unmapped kind; editing the dictionary does
|
||||
|
||||
@@ -37,7 +37,10 @@
|
||||
"Viewport",
|
||||
"ViewBorder",
|
||||
"Light",
|
||||
"Ole2Frame"
|
||||
"Ole2Frame",
|
||||
"Solid3D",
|
||||
"Body",
|
||||
"Region"
|
||||
],
|
||||
"manual_kinds": [
|
||||
"Dimension"
|
||||
@@ -807,6 +810,87 @@
|
||||
"read_only": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Solid3D": {
|
||||
"update_only": true,
|
||||
"update_only_message": "Solid3D is built and edited by the geometry kernel: use doc.solids to create solids and move them; a script never writes the ACIS payload",
|
||||
"fields": {
|
||||
"acis_data": {
|
||||
"exclude": true,
|
||||
"_comment": "Opaque ACIS payload; snapshot-readable and never rewritten by a script."
|
||||
},
|
||||
"wires": {
|
||||
"exclude": true,
|
||||
"_comment": "Derived edge wires; regenerated by the host."
|
||||
},
|
||||
"silhouettes": {
|
||||
"exclude": true,
|
||||
"_comment": "Derived; regenerated by the host."
|
||||
},
|
||||
"uid": {
|
||||
"read_only": true
|
||||
},
|
||||
"point_of_reference": {
|
||||
"read_only": true
|
||||
},
|
||||
"history_handle": {
|
||||
"read_only": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Body": {
|
||||
"update_only": true,
|
||||
"update_only_message": "Body is built and edited by the geometry kernel: use doc.solids to create solids and move them; a script never writes the ACIS payload",
|
||||
"fields": {
|
||||
"acis_data": {
|
||||
"exclude": true,
|
||||
"_comment": "Opaque ACIS payload; snapshot-readable and never rewritten by a script."
|
||||
},
|
||||
"wires": {
|
||||
"exclude": true,
|
||||
"_comment": "Derived edge wires; regenerated by the host."
|
||||
},
|
||||
"silhouettes": {
|
||||
"exclude": true,
|
||||
"_comment": "Derived; regenerated by the host."
|
||||
},
|
||||
"uid": {
|
||||
"read_only": true
|
||||
},
|
||||
"point_of_reference": {
|
||||
"read_only": true
|
||||
},
|
||||
"history_handle": {
|
||||
"read_only": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Region": {
|
||||
"update_only": true,
|
||||
"update_only_message": "Region is built and edited by the geometry kernel: use doc.solids to create solids and move them; a script never writes the ACIS payload",
|
||||
"fields": {
|
||||
"acis_data": {
|
||||
"exclude": true,
|
||||
"_comment": "Opaque ACIS payload; snapshot-readable and never rewritten by a script."
|
||||
},
|
||||
"wires": {
|
||||
"exclude": true,
|
||||
"_comment": "Derived edge wires; regenerated by the host."
|
||||
},
|
||||
"silhouettes": {
|
||||
"exclude": true,
|
||||
"_comment": "Derived; regenerated by the host."
|
||||
},
|
||||
"uid": {
|
||||
"read_only": true
|
||||
},
|
||||
"point_of_reference": {
|
||||
"read_only": true
|
||||
},
|
||||
"history_handle": {
|
||||
"read_only": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,10 +84,65 @@ class _Transaction:
|
||||
return False
|
||||
|
||||
|
||||
class _Solids:
|
||||
"""Kernel-backed solid creation and rigid moves.
|
||||
|
||||
The host builds and edits the geometry; a script never sees the ACIS
|
||||
payload. Every call is its own operation in the script's undo group and
|
||||
fails, leaving the drawing unchanged, when the kernel cannot do it.
|
||||
"""
|
||||
|
||||
def __init__(self, document):
|
||||
self._document = document
|
||||
|
||||
def _create(self, primitive, values, layer):
|
||||
handle = ocs.solid_create(primitive, [float(v) for v in values], layer)
|
||||
return self._document.entities[handle]
|
||||
|
||||
def box(self, center=(0, 0, 0), size=(1, 1, 1), layer=None):
|
||||
return self._create("box", tuple(center) + tuple(size), layer)
|
||||
|
||||
def wedge(self, origin=(0, 0, 0), size=(1, 1, 1), layer=None):
|
||||
return self._create("wedge", tuple(origin) + tuple(size), layer)
|
||||
|
||||
def cylinder(self, center=(0, 0, 0), radius=1, height=1, layer=None):
|
||||
return self._create("cylinder", tuple(center) + (radius, height), layer)
|
||||
|
||||
def sphere(self, center=(0, 0, 0), radius=1, layer=None):
|
||||
return self._create("sphere", tuple(center) + (radius,), layer)
|
||||
|
||||
def torus(self, center=(0, 0, 0), major=2, minor=0.5, layer=None):
|
||||
return self._create("torus", tuple(center) + (major, minor), layer)
|
||||
|
||||
def pyramid(self, center=(0, 0, 0), radius=1, height=1, sides=4, layer=None):
|
||||
return self._create("pyramid", tuple(center) + (radius, height, sides), layer)
|
||||
|
||||
def transform(self, entity, matrix):
|
||||
"""Apply a column-major 4x4 rigid transform (16 numbers) in place."""
|
||||
handle = entity.handle if isinstance(entity, _Entity) else int(entity)
|
||||
ocs.solid_transform(handle, [float(v) for v in matrix])
|
||||
return self._document.entities[handle]
|
||||
|
||||
def translate(self, entity, offset):
|
||||
dx, dy, dz = (float(v) for v in offset)
|
||||
return self.transform(entity, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, dx, dy, dz, 1])
|
||||
|
||||
def rotate(self, entity, axis, angle, about=(0, 0, 0)):
|
||||
"""Turn about the world axis 'x', 'y' or 'z' through `about`."""
|
||||
index = {"x": 0, "y": 1, "z": 2}[axis]
|
||||
return self.transform(entity, ocs.rotation_matrix(index, float(angle), [float(v) for v in about]))
|
||||
|
||||
def mirror(self, entity, axis, about=(0, 0, 0)):
|
||||
"""Reflect across the plane normal to the world axis through `about`."""
|
||||
index = {"x": 0, "y": 1, "z": 2}[axis]
|
||||
return self.transform(entity, ocs.mirror_matrix(index, [float(v) for v in about]))
|
||||
|
||||
|
||||
class _Document:
|
||||
def __init__(self):
|
||||
self._pending = None
|
||||
self.entities = _Entities(self)
|
||||
self.solids = _Solids(self)
|
||||
|
||||
def transaction(self, label):
|
||||
return _Transaction(self, label)
|
||||
|
||||
@@ -411,6 +411,114 @@ mod ocs {
|
||||
.ok_or_else(|| vm.new_runtime_error("ocs: not running inside a PY_ command".to_owned()))
|
||||
}
|
||||
|
||||
/// Build a kernel solid. `values` are the primitive's numbers in a fixed
|
||||
/// order (see `document_model.py`); the host validates and builds it.
|
||||
#[pyfunction]
|
||||
fn solid_create(
|
||||
primitive: String,
|
||||
values: Vec<f64>,
|
||||
layer: Option<String>,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<u64> {
|
||||
use ocs_plugin_api::host::{SolidOperation, SolidPrimitive};
|
||||
let expect = |count: usize| -> PyResult<()> {
|
||||
if values.len() == count {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(vm.new_value_error(format!(
|
||||
"ocs.solid_create: {primitive} takes {count} numbers, got {}",
|
||||
values.len()
|
||||
)))
|
||||
}
|
||||
};
|
||||
let v3 = |i: usize| [values[i], values[i + 1], values[i + 2]];
|
||||
let primitive = match primitive.as_str() {
|
||||
"box" => { expect(6)?; SolidPrimitive::Box { center: v3(0), size: v3(3) } }
|
||||
"wedge" => { expect(6)?; SolidPrimitive::Wedge { origin: v3(0), size: v3(3) } }
|
||||
"cylinder" => { expect(5)?; SolidPrimitive::Cylinder { center: v3(0), radius: values[3], height: values[4] } }
|
||||
"sphere" => { expect(4)?; SolidPrimitive::Sphere { center: v3(0), radius: values[3] } }
|
||||
"torus" => { expect(5)?; SolidPrimitive::Torus { center: v3(0), major: values[3], minor: values[4] } }
|
||||
"pyramid" => {
|
||||
expect(6)?;
|
||||
if values[5] < 0.0 || values[5].fract() != 0.0 || values[5] > f64::from(u32::MAX) {
|
||||
return Err(vm.new_value_error("ocs.solid_create: sides must be a whole number".to_owned()));
|
||||
}
|
||||
SolidPrimitive::Pyramid { center: v3(0), radius: values[3], height: values[4], sides: values[5] as u32 }
|
||||
}
|
||||
other => return Err(vm.new_value_error(format!("ocs.solid_create: unknown primitive {other:?}"))),
|
||||
};
|
||||
// The host records its own undo step once the operation validates, like
|
||||
// an entity transaction, so a refused operation leaves no empty step.
|
||||
let result = host_ctx::with_host(|host| {
|
||||
host.solid_operation(SolidOperation::Create { primitive, layer })
|
||||
})
|
||||
.ok_or_else(|| vm.new_runtime_error("ocs: not running inside a PY_ command".to_owned()))?;
|
||||
result
|
||||
.map(|handle| handle.value())
|
||||
.map_err(|error| vm.new_runtime_error(format!("ocs.solid_create: {error}")))
|
||||
}
|
||||
|
||||
/// Apply a column-major 4x4 rigid transform to a solid, body or region.
|
||||
#[pyfunction]
|
||||
fn solid_transform(handle: u64, matrix: Vec<f64>, vm: &VirtualMachine) -> PyResult<u64> {
|
||||
use ocs_plugin_api::host::SolidOperation;
|
||||
let matrix: [f64; 16] = matrix.try_into().map_err(|_| {
|
||||
vm.new_value_error("ocs.solid_transform: the matrix needs 16 numbers".to_owned())
|
||||
})?;
|
||||
let result = host_ctx::with_host(|host| {
|
||||
host.solid_operation(SolidOperation::Transform { handle: Handle::new(handle), matrix })
|
||||
})
|
||||
.ok_or_else(|| vm.new_runtime_error("ocs: not running inside a PY_ command".to_owned()))?;
|
||||
result
|
||||
.map(|handle| handle.value())
|
||||
.map_err(|error| vm.new_runtime_error(format!("ocs.solid_transform: {error}")))
|
||||
}
|
||||
|
||||
/// Column-major matrix that turns about a world axis (0=X, 1=Y, 2=Z)
|
||||
/// through the point `about`. The sandbox has no `math` module.
|
||||
#[pyfunction]
|
||||
fn rotation_matrix(axis: usize, angle: f64, about: Vec<f64>, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
|
||||
if axis > 2 || about.len() != 3 || !angle.is_finite() || about.iter().any(|v| !v.is_finite()) {
|
||||
return Err(vm.new_value_error("ocs.rotation_matrix: axis 0-2, a finite angle and a 3-point".to_owned()));
|
||||
}
|
||||
let (sin, cos) = angle.sin_cos();
|
||||
let (x, y, z) = match axis {
|
||||
0 => ([1.0, 0.0, 0.0], [0.0, cos, sin], [0.0, -sin, cos]),
|
||||
1 => ([cos, 0.0, -sin], [0.0, 1.0, 0.0], [sin, 0.0, cos]),
|
||||
_ => ([cos, sin, 0.0], [-sin, cos, 0.0], [0.0, 0.0, 1.0]),
|
||||
};
|
||||
Ok(matrix_to_py(vm, frame_matrix(x, y, z, &about)))
|
||||
}
|
||||
|
||||
/// Column-major matrix that reflects across the plane normal to a world
|
||||
/// axis through the point `about`.
|
||||
#[pyfunction]
|
||||
fn mirror_matrix(axis: usize, about: Vec<f64>, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
|
||||
if axis > 2 || about.len() != 3 || about.iter().any(|v| !v.is_finite()) {
|
||||
return Err(vm.new_value_error("ocs.mirror_matrix: axis 0-2 and a finite 3-point".to_owned()));
|
||||
}
|
||||
let mut columns = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
|
||||
columns[axis][axis] = -1.0;
|
||||
let [x, y, z] = columns;
|
||||
Ok(matrix_to_py(vm, frame_matrix(x, y, z, &about)))
|
||||
}
|
||||
|
||||
fn matrix_to_py(vm: &VirtualMachine, matrix: Vec<f64>) -> PyObjectRef {
|
||||
vm.ctx.new_list(matrix.into_iter().map(|value| vm.new_pyobj(value)).collect()).into()
|
||||
}
|
||||
|
||||
/// A transform about `about` has origin `about - M * about`.
|
||||
fn frame_matrix(x: [f64; 3], y: [f64; 3], z: [f64; 3], about: &[f64]) -> Vec<f64> {
|
||||
let mut origin = [about[0], about[1], about[2]];
|
||||
for axis in 0..3 {
|
||||
origin[axis] -= x[axis] * about[0] + y[axis] * about[1] + z[axis] * about[2];
|
||||
}
|
||||
vec![
|
||||
x[0], x[1], x[2], 0.0, y[0], y[1], y[2], 0.0, z[0], z[1], z[2], 0.0,
|
||||
origin[0], origin[1], origin[2], 1.0,
|
||||
]
|
||||
}
|
||||
|
||||
/// Delete one entity in the script's shared undo group.
|
||||
#[pyfunction]
|
||||
fn remove_entity(handle: u64, vm: &VirtualMachine) -> PyResult<()> {
|
||||
@@ -4780,7 +4888,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(emitted.len(), 38);
|
||||
assert_eq!(emitted.len(), 41);
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-host-model")]
|
||||
|
||||
Reference in new issue
Block a user