forked from 1b-agent/opencadstudio-upstream
Add persistent sketch-constraint system (ocs_gcs port) and named-parameters handoff docs
Ports FreeCAD's planegcs 2D geometric constraint solver to Rust as a new crates/ocs_gcs workspace member, and wires it into the document/scene model as a persistent SketchConstraintSet: Coincident, Horizontal/Vertical, Parallel/Perpendicular, Equal, Tangent, Distance, Angle and Radius constraints that re-solve live (including mid-drag, not just on release), render glyphs and a DOF badge, flag conflicts with a one-click resolver pill, and round-trip through save/load via an XRecord blob. Also includes docs/command_reference.md (a statically generated reference of every registered command and its prompt sequence) and docs/named_parameters_design.md + docs/backlog.md, which hand off the next queued feature (a named-parameter/expression table) and consolidate every deliberate scope-down and open gap found along the way, so a fresh session has a complete starting point. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
a39c7b8f71
commit
63a74b19a9
63 files changed
+12193
-49
No files matched your search
@@ -0,0 +1,73 @@
|
||||
//! CCONSTRAINT — the manual Coincident UI (design doc §6.1's open item):
|
||||
//! unlike the rest of this module's plain select-and-click constraints,
|
||||
//! Coincident addresses a *point* on an entity (an endpoint, a circle/arc
|
||||
//! center), not the whole entity, so it needs two point picks instead of a
|
||||
//! selection.
|
||||
//!
|
||||
//! A `CadCommand` has no document access (see `value.rs`'s module doc
|
||||
//! comment for the same constraint on `DCONSTRAINT`/`ACONSTRAINT`), so this
|
||||
//! command only accumulates the two raw points `on_point` receives and hands
|
||||
//! them back as `CmdResult::AddCoincidentConstraint` — the host resolves
|
||||
//! each point to a real `SketchRef` (`sketch_constraints::nearest_sketch_point`)
|
||||
//! and adds the constraint, the same split `ReassociateCenterMark` already
|
||||
//! uses for a similar reason.
|
||||
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub mod coincident_tool {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "CCONSTRAINT",
|
||||
label: "Coincident",
|
||||
icon: IconKind::Glyph("≡"),
|
||||
event: ModuleEvent::Command("CCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CoincidentConstraintCommand {
|
||||
first: Option<DVec3>,
|
||||
}
|
||||
|
||||
impl CoincidentConstraintCommand {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for CoincidentConstraintCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"CCONSTRAINT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.first.is_none() {
|
||||
"COINCIDENT Specify first point:".to_string()
|
||||
} else {
|
||||
"COINCIDENT Specify second point:".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
match self.first {
|
||||
None => {
|
||||
self.first = Some(pt);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Some(first) => CmdResult::AddCoincidentConstraint { point_a: first, point_b: pt, label: "Coincident constraint" },
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//! Ribbon tools and typed-value commands for the persistent "Constraints"
|
||||
//! group (design doc `docs/parametric_system_design.md` §6.1).
|
||||
//!
|
||||
//! All eight — Horizontal, Vertical, Parallel, Perpendicular, Equal,
|
||||
//! Tangent, Distance, Angle — add a [`crate::scene::sketch_constraints::SketchConstraint`]
|
||||
//! to the current scope and let `Scene::bump_entities` solve it, the same
|
||||
//! path any later edit to the constrained entities re-solves through
|
||||
//! (`src/scene/sketch_solve.rs`). Horizontal/Vertical/Parallel/Perpendicular/
|
||||
//! Equal/Tangent dispatch straight to `CmdResult::AddSketchConstraint` from
|
||||
//! `src/app/commands/draw.rs` (no typed value needed, so no `CadCommand` of
|
||||
//! their own); Distance and Angle need one (`value.rs`), since the target
|
||||
//! value comes from the command line after the tool is clicked; Coincident
|
||||
//! needs one too (`coincident.rs`), since it addresses a *point* on an
|
||||
//! entity rather than the whole thing, so it can't reuse plain selection.
|
||||
|
||||
mod coincident;
|
||||
mod tools;
|
||||
mod value;
|
||||
pub use coincident::{coincident_tool, CoincidentConstraintCommand};
|
||||
pub use tools::{equal, horizontal, parallel, perpendicular, tangent, vertical};
|
||||
pub use value::{angle_tool, distance_tool, AngleConstraintCommand, DistanceConstraintCommand};
|
||||
@@ -0,0 +1,75 @@
|
||||
//! Ribbon button definitions for the Constraints group.
|
||||
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub mod horizontal {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "HCONSTRAINT",
|
||||
label: "Horizontal",
|
||||
icon: IconKind::Glyph("—"),
|
||||
event: ModuleEvent::Command("HCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod vertical {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "VCONSTRAINT",
|
||||
label: "Vertical",
|
||||
icon: IconKind::Glyph("│"),
|
||||
event: ModuleEvent::Command("VCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod parallel {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "PCONSTRAINT",
|
||||
label: "Parallel",
|
||||
icon: IconKind::Glyph("∥"),
|
||||
event: ModuleEvent::Command("PCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod perpendicular {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "QCONSTRAINT",
|
||||
label: "Perpendicular",
|
||||
icon: IconKind::Glyph("⊥"),
|
||||
event: ModuleEvent::Command("QCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod equal {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "ECONSTRAINT",
|
||||
label: "Equal",
|
||||
icon: IconKind::Glyph("="),
|
||||
event: ModuleEvent::Command("ECONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod tangent {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "TCONSTRAINT",
|
||||
label: "Tangent",
|
||||
icon: IconKind::Glyph("⌒"),
|
||||
event: ModuleEvent::Command("TCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
//! DCONSTRAINT / ACONSTRAINT — constraints that need a typed target value
|
||||
//! (a distance or an angle), unlike the plain select-and-click constraints
|
||||
//! in `mod.rs`.
|
||||
//!
|
||||
//! Persistent (design doc `docs/parametric_system_design.md` §6.1): these
|
||||
//! don't solve anything themselves. A `CadCommand`'s `on_text_input` only
|
||||
//! gets `&mut self` — no document access — so it can't add a
|
||||
//! `SketchConstraint` to the scene directly; instead these commands hand the
|
||||
//! typed value back as `CmdResult::AddSketchConstraint`, and the host (which
|
||||
//! does have `&mut Scene`) adds the record and solves it via the same
|
||||
//! `Scene::bump_entities` path any later edit to these entities will use too.
|
||||
//! `default_value` (what the prompt shows in `<...>`, and what a bare Enter
|
||||
//! submits) is still computed from a one-time read of current geometry —
|
||||
//! that part doesn't change.
|
||||
|
||||
use acadrust::entities::EntityType;
|
||||
use acadrust::types::Handle;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, InputKind};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
use crate::scene::Scene;
|
||||
|
||||
pub mod distance_tool {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "DCONSTRAINT",
|
||||
label: "Distance",
|
||||
icon: IconKind::Glyph("↔"),
|
||||
event: ModuleEvent::Command("DCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod angle_tool {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "ACONSTRAINT",
|
||||
label: "Angle",
|
||||
icon: IconKind::Glyph("∠"),
|
||||
event: ModuleEvent::Command("ACONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Constrains a single line's length (between its two endpoints) or a
|
||||
/// circle's radius, to a typed target value.
|
||||
pub struct DistanceConstraintCommand {
|
||||
handle: Handle,
|
||||
/// Which kind this becomes once a value comes in — decided once at
|
||||
/// construction from the entity's type, matching `ConstraintKind`'s own
|
||||
/// split between line-length (`Distance`) and circle-radius (`Radius`).
|
||||
is_circle: bool,
|
||||
default_value: f64,
|
||||
}
|
||||
|
||||
impl DistanceConstraintCommand {
|
||||
/// `None` if `handle` isn't a Line or Circle.
|
||||
pub fn new(scene: &Scene, handle: Handle) -> Option<Self> {
|
||||
let entity = scene.document.get_entity(handle)?;
|
||||
let (is_circle, default_value) = match entity {
|
||||
EntityType::Line(l) => {
|
||||
let dx = l.end.x - l.start.x;
|
||||
let dy = l.end.y - l.start.y;
|
||||
(false, (dx * dx + dy * dy).sqrt())
|
||||
}
|
||||
EntityType::Circle(c) => (true, c.radius),
|
||||
_ => return None,
|
||||
};
|
||||
Some(Self { handle, is_circle, default_value })
|
||||
}
|
||||
|
||||
fn build(&self, target: f64) -> Option<CmdResult> {
|
||||
if target <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let (kind, refs, label) = if self.is_circle {
|
||||
(ConstraintKind::Radius, vec![SketchRef::whole(self.handle)], "Radius constraint")
|
||||
} else {
|
||||
(
|
||||
ConstraintKind::Distance,
|
||||
vec![SketchRef::point(self.handle, 0), SketchRef::point(self.handle, 1)],
|
||||
"Distance constraint",
|
||||
)
|
||||
};
|
||||
Some(CmdResult::AddSketchConstraint { kind, refs, driving_param: Some(target), label })
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DistanceConstraintCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"DCONSTRAINT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
format!("Specify distance <{:.4}>: ", self.default_value)
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
InputKind::SingleToken
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
self.build(self.default_value).unwrap_or(CmdResult::Cancel)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let value: f64 = text.trim().parse().ok()?;
|
||||
self.build(value)
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
/// Constrains the angle (in degrees) from `fixed`'s direction to `moving`'s.
|
||||
pub struct AngleConstraintCommand {
|
||||
fixed_handle: Handle,
|
||||
moving_handle: Handle,
|
||||
default_value: f64,
|
||||
}
|
||||
|
||||
impl AngleConstraintCommand {
|
||||
/// `None` unless both `fixed` and `moving` are lines.
|
||||
pub fn new(scene: &Scene, fixed: Handle, moving: Handle) -> Option<Self> {
|
||||
let fixed_entity = scene.document.get_entity(fixed)?;
|
||||
let moving_entity = scene.document.get_entity(moving)?;
|
||||
let (EntityType::Line(f), EntityType::Line(m)) = (fixed_entity, moving_entity) else {
|
||||
return None;
|
||||
};
|
||||
let a1 = (f.end.y - f.start.y).atan2(f.end.x - f.start.x);
|
||||
let a2 = (m.end.y - m.start.y).atan2(m.end.x - m.start.x);
|
||||
let default_value = (a2 - a1).to_degrees();
|
||||
Some(Self { fixed_handle: fixed, moving_handle: moving, default_value })
|
||||
}
|
||||
|
||||
fn build(&self, target_degrees: f64) -> Option<CmdResult> {
|
||||
Some(CmdResult::AddSketchConstraint {
|
||||
kind: ConstraintKind::Angle,
|
||||
refs: vec![SketchRef::whole(self.fixed_handle), SketchRef::whole(self.moving_handle)],
|
||||
driving_param: Some(target_degrees),
|
||||
label: "Angle constraint",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for AngleConstraintCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"ACONSTRAINT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
format!("Specify angle in degrees <{:.4}>: ", self.default_value)
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
InputKind::SingleToken
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
self.build(self.default_value).unwrap_or(CmdResult::Cancel)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let value: f64 = text.trim().parse().ok()?;
|
||||
self.build(value)
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
mod changelog;
|
||||
pub mod clipboard;
|
||||
pub mod constrain;
|
||||
pub mod defaults;
|
||||
mod donate;
|
||||
pub mod draw;
|
||||
@@ -33,6 +34,7 @@ impl CadModule for DrawModule {
|
||||
};
|
||||
use crate::modules::insert::{create_block, insert_block};
|
||||
use clipboard::{copy_clip, cut, paste};
|
||||
use constrain::{angle_tool, coincident_tool, distance_tool, equal, horizontal, parallel, perpendicular, tangent, vertical};
|
||||
use draw::{arc, circle, ellipse, hatch, line, polyline, shapes};
|
||||
use groups::{group, ungroup};
|
||||
use layers::{
|
||||
@@ -120,6 +122,20 @@ impl CadModule for DrawModule {
|
||||
offset::tool().into(),
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Constraints",
|
||||
tools: vec![
|
||||
horizontal::tool().into(),
|
||||
vertical::tool().into(),
|
||||
parallel::tool().into(),
|
||||
perpendicular::tool().into(),
|
||||
equal::tool().into(),
|
||||
tangent::tool().into(),
|
||||
coincident_tool::tool().into(),
|
||||
distance_tool::tool().into(),
|
||||
angle_tool::tool().into(),
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Annotation",
|
||||
tools: vec![
|
||||
|
||||
Reference in new issue
Block a user