From 714fed7f0078b5888676c69dcadfe9ea1d328fdf Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 9 Jul 2026 23:03:25 +0300 Subject: [PATCH] fix(spline): terminate on finish and preview the real curve (#282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit on_enter/on_escape returned CmdResult::CommitEntity, which commits the spline but keeps the command armed with its accumulated points — the cursor stayed in spline mode and the next click began a new spline, trapping the user in the command loop. Return CommitAndExit so Enter/Esc finalize the spline and return to the selection state. The in-progress preview drew only the straight control polygon. Sample the actual degree-3 clamped B-spline (same control points + knot vector build() commits) so the rubber-band traces the exact final curve. Co-Authored-By: Claude Opus 4.8 --- src/app/command_driver.rs | 2 +- src/modules/draw/draw/spline.rs | 46 ++++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 58d352c2a..2d02999ba 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -221,7 +221,7 @@ impl OpenCADStudio { // A line/arc drawn by a repeating command advances the ARC_CONT // continuation anchor, so ending one run and launching another // keeps continuing from the last segment (mirrors the - // CommitAndExit arm). Non-continuable repeats (point/ray/spline) + // CommitAndExit arm). Non-continuable repeats (point/ray) // leave the anchor untouched. (#327) if matches!( entity, diff --git a/src/modules/draw/draw/spline.rs b/src/modules/draw/draw/spline.rs index 4fd3b1a77..3bfcb0d57 100644 --- a/src/modules/draw/draw/spline.rs +++ b/src/modules/draw/draw/spline.rs @@ -10,6 +10,8 @@ use crate::command::{CadCommand, CmdResult}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use glam::{DVec3, Vec3}; +use truck_modeling::base::{BoundedCurve, ParametricCurve}; +use truck_modeling::{BSplineCurve, KnotVec, Point3}; #[allow(dead_code)] pub fn tool() -> ToolDef { @@ -71,6 +73,33 @@ fn uniform_knots(n: usize, d: usize) -> Vec { .collect() } +/// Sample the degree-3 clamped B-spline through `pts` into a dense polyline, +/// built with the same control points + knot vector `build()` commits so the +/// preview traces the exact final curve — not the straight control polygon. +/// Fewer than 2 points has no curve; the points are returned as-is. +fn sample_curve(pts: &[Vec3]) -> Vec<[f32; 3]> { + let n = pts.len(); + if n < 2 { + return pts.iter().map(|p| [p.x, p.y, p.z]).collect(); + } + let degree = 3_usize.min(n - 1); + let ctrl: Vec = pts + .iter() + .map(|p| Point3::new(p.x as f64, p.y as f64, p.z as f64)) + .collect(); + let curve = BSplineCurve::new(KnotVec::from(uniform_knots(n, degree)), ctrl); + let (t0, t1) = curve.range_tuple(); + // Resolution scales with span count so long splines stay smooth. + let steps = 24 * (n - 1); + (0..=steps) + .map(|i| { + let t = t0 + (t1 - t0) * (i as f64 / steps as f64); + let p = curve.subs(t); + [p.x as f32, p.y as f32, p.z as f32] + }) + .collect() +} + impl CadCommand for SplineCommand { fn name(&self) -> &'static str { "SPLINE" @@ -102,15 +131,18 @@ impl CadCommand for SplineCommand { } fn on_enter(&mut self) -> CmdResult { + // A spline gathers many points into ONE entity, so finishing must end + // the command (CommitAndExit) — CommitEntity keeps the command armed + // with the accumulated points and traps the cursor in a redraw loop. match self.build(false) { - Some(e) => CmdResult::CommitEntity(e), + Some(e) => CmdResult::CommitAndExit(e), None => CmdResult::Cancel, } } fn on_escape(&mut self) -> CmdResult { match self.build(false) { - Some(e) => CmdResult::CommitEntity(e), + Some(e) => CmdResult::CommitAndExit(e), None => CmdResult::Cancel, } } @@ -142,12 +174,14 @@ impl CadCommand for SplineCommand { if self.pts.is_empty() { return None; } - // Show all committed points + cursor as a connected polyline. - let mut pts: Vec<[f32; 3]> = self.pts.iter().map(|p| [p.x, p.y, p.z]).collect(); - pts.push([pt.x, pt.y, pt.z]); + // Preview the actual B-spline curve, treating the cursor as the next + // control point, sampled exactly like the committed entity — not the + // straight control polygon. + let mut ctrl = self.pts.clone(); + ctrl.push(pt); Some(WireModel::solid( "rubber_band".into(), - pts, + sample_curve(&ctrl), WireModel::CYAN, false, ))