Files
OpenNest/Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberPostProcessor.cs
T
ajandClaude Opus 5.5 f9b2713420 feat(common-line): cut each shared line once through Plan Cutting and the CI Fiber post
N26-295's PT42 pairs saved spacing in slice 1 but every shared edge was
still cut twice, costing a pass per edge and risking a freed part
tipping under the head on the second pass.

Plan Cutting now cuts a plate's common lines once unless the plate is set
to cut them twice. Each group is cut member by member, owner before
partner (a captured dependency that search and replay enforce, so a kept
order cutting the partner first is a constraint conflict): the member's
holes, then the shared cuts it owns as open CommonLine centerline cuts
entered through the notch on the span's axis, then its remainder pieces,
each ramped in from the kerf the shared cut opened. Capture marks the
shared spans as tabs on the owned clean copy, so replay's contour
accounting proves each perimeter is cut exactly once; a lead that fits
at neither end refuses as UnsupportedGeometry naming the span, never
shortened. Material still comes from the drawing, so utilization,
quantity and overlap results do not change. Members are never
support-tabbed; locked or excluded members and broken groups refuse.

The CI Fiber post writes shared cuts in the cut-off form with a lead-in
and no G41/G42, and remainders in the contour form on their perimeter's
compensation side, named by a remainder comment. Cincinnati CL (whose
PreApplied mode offsets nothing) and Gravograph refuse such nests, as do
the desktop and console for any post without ICommonLinePostProcessor;
nothing falls back to a silent double cut. Plate > Arrange > Cut Common
Lines Twice keeps slice 1's behavior, persists with the plate and makes
an open plan stale.

Pre-post review now checks each span is cut once, on its centerline, by
the member cut first, and the rapid check treats a finished member's
remainders as an obstacle. Compensation start in void and cancel in
kerf are unconfirmed on the CI FIBER 4020; release waits on a 3/16
stainless coupon trial.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:51:42 -05:00

160 lines
6.3 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using OpenNest.CNC;
namespace OpenNest.Posts.CincinnatiCIFiber
{
/// <summary>
/// Post-processor for the CI Fiber family (e.g. the CI Fiber 4020 8kW with
/// nLight CLX / Beckhoff TF5200 / Precitec ProCutter). Named by machine
/// family, not table size: the same post drives other CI Fiber tables via
/// the configured table envelope. Emits the TF5200 skippable-macro program
/// structure of the machine sample (see <see cref="CIFiberProgramWriter"/>).
/// </summary>
public sealed class CIFiberPostProcessor
: IJsonConfigPostProcessor,
IMultiFilePostProcessor,
IPostVerificationSupport,
ICommonLinePostProcessor
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
};
public string Name => "Cincinnati CI Fiber";
public bool PreservesPlacedProgramOrder => true;
public string Author => "OpenNest";
public string Description =>
"CI Fiber family laser (TF5200 / nLight CLX), e.g. CI Fiber 4020 8kW";
public CIFiberPostConfig Config { get; private set; }
object IConfigurablePostProcessor.Config => Config;
/// <summary>Loads this PC's saved settings, else the shipped defaults (see <see cref="PostSettingsFile"/>).</summary>
public CIFiberPostProcessor()
{
var json = PostSettingsFile.Read(typeof(CIFiberPostProcessor).Assembly);
Config = json == null
? new CIFiberPostConfig()
: JsonSerializer.Deserialize<CIFiberPostConfig>(json, JsonOptions) ?? new CIFiberPostConfig();
}
public CIFiberPostProcessor(CIFiberPostConfig config)
{
Config = config ?? throw new ArgumentNullException(nameof(config));
}
public void SaveConfig() => PostSettingsFile.Write(typeof(CIFiberPostProcessor).Assembly, SerializeConfig());
public string SerializeConfig() => JsonSerializer.Serialize(Config, JsonOptions);
public void LoadConfig(string json) =>
Config =
JsonSerializer.Deserialize<CIFiberPostConfig>(json, JsonOptions)
?? throw new JsonException("The post settings are empty.");
/// <summary>
/// Writes every sheet into one program. A stream holds a single program,
/// so this throws when "One program per sheet" is on and the nest has more
/// than one sheet; use <see cref="Post(Nest, string)"/> for that.
/// </summary>
public void Post(Nest nest, Stream outputStream)
{
if (nest == null)
throw new ArgumentNullException(nameof(nest));
if (outputStream == null)
throw new ArgumentNullException(nameof(outputStream));
if (SplitsSheets(nest))
throw new InvalidOperationException(
"\"One program per sheet\" writes several files; post this nest to a file path."
);
var program = Render(w => new CIFiberProgramWriter(Config).Write(nest, w));
outputStream.Write(program, 0, program.Length);
}
/// <summary>
/// Writes the program(s) for the nest. With "One program per sheet" and more
/// than one sheet, <c>JOB.cnc</c> becomes <c>JOB-1.cnc</c>, <c>JOB-2.cnc</c>, ...;
/// otherwise the single program goes to <paramref name="outputFile"/>. All
/// programs are generated before any file is written, so a validation
/// failure leaves no partial output.
/// </summary>
public void Post(Nest nest, string outputFile)
{
if (nest == null)
throw new ArgumentNullException(nameof(nest));
if (string.IsNullOrEmpty(outputFile))
throw new ArgumentNullException(nameof(outputFile));
var files = GetOutputFiles(nest, outputFile);
var programs = new List<byte[]>();
if (files.Count == 1)
{
programs.Add(Render(w => new CIFiberProgramWriter(Config).Write(nest, w)));
}
else
{
var sheets = CIFiberProgramWriter.PostedSheets(nest);
var writer = new CIFiberProgramWriter(Config);
writer.Validate(sheets, singleProgram: false);
for (var i = 0; i < sheets.Count; i++)
{
var sheetNumber = i + 1;
var sheet = sheets[i];
programs.Add(Render(w => writer.WriteSheet(nest, sheet, sheetNumber, w)));
}
}
for (var i = 0; i < files.Count; i++)
File.WriteAllBytes(files[i], programs[i]);
}
public IReadOnlyList<string> GetOutputFiles(Nest nest, string outputFile)
{
if (nest == null)
throw new ArgumentNullException(nameof(nest));
if (string.IsNullOrEmpty(outputFile))
throw new ArgumentNullException(nameof(outputFile));
if (!SplitsSheets(nest))
return new[] { outputFile };
var dir = Path.GetDirectoryName(outputFile) ?? "";
var name = Path.GetFileNameWithoutExtension(outputFile);
var ext = Path.GetExtension(outputFile);
var count = CIFiberProgramWriter.PostedSheets(nest).Count;
return Enumerable
.Range(1, count)
.Select(n => Path.Combine(dir, $"{name}-{n}{ext}"))
.ToList();
}
private bool SplitsSheets(Nest nest) =>
Config.OneProgramPerSheet && CIFiberProgramWriter.PostedSheets(nest).Count > 1;
// CRLF file, ASCII (UTF-8 without BOM), matching the machine sample.
private static byte[] Render(Action<TextWriter> write)
{
using var ms = new MemoryStream();
using (var writer = new StreamWriter(ms, new UTF8Encoding(false), 1024, leaveOpen: true))
{
write(writer);
}
return ms.ToArray();
}
}
}