diff --git a/ROADMAP.md b/ROADMAP.md index 4aec3648..5a60515f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -46,7 +46,7 @@ headline counts in-scope tabs only and lists the deferred ones separately. | M4 | Files: STEP import (done: all 29 Fusion STEP files — [docs/step-import.md](docs/step-import.md)), 3MF export and 3MF/STL import as mesh bodies (done), DXF sketches, OBJ import | in progress | 6 | | M5 | Persistent naming of faces and edges | planned | 20 | | M6 | UI depth: selection feedback and origin (done), timeline editing (done: edit feature, history marker drag, reorder with dependency checks, suppress, rename, delete with dependents, re-resolved references), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator; taper handle open), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearances, preferences | in progress | 6 | -| M7 | Sweep, loft (incl. to a point), rib and web (to next / depth), emboss (planar faces), thread (cosmetic), coil (helical sweep, ruled), pipe, pattern on path, patterns of bodies and components, replace face (parallel), align, remove, feature copy/paste: done. Left: exact single-surface helical sweep, emboss on curved faces, thicken (needs surfaces), split face, modelled threads | in progress | 12 | +| M7 | Sweep, loft (incl. to a point), rib and web (to next / depth), emboss (planar faces), thread (cosmetic), coil (smooth B-spline helical walls), pipe, pattern on path, patterns of bodies and components, replace face (parallel), align, remove, feature copy/paste: done. Left: emboss on curved faces, thicken (needs surfaces), split face, modelled threads | in progress | 12 | | M8 | Surface workspace | deferred | — | | M9 | Components and assemblies (Fusion-style): New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, As-built Joint, Rigid Group, contact sets later; interference, per-component physical properties; STEP with assembly structure. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches | in progress | 32 | | M10 | Sheet metal: flange, contour flange, bend, unfold/refold, flat pattern + DXF, rules (thickness, K-factor, bend radius); then the PLASTIC enclosure subset (Boss, Rib, Web, Lip/Groove, Snap Fit) | planned | 36 | diff --git a/crates/kernel/src/helix.rs b/crates/kernel/src/helix.rs index 878f4f42..fe3cce8d 100644 --- a/crates/kernel/src/helix.rs +++ b/crates/kernel/src/helix.rs @@ -15,8 +15,9 @@ fn rot(v: Vec3, k: Vec3, ang: f64) -> Vec3 { /// Sweep a planar region's outer loop along a helix about `axis` through `axis_origin`: /// `turns` revolutions (right-handed about `axis`), rising `pitch` per turn. The profile keeps -/// its attitude relative to the axis, as a screw motion does; between stations (3° apart) the -/// walls are ruled. +/// its attitude relative to the axis, as a screw motion does. The walls are B-spline surfaces +/// (one per profile segment); if those cannot be built, ruled walls between stations 3° apart +/// stand in. pub fn sweep_helix(plane: &Plane, region: &Region2, axis_origin: Vec3, axis: Vec3, pitch: f64, turns: f64) -> Result
{ let k = axis.normalized().ok_or_else(|| KernelError::Invalid("helix axis".into()))?; if !(pitch.is_finite() && turns.is_finite() && turns > 0.0 && turns <= 200.0 && pitch.abs() < 1e7 && axis_origin.is_finite()) { @@ -33,6 +34,10 @@ pub fn sweep_helix(plane: &Plane, region: &Region2, axis_origin: Vec3, axis: Vec if turns > 1.0 && pitch.abs() <= extent { return Err(KernelError::Invalid(format!("the coil runs into itself: the pitch must exceed the section ({extent:.3} mm)"))); } + // One smooth B-spline wall per profile segment; the ruled walls below are the fallback. + if let Ok(b) = smooth_helix(plane, &lp, axis_origin, k, pitch, turns) { + return Ok(b); + } let n = ((turns * 120.0).ceil() as usize).clamp(4, 24_000); let total = std::f64::consts::TAU * turns; let mut wires = Vec::with_capacity(n + 1); @@ -54,6 +59,101 @@ pub fn sweep_helix(plane: &Plane, region: &Region2, axis_origin: Vec3, axis: Vec }) } +/// Samples per profile segment and stations per turn of the smooth helical walls. +const SEG_SAMPLES: usize = 12; +const STATIONS_PER_TURN: f64 = 12.0; + +/// Helical walls as B-spline surfaces: each profile segment, sampled at every station of the +/// screw motion, interpolated by cubics across the profile and then along the helix. +fn smooth_helix(plane: &Plane, lp: &Loop2, o: Vec3, k: Vec3, pitch: f64, turns: f64) -> Result { + let nseg = lp.segs.len(); + if nseg == 0 || nseg > 64 { + return Err(KernelError::Invalid("helix profile".into())); + } + let n = ((turns * STATIONS_PER_TURN).ceil() as usize).clamp(4, 4800); + let total = std::f64::consts::TAU * turns; + let at = |i: usize, q: solvecraft_geom::Vec2| { + let a = total * i as f64 / n as f64; + let w = plane.to_world(q); + o + rot(w - o, k, a) + k * (pitch * a / std::f64::consts::TAU) + }; + let fail = |w: &str| KernelError::Failed(format!("smooth helix: {w}")); + // Per segment: the station curves (control points along v) and the surface net. + let mut faces_data = Vec::with_capacity(nseg); + for s in &lp.segs { + let mut station_curves = Vec::with_capacity(n + 1); + for i in 0..=n { + let pts: Vec