forked from 1b-agent/opencadstudio-upstream
feat(python): linetypes and layouts (doc.linetypes, doc.layouts)
TableOperation gains simple-linetype create/modify/rename/delete (standard names taken, rename follows layers and entities, guarded delete) and paper-space layout create/rename/delete/set-current/set-page (default page setup and sheet viewport, the application's own layout switch, Model protected). CTAB now reports the current layout. Real-IPC audit with DWG/DXF persistence and undo, IPC and Python unit tests, docs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1edc9f578
commit
795aa922b4
8 files changed
+960
No files matched your search
@@ -460,6 +460,126 @@ class _Blocks:
|
||||
ocs.block_operation("delete", str(name), None)
|
||||
|
||||
|
||||
class _Named:
|
||||
"""Shared lookup for the linetype and layout collections."""
|
||||
|
||||
_RECORDS = None
|
||||
|
||||
def _records(self):
|
||||
return getattr(ocs, self._RECORDS)()
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._records())
|
||||
|
||||
def __len__(self):
|
||||
return len(self._records())
|
||||
|
||||
def __contains__(self, name):
|
||||
wanted = str(name).strip().upper()
|
||||
return any(r["name"].upper() == wanted for r in self._records())
|
||||
|
||||
def __getitem__(self, name):
|
||||
wanted = str(name).strip().upper()
|
||||
for record in self._records():
|
||||
if record["name"].upper() == wanted:
|
||||
return record
|
||||
raise KeyError(name)
|
||||
|
||||
def get(self, name, default=None):
|
||||
try:
|
||||
return self[name]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def names(self):
|
||||
return [r["name"] for r in self._records()]
|
||||
|
||||
|
||||
class _Linetypes(_Named):
|
||||
"""`ocs.active_document.linetypes`. A pattern is signed lengths in drawing
|
||||
units: positive is a dash, negative a gap, zero a dot (2-12 elements with at
|
||||
least one gap and one dash or dot). Text and shape linetypes can be read
|
||||
but not created or edited. Every change is validated by the host, is one
|
||||
undo step, and a refusal raises `RuntimeError` without changing anything.
|
||||
`Continuous`, `ByLayer` and `ByBlock` are never changed; a linetype used by
|
||||
a layer, an entity, a dimension style or the current setting is never
|
||||
deleted."""
|
||||
|
||||
_RECORDS = "linetype_records"
|
||||
|
||||
def create(self, name, pattern, description=""):
|
||||
ocs.linetype_operation("create", str(name), {"pattern": [float(v) for v in pattern],
|
||||
"description": str(description)})
|
||||
return self[name]
|
||||
|
||||
def modify(self, name, pattern=None, description=None):
|
||||
options = {}
|
||||
if pattern is not None:
|
||||
options["pattern"] = [float(v) for v in pattern]
|
||||
if description is not None:
|
||||
options["description"] = str(description)
|
||||
ocs.linetype_operation("modify", str(name), options)
|
||||
return self[name]
|
||||
|
||||
def rename(self, name, new_name):
|
||||
ocs.linetype_operation("rename", str(name), {"to": str(new_name)})
|
||||
return self[new_name]
|
||||
|
||||
def delete(self, name):
|
||||
ocs.linetype_operation("delete", str(name), None)
|
||||
|
||||
|
||||
class _Layouts(_Named):
|
||||
"""`ocs.active_document.layouts`: `Model` and the paper-space layouts in tab
|
||||
order. Creation entities into a layout by making it current first
|
||||
(`layouts.current = "Sheet1"`), then `create_entity(...)`. Every change is
|
||||
validated by the host and is one undo step (switching layout is not); a
|
||||
refusal raises `RuntimeError`. Layout operations act on the active
|
||||
document."""
|
||||
|
||||
_RECORDS = "layout_records"
|
||||
|
||||
@property
|
||||
def current(self):
|
||||
for record in self._records():
|
||||
if record["current"]:
|
||||
return record
|
||||
return None
|
||||
|
||||
@current.setter
|
||||
def current(self, name):
|
||||
self.set_current(name)
|
||||
|
||||
def create(self, name):
|
||||
ocs.layout_operation("create", str(name), None)
|
||||
return self[name]
|
||||
|
||||
def rename(self, name, new_name):
|
||||
ocs.layout_operation("rename", str(name), {"to": str(new_name)})
|
||||
return self[new_name]
|
||||
|
||||
def delete(self, name):
|
||||
"""Delete a paper-space layout and everything on it (never `Model`)."""
|
||||
ocs.layout_operation("delete", str(name), None)
|
||||
|
||||
def set_current(self, name):
|
||||
ocs.layout_operation("set_current", str(name), None)
|
||||
return self[name]
|
||||
|
||||
def set_page(self, name, paper_size=None, rotation=None, scale=None):
|
||||
"""`paper_size` is (width, height) in mm, `rotation` 0/90/180/270 degrees
|
||||
and `scale` a custom plot scale (numerator, denominator)."""
|
||||
options = {}
|
||||
if paper_size is not None:
|
||||
options["paper_size"] = [float(v) for v in paper_size]
|
||||
if rotation is not None:
|
||||
options["rotation"] = int(rotation)
|
||||
if scale is not None:
|
||||
options["scale"] = [float(v) for v in scale]
|
||||
ocs.layout_operation("set_page", str(name), options)
|
||||
return self[name]
|
||||
|
||||
|
||||
class _Document:
|
||||
def __init__(self):
|
||||
self._pending = None
|
||||
@@ -467,6 +587,8 @@ class _Document:
|
||||
self.text_styles = _TextStyles()
|
||||
self.dim_styles = _DimStyles()
|
||||
self.blocks = _Blocks()
|
||||
self.linetypes = _Linetypes()
|
||||
self.layouts = _Layouts()
|
||||
self.entities = _Entities(self)
|
||||
self.solids = _Solids(self)
|
||||
|
||||
|
||||
@@ -1398,6 +1398,214 @@ mod ocs {
|
||||
Ok(vm.ctx.new_list(output).into())
|
||||
}
|
||||
|
||||
/// Linetype change through the host. `op` is `create` or `modify`
|
||||
/// (`description`, `pattern`: signed lengths, positive dash, negative gap,
|
||||
/// zero dot), `rename` (`to`) or `delete`. Returns the linetype's handle.
|
||||
#[cfg(feature = "experimental-host-model")]
|
||||
#[pyfunction]
|
||||
fn linetype_operation(op: String, name: String, options: PyObjectRef, vm: &VirtualMachine) -> PyResult<u64> {
|
||||
use ocs_plugin_api::host::TableOperation;
|
||||
let options = if vm.is_none(&options) {
|
||||
vm.ctx.new_dict()
|
||||
} else {
|
||||
options.try_into_value::<rustpython_vm::builtins::PyDictRef>(vm)?
|
||||
};
|
||||
let present = |key: &str| -> PyResult<Option<PyObjectRef>> {
|
||||
Ok(options.get_item_opt(key, vm)?.filter(|v| !vm.is_none(v)))
|
||||
};
|
||||
let pattern = |value: PyObjectRef| -> PyResult<Vec<f64>> {
|
||||
let items: Vec<PyObjectRef> = value.try_into_value(vm)?;
|
||||
items.into_iter().map(|item| py_number_to_f64(item, vm)).collect()
|
||||
};
|
||||
let operation = match op.as_str() {
|
||||
"create" => {
|
||||
ensure_known_entity_keys(&options, "linetype", &["description", "pattern"], vm)?;
|
||||
TableOperation::LinetypeCreate {
|
||||
name,
|
||||
description: get_opt_string(&options, "description", "", vm)?,
|
||||
pattern: pattern(present("pattern")?.ok_or_else(|| vm.new_value_error("ocs: a linetype needs a pattern".to_owned()))?)?,
|
||||
}
|
||||
}
|
||||
"modify" => {
|
||||
ensure_known_entity_keys(&options, "linetype", &["description", "pattern"], vm)?;
|
||||
TableOperation::LinetypeModify {
|
||||
name,
|
||||
description: present("description")?.map(|v| v.try_into_value::<String>(vm)).transpose()?,
|
||||
pattern: present("pattern")?.map(pattern).transpose()?,
|
||||
}
|
||||
}
|
||||
"rename" => {
|
||||
ensure_known_entity_keys(&options, "linetype rename", &["to"], vm)?;
|
||||
let to = options
|
||||
.get_item_opt("to", vm)?
|
||||
.ok_or_else(|| vm.new_value_error("ocs: rename needs the new name".to_owned()))?
|
||||
.try_into_value::<String>(vm)?;
|
||||
TableOperation::LinetypeRename { from: name, to }
|
||||
}
|
||||
"delete" => {
|
||||
ensure_known_entity_keys(&options, "linetype delete", &[], vm)?;
|
||||
TableOperation::LinetypeDelete { name }
|
||||
}
|
||||
other => return Err(vm.new_value_error(format!("ocs.linetype_operation: unknown operation {other:?}"))),
|
||||
};
|
||||
let result = host_ctx::with_host(|host| host.table_operation(operation))
|
||||
.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.linetype_operation: {error}")))
|
||||
}
|
||||
|
||||
/// Every linetype: pattern as signed lengths plus how many layers and
|
||||
/// entities use it.
|
||||
#[cfg(feature = "experimental-host-model")]
|
||||
#[pyfunction]
|
||||
fn linetype_records(vm: &VirtualMachine) -> PyResult<PyObjectRef> {
|
||||
let rows = host_ctx::with_host(|host| {
|
||||
let document = host.document();
|
||||
let mut uses = std::collections::HashMap::<String, usize>::new();
|
||||
for layer in document.layers.iter() {
|
||||
*uses.entry(layer.line_type.to_uppercase()).or_default() += 1;
|
||||
}
|
||||
for entity in document.entities() {
|
||||
*uses.entry(entity.common().linetype.to_uppercase()).or_default() += 1;
|
||||
}
|
||||
document
|
||||
.line_types
|
||||
.iter()
|
||||
.map(|lt| (lt.clone(), uses.get(<.name.to_uppercase()).copied().unwrap_or(0)))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.ok_or_else(|| vm.new_runtime_error("ocs: not running inside a PY_ command".to_owned()))?;
|
||||
let mut output = Vec::with_capacity(rows.len());
|
||||
for (lt, used) in rows {
|
||||
let dict = vm.ctx.new_dict();
|
||||
dict.set_item("handle", vm.new_pyobj(lt.handle.value()), vm)?;
|
||||
dict.set_item("name", vm.new_pyobj(lt.name.clone()), vm)?;
|
||||
dict.set_item("description", vm.new_pyobj(lt.description.clone()), vm)?;
|
||||
let pattern: Vec<PyObjectRef> = lt.elements.iter().map(|e| vm.new_pyobj(e.length)).collect();
|
||||
dict.set_item("pattern", PyObjectRef::from(vm.ctx.new_list(pattern)), vm)?;
|
||||
dict.set_item("pattern_length", vm.new_pyobj(lt.pattern_length), vm)?;
|
||||
dict.set_item("complex", vm.new_pyobj(lt.elements.iter().any(|e| e.complex.is_some())), vm)?;
|
||||
dict.set_item("used_by", vm.new_pyobj(used), vm)?;
|
||||
output.push(dict.into());
|
||||
}
|
||||
Ok(vm.ctx.new_list(output).into())
|
||||
}
|
||||
|
||||
/// Layout change through the host. `op` is `create`, `rename` (`to`),
|
||||
/// `delete`, `set_current` or `set_page` (`paper_size`: (width, height) mm,
|
||||
/// `rotation`: 0/90/180/270, `scale`: (numerator, denominator)).
|
||||
#[cfg(feature = "experimental-host-model")]
|
||||
#[pyfunction]
|
||||
fn layout_operation(op: String, name: String, options: PyObjectRef, vm: &VirtualMachine) -> PyResult<u64> {
|
||||
use ocs_plugin_api::host::TableOperation;
|
||||
let options = if vm.is_none(&options) {
|
||||
vm.ctx.new_dict()
|
||||
} else {
|
||||
options.try_into_value::<rustpython_vm::builtins::PyDictRef>(vm)?
|
||||
};
|
||||
let present = |key: &str| -> PyResult<Option<PyObjectRef>> {
|
||||
Ok(options.get_item_opt(key, vm)?.filter(|v| !vm.is_none(v)))
|
||||
};
|
||||
let operation = match op.as_str() {
|
||||
"create" => TableOperation::LayoutCreate { name },
|
||||
"delete" => TableOperation::LayoutDelete { name },
|
||||
"set_current" => TableOperation::LayoutSetCurrent { name },
|
||||
"rename" => {
|
||||
ensure_known_entity_keys(&options, "layout rename", &["to"], vm)?;
|
||||
let to = options
|
||||
.get_item_opt("to", vm)?
|
||||
.ok_or_else(|| vm.new_value_error("ocs: rename needs the new name".to_owned()))?
|
||||
.try_into_value::<String>(vm)?;
|
||||
TableOperation::LayoutRename { from: name, to }
|
||||
}
|
||||
"set_page" => {
|
||||
ensure_known_entity_keys(&options, "layout page", &["paper_size", "rotation", "scale"], vm)?;
|
||||
TableOperation::LayoutSetPage {
|
||||
name,
|
||||
paper_size: present("paper_size")?.map(|v| py_to_f64_array::<2>(v, vm)).transpose()?,
|
||||
rotation: present("rotation")?
|
||||
.map(|v| -> PyResult<u16> {
|
||||
let degrees = v.try_into_value::<i64>(vm)?;
|
||||
u16::try_from(degrees).map_err(|_| vm.new_value_error("ocs: the rotation must be 0, 90, 180 or 270".to_owned()))
|
||||
})
|
||||
.transpose()?,
|
||||
scale: present("scale")?.map(|v| py_to_f64_array::<2>(v, vm)).transpose()?,
|
||||
}
|
||||
}
|
||||
other => return Err(vm.new_value_error(format!("ocs.layout_operation: unknown operation {other:?}"))),
|
||||
};
|
||||
let result = host_ctx::with_host(|host| host.table_operation(operation))
|
||||
.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.layout_operation: {error}")))
|
||||
}
|
||||
|
||||
/// `Model` and every paper-space layout in tab order with its page setup.
|
||||
#[cfg(feature = "experimental-host-model")]
|
||||
#[pyfunction]
|
||||
fn layout_records(vm: &VirtualMachine) -> PyResult<PyObjectRef> {
|
||||
use acadrust::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,
|
||||
_ => "Model".to_owned(),
|
||||
};
|
||||
let document = host.document();
|
||||
let mut owned = std::collections::HashMap::<u64, usize>::new();
|
||||
let mut viewports = std::collections::HashMap::<u64, usize>::new();
|
||||
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(_)) {
|
||||
*viewports.entry(owner).or_default() += 1;
|
||||
}
|
||||
}
|
||||
let mut layouts: Vec<_> = document
|
||||
.objects
|
||||
.values()
|
||||
.filter_map(|object| match object {
|
||||
ObjectType::Layout(layout) if !layout.block_record.is_null() => Some(layout.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
layouts.sort_by_key(|layout| (layout.name != "Model", layout.tab_order));
|
||||
layouts
|
||||
.into_iter()
|
||||
.map(|layout| {
|
||||
let count = owned.get(&layout.block_record.value()).copied().unwrap_or(0);
|
||||
let vps = viewports.get(&layout.block_record.value()).copied().unwrap_or(0);
|
||||
let is_current = layout.name == current;
|
||||
(layout, count, vps, is_current)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.ok_or_else(|| vm.new_runtime_error("ocs: not running inside a PY_ command".to_owned()))?;
|
||||
let mut output = Vec::with_capacity(rows.len());
|
||||
for (layout, count, vps, is_current) in rows {
|
||||
let dict = vm.ctx.new_dict();
|
||||
dict.set_item("handle", vm.new_pyobj(layout.handle.value()), vm)?;
|
||||
dict.set_item("name", vm.new_pyobj(layout.name.clone()), vm)?;
|
||||
dict.set_item("current", vm.new_pyobj(is_current), vm)?;
|
||||
dict.set_item("tab_order", vm.new_pyobj(i64::from(layout.tab_order)), vm)?;
|
||||
dict.set_item("entity_count", vm.new_pyobj(count), vm)?;
|
||||
dict.set_item("viewport_count", vm.new_pyobj(vps), vm)?;
|
||||
dict.set_item("paper_size", PyObjectRef::from(vm.ctx.new_list(vec![vm.new_pyobj(layout.paper_width), vm.new_pyobj(layout.paper_height)])), vm)?;
|
||||
dict.set_item("paper_name", vm.new_pyobj(layout.paper_size.clone()), vm)?;
|
||||
let rotation = match layout.plot_rotation {
|
||||
1 => 90,
|
||||
2 => 180,
|
||||
3 => 270,
|
||||
_ => 0,
|
||||
};
|
||||
dict.set_item("rotation", vm.new_pyobj(rotation), vm)?;
|
||||
dict.set_item("scale", PyObjectRef::from(vm.ctx.new_list(vec![vm.new_pyobj(layout.plot_scale_numerator), vm.new_pyobj(layout.plot_scale_denominator)])), vm)?;
|
||||
output.push(dict.into());
|
||||
}
|
||||
Ok(vm.ctx.new_list(output).into())
|
||||
}
|
||||
|
||||
/// Every layer with its full properties, in table order.
|
||||
#[cfg(feature = "experimental-host-model")]
|
||||
#[pyfunction]
|
||||
|
||||
Reference in new issue
Block a user