Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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):**
Expand Down Expand Up @@ -268,6 +270,8 @@ classDiagram
int FramesPerLayer
bool SpiralSlice
bool OuterWallBracing
int BracingContactPoints
bool SinusoidalBracing
bool SpiralFollowsCurveNormal
double StartAngle
}
Expand Down
53 changes: 53 additions & 0 deletions CCL_Clay3DP/Models/ClayBeadGeometry.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
public static class ClayBeadGeometry
{
/// <summary>
/// Full deposited bead width in mm, given nominal bead diameter
/// (= nozzle diameter, by convention) and layer height.
/// </summary>
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;
}
}
}
18 changes: 17 additions & 1 deletion CCL_Clay3DP/Models/Parameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions CCL_Clay3DP/Settings/SettingsDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public class SettingsDialog : Dialog<bool>
// 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;
Expand Down Expand Up @@ -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)",
Expand Down Expand Up @@ -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 " +
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -651,6 +687,7 @@ private void UpdateShrinkageFieldsEnabled()
/// <summary>
/// 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
/// </summary>
private void UpdateToolpathFieldsEnabled()
Expand All @@ -670,6 +707,9 @@ private void UpdateToolpathFieldsEnabled()
_spiralFollowsCurveNormalCheck.Checked = false;
_suppressSpiralNormalWarning = false;
}
bool bracingActive = !spiral && (_outerWallBracingCheck.Checked ?? false);
_bracingContactPoints.Enabled = bracingActive;
_sinusoidalBracingCheck.Enabled = bracingActive;
}

/// <summary>
Expand Down
77 changes: 49 additions & 28 deletions CCL_Clay3DP/UI/CCL_Clay3DPPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}

Expand All @@ -1487,12 +1507,20 @@ private void RunLayerSlice(GeometrySelection selection)
var goodContours = new List<Curve>();
var results = new List<Zigzag.SimpleZigzagResult>();
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]);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2013,7 +2030,8 @@ private void OnPreviewClayModelClick(object sender, EventArgs e)
/// can see which side the algorithm picked as inward.
/// </summary>
private void BakePreviewArrows(
List<Curve> contours, int numPoints, double length, bool flip)
List<Curve> 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;
Expand All @@ -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++)
{
Expand Down
Loading
Loading