feat: SPLINEDIT — close, open, reverse spline control-point editing
- splinedit.rs: SplineditCommand picks a spline (entity-pick phase) then
accepts CLOSE/OPEN/REVERSE/EXIT sub-commands via text input
- apply_spline_op() mutates the spline in the document directly, with knot
vector regeneration via generate_clamped_knots()
- Sentinel XLine trick: ReplaceEntity with a magic __SPLINEDIT_* layer name
is intercepted in cmd_result.rs before erasing the original entity
- Control-point dragging already works via the existing grip editing system
- ROADMAP.md: SPLINEDIT marked ✅
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
51e6c4b359
commit
0fc3766da7
5 changed files with 215 additions and 1 deletions
|
|
@ -125,7 +125,7 @@ Underlay (PDF/DWF/DGN)
|
|||
| EXPLODE (X) | ✅ | — |
|
||||
| PEDIT (PE) | ✅ | — |
|
||||
| STRETCH (SS) | ✅ | — |
|
||||
| SPLINEDIT | ⬜ | — |
|
||||
| SPLINEDIT | ✅ CLOSE/OPEN/REVERSE + grip editing | — |
|
||||
| HATCHEDIT | ✅ | — |
|
||||
| ATTEDIT | ⬜ | Attribute değerlerini düzenleme |
|
||||
| DDEDIT (çift tık metin) | ✅ | — |
|
||||
|
|
|
|||
|
|
@ -170,6 +170,25 @@ impl H7CAD {
|
|||
self.refresh_properties();
|
||||
}
|
||||
CmdResult::ReplaceEntity(handle, new_entities) => {
|
||||
// Detect SPLINEDIT sentinel: a single XLine with a magic layer name.
|
||||
if new_entities.len() == 1 {
|
||||
if let acadrust::EntityType::XLine(ref xl) = new_entities[0] {
|
||||
let op = xl.common.layer.clone();
|
||||
if op.starts_with("__SPLINEDIT_") {
|
||||
let label = self.history_label_from_active_cmd(i, "SPLINEDIT");
|
||||
self.push_undo_snapshot(i, label);
|
||||
crate::modules::home::modify::splinedit::apply_spline_op(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
&op,
|
||||
);
|
||||
self.tabs[i].dirty = true;
|
||||
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||
if let Some(p) = prompt { self.command_line.push_info(&p); }
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
}
|
||||
let label = self.history_label_from_active_cmd(i, "TRIM");
|
||||
self.push_undo_snapshot(i, label);
|
||||
self.tabs[i].scene.erase_entities(&[handle]);
|
||||
|
|
|
|||
|
|
@ -1435,6 +1435,13 @@ impl H7CAD {
|
|||
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
||||
}
|
||||
|
||||
"SPLINEDIT"|"SPE" => {
|
||||
use crate::modules::home::modify::splinedit::SplineditCommand;
|
||||
let cmd_obj = SplineditCommand::new();
|
||||
self.command_line.push_info(&cmd_obj.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
||||
}
|
||||
|
||||
"ALIGN"|"AL" => {
|
||||
use crate::modules::home::modify::align::AlignCommand;
|
||||
let cmd = AlignCommand::new();
|
||||
|
|
|
|||
|
|
@ -16,4 +16,5 @@ pub mod scale;
|
|||
pub mod stretch;
|
||||
pub mod translate;
|
||||
pub mod spline_ops;
|
||||
pub mod splinedit;
|
||||
pub mod trim;
|
||||
|
|
|
|||
187
src/modules/home/modify/splinedit.rs
Normal file
187
src/modules/home/modify/splinedit.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
// 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::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::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 => "SPLINEDIT Select spline:".to_string(),
|
||||
Step::SubCommand { .. } => {
|
||||
"SPLINEDIT [CLOSE/OPEN/REVERSE/EXIT]:".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(self.step, Step::SelectSpline)
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: Vec3) -> 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: Vec3) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_preview_wires(&mut self, _pt: Vec3) -> 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(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue