forked from 1b-agent/opencadstudio-upstream
207 lines
7.3 KiB
Rust
207 lines
7.3 KiB
Rust
// SPLINEDIT command — interactive spline editing.
|
|
//
|
|
// Phase 1: select a spline (entity pick)
|
|
// Phase 2: choose a sub-command:
|
|
// CLOSE — set the spline as closed (wrap last control point to first)
|
|
// OPEN — remove the closure
|
|
// REVERSE— reverse the control point order
|
|
// EXIT — done (Enter / Escape)
|
|
//
|
|
// Control-point dragging is already supported via the grip editing system.
|
|
|
|
use acadrust::EntityType;
|
|
use glam::DVec3;
|
|
|
|
use crate::command::{CadCommand, CmdResult};
|
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
|
use crate::scene::model::wire_model::WireModel;
|
|
|
|
#[allow(dead_code)]
|
|
pub fn tool() -> ToolDef {
|
|
ToolDef {
|
|
id: "SPLINEDIT",
|
|
label: "Spline Edit",
|
|
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/spline.svg")),
|
|
event: ModuleEvent::Command("SPLINEDIT".to_string()),
|
|
}
|
|
}
|
|
|
|
enum Step {
|
|
/// Waiting for the user to pick a spline entity.
|
|
SelectSpline,
|
|
/// Spline selected; waiting for sub-command text input.
|
|
SubCommand { handle: acadrust::Handle },
|
|
}
|
|
|
|
pub struct SplineditCommand {
|
|
step: Step,
|
|
}
|
|
|
|
impl SplineditCommand {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
step: Step::SelectSpline,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CadCommand for SplineditCommand {
|
|
fn name(&self) -> &'static str {
|
|
"SPLINEDIT"
|
|
}
|
|
|
|
fn prompt(&self) -> String {
|
|
match &self.step {
|
|
Step::SelectSpline => crate::t!("SPLINEDIT Select spline:").into_owned(),
|
|
Step::SubCommand { .. } => crate::t!("SPLINEDIT [CLOSE/OPEN/REVERSE/EXIT]:").into_owned(),
|
|
}
|
|
}
|
|
|
|
fn needs_entity_pick(&self) -> bool {
|
|
matches!(self.step, Step::SelectSpline)
|
|
}
|
|
|
|
fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: DVec3) -> CmdResult {
|
|
if handle.is_null() {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
self.step = Step::SubCommand { handle };
|
|
CmdResult::NeedPoint
|
|
}
|
|
|
|
fn wants_text_input(&self) -> bool {
|
|
matches!(self.step, Step::SubCommand { .. })
|
|
}
|
|
|
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
|
let Step::SubCommand { handle } = &self.step else {
|
|
return None;
|
|
};
|
|
let handle = *handle;
|
|
match text.trim().to_uppercase().as_str() {
|
|
"CLOSE" | "C" => Some(CmdResult::ReplaceEntity(
|
|
handle,
|
|
vec![SplineOp::Close.into_marker(handle)],
|
|
)),
|
|
"OPEN" | "O" => Some(CmdResult::ReplaceEntity(
|
|
handle,
|
|
vec![SplineOp::Open.into_marker(handle)],
|
|
)),
|
|
"REVERSE" | "R" | "REV" => Some(CmdResult::ReplaceEntity(
|
|
handle,
|
|
vec![SplineOp::Reverse.into_marker(handle)],
|
|
)),
|
|
"EXIT" | "X" | "" => Some(CmdResult::Cancel),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
|
CmdResult::NeedPoint
|
|
}
|
|
fn on_enter(&mut self) -> CmdResult {
|
|
CmdResult::Cancel
|
|
}
|
|
fn on_preview_wires(&mut self, _pt: DVec3) -> Vec<WireModel> {
|
|
vec![]
|
|
}
|
|
}
|
|
|
|
// ── Spline operation helpers ───────────────────────────────────────────────
|
|
//
|
|
// Because `CmdResult::ReplaceEntity` takes `Vec<EntityType>` and we need the
|
|
// host to apply it to the actual document, we use a small "deferred op" trick:
|
|
// the host applies the spline transform in `apply_cmd_result` by detecting
|
|
// that we replaced an entity with zero new entities (delete) or one new one.
|
|
//
|
|
// Here we actually build the modified entity without accessing the document —
|
|
// we only have the handle. The real transformation (close/open/reverse) is
|
|
// applied in the Scene via `apply_spline_op`.
|
|
|
|
enum SplineOp {
|
|
Close,
|
|
Open,
|
|
Reverse,
|
|
}
|
|
|
|
impl SplineOp {
|
|
/// Return a placeholder that encodes the op in a comment field.
|
|
/// The host `cmd_result.rs` detects SPLINEDIT replaces and calls
|
|
/// `Scene::apply_spline_op`.
|
|
fn into_marker(self, _handle: acadrust::Handle) -> EntityType {
|
|
// We can't build the modified spline here without the document.
|
|
// Return a sentinel — the actual transformation is handled by the
|
|
// apply_spline_op path in Scene. We use an XLine as a sentinel
|
|
// that the host will never actually commit (it detects the SPLINEDIT
|
|
// replace path and calls apply_spline_op instead).
|
|
//
|
|
// In practice: use ReplaceMany with empty new entities + a SplineOp
|
|
// side-channel via the xattach_path mechanism isn't clean.
|
|
//
|
|
// Simpler: encode op in the sentinel entity's layer field.
|
|
let mut sentinel = acadrust::entities::XLine::new(
|
|
acadrust::types::Vector3::zero(),
|
|
acadrust::types::Vector3::new(1.0, 0.0, 0.0),
|
|
);
|
|
sentinel.common.layer = match self {
|
|
SplineOp::Close => "__SPLINEDIT_CLOSE__".to_string(),
|
|
SplineOp::Open => "__SPLINEDIT_OPEN__".to_string(),
|
|
SplineOp::Reverse => "__SPLINEDIT_REVERSE__".to_string(),
|
|
};
|
|
EntityType::XLine(sentinel)
|
|
}
|
|
}
|
|
|
|
/// Apply a spline operation (CLOSE/OPEN/REVERSE) to a spline entity.
|
|
/// Called from `cmd_result.rs` when the ReplaceEntity sentinel is detected.
|
|
pub fn apply_spline_op(doc: &mut acadrust::CadDocument, handle: acadrust::Handle, op: &str) {
|
|
let Some(EntityType::Spline(spline)) = doc.get_entity_mut(handle) else {
|
|
return;
|
|
};
|
|
match op {
|
|
"__SPLINEDIT_CLOSE__" => {
|
|
if spline.control_points.len() >= 2 {
|
|
let first = spline.control_points[0];
|
|
spline.control_points.push(first);
|
|
// Regenerate clamped knots.
|
|
spline.knots = acadrust::entities::Spline::generate_clamped_knots(
|
|
spline.degree as usize,
|
|
spline.control_points.len(),
|
|
);
|
|
spline.fit_points.clear();
|
|
}
|
|
}
|
|
"__SPLINEDIT_OPEN__" => {
|
|
if spline.control_points.len() >= 2 {
|
|
let n = spline.control_points.len();
|
|
let first = spline.control_points[0];
|
|
let last = spline.control_points[n - 1];
|
|
if (first.x - last.x).abs() < 1e-9
|
|
&& (first.y - last.y).abs() < 1e-9
|
|
&& (first.z - last.z).abs() < 1e-9
|
|
{
|
|
spline.control_points.pop();
|
|
spline.knots = acadrust::entities::Spline::generate_clamped_knots(
|
|
spline.degree as usize,
|
|
spline.control_points.len(),
|
|
);
|
|
spline.fit_points.clear();
|
|
}
|
|
}
|
|
}
|
|
"__SPLINEDIT_REVERSE__" => {
|
|
spline.control_points.reverse();
|
|
spline.fit_points.reverse();
|
|
spline.knots = acadrust::entities::Spline::generate_clamped_knots(
|
|
spline.degree as usize,
|
|
spline.control_points.len(),
|
|
);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
|
|
// ── Autocomplete registry ─────────────────────────────────
|
|
inventory::submit!(crate::command::CommandRegistration { names: &["SPLINEDIT"] }); // SplineditCommand
|