From 047016cb2d076f521423c14b64634b523c81c181 Mon Sep 17 00:00:00 2001 From: Adam Chalmers Date: Thu, 3 Sep 2026 10:36:04 -0500 Subject: [PATCH 1/4] Draft API for assembly blocks --- modeling-cmds/src/def_enum.rs | 53 ++++++++++++++++++++++++++++++-- modeling-cmds/src/ok_response.rs | 30 ++++++++++++++++++ modeling-cmds/src/shared.rs | 43 ++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/modeling-cmds/src/def_enum.rs b/modeling-cmds/src/def_enum.rs index df4fce5f..76d2dead 100644 --- a/modeling-cmds/src/def_enum.rs +++ b/modeling-cmds/src/def_enum.rs @@ -25,6 +25,8 @@ define_modeling_cmd_enum! { length_unit::LengthUnit, shared::{ Angle, + AssemblyConstraint, + MateFrameOn, RegionVersion, BlendType, BodyType, @@ -2422,7 +2424,7 @@ define_modeling_cmd_enum! { /// /// Most successful unions come from solids who's faces do not overlap /// aka non-coplanar. - /// + /// /// Failure cases: /// * A common failure is unsupported coincident faces try to be unioned. /// @@ -2491,7 +2493,7 @@ define_modeling_cmd_enum! { /// aka non-coplanar. /// /// Prefer one `tool` over multiple when calling this feature. - /// + /// /// Failure cases: /// * A common failure is unsupported coplanar faces try to be unioned. /// @@ -2838,6 +2840,53 @@ define_modeling_cmd_enum! { /// Which path to query pub path_id: ModelingCmdId, } + + /// Create a new assembly + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, ModelingCmdVariant, Builder)] + #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] + #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] + #[cfg_attr(feature = "ts-rs", ts(export_to = "ModelingCmd.ts"))] + #[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)] + pub struct AssemblyCreate { + /// Which entities, if any, to move into this assembly. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub children: Vec, + } + + /// Add children to the assembly + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, ModelingCmdVariant, Builder)] + #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] + #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] + #[cfg_attr(feature = "ts-rs", ts(export_to = "ModelingCmd.ts"))] + #[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)] + pub struct AssemblyAddChildren { + /// Which entities, to move into this assembly. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub children: Vec, + } + + /// Create a mate frame + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, ModelingCmdVariant, Builder)] + #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] + #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] + #[cfg_attr(feature = "ts-rs", ts(export_to = "ModelingCmd.ts"))] + #[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)] + pub struct MateFrameCreate { + /// What to create the mate frame on. + pub on: MateFrameOn, + } + + /// Add constraints to an assembly. + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, ModelingCmdVariant, Builder)] + #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] + #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] + #[cfg_attr(feature = "ts-rs", ts(export_to = "ModelingCmd.ts"))] + #[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)] + pub struct AssemblyAddConstraints { + /// Which constraints to apply. + pub constraints: Vec, + } + } } diff --git a/modeling-cmds/src/ok_response.rs b/modeling-cmds/src/ok_response.rs index 779b6c19..87693edc 100644 --- a/modeling-cmds/src/ok_response.rs +++ b/modeling-cmds/src/ok_response.rs @@ -1427,5 +1427,35 @@ define_ok_modeling_cmd_response_enum! { /// How many regions the Toolpaths library thinks exist pub region_count: u16, } + + /// The response from the 'AssemblyCreate'. + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema, ModelingCmdOutput)] + #[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)] + pub struct AssemblyCreate { + } + + /// The response from the 'AssemblyAddChildren'. + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema, ModelingCmdOutput)] + #[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)] + pub struct AssemblyAddChildren { + } + + /// The response from the 'MateFrameCreate'. + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema, ModelingCmdOutput)] + #[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)] + pub struct MateFrameCreate { + } + + /// The response from the 'AssemblyAddConstraints'. + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema, ModelingCmdOutput)] + #[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)] + pub struct AssemblyAddConstraints { + // TODO: Engine should also return a solution, so that the KCL + // source can be updated with a starting translate/rotate that + // each constrained entity has applied. This way, if the block + // is solved again in the future, the system will already find + // itself in a nearly-solved state, leading to faster and more + // stable assembly blocks. + } } } diff --git a/modeling-cmds/src/shared.rs b/modeling-cmds/src/shared.rs index c1c6e20a..ad51e287 100644 --- a/modeling-cmds/src/shared.rs +++ b/modeling-cmds/src/shared.rs @@ -2248,3 +2248,46 @@ pub enum CurveTypeDebug { /// Circle with a center and radius. Circle, } + +/// What to create a mate frame on +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "ts-rs", ts(export_to = "ModelingCmd.ts"))] +#[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)] +pub enum MateFrameOn { + /// Mate frame at the centroid of this face. + FaceCentroid { + /// The face's ID + face_id: Uuid, + }, + /// Mate frame along this edge. + Edge { + /// Which edge? + specifier: EdgeSpecifier, + }, +} + +/// How to constrain bodies in an assembly. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "ts-rs", ts(export_to = "ModelingCmd.ts"))] +#[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)] +pub enum AssemblyConstraint { + /// Makes two entities occupy the same line/plane or places a point on a surface. + Coincident { + /// IDs of two mate frames in the assembly. + targets: [Uuid; 2], + }, + /// Aligns cylindrical, conical, or spherical surfaces to a common center. + Concentric { + /// Which edge? + /// IDs of two mate frames in the assembly. + /// Must be >= 2 items. + #[schemars(range(min = 2))] + targets: Vec, + }, +} From a38a93d662778f96263f7adfd0139fb7dc9f0f8a Mon Sep 17 00:00:00 2001 From: Adam Chalmers Date: Thu, 3 Sep 2026 10:40:31 -0500 Subject: [PATCH 2/4] Redo OpenAPI --- modeling-cmds/openapi/api.json | 206 +++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/modeling-cmds/openapi/api.json b/modeling-cmds/openapi/api.json index 8c98f1cb..67ca3131 100644 --- a/modeling-cmds/openapi/api.json +++ b/modeling-cmds/openapi/api.json @@ -665,6 +665,65 @@ } ] }, + "AssemblyConstraint": { + "description": "How to constrain bodies in an assembly.", + "oneOf": [ + { + "description": "Makes two entities occupy the same line/plane or places a point on a surface.", + "type": "object", + "properties": { + "coincident": { + "type": "object", + "properties": { + "targets": { + "description": "IDs of two mate frames in the assembly.", + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "minItems": 2, + "maxItems": 2 + } + }, + "required": [ + "targets" + ] + } + }, + "required": [ + "coincident" + ], + "additionalProperties": false + }, + { + "description": "Aligns cylindrical, conical, or spherical surfaces to a common center.", + "type": "object", + "properties": { + "concentric": { + "type": "object", + "properties": { + "targets": { + "description": "Which edge? IDs of two mate frames in the assembly. Must be >= 2 items.", + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + "required": [ + "targets" + ] + } + }, + "required": [ + "concentric" + ], + "additionalProperties": false + } + ] + }, "Axis": { "description": "Co-ordinate axis specifier.\n\nSee [cglearn.eu] for background reading.\n\n[cglearn.eu]: https://cglearn.eu/pub/computer-graphics/introduction-to-geometry#material-coordinate-systems-1", "oneOf": [ @@ -2532,6 +2591,60 @@ "type": "number", "format": "double" }, + "MateFrameOn": { + "description": "What to create a mate frame on", + "oneOf": [ + { + "description": "Mate frame at the centroid of this face.", + "type": "object", + "properties": { + "face_centroid": { + "type": "object", + "properties": { + "face_id": { + "description": "The face's ID", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "face_id" + ] + } + }, + "required": [ + "face_centroid" + ], + "additionalProperties": false + }, + { + "description": "Mate frame along this edge.", + "type": "object", + "properties": { + "edge": { + "type": "object", + "properties": { + "specifier": { + "description": "Which edge?", + "allOf": [ + { + "$ref": "#/components/schemas/EdgeSpecifier" + } + ] + } + }, + "required": [ + "specifier" + ] + } + }, + "required": [ + "edge" + ], + "additionalProperties": false + } + ] + }, "MbdSymbol": { "description": "MBD symbol type", "type": "string", @@ -8190,6 +8303,99 @@ "path_id", "type" ] + }, + { + "description": "Create a new assembly", + "type": "object", + "properties": { + "children": { + "description": "Which entities, if any, to move into this assembly.", + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "type": { + "type": "string", + "enum": [ + "assembly_create" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Add children to the assembly", + "type": "object", + "properties": { + "children": { + "description": "Which entities, to move into this assembly.", + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "type": { + "type": "string", + "enum": [ + "assembly_add_children" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Create a mate frame", + "type": "object", + "properties": { + "on": { + "description": "What to create the mate frame on.", + "allOf": [ + { + "$ref": "#/components/schemas/MateFrameOn" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "mate_frame_create" + ] + } + }, + "required": [ + "on", + "type" + ] + }, + { + "description": "Add constraints to an assembly.", + "type": "object", + "properties": { + "constraints": { + "description": "Which constraints to apply.", + "type": "array", + "items": { + "$ref": "#/components/schemas/AssemblyConstraint" + } + }, + "type": { + "type": "string", + "enum": [ + "assembly_add_constraints" + ] + } + }, + "required": [ + "constraints", + "type" + ] } ] }, From b955eae340892b276d73b27b1c6f00f9ee77da9b Mon Sep 17 00:00:00 2001 From: Adam Chalmers Date: Wed, 9 Sep 2026 20:55:58 -0500 Subject: [PATCH 3/4] Allow for multiple ways to position a mate frame --- modeling-cmds/openapi/api.json | 122 +++++++++++++++++++++++++++++++-- modeling-cmds/src/shared.rs | 50 +++++++++++++- 2 files changed, 164 insertions(+), 8 deletions(-) diff --git a/modeling-cmds/openapi/api.json b/modeling-cmds/openapi/api.json index 67ca3131..f181b2c2 100644 --- a/modeling-cmds/openapi/api.json +++ b/modeling-cmds/openapi/api.json @@ -2601,14 +2601,23 @@ "face_centroid": { "type": "object", "properties": { - "face_id": { - "description": "The face's ID", + "where_on_face": { + "description": "Where on the given face?", + "allOf": [ + { + "$ref": "#/components/schemas/PointOnFace" + } + ] + }, + "which_face": { + "description": "Which face?", "type": "string", "format": "uuid" } }, "required": [ - "face_id" + "where_on_face", + "which_face" ] } }, @@ -2624,7 +2633,15 @@ "edge": { "type": "object", "properties": { - "specifier": { + "where_on_edge": { + "description": "Where on the given edge?", + "allOf": [ + { + "$ref": "#/components/schemas/WhereOnEdge" + } + ] + }, + "which_edge": { "description": "Which edge?", "allOf": [ { @@ -2634,7 +2651,8 @@ } }, "required": [ - "specifier" + "where_on_edge", + "which_edge" ] } }, @@ -9324,6 +9342,57 @@ "z" ] }, + "PointOnFace": { + "description": "Identifies a point on a specific face.", + "oneOf": [ + { + "description": "Centermost point of the face.", + "type": "object", + "properties": { + "centroid": { + "type": "object" + } + }, + "required": [ + "centroid" + ], + "additionalProperties": false + }, + { + "description": "Somewhere along the face.", + "type": "object", + "properties": { + "at_parameter": { + "type": "object", + "properties": { + "u": { + "description": "Value for the face's 2D parametric equation.", + "type": "number", + "format": "double", + "minimum": 0.0, + "maximum": 1.0 + }, + "v": { + "description": "Value for the face's 2D parametric equation.", + "type": "number", + "format": "double", + "minimum": 0.0, + "maximum": 1.0 + } + }, + "required": [ + "u", + "v" + ] + } + }, + "required": [ + "at_parameter" + ], + "additionalProperties": false + } + ] + }, "PrimitiveTopologyFallback": { "description": "Optional fallback when primary UUIDs are missing from the client artifact graph (e.g. stale or engine-only ids). Identifies the same topology via a **parent** entity UUID and a **primitive index** on that parent.\n\nSemantics by selection kind (aligned with engine BREP topology):\n\n- **Face / Edge (3D)**: `parent_id` is the owning [`EntityType::Solid3D`] body UUID; `primitive_index` matches the index returned by **EntityGetPrimitiveIndex** for that face or edge (and matches **EntityGetParentId** → parent + **EntityGetPrimitiveIndex** → index). - **Vertex (3D)**: same `parent_id` (solid); `primitive_index` is the BREP vertex index on that solid. - **Solid2dEdge**: `parent_id` is the **Solid2D** profile UUID; `primitive_index` is the curve index within that profile. - **Segment**: `parent_id` is the **Path** UUID; `primitive_index` is the curve index within that path.\n\nOther [`EntityReference`] variants may omit this field or leave it unset when not applicable.", "type": "object", @@ -10384,6 +10453,49 @@ } ] }, + "WhereOnEdge": { + "description": "Identifies a point on a specific edge.", + "oneOf": [ + { + "description": "Midpoint along the edge.", + "type": "object", + "properties": { + "midpoint": { + "type": "object" + } + }, + "required": [ + "midpoint" + ], + "additionalProperties": false + }, + { + "description": "Somewhere along the edge's length.", + "type": "object", + "properties": { + "at_parameter": { + "type": "object", + "properties": { + "t": { + "description": "Value which, substituted into the edge's parametric equation, gives a point. Must be between 0 and 1 (inclusive).", + "type": "number", + "format": "double", + "minimum": 0.0, + "maximum": 1.0 + } + }, + "required": [ + "t" + ] + } + }, + "required": [ + "at_parameter" + ], + "additionalProperties": false + } + ] + }, "WorldCoordinateSystem": { "type": "string", "enum": [ diff --git a/modeling-cmds/src/shared.rs b/modeling-cmds/src/shared.rs index ad51e287..9265da39 100644 --- a/modeling-cmds/src/shared.rs +++ b/modeling-cmds/src/shared.rs @@ -2259,13 +2259,57 @@ pub enum CurveTypeDebug { pub enum MateFrameOn { /// Mate frame at the centroid of this face. FaceCentroid { - /// The face's ID - face_id: Uuid, + /// Which face? + which_face: Uuid, + /// Where on the given face? + where_on_face: PointOnFace, }, /// Mate frame along this edge. Edge { /// Which edge? - specifier: EdgeSpecifier, + which_edge: EdgeSpecifier, + /// Where on the given edge? + where_on_edge: WhereOnEdge, + }, +} + +/// Identifies a point on a specific face. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "ts-rs", ts(export_to = "ModelingCmd.ts"))] +#[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)] +pub enum PointOnFace { + /// Centermost point of the face. + Centroid {}, + /// Somewhere along the face. + AtParameter { + /// Value for the face's 2D parametric equation. + #[schemars(range(min = 0.0, max = 1.0))] + u: f64, + /// Value for the face's 2D parametric equation. + #[schemars(range(min = 0.0, max = 1.0))] + v: f64, + }, +} + +/// Identifies a point on a specific edge. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "ts-rs", ts(export_to = "ModelingCmd.ts"))] +#[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)] +pub enum WhereOnEdge { + /// Midpoint along the edge. + Midpoint {}, + /// Somewhere along the edge's length. + AtParameter { + /// Value which, substituted into the edge's parametric equation, + /// gives a point. Must be between 0 and 1 (inclusive). + #[schemars(range(min = 0.0, max = 1.0))] + t: f64, }, } From 39695507ab3a397a20ea57c9c5ba07c44bb47c47 Mon Sep 17 00:00:00 2001 From: Adam Chalmers Date: Wed, 9 Sep 2026 21:18:58 -0500 Subject: [PATCH 4/4] API for assembly solver to return its solutions to client --- modeling-cmds/src/ok_response.rs | 9 +++------ modeling-cmds/src/shared.rs | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/modeling-cmds/src/ok_response.rs b/modeling-cmds/src/ok_response.rs index 87693edc..6179bd5f 100644 --- a/modeling-cmds/src/ok_response.rs +++ b/modeling-cmds/src/ok_response.rs @@ -25,6 +25,7 @@ define_ok_modeling_cmd_response_enum! { CameraViewState, BodyType, EntityReference, + SolvedAssemblyItem, }; use std::collections::HashMap; @@ -1450,12 +1451,8 @@ define_ok_modeling_cmd_response_enum! { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema, ModelingCmdOutput)] #[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)] pub struct AssemblyAddConstraints { - // TODO: Engine should also return a solution, so that the KCL - // source can be updated with a starting translate/rotate that - // each constrained entity has applied. This way, if the block - // is solved again in the future, the system will already find - // itself in a nearly-solved state, leading to faster and more - // stable assembly blocks. + /// For each item in the assembly, map its ID to its solution + pub solution: HashMap, } } } diff --git a/modeling-cmds/src/shared.rs b/modeling-cmds/src/shared.rs index 9265da39..09a643c8 100644 --- a/modeling-cmds/src/shared.rs +++ b/modeling-cmds/src/shared.rs @@ -2335,3 +2335,22 @@ pub enum AssemblyConstraint { targets: Vec, }, } + +/// Result of constraining an item in an assembly. +/// This is in the coordinate system of the assembly's parent. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "ts-rs", ts(export_to = "ModelingCmd.ts"))] +#[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)] +pub struct SolvedAssemblyItem { + /// How to translate the item. + pub translate_by: Point3d, + /// How to rotate the item. + pub roll: Angle, + /// How to rotate the item. + pub pitch: Angle, + /// How to rotate the item. + pub yaw: Angle, +}