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