diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0a3cee6..3115dee 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -22,6 +22,7 @@ CCL_Clay3DP/ │ ├── BaseSettings.cs # Multi-layer raft (Issue #10) │ ├── RobotSettings.cs # Feed rate, spindle, nozzle, RoboDK paths │ ├── Parameters.cs # GeometrySelection, BuildVolumeSettings, HelixParameters, HeightParameters +│ ├── ClayBeadGeometry.cs # Bead-width helper (W = D²/H); shared by viz + bracing kiss offset │ └── ClayPresets.cs # Porcelain / Stoneware / Earthenware presets ├── Core/ # Geometry pipeline primitives │ ├── GeometrySelector.cs # Rhino object picker (Brep/Surface/Mesh) @@ -40,7 +41,8 @@ CCL_Clay3DP/ │ ├── FrameSerializer.cs # SpiralResult.Frames → JSON for the Python script │ └── RoboDKSubprocess.cs # Runs the Python script via cmd.exe; Python connects to RoboDK └── Zigzag/ # Outer Wall Bracing (Layer Slice mode) - └── ... + └── ZigzagGenerator.cs # Triangle-wave + cosine-wave bracing generators + # (BuildSingleContour, BuildSinusoidalSingleContour) ``` **Dependency direction (lower depends on higher, never the reverse):** @@ -268,6 +270,8 @@ classDiagram int FramesPerLayer bool SpiralSlice bool OuterWallBracing + int BracingContactPoints + bool SinusoidalBracing bool SpiralFollowsCurveNormal double StartAngle } diff --git a/CCL_Clay3DP/Models/ClayBeadGeometry.cs b/CCL_Clay3DP/Models/ClayBeadGeometry.cs new file mode 100644 index 0000000..6deab85 --- /dev/null +++ b/CCL_Clay3DP/Models/ClayBeadGeometry.cs @@ -0,0 +1,53 @@ +// Copyright 2026 CyberCraft Lab, OTH Regensburg, Prof. Christophe Barlieb +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; + +namespace CCL_Clay3DP.Models +{ + /// + /// Derived geometric properties of an extruded clay bead. + /// + /// A nominally-circular bead of diameter D, squashed to layer height + /// H during deposition, conserves its cross-sectional area: + /// + /// π·(D/2)² = π·(W/2)·(H/2) → W = D² / H + /// + /// When H == D the bead stays circular (W = D). H > D is physically + /// impossible (the bead can't span a vertical gap larger than its + /// own diameter) and is rejected upstream before this helper runs. + /// + /// Single source of truth so the elliptical-tube preview, the outer- + /// wall bracing kiss offset (Issue #11), and any future bead-aware + /// path math all agree on the same width. + /// + public static class ClayBeadGeometry + { + /// + /// Full deposited bead width in mm, given nominal bead diameter + /// (= nozzle diameter, by convention) and layer height. + /// + public static double ComputeWidth(double diameter, double layerHeight) + { + if (diameter <= 0) + throw new ArgumentException("diameter must be positive", nameof(diameter)); + if (layerHeight <= 0) + throw new ArgumentException("layer height must be positive", nameof(layerHeight)); + + // Circular case (within tolerance) — no squish, width == diameter. + if (Math.Abs(layerHeight - diameter) < 1e-6) return diameter; + return (diameter * diameter) / layerHeight; + } + } +} diff --git a/CCL_Clay3DP/Models/Parameters.cs b/CCL_Clay3DP/Models/Parameters.cs index 5a0d8cc..72ed555 100644 --- a/CCL_Clay3DP/Models/Parameters.cs +++ b/CCL_Clay3DP/Models/Parameters.cs @@ -122,9 +122,25 @@ public class HelixParameters // Layer-slice only — ignored when SpiralSlice is true. Generates a // zigzag bracing pattern attached to the outer wall, anchored to a // virtual inner offset (computed but neither baked nor printed). - // FramesPerLayer also controls the zigzag point count in this mode. public bool OuterWallBracing { get; set; } = false; + // Number of times the bracing toolpath touches the outer wall + // around each layer when OuterWallBracing is on. Visually = the + // number of "kisses" the bracing makes with the wall, countable + // by eye in the viewport. The generator samples 2× this many + // points internally (alternating outer/inner) so each touch pairs + // with one inner anchor. Decoupled from FramesPerLayer (Issue #11); + // range 4..500 enforced at the UI. + public int BracingContactPoints { get; set; } = 60; + + // Bracing pattern selector (Issue #11 slice C). When false the + // bracing is a sharp triangle-wave zigzag; when true it's a smooth + // cosine wave with the same contact-point count (peaks = wall + // kisses, troughs = inner anchors). The sinusoidal path is gentler + // on robot acceleration since there are no corner reversals. + // Ignored unless OuterWallBracing is on. + public bool SinusoidalBracing { get; set; } = false; + // Spiral-slice only — ignored when SpiralSlice is false. When true the // build plate tilts so the tool follows the spiral curve like an // airplane: fuselage along the curve tangent T, wings along the diff --git a/CCL_Clay3DP/Settings/SettingsDialog.cs b/CCL_Clay3DP/Settings/SettingsDialog.cs index efc4d10..38d83f4 100644 --- a/CCL_Clay3DP/Settings/SettingsDialog.cs +++ b/CCL_Clay3DP/Settings/SettingsDialog.cs @@ -49,6 +49,8 @@ public class SettingsDialog : Dialog // spiral interpolator and stays in the model with default 0. private CheckBox _spiralSliceCheck; private CheckBox _outerWallBracingCheck; + private NumericStepper _bracingContactPoints; + private CheckBox _sinusoidalBracingCheck; private CheckBox _spiralFollowsCurveNormalCheck; private NumericStepper _layerHeight; private DropDown _directionDropDown; @@ -190,6 +192,22 @@ private void BuildUI() "the outer wall, anchored to a virtual inner offset. Improves " + "rigidity of layered prints.", }; + _outerWallBracingCheck.CheckedChanged += + (s, e) => UpdateToolpathFieldsEnabled(); + // Any integer 4..500 is valid — the generator samples 2× this + // many points internally so the alternating outer/inner pattern + // closes cleanly regardless of parity. + _bracingContactPoints = CreateStepper(4, 500, 1, 0); + _sinusoidalBracingCheck = new CheckBox + { + Text = "Sinusoidal bracing (off = zigzag)", + ToolTip = + "When checked, the bracing follows a smooth cosine wave " + + "(peaks = wall kisses, troughs = inner anchors) instead of " + + "a sharp triangle-wave zigzag. Smoother robot motion — no " + + "corner reversals — at the cost of denser internal sampling. " + + "Same contact-point count either way.", + }; _spiralFollowsCurveNormalCheck = new CheckBox { Text = "Spiral follows curve normal (Spiral Slice only)", @@ -223,6 +241,14 @@ private void BuildUI() "Default 3 - more = stronger adhesion but more material and time."), new TableRow(null, _spiralSliceCheck), new TableRow(null, _outerWallBracingCheck), + LabeledRow("Bracing contact points (4-500)", + _bracingContactPoints, + "Number of times the bracing toolpath touches the outer " + + "wall around each layer — i.e. the number of \"kisses\" " + + "you can count by eye in the viewport. Decoupled from " + + "Frames per layer so bracing density is independent of " + + "toolpath sampling."), + new TableRow(null, _sinusoidalBracingCheck), new TableRow(null, _spiralFollowsCurveNormalCheck), LabeledRow("Layer height (mm)", _layerHeight, "Vertical distance between layers. For clay, typically " + @@ -543,6 +569,14 @@ private void LoadValues() _layerHeight.Value = _settings.Helix.LayerHeight; _directionDropDown.SelectedIndex = _settings.Helix.DirectionCCW ? 0 : 1; _framesPerLayer.Value = _settings.Helix.FramesPerLayer; + // Clamp imported value to [4,500] so an out-of-spec settings.json + // doesn't trip the stepper. No parity constraint — the generator + // doubles this internally, guaranteeing an even sample count. + int clampedBracingPts = _settings.Helix.BracingContactPoints; + if (clampedBracingPts < 4) clampedBracingPts = 4; + if (clampedBracingPts > 500) clampedBracingPts = 500; + _bracingContactPoints.Value = clampedBracingPts; + _sinusoidalBracingCheck.Checked = _settings.Helix.SinusoidalBracing; // Robot _feedRate.Value = _settings.Robot.FeedRate; @@ -597,6 +631,8 @@ private void ApplyDialogToSettings() _settings.Helix.LayerHeight = _layerHeight.Value; _settings.Helix.DirectionCCW = _directionDropDown.SelectedIndex == 0; _settings.Helix.FramesPerLayer = (int)_framesPerLayer.Value; + _settings.Helix.BracingContactPoints = (int)_bracingContactPoints.Value; + _settings.Helix.SinusoidalBracing = _sinusoidalBracingCheck.Checked ?? false; // Robot _settings.Robot.FeedRate = _feedRate.Value; @@ -651,6 +687,7 @@ private void UpdateShrinkageFieldsEnabled() /// /// Gray out fields that only apply to one toolpath mode: /// - Outer Wall Bracing: layer-slice only → disabled AND auto-unchecked when Spiral Slice + /// - Bracing contact points: only when Layer Slice AND bracing is on /// - Spiral follows curve normal: spiral-only → disabled AND auto-unchecked when Layer Slice /// private void UpdateToolpathFieldsEnabled() @@ -670,6 +707,9 @@ private void UpdateToolpathFieldsEnabled() _spiralFollowsCurveNormalCheck.Checked = false; _suppressSpiralNormalWarning = false; } + bool bracingActive = !spiral && (_outerWallBracingCheck.Checked ?? false); + _bracingContactPoints.Enabled = bracingActive; + _sinusoidalBracingCheck.Enabled = bracingActive; } /// diff --git a/CCL_Clay3DP/UI/CCL_Clay3DPPanel.cs b/CCL_Clay3DP/UI/CCL_Clay3DPPanel.cs index bb5c420..9681986 100644 --- a/CCL_Clay3DP/UI/CCL_Clay3DPPanel.cs +++ b/CCL_Clay3DP/UI/CCL_Clay3DPPanel.cs @@ -1441,10 +1441,29 @@ private void RunLayerSlice(GeometrySelection selection) } // Outer Wall Bracing: preview inward arrows so the user can - // flip the side before picking the offset distance. - int numPoints = _settings.Helix.FramesPerLayer; + // flip the side before picking the offset distance. Contact- + // point count is decoupled from FramesPerLayer (Issue #11) + // so bracing density can be tuned independently of toolpath + // sampling. BracingContactPoints means "number of times the + // bracing touches the wall" — we double it for the generator + // so each pair (outer-touch, inner-anchor) accounts for one + // wall contact, matching what the user counts by eye. + int numPoints = 2 * _settings.Helix.BracingContactPoints; + // Issue #11 slice B: "french kiss" bead overlap. The bracing's + // wall-contact points sit HALF a bead width inboard from the + // contour centerline so the bracing bead's outer edge is + // tangent to the outer-wall bead's *centerline* (the contour + // itself), not its inner edge. This penetrates the outer- + // wall bead by W/2 → solid structural bond instead of an + // edge-to-edge tap. Computed once per slice from the + // current bead diameter + layer height. + double wallOffset = ClayBeadGeometry.ComputeWidth( + _settings.Clay.BeadDiameter, _settings.Helix.LayerHeight) * 0.5; const double previewArrowLength = 5.0; - BakePreviewArrows(contours, numPoints, previewArrowLength, false); + bool previewSinusoidal = _settings.Helix.SinusoidalBracing; + int previewContactPoints = _settings.Helix.BracingContactPoints; + BakePreviewArrows(contours, numPoints, previewArrowLength, false, + wallOffset, previewSinusoidal, previewContactPoints); RhinoApp.Wait(); bool flipInward = false; @@ -1461,7 +1480,8 @@ private void RunLayerSlice(GeometrySelection selection) if (flipInward) { ClearLayerObjects(RhinoDoc.ActiveDoc, "3DP::Bracing Vectors"); - BakePreviewArrows(contours, numPoints, previewArrowLength, true); + BakePreviewArrows(contours, numPoints, previewArrowLength, true, + wallOffset, previewSinusoidal, previewContactPoints); RhinoApp.Wait(); } @@ -1487,12 +1507,20 @@ private void RunLayerSlice(GeometrySelection selection) var goodContours = new List(); var results = new List(); int skipped = 0; + // Pattern selector (Issue #11 slice C). Sinusoidal generator + // takes contact-point count directly (one period per touch); + // zigzag generator takes 2× (alternating outer/inner). + bool sinusoidal = _settings.Helix.SinusoidalBracing; + int contactPoints = _settings.Helix.BracingContactPoints; for (int i = 0; i < contours.Count; i++) { try { - var r = Zigzag.ZigzagGenerator.BuildSingleContour( - contours[i], numPoints, distance, flipInward); + var r = sinusoidal + ? Zigzag.ZigzagGenerator.BuildSinusoidalSingleContour( + contours[i], contactPoints, distance, flipInward, wallOffset) + : Zigzag.ZigzagGenerator.BuildSingleContour( + contours[i], numPoints, distance, flipInward, wallOffset); results.Add(r); goodContours.Add(contours[i]); } @@ -1789,25 +1817,14 @@ private void OnPreviewClayModelClick(object sender, EventArgs e) } // Cross-section dimensions. Layer height squashes the bead - // vertically; mass / cross-section-area conservation means - // it spreads horizontally: - // π × (D/2)² = π × (W/2) × (H/2) - // → W = D² / H - // When H == D the bead stays circular (W = D); when H < D - // it widens into an ellipse with minor axis vertical. - double widthRadius, heightRadius; - if (Math.Abs(layerHeight - diameter) < 1e-6) - { - // Circle — preserved exactly so the existing optimised - // CreateFromCurvePipe path is used. - widthRadius = diameter * 0.5; - heightRadius = diameter * 0.5; - } - else - { - widthRadius = (diameter * diameter) / (2.0 * layerHeight); - heightRadius = layerHeight * 0.5; - } + // vertically; mass / cross-section-area conservation gives + // width via ClayBeadGeometry.ComputeWidth (single source + // of truth shared with the Outer Wall Bracing kiss offset). + double widthRadius = ClayBeadGeometry.ComputeWidth(diameter, layerHeight) * 0.5; + double heightRadius = layerHeight * 0.5; + // When H == D the helper returns D, so widthRadius == + // heightRadius == D/2 and we route through the optimised + // circular CreateFromCurvePipe path below. bool elliptical = Math.Abs(widthRadius - heightRadius) > 1e-6; // Source layers: covers every mode that produces a toolpath @@ -2013,7 +2030,8 @@ private void OnPreviewClayModelClick(object sender, EventArgs e) /// can see which side the algorithm picked as inward. /// private void BakePreviewArrows( - List contours, int numPoints, double length, bool flip) + List contours, int numPoints, double length, bool flip, + double wallOffset = 0.0, bool sinusoidal = false, int contactPoints = 0) { var doc = RhinoDoc.ActiveDoc; if (doc == null) return; @@ -2035,8 +2053,11 @@ private void BakePreviewArrows( if (contour == null) continue; try { - var r = Zigzag.ZigzagGenerator.BuildSingleContour( - contour, numPoints, length, flip); + var r = sinusoidal + ? Zigzag.ZigzagGenerator.BuildSinusoidalSingleContour( + contour, contactPoints, length, flip, wallOffset) + : Zigzag.ZigzagGenerator.BuildSingleContour( + contour, numPoints, length, flip, wallOffset); int n = Math.Min(r.OuterPoints.Count, r.InnerPoints.Count); for (int k = 0; k < n; k++) { diff --git a/CCL_Clay3DP/Zigzag/ZigzagGenerator.cs b/CCL_Clay3DP/Zigzag/ZigzagGenerator.cs index 794f768..4132f01 100644 --- a/CCL_Clay3DP/Zigzag/ZigzagGenerator.cs +++ b/CCL_Clay3DP/Zigzag/ZigzagGenerator.cs @@ -42,10 +42,12 @@ public static class ZigzagGenerator private const double CloseEndpointTol = 0.001; public static SimpleZigzagResult BuildSingleContour( - Curve contour, int numPoints, double inwardDistance, bool flipInward = false) + Curve contour, int numPoints, double inwardDistance, + bool flipInward = false, double wallOffset = 0.0) { if (contour == null) throw new Exception("Contour is null"); if (inwardDistance <= 0) throw new Exception("Inward distance must be positive"); + if (wallOffset < 0) throw new Exception("Wall offset must be non-negative"); if (numPoints < 4) throw new Exception("Need at least 4 points"); if (numPoints % 2 != 0) numPoints++; // even N closes the zigzag cleanly @@ -109,8 +111,15 @@ public static SimpleZigzagResult BuildSingleContour( : new Vector3d(tan.Y, -tan.X, 0); if (flipInward) inwardDir = -inwardDir; - outer.Add(p); - inner.Add(p + inwardDir * inwardDistance); + // wallOffset shifts the bracing's wall-contact point inward + // from the contour centerline so the bracing bead's outer + // edge tangentially meets the outer-wall bead at the + // contour centerline ("french kiss" overlap — Issue #11 + // slice B). Inner anchor follows by the same offset, so + // the user-facing inwardDistance is the tooth depth measured + // from the kiss point — not from the contour. + outer.Add(p + inwardDir * wallOffset); + inner.Add(p + inwardDir * (wallOffset + inwardDistance)); } // Zigzag alternation. For closed curves append start to close the @@ -144,6 +153,135 @@ public static SimpleZigzagResult BuildSingleContour( }; } + // Discretization density for the sinusoidal path: how many + // polyline samples we emit per full cosine period (= per pair of + // wall touch + inner anchor). 12 → a sample every 30° of phase, + // smooth enough that the polyline visually reads as a sine curve + // even on coarse zoom, without blowing up frame counts downstream. + private const int SinusoidalSamplesPerPeriod = 12; + + /// + /// Smooth-cosine bracing variant of . + /// Emits a closed (or open) polyline that swings inward from the + /// contour according to: + /// + /// offset(s) = wallOffset + (A/2) · (1 − cos(2π·N·s/L)) + /// + /// where N = , A = , + /// s = arclength along the contour, L = total contour length. + /// Peaks of the cosine (cos=+1 → offset = wallOffset) are the wall + /// kisses; troughs (cos=−1 → offset = wallOffset + A) are the inner + /// anchors. One period per contact point, so N wall touches around + /// the contour. Internally sampled at + /// points per period so the robot toolpath reads as a smooth wave + /// (no corner reversals → gentler on accel/decel). + /// + /// OuterPoints returned = the N peaks (= wall touches). + /// InnerPoints returned = the N troughs (= inner anchors). + /// + public static SimpleZigzagResult BuildSinusoidalSingleContour( + Curve contour, int contactPoints, double inwardDistance, + bool flipInward = false, double wallOffset = 0.0) + { + if (contour == null) throw new Exception("Contour is null"); + if (inwardDistance <= 0) throw new Exception("Inward distance must be positive"); + if (wallOffset < 0) throw new Exception("Wall offset must be non-negative"); + if (contactPoints < 2) throw new Exception("Need at least 2 contact points"); + + bool isClosed = contour.IsClosed + || contour.PointAtStart.DistanceTo(contour.PointAtEnd) < CloseEndpointTol; + + if (isClosed && !contour.IsClosed) + { + if (!contour.MakeClosed(CloseEndpointTol)) + isClosed = false; + } + + // Mirror BuildSingleContour's seam handling so the wave peaks + // stack across layers instead of drifting around the contour. + bool isCCW = true; + if (isClosed) + { + var orient = contour.ClosedCurveOrientation(Vector3d.ZAxis); + isCCW = orient != CurveOrientation.Clockwise; + + var areaProps = AreaMassProperties.Compute(contour); + if (areaProps != null) + { + var c = areaProps.Centroid; + var seamTarget = new Point3d(c.X + 10000, c.Y, c.Z); + if (contour.ClosestPoint(seamTarget, out double seamT)) + contour.ChangeClosedCurveSeam(seamT); + } + } + + int N = contactPoints; + int K = SinusoidalSamplesPerPeriod; + int totalSamples = N * K; + + // DivideByCount: closed → N params, open → N+1. Same convention + // as BuildSingleContour. + var ts = contour.DivideByCount(totalSamples, true); + if (ts == null || ts.Length < totalSamples) + throw new Exception("Could not divide contour into equal parts"); + + int count = ts.Length; + var samples = new List(count); + var peaks = new List(N); + var troughs = new List(N); + double halfAmplitude = inwardDistance * 0.5; + + for (int j = 0; j < count; j++) + { + double t = ts[j]; + Point3d p = contour.PointAt(t); + Vector3d tan = contour.TangentAt(t); + tan.Z = 0; + if (!tan.Unitize()) + { + samples.Add(p); + continue; + } + + Vector3d inwardDir = isCCW + ? new Vector3d(-tan.Y, tan.X, 0) + : new Vector3d(tan.Y, -tan.X, 0); + if (flipInward) inwardDir = -inwardDir; + + // phase = 2π · j / K — independent of N. j=0,K,2K,… land + // on peaks (cos=1 → offset=wallOffset = kiss); j=K/2,3K/2,… + // land on troughs (cos=−1 → offset=wallOffset+A = anchor). + double phase = 2.0 * Math.PI * j / K; + double offset = wallOffset + halfAmplitude * (1.0 - Math.Cos(phase)); + var pt = p + inwardDir * offset; + samples.Add(pt); + + // Pull out the peaks / troughs by phase position so the + // OuterPoints / InnerPoints visualizations show N each, + // independent of K. + if (j % K == 0) peaks.Add(pt); + else if (K % 2 == 0 && j % K == K / 2) troughs.Add(pt); + } + + // Closure dup — same convention as the zigzag generator so the + // robot returns to its starting kiss after each layer. + if (isClosed) samples.Add(samples[0]); + + // Inner curve through the troughs only (analog to the zigzag's + // innerLoop). Not printed; useful as a debug overlay. + var innerLoop = new List(troughs); + if (isClosed && innerLoop.Count > 0) innerLoop.Add(innerLoop[0]); + + return new SimpleZigzagResult + { + OuterPoints = peaks, + InnerPoints = troughs, + InnerCurve = innerLoop.Count >= 2 ? new PolylineCurve(innerLoop) : null, + Zigzag = new PolylineCurve(samples), + IsClosed = isClosed, + }; + } + /// /// Remove polyline vertices sitting in a hairpin fold, detected by a /// small shortcut ratio |AC|/(|AB|+|BC|). Iterates until no more diff --git a/README.md b/README.md index d7194a3..a7dec0d 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,15 @@ configuration in RoboDK. - **Spiral Slice** — continuous spiral between horizontal contours (vase mode, no Z-seam). - **Layer Slice** — discrete planar layer contours. With the optional - **Outer Wall Bracing** flag, each layer also gets a triangle-wave - bracing rib anchored to an inward-offset projection (the - projection itself is not baked or printed, only the outer wall - and the bracing). Robot print order per layer is + **Outer Wall Bracing** flag, each layer also gets a bracing rib + anchored to an inward-offset projection (the projection itself is + not baked or printed, only the outer wall and the bracing). The + bracing pattern is either a sharp triangle-wave **zigzag** (default) + or a smooth **cosine wave** (`Sinusoidal bracing` toggle); both + share the same contact-point count and "french kiss" half-bead + overlap with the outer-wall bead at each touch point. The number of + contact points is set independently of the toolpath sampling density + (`Bracing contact points`, Issue #11). Robot print order per layer is **Outer Toolpath → Bracing Toolpath**, then up to the next layer. Outer Wall Bracing only runs on ruled / extrudable geometry (cylinders, prisms, cones, planar extrusions); the algorithm @@ -153,9 +158,12 @@ CCL_Clay3DP/ │ ├─ PrintabilityResult.cs Score container + issue report │ └─ HeatmapDisplay.cs Vertex-colored mesh overlay in viewport ├─ Zigzag/ -│ └─ ZigzagGenerator.cs Outer Wall Bracing generator: triangle-wave -│ weave anchored to an inward in-plane projection, -│ with hairpin trim for concave geometry +│ └─ ZigzagGenerator.cs Outer Wall Bracing generators: triangle-wave +│ weave (BuildSingleContour) or smooth cosine +│ (BuildSinusoidalSingleContour). Both anchor +│ to an inward in-plane projection, apply a +│ half-bead kiss offset (Issue #11), and the +│ zigzag adds hairpin trim for concave geometry └─ RoboDK/ ├─ FrameSerializer.cs Frames + settings → temp JSON └─ RoboDKSubprocess.cs Generates & runs Python 3 script via RoboDK's @@ -172,7 +180,7 @@ settings) removes the others on the next Slice. |---|---|---| | `3DP::Spiral Toolpath` | The spiral curve (Spiral mode) | yes | | `3DP::Outer Toolpath` | Outer wall curve per layer (Layer mode) | yes | -| `3DP::Bracing Toolpath` | Triangle-wave rib per layer (Layer + Bracing) | yes | +| `3DP::Bracing Toolpath` | Zigzag or sinusoidal rib per layer (Layer + Bracing) | yes | | `3DP::Bracing Vectors` | Inward-direction arrows (Layer + Bracing) | no (flip preview) | | `3DP::Bracing Outer Points` | Sample points on outer wall (Layer + Bracing) | no | | `3DP::Bracing Inner Points` | Sample points on inward projection (Layer + Bracing) | no | @@ -294,9 +302,10 @@ Rhino via `_PlugInManager → Install…`. - **Clay material**: preset (Porcelain / Stoneware / Earthenware) or custom bead diameter, max overhang, bond ratio, density. - **Toolpath**: Spiral Slice (vase mode) or Layer Slice; Outer Wall - Bracing (Layer mode only, ruled-geometry only); Spiral follows - curve normal (Spiral mode only); layer height; frames per layer; - start angle; CCW/CW direction. + Bracing (Layer mode only, ruled-geometry only); Bracing contact + points (4–500, independent of Frames per layer); Sinusoidal bracing + (smooth cosine path vs. zigzag); Spiral follows curve normal (Spiral + mode only); layer height; frames per layer; CCW/CW direction. - **Robot / printer**: feed rate (mm/s), travel speed, spindle S value, nozzle tool (T10/T11/T12), RoboDK paths. - **Build Volume (mm)**: X min/max, Y min/max, Z height of the cell