forked from 1b-agent/opencadstudio-upstream
Merge pull request #1423 from felixriestra:pr/python-host-model
The bundled `opencad.python` plugin drives OCS through plugin API v7: all 43 canvas entity kinds, the drawing tables, and OCS's own commands run step by step. RustPython stays in the plugin process — the plugin crate is outside the workspace with its own lock file, so the application's own dependencies are unchanged. Reconciled with the plugin command work that landed as #1425, which the branch was written before: - Both added `impl Drop for PluginProcessInteractiveAdapter`, which git merges silently into two impls that do not compile. One body now cancels the command and then drops it from the runner, in that order: a plugin waiting on a step has to be released before the runner forgets the command. - `execute_command` (#1425) and `run_command` (#1423) are both routes from a script to the command line, and only the second was guarded. The SCRIPTCOMMANDS switch and the denylist are shared now, so neither route is the unguarded one. - `run_command` dispatched with plugin dispatch still enabled, so a script naming a plugin command re-entered the plugin that was blocked waiting for it. It sets the suppression flag for the length of the dispatch, as `execute_command` does. The denylist read the command as typed, while the dispatcher resolves aliases: a user-defined `Q` for `QUIT` (ALIASEDIT writes those) walked past it. It resolves the alias first. Also: the active-command comparison ran on every message, a mouse move included, allocating two strings for names that are already `&'static str`; SCRIPTCOMMANDS' own messages go through the translation macros; and `docs/.DS_Store` (now ignored) and an unrelated RTF note about constraint commands are out of the tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
41ecc0c458
72 files changed
+30614
-91
No files matched your search
@@ -60,6 +60,243 @@ pub enum LogLevel {
|
||||
Error,
|
||||
}
|
||||
|
||||
/// A solid primitive the host can build with its geometry kernel. Lengths are
|
||||
/// in drawing units; `center`/`origin` are world coordinates.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SolidPrimitive {
|
||||
/// Axis-aligned box from its center and full extents.
|
||||
Box { center: [f64; 3], size: [f64; 3] },
|
||||
/// Right triangular prism from its minimum corner and extents.
|
||||
Wedge { origin: [f64; 3], size: [f64; 3] },
|
||||
/// Cylinder standing on the plane through `center`.
|
||||
Cylinder { center: [f64; 3], radius: f64, height: f64 },
|
||||
Sphere { center: [f64; 3], radius: f64 },
|
||||
Torus { center: [f64; 3], major: f64, minor: f64 },
|
||||
/// Regular pyramid with `sides` base edges.
|
||||
Pyramid { center: [f64; 3], radius: f64, height: f64, sides: u32 },
|
||||
}
|
||||
|
||||
/// How two solids combine.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SolidBoolean {
|
||||
Union,
|
||||
/// Remove the second solid from the first.
|
||||
Subtract,
|
||||
Intersect,
|
||||
}
|
||||
|
||||
/// One step of driving an OCS command from a script (API v7, additive). The
|
||||
/// host runs the real command, so every tool behaves exactly as at the command
|
||||
/// line, and answers with where the command stands.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum CommandRequest {
|
||||
/// Run a whole command line (`OFFSET 5 ...`): the tokens after the name
|
||||
/// answer the prompts in order and a final Enter finishes the command.
|
||||
Run { line: String },
|
||||
/// Start a command and leave it waiting for its first input.
|
||||
Start { name: String },
|
||||
/// Answer the current prompt with a world-coordinate point.
|
||||
Point { point: [f64; 3] },
|
||||
/// Answer with typed text (a distance, an angle, a name).
|
||||
Text { text: String },
|
||||
/// Answer with a keyword option.
|
||||
Token { text: String },
|
||||
/// Pick an entity at a point.
|
||||
Entity { handle: Handle, point: [f64; 3] },
|
||||
/// Complete an object-selection prompt with the current selection.
|
||||
Selection,
|
||||
/// Press Enter.
|
||||
Enter,
|
||||
/// Cancel the running command and close anything it left open.
|
||||
Cancel,
|
||||
}
|
||||
|
||||
/// Where a command stands after a [`CommandRequest`].
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
pub struct CommandOutcome {
|
||||
/// `completed` when nothing is waiting, `waiting_input` when the command
|
||||
/// (or an editor or dialog it opened) needs more.
|
||||
pub status: String,
|
||||
/// What is still open: `command`, `text_editor`, `mtext_editor` or `modal:<kind>`.
|
||||
pub blocked_by: Option<String>,
|
||||
/// The waiting command's name and prompt (empty when none).
|
||||
pub command: String,
|
||||
pub prompt: String,
|
||||
/// The kinds of input the prompt accepts (`point`, `entity`, `selection`,
|
||||
/// `text`, `token`, `enter`) and its keyword options.
|
||||
pub accepts: Vec<String>,
|
||||
pub options: Vec<String>,
|
||||
/// Entities in the drawing after the step, and the change.
|
||||
pub entities: u64,
|
||||
pub added: i64,
|
||||
/// Typed tokens no prompt asked for.
|
||||
pub unconsumed: Vec<String>,
|
||||
/// The command line's error message when the step failed.
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// A change to a drawing table record (API v7, additive). The host validates
|
||||
/// the request, records one undo step and refuses without changing anything
|
||||
/// when it cannot honour it; the result is the record's handle.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum TableOperation {
|
||||
/// Create a layer. `config.name` must be unused and valid.
|
||||
LayerCreate { config: LayerConfig },
|
||||
/// Change only the properties `config` sets on an existing layer.
|
||||
LayerModify { config: LayerConfig },
|
||||
/// Rename a layer; entities on it follow. Layer `0` and `Defpoints` stay.
|
||||
LayerRename { from: String, to: String },
|
||||
/// Delete a layer. A layer that still holds objects is refused unless
|
||||
/// `erase_objects` is set, which erases them with the layer. Layer `0`,
|
||||
/// `Defpoints` and the current layer are never deleted.
|
||||
LayerDelete { name: String, erase_objects: bool },
|
||||
/// Make an existing layer current.
|
||||
LayerSetCurrent { name: String },
|
||||
/// Create a text style; unset properties take the AutoCAD defaults.
|
||||
TextStyleCreate { config: TextStyleConfig },
|
||||
/// Change only the properties `config` sets on an existing text style.
|
||||
TextStyleModify { config: TextStyleConfig },
|
||||
/// Create a dimension style, optionally copying an existing one first.
|
||||
/// `properties` is a JSON object of DimStyle fields (see the Python docs);
|
||||
/// handles and xref fields are managed by the host and refused.
|
||||
DimStyleCreate { name: String, copy_from: Option<String>, properties: String },
|
||||
/// Change only the DimStyle fields the JSON object `properties` names.
|
||||
DimStyleModify { name: String, properties: String },
|
||||
/// Rename a text or dimension style; references to it follow. `Standard`
|
||||
/// is never renamed and a case-only change is refused.
|
||||
StyleRename { kind: TableStyleKind, from: String, to: String },
|
||||
/// Delete a text or dimension style that is neither current nor in use.
|
||||
/// `Standard` is never deleted.
|
||||
StyleDelete { kind: TableStyleKind, name: String },
|
||||
/// Make a text or dimension style current.
|
||||
StyleSetCurrent { kind: TableStyleKind, name: String },
|
||||
/// Define a block from existing drawing entities. The entities are copied
|
||||
/// into the definition shifted by `-base_point` (the block's origin);
|
||||
/// `erase_originals` then removes them from the drawing. No insert is
|
||||
/// placed. Returns the block record's handle.
|
||||
BlockCreate {
|
||||
name: String,
|
||||
entities: Vec<Handle>,
|
||||
base_point: [f64; 3],
|
||||
erase_originals: bool,
|
||||
description: Option<String>,
|
||||
},
|
||||
/// Change a block's description, explodable or uniform-scale settings.
|
||||
BlockModify {
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
explodable: Option<bool>,
|
||||
scale_uniformly: Option<bool>,
|
||||
},
|
||||
/// Rename a block; every insert of it follows.
|
||||
BlockRename { from: String, to: String },
|
||||
/// Delete an unreferenced block definition and its contents.
|
||||
BlockDelete { name: String },
|
||||
/// Add a new entity to a block definition, validated exactly as a new
|
||||
/// model-space entity is. The entity's owner is set by the host. Returns the
|
||||
/// new entity's handle. Edit and delete members with the ordinary entity
|
||||
/// operations.
|
||||
BlockEntityAdd { block: String, entity: EntityType },
|
||||
/// Create a simple linetype. `pattern` is signed lengths in drawing units:
|
||||
/// positive is a dash, negative a gap, zero a dot (2-12 elements with at
|
||||
/// least one dash or dot and one gap). Returns the linetype's handle.
|
||||
LinetypeCreate { name: String, description: String, pattern: Vec<f64> },
|
||||
/// Change a simple linetype's description or pattern in place.
|
||||
LinetypeModify { name: String, description: Option<String>, pattern: Option<Vec<f64>> },
|
||||
/// Rename a linetype; layers and entities that use it follow.
|
||||
LinetypeRename { from: String, to: String },
|
||||
/// Delete a linetype that no layer, entity, dimension style or the current
|
||||
/// setting uses. `Continuous`, `ByLayer` and `ByBlock` are never deleted.
|
||||
LinetypeDelete { name: String },
|
||||
/// Create a paper-space layout with the default page setup and sheet viewport.
|
||||
LayoutCreate { name: String },
|
||||
/// Rename a paper-space layout (never `Model`).
|
||||
LayoutRename { from: String, to: String },
|
||||
/// Delete a paper-space layout and everything on it (never `Model`).
|
||||
LayoutDelete { name: String },
|
||||
/// Switch the active layout (`Model` or a paper-space layout).
|
||||
LayoutSetCurrent { name: String },
|
||||
/// Change a paper-space layout's sheet: size in millimetres, rotation in
|
||||
/// degrees (0, 90, 180 or 270) and a custom plot scale (numerator,
|
||||
/// denominator).
|
||||
LayoutSetPage {
|
||||
name: String,
|
||||
paper_size: Option<[f64; 2]>,
|
||||
rotation: Option<u16>,
|
||||
scale: Option<[f64; 2]>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The style tables `TableOperation` can rename, delete or make current.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TableStyleKind {
|
||||
Text,
|
||||
Dim,
|
||||
}
|
||||
|
||||
/// Properties for creating or changing a text style; `None` leaves a property
|
||||
/// as it was (or the default for a new style). Angles are in radians.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
pub struct TextStyleConfig {
|
||||
pub name: String,
|
||||
/// Fixed height; `0` lets each text choose its own.
|
||||
pub height: Option<f64>,
|
||||
pub width_factor: Option<f64>,
|
||||
pub oblique_angle: Option<f64>,
|
||||
pub font_file: Option<String>,
|
||||
pub big_font_file: Option<String>,
|
||||
pub true_type_font: Option<String>,
|
||||
pub backward: Option<bool>,
|
||||
pub upside_down: Option<bool>,
|
||||
pub vertical: Option<bool>,
|
||||
pub annotative: Option<bool>,
|
||||
}
|
||||
|
||||
/// A kernel operation on ACIS-backed solids (API v7, additive). The host owns
|
||||
/// the geometry: a script never sees or rewrites the payload.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SolidOperation {
|
||||
/// Build a new solid on `layer` (layer `0` when `None`).
|
||||
Create { primitive: SolidPrimitive, layer: Option<String> },
|
||||
/// Apply a rigid transform, given as a column-major 4x4 matrix, to an
|
||||
/// existing solid. Scaling and shear are refused.
|
||||
Transform { handle: Handle, matrix: [f64; 16] },
|
||||
/// Build a planar region from one closed planar profile entity (a circle,
|
||||
/// ellipse, closed polyline or spline, ...). The new region goes on
|
||||
/// `layer`, or on the source's layer when `None`; `delete_source` erases
|
||||
/// the profile afterwards, as the REGION command does.
|
||||
RegionFromProfile { source: Handle, layer: Option<String>, delete_source: bool },
|
||||
/// Build a plane surface from one closed planar profile entity.
|
||||
SurfaceFromProfile { source: Handle, layer: Option<String>, delete_source: bool },
|
||||
/// Extrude a planar profile along `direction`: a solid when the profile is
|
||||
/// closed, a surface when it is open.
|
||||
Extrude { source: Handle, direction: [f64; 3], layer: Option<String>, delete_source: bool },
|
||||
/// Combine two solids into a new one. Both operands are consumed unless
|
||||
/// `keep_operands`; the result goes on `layer` or the first operand's
|
||||
/// layer. The kernel refuses some cases (coincident faces, cuts it has no
|
||||
/// closed form for); a refusal changes nothing. Curved operands can take a
|
||||
/// second or two, and the host thread is busy meanwhile.
|
||||
/// Embed a picture file as an OLE frame whose bottom-left corner is at
|
||||
/// `origin`, `width` drawing units wide, keeping the picture's aspect.
|
||||
/// PNG, JPEG and BMP are stored as read; other formats are re-encoded as
|
||||
/// PNG. The frame goes on `layer` (layer `0` when `None`).
|
||||
EmbedPicture { path: String, origin: [f64; 3], width: f64, layer: Option<String> },
|
||||
Boolean {
|
||||
first: Handle,
|
||||
second: Handle,
|
||||
operation: SolidBoolean,
|
||||
layer: Option<String>,
|
||||
keep_operands: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Value of a host-managed drafting or document setting exposed to plugins.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum HostSettingValue {
|
||||
Text(String),
|
||||
Number(f64),
|
||||
}
|
||||
|
||||
/// A notification the host sends to a plugin. These are best-effort,
|
||||
/// full-duplex messages correlated with an optional `command_id`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -76,6 +313,10 @@ pub enum HostNotification {
|
||||
DocumentTabClosed { tab_id: u64 },
|
||||
/// V4 selection changed for a specific tab. Discriminant 7.
|
||||
SelectionChangedV4 { tab_id: u64, handles: Vec<Handle> },
|
||||
/// V7 active interactive-command state for a tab. Discriminant 8.
|
||||
CommandStateChanged { tab_id: u64, command: Option<String> },
|
||||
/// V7 drawing epoch, broadcast even without an open shared view. Discriminant 9.
|
||||
DrawingChanged { tab_id: u64, epoch: u64 },
|
||||
/// Fallback for notification variants added in future minor revisions.
|
||||
/// Carries the raw bincode payload so an older peer can ignore it without
|
||||
/// failing deserialization.
|
||||
@@ -126,6 +367,20 @@ impl Serialize for HostNotification {
|
||||
bincode::serialize_into(&mut bytes, handles)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
HostNotification::CommandStateChanged { tab_id, command } => {
|
||||
bytes.push(8);
|
||||
bincode::serialize_into(&mut bytes, tab_id)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, command)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
HostNotification::DrawingChanged { tab_id, epoch } => {
|
||||
bytes.push(9);
|
||||
bincode::serialize_into(&mut bytes, tab_id)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, epoch)
|
||||
.map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
HostNotification::Unknown(raw) => bytes.extend_from_slice(raw),
|
||||
}
|
||||
bytes.serialize(serializer)
|
||||
@@ -163,6 +418,12 @@ impl<'de> Deserialize<'de> for HostNotification {
|
||||
7 => bincode::deserialize(rest)
|
||||
.map(|(tab_id, handles)| HostNotification::SelectionChangedV4 { tab_id, handles })
|
||||
.map_err(serde::de::Error::custom),
|
||||
8 => bincode::deserialize(rest)
|
||||
.map(|(tab_id, command)| HostNotification::CommandStateChanged { tab_id, command })
|
||||
.map_err(serde::de::Error::custom),
|
||||
9 => bincode::deserialize(rest)
|
||||
.map(|(tab_id, epoch)| HostNotification::DrawingChanged { tab_id, epoch })
|
||||
.map_err(serde::de::Error::custom),
|
||||
_ => Ok(HostNotification::Unknown(bytes)),
|
||||
}
|
||||
}
|
||||
@@ -553,6 +814,62 @@ pub trait HostApi {
|
||||
None
|
||||
}
|
||||
|
||||
/// Read a named host setting without invoking the command dispatcher.
|
||||
/// Names are case-insensitive. Hosts may support only a subset.
|
||||
fn system_variable(&self, _name: &str) -> Option<HostSettingValue> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Set a named host setting without re-entering command dispatch.
|
||||
/// Returns the effective value after validation and normalization.
|
||||
fn set_system_variable(
|
||||
&mut self,
|
||||
_name: &str,
|
||||
_value: HostSettingValue,
|
||||
) -> Result<HostSettingValue, String> {
|
||||
Err("system variable is not supported by this host".to_owned())
|
||||
}
|
||||
|
||||
/// Validate and replace existing entities as one undo step (API v7).
|
||||
/// Callers clone entities from `document()`, change only supported fields,
|
||||
/// and submit all replacements together. Identity and kind must be kept.
|
||||
/// An error leaves the drawing and undo history unchanged.
|
||||
fn update_entities_transaction(
|
||||
&mut self,
|
||||
_label: &str,
|
||||
_entities: Vec<EntityType>,
|
||||
) -> Result<(), String> {
|
||||
Err("entity transactions are not supported by this host".to_owned())
|
||||
}
|
||||
|
||||
/// Current ordered selection in this session's tab (API v7).
|
||||
fn selection(&self) -> Vec<Handle> { Vec::new() }
|
||||
|
||||
/// Replace this tab's selection with exactly these handles, in order.
|
||||
/// Missing or duplicate handles reject the request (API v7).
|
||||
fn set_selection(&mut self, _handles: &[Handle]) -> Result<(), String> {
|
||||
Err("selection writes are not supported by this host".to_owned())
|
||||
}
|
||||
|
||||
/// Run a kernel-backed solid operation and return the created or updated
|
||||
/// entity's handle. Refused, without any change, when the kernel cannot
|
||||
/// perform it losslessly (API v7, additive).
|
||||
fn solid_operation(&mut self, _operation: SolidOperation) -> Result<Handle, String> {
|
||||
Err("solid operations are not supported by this host".to_owned())
|
||||
}
|
||||
|
||||
/// Drive an OCS command from a script (API v7, additive). Refused, with
|
||||
/// nothing run, when another command is already active or the request names
|
||||
/// a command that could end the session or re-enter the plugin.
|
||||
fn run_command(&mut self, _request: CommandRequest) -> Result<CommandOutcome, String> {
|
||||
Err("running commands is not supported by this host".to_owned())
|
||||
}
|
||||
|
||||
/// Create, change, rename, delete or select a drawing table record (API v7,
|
||||
/// additive). Returns the record's handle; a refusal changes nothing.
|
||||
fn table_operation(&mut self, _operation: TableOperation) -> Result<Handle, String> {
|
||||
Err("table operations are not supported by this host".to_owned())
|
||||
}
|
||||
/// Add a layer to the active document with full initial properties.
|
||||
/// If an optional property in `config` is `None`, standard CAD defaults are applied.
|
||||
/// Returns `None` if the layer already exists or `config.name` is invalid.
|
||||
|
||||
Reference in new issue
Block a user