mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-30 23:31:09 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1af1d5433 | ||
|
|
d6135bf9d0 | ||
|
|
984b0419ab | ||
|
|
86b78393d6 | ||
|
|
7474eaa989 | ||
|
|
242ef88a97 | ||
|
|
5cd7d4132c |
No files matched your search
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"vpk": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"commands": [
|
||||||
|
"vpk"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,6 +96,11 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
artifacts/*.zip
|
artifacts/*.zip
|
||||||
artifacts/*.sha256
|
artifacts/*.sha256
|
||||||
|
artifacts/velopack/OpenNest-win-Setup.exe
|
||||||
|
artifacts/velopack/OpenNest-win-Portable.zip
|
||||||
|
artifacts/velopack/*-full.nupkg
|
||||||
|
artifacts/velopack/releases.win.json
|
||||||
|
artifacts/velopack/RELEASES
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
compression-level: 0
|
compression-level: 0
|
||||||
|
|||||||
@@ -70,6 +70,6 @@ Read the relevant contract before changing its behavior:
|
|||||||
- [Cut-time estimates](docs/cut-time-estimates.md): simulator/Calculate Cut Time acceleration planner and placeholder machine limits
|
- [Cut-time estimates](docs/cut-time-estimates.md): simulator/Calculate Cut Time acceleration planner and placeholder machine limits
|
||||||
- [Cincinnati CL](docs/cincinnati-post-output.md) and [CI Fiber](docs/cincinnati-ci-fiber-post-output.md)
|
- [Cincinnati CL](docs/cincinnati-post-output.md) and [CI Fiber](docs/cincinnati-ci-fiber-post-output.md)
|
||||||
- [Fill verification](docs/performance/fill-verification.md): opt-in benchmarks, frozen oracles, Debug-only counters and predictor initialization. Zero Release counters do not prove work removal.
|
- [Fill verification](docs/performance/fill-verification.md): opt-in benchmarks, frozen oracles, Debug-only counters and predictor initialization. Zero Release counters do not prove work removal.
|
||||||
- [Desktop auto-update](docs/auto-update.md): Velopack feed, `UpdateFeedUrl` setting and `Deploy-OpenNest.ps1`.
|
- [Desktop auto-update](docs/auto-update.md): GitHub Releases by default, `UpdateFeedUrl` for a self-hosted feed, release Velopack assets and `Deploy-OpenNest.ps1`.
|
||||||
- [Undo and Redo](docs/undo-redo.md): whole-nest history, the recording rule, what a restore rebuilds and what is excluded.
|
- [Undo and Redo](docs/undo-redo.md): whole-nest history, the recording rule, what a restore rebuilds and what is excluded.
|
||||||
- [Desktop help](docs/desktop-help.md): WebView2 help window, F1 topic IDs and the pinned help release both packaging scripts install.
|
- [Desktop help](docs/desktop-help.md): WebView2 help window, F1 topic IDs and the pinned help release both packaging scripts install.
|
||||||
@@ -447,6 +447,8 @@ public sealed class CuttingPlanProposal
|
|||||||
Quadrant = source.Quadrant,
|
Quadrant = source.Quadrant,
|
||||||
PartSpacing = source.PartSpacing,
|
PartSpacing = source.PartSpacing,
|
||||||
EdgeSpacing = source.EdgeSpacing,
|
EdgeSpacing = source.EdgeSpacing,
|
||||||
|
// View > Draw Support shows the proposal on the same bed.
|
||||||
|
SupportBed = source.SupportBed,
|
||||||
};
|
};
|
||||||
// Machine clamps stay visible while inspecting the proposal; zones are immutable.
|
// Machine clamps stay visible while inspecting the proposal; zones are immutable.
|
||||||
foreach (var zone in source.ClampZones)
|
foreach (var zone in source.ClampZones)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ internal static class JointCuttingPlanSearch
|
|||||||
{
|
{
|
||||||
var inserted = CutOffInsertion.Insert(snapshot, outcome.Order, attached, walk.CountExpansion, token);
|
var inserted = CutOffInsertion.Insert(snapshot, outcome.Order, attached, walk.CountExpansion, token);
|
||||||
if (inserted != null)
|
if (inserted != null)
|
||||||
return walk.Ready(inserted);
|
return walk.Ready(walk.FollowSegments(inserted));
|
||||||
}
|
}
|
||||||
catch (BudgetExceededException)
|
catch (BudgetExceededException)
|
||||||
{
|
{
|
||||||
@@ -196,26 +196,35 @@ internal static class JointCuttingPlanSearch
|
|||||||
}
|
}
|
||||||
|
|
||||||
// "Cut the blocked part before every part whose cut contour its approach crossed", unless that
|
// "Cut the blocked part before every part whose cut contour its approach crossed", unless that
|
||||||
// would contradict an order already required. False when nothing new was learned.
|
// would contradict an order already required. When every such rule would (a sheet separator
|
||||||
|
// is cut after everything), the blocked part cannot come earlier, so the tool must arrive
|
||||||
|
// from beyond those parts instead: "cut the part it left from before them". False when
|
||||||
|
// nothing new was learned.
|
||||||
private static bool Learn(Attempt attempt, HashSet<int>[] prerequisites)
|
private static bool Learn(Attempt attempt, HashSet<int>[] prerequisites)
|
||||||
{
|
{
|
||||||
if (attempt.Blocked is not int blocked)
|
var rules = Rules(attempt, prerequisites).ToArray();
|
||||||
return false;
|
foreach (var (first, then) in rules)
|
||||||
var learned = false;
|
prerequisites[then].Add(first);
|
||||||
foreach (var crossed in attempt.Crossed.Order())
|
return rules.Length != 0;
|
||||||
{
|
|
||||||
if (crossed == blocked || prerequisites[crossed].Contains(blocked) || Precedes(crossed, blocked, prerequisites))
|
|
||||||
continue;
|
|
||||||
prerequisites[crossed].Add(blocked);
|
|
||||||
learned = true;
|
|
||||||
}
|
|
||||||
return learned;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// True when a crossing of the blocked part's approach would teach a new order rule.
|
// True when a crossing of the blocked part's approach would teach a new order rule.
|
||||||
private static bool CanLearn(Attempt attempt, HashSet<int>[] prerequisites) =>
|
private static bool CanLearn(Attempt attempt, HashSet<int>[] prerequisites) =>
|
||||||
attempt.Blocked is int blocked && attempt.Crossed.Any(crossed => crossed != blocked
|
Rules(attempt, prerequisites).Any();
|
||||||
&& !prerequisites[crossed].Contains(blocked) && !Precedes(crossed, blocked, prerequisites));
|
|
||||||
|
// The rules Learn would add, as (cut first, cut then) pairs.
|
||||||
|
private static IEnumerable<(int First, int Then)> Rules(Attempt attempt, HashSet<int>[] prerequisites)
|
||||||
|
{
|
||||||
|
if (attempt.Blocked is not int blocked)
|
||||||
|
return [];
|
||||||
|
var before = New(blocked);
|
||||||
|
return before.Length != 0 || attempt.LastCut?.SourceOrdinal is not int departed ? before : New(departed);
|
||||||
|
|
||||||
|
(int, int)[] New(int first) => attempt.Crossed.Order()
|
||||||
|
.Where(crossed => crossed != blocked && crossed != first && !prerequisites[crossed].Contains(first)
|
||||||
|
&& !Precedes(crossed, first, prerequisites))
|
||||||
|
.Select(crossed => (first, crossed)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
// True when 'first' must already come before 'second' through the prerequisite chain.
|
// True when 'first' must already come before 'second' through the prerequisite chain.
|
||||||
private static bool Precedes(int first, int second, HashSet<int>[] prerequisites)
|
private static bool Precedes(int first, int second, HashSet<int>[] prerequisites)
|
||||||
@@ -334,10 +343,10 @@ internal static class JointCuttingPlanSearch
|
|||||||
}
|
}
|
||||||
// A cutoff is a fixed program cut the same way wherever it goes, so replanning
|
// A cutoff is a fixed program cut the same way wherever it goes, so replanning
|
||||||
// the parts before it can only change where its approach starts. Cutting it
|
// the parts before it can only change where its approach starts. Cutting it
|
||||||
// earlier never makes its own cut harder; learn that rather than stall here.
|
// earlier never makes its own cut harder, and a sheet separator that cannot come
|
||||||
|
// earlier needs the approach from elsewhere; learn that rather than stall here.
|
||||||
if (prerequisites != null && sequence != null && attempt.Blocked is int blocked
|
if (prerequisites != null && sequence != null && attempt.Blocked is int blocked
|
||||||
&& snapshot.Placements[blocked] is { IsCutOff: true, SeparatesSheet: false }
|
&& snapshot.Placements[blocked].IsCutOff && CanLearn(attempt, prerequisites))
|
||||||
&& CanLearn(attempt, prerequisites))
|
|
||||||
return attempt;
|
return attempt;
|
||||||
if (attempt.Advance(node))
|
if (attempt.Advance(node))
|
||||||
progressExpansions = Expansions;
|
progressExpansions = Expansions;
|
||||||
@@ -523,8 +532,9 @@ internal static class JointCuttingPlanSearch
|
|||||||
CountExpansion(source);
|
CountExpansion(source);
|
||||||
var checker = node.Checker.Copy();
|
var checker = node.Checker.Copy();
|
||||||
if (Check(cut, cut.Execution, node.Position, checker, attempt))
|
if (Check(cut, cut.Execution, node.Position, checker, attempt))
|
||||||
// A segment is transparent: the part after it ranks its entries and hole route
|
// A segment is transparent to the part after it: that part ranks its outside entry
|
||||||
// from where the tool was before the segment, as planned without it.
|
// from where the tool was before the segment, as planned without it. Its hole
|
||||||
|
// route starts where the tool is, the segment's end, as FollowSegments plans it.
|
||||||
yield return new(new([.. node.Order, cut], cut.Execution.DeparturePoint, checker, null, node,
|
yield return new(new([.. node.Order, cut], cut.Execution.DeparturePoint, checker, null, node,
|
||||||
IsSegment(cut) ? node.RoutePosition : null),
|
IsSegment(cut) ? node.RoutePosition : null),
|
||||||
cut.Execution.RapidDistanceFrom(node.Position), cut.SourceOrdinal, -1, -1);
|
cut.Execution.RapidDistanceFrom(node.Position), cut.SourceOrdinal, -1, -1);
|
||||||
@@ -559,7 +569,7 @@ internal static class JointCuttingPlanSearch
|
|||||||
}
|
}
|
||||||
var preference = node.Active?.Preference;
|
var preference = node.Active?.Preference;
|
||||||
if (node.Active?.Perimeter is { } outside && preference == null)
|
if (node.Active?.Perimeter is { } outside && preference == null)
|
||||||
preference = PlanHoles(node.Active, outside);
|
preference = PlanHoles(node.Active.Source, node.Active.Boundary.Position, outside);
|
||||||
var choices = node.Active?.Choices ?? [];
|
var choices = node.Active?.Choices ?? [];
|
||||||
var arrival = node.Active?.Arrival ?? node.Position;
|
var arrival = node.Active?.Arrival ?? node.Position;
|
||||||
var before = node.Active?.Before ?? node.Checker;
|
var before = node.Active?.Before ?? node.Checker;
|
||||||
@@ -1216,12 +1226,13 @@ internal static class JointCuttingPlanSearch
|
|||||||
return ordered;
|
return ordered;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HolePreference PlanHoles(ActivePart active, ContourChoice perimeter)
|
// The hole route and preferred hole entries from the tool's global arrival onto the part,
|
||||||
|
// ending at the outside entry.
|
||||||
|
private HolePreference PlanHoles(FixedProgramPlacement source, Vector arrival, ContourChoice perimeter)
|
||||||
{
|
{
|
||||||
var source = active.Source;
|
|
||||||
var prepared = source.Prepared;
|
var prepared = source.Prepared;
|
||||||
var holes = Enumerable.Range(0, prepared.PerimeterOrdinal).ToArray();
|
var holes = Enumerable.Range(0, prepared.PerimeterOrdinal).ToArray();
|
||||||
var localArrival = active.Boundary.RoutePosition - source.Location;
|
var localArrival = arrival - source.Location;
|
||||||
var centres = CuttingHoleOrder.Centres(prepared, localArrival, token);
|
var centres = CuttingHoleOrder.Centres(prepared, localArrival, token);
|
||||||
var endpoint = perimeter.Point;
|
var endpoint = perimeter.Point;
|
||||||
var entries = new Dictionary<int, ContourChoice>();
|
var entries = new Dictionary<int, ContourChoice>();
|
||||||
@@ -1255,6 +1266,72 @@ internal static class JointCuttingPlanSearch
|
|||||||
return new(route, entries, pierces, endpoint);
|
return new(route, entries, pierces, endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The order with segments placed, where each holed part cut straight after a segment
|
||||||
|
/// takes the hole route and entries planned from the segment's end, where the tool
|
||||||
|
/// really is. The free search planned the part as if the segment were not there, from
|
||||||
|
/// where the tool was before it. The part's outside entry and the order stay, so the
|
||||||
|
/// route is still the one planned without the lines; a kept order replanned later plans
|
||||||
|
/// the same holes from the same point. A part keeps its planned holes when the new
|
||||||
|
/// route has no preferred entry for every hole or does not pass the checks.
|
||||||
|
/// </summary>
|
||||||
|
internal IReadOnlyList<FixedProgramPlacement> FollowSegments(IReadOnlyList<FixedProgramPlacement> order)
|
||||||
|
{
|
||||||
|
var followed = order.ToArray();
|
||||||
|
var checker = new ReleasedContourState(reportMissingLeadIns: false);
|
||||||
|
var position = snapshot.StartPoint;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (var index = 0; index < followed.Length; index++)
|
||||||
|
{
|
||||||
|
if (index > 0 && IsSegment(followed[index - 1])
|
||||||
|
&& FromSegment(followed[index], position, checker) is { } replanned)
|
||||||
|
followed[index] = replanned;
|
||||||
|
var placement = followed[index];
|
||||||
|
checker.Check(placement.Execution, position, placement.SourceOrdinal + 1, placement.IsCutOff,
|
||||||
|
placement.FixedMotions(placement.Execution), token);
|
||||||
|
position = placement.Execution.DeparturePoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (BudgetExceededException)
|
||||||
|
{
|
||||||
|
// The plan is complete without this refinement; keep it as placed.
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
return followed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The part re-planned from arrival with its outside entry kept, or null to keep it.
|
||||||
|
private FixedProgramPlacement FromSegment(FixedProgramPlacement placement, Vector arrival,
|
||||||
|
ReleasedContourState checker)
|
||||||
|
{
|
||||||
|
var source = snapshot.Placements[placement.SourceOrdinal];
|
||||||
|
var prepared = source.Prepared;
|
||||||
|
if (prepared is not { HasCutPerimeter: true } || prepared.PerimeterOrdinal == 0
|
||||||
|
|| placement.ContourChoices.Count != prepared.Count
|
||||||
|
|| placement.ContourChoices[^1].ContourOrdinal != prepared.PerimeterOrdinal)
|
||||||
|
return null;
|
||||||
|
var perimeter = placement.ContourChoices[^1];
|
||||||
|
var preference = PlanHoles(source, arrival, perimeter);
|
||||||
|
if (preference.Entries.Count != prepared.PerimeterOrdinal)
|
||||||
|
return null;
|
||||||
|
var choices = preference.Route.Select(hole => preference.Entries[hole]).Append(perimeter).ToArray();
|
||||||
|
Program program;
|
||||||
|
OwnedExecution execution;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CountExpansion(source);
|
||||||
|
program = prepared.Emit(choices);
|
||||||
|
execution = ExecutionMotionReader.Read(program, source.Location, arrival, token);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException or ArithmeticException or NotSupportedException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Check(source, execution, arrival, checker.Copy(), new Attempt(null), choices)
|
||||||
|
? source.Propose(program, execution, choices, token) : null;
|
||||||
|
}
|
||||||
|
|
||||||
// The placements cut at a node, and the excluded ones, which count as cut throughout.
|
// The placements cut at a node, and the excluded ones, which count as cut throughout.
|
||||||
private HashSet<int> Finished(Node node)
|
private HashSet<int> Finished(Node node)
|
||||||
{
|
{
|
||||||
@@ -1568,8 +1645,8 @@ internal static class JointCuttingPlanSearch
|
|||||||
ContourChoice Perimeter = null, HolePreference Preference = null);
|
ContourChoice Perimeter = null, HolePreference Preference = null);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A search state; Previous links a whole-part boundary to the boundary before it. Anchor,
|
/// A search state; Previous links a whole-part boundary to the boundary before it. Anchor,
|
||||||
/// when set, is where the tool was before the cut-off segments cut last: entries and hole
|
/// when set, is where the tool was before the cut-off segments cut last: outside entries
|
||||||
/// routes rank from <see cref="RoutePosition"/>, every rapid is checked from Position.
|
/// rank from <see cref="RoutePosition"/>; hole routes and every rapid start from Position.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private sealed record Node(FixedProgramPlacement[] Order, Vector Position, ReleasedContourState Checker,
|
private sealed record Node(FixedProgramPlacement[] Order, Vector Position, ReleasedContourState Checker,
|
||||||
ActivePart Active, Node Previous, Vector? Anchor = null)
|
ActivePart Active, Node Previous, Vector? Anchor = null)
|
||||||
|
|||||||
@@ -19,4 +19,17 @@ public class ClampZonePreviewTests
|
|||||||
Assert.Equal(2, preview.ClampZones.Count);
|
Assert.Equal(2, preview.ClampZones.Count);
|
||||||
Assert.Null(preview.ClampRail); // a detached display copy never re-resolves
|
Assert.Null(preview.ClampRail); // a detached display copy never re-resolves
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreviewCarriesThePlatesSupportBed()
|
||||||
|
{
|
||||||
|
var plate = BestEffortCuttingPlanTests.Plate(BestEffortCuttingPlanTests.TouchingContours());
|
||||||
|
plate.SupportBed = new OpenNest.Support.SupportGrid(25, 100, 2, 2, stabilityMargin: 0.1);
|
||||||
|
|
||||||
|
var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan();
|
||||||
|
var preview = proposal.BuildPreview(0);
|
||||||
|
|
||||||
|
Assert.NotNull(preview);
|
||||||
|
Assert.Same(plate.SupportBed, preview.SupportBed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -109,6 +109,65 @@ public class CutOffSegmentPlanningTests
|
|||||||
Assert.Equal(result.RapidDistance, kept.RapidDistance, 6);
|
Assert.Equal(result.RapidDistance, kept.RapidDistance, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A frame with two small holes on its left strip, one above the other, and a window,
|
||||||
|
// between a locked square A at the top left, cut before it, and a locked square B to its
|
||||||
|
// right, cut after it. A line through the frame's right strip stops halfway up, so its one
|
||||||
|
// segment runs from the bottom edge to just below the frame and is cut straight before it.
|
||||||
|
// Planned without the segment, from A, the frame would cut its upper hole first; the tool
|
||||||
|
// is really at the bottom, so it cuts the lower hole first instead of crossing its own
|
||||||
|
// rapid. Replanned with the order kept, the frame plans the same holes from the same point.
|
||||||
|
[Fact]
|
||||||
|
public void PartAfterSegment_PlansItsHolesFromTheSegmentsEnd()
|
||||||
|
{
|
||||||
|
var parameters = ExplicitContourTests.Parameters();
|
||||||
|
var nest = new Nest();
|
||||||
|
var plate = nest.CreatePlate();
|
||||||
|
plate.Size = new Size(60, 120);
|
||||||
|
plate.Parts.Add(LockedSquare("A", new Vector(2, 42), topLeft: false));
|
||||||
|
var frame = new Part(new Drawing("frame", Frame()), new Vector(30, 5));
|
||||||
|
plate.Parts.Add(frame);
|
||||||
|
plate.Parts.Add(LockedSquare("B", new Vector(80, 40), topLeft: false));
|
||||||
|
var cutOff = new CutOff(new Vector(65, 0), CutOffAxis.Vertical) { EndLimit = 25 };
|
||||||
|
plate.CutOffs.Add(cutOff);
|
||||||
|
plate.RegenerateCutOffs(new CutOffSettings());
|
||||||
|
PlateSequencing.PlaceCutOffs(plate, [cutOff]);
|
||||||
|
Assert.Single(CutOffSegments.Of(plate, cutOff));
|
||||||
|
|
||||||
|
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, confirmedParameters: parameters));
|
||||||
|
|
||||||
|
Assert.True(result.Status == CuttingPlanStatus.Ready && result.IndependentlyReplayed, Describe(result));
|
||||||
|
var order = result.ProposedOrder.Select(o => o.SourcePart.BaseDrawing.IsCutOff ? "s" : o.SourcePart.BaseDrawing.Name);
|
||||||
|
Assert.Equal("A s frame B", string.Join(" ", order));
|
||||||
|
var planned = result.ProposedOrder[2];
|
||||||
|
var rapids = planned.Execution.Motions.Where(m => m.Rapid).Select(m => m.End).ToArray();
|
||||||
|
// Into the lower hole (y 14.5 to 15.1 on the sheet), then the upper one (34.5 to 35.1).
|
||||||
|
Assert.InRange(rapids[0].Y, 14, 16);
|
||||||
|
Assert.InRange(rapids[1].Y, 34, 36);
|
||||||
|
|
||||||
|
Assert.Equal(CuttingCommitStatus.Applied, CuttingPlanService.Apply([result]).Status);
|
||||||
|
var kept = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, confirmedParameters: parameters,
|
||||||
|
preservePartOrder: true));
|
||||||
|
Assert.True(kept.Status == CuttingPlanStatus.Ready && kept.IndependentlyReplayed, Describe(kept));
|
||||||
|
Assert.Equal(result.RapidDistance, kept.RapidDistance, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 40 x 40 with 0.6 square holes at (2.5, 9.5) and (2.5, 29.5) and an 18 x 30 window.
|
||||||
|
private static Program Frame()
|
||||||
|
{
|
||||||
|
var program = new Program();
|
||||||
|
void Loop(params Vector[] points)
|
||||||
|
{
|
||||||
|
program.MoveTo(points[0].X, points[0].Y);
|
||||||
|
foreach (var point in points.Skip(1).Append(points[0]))
|
||||||
|
program.LineTo(point.X, point.Y);
|
||||||
|
}
|
||||||
|
Loop(new(0, 0), new(0, 40), new(40, 40), new(40, 0));
|
||||||
|
foreach (var y in new[] { 29.5, 9.5 })
|
||||||
|
Loop(new(2.5, y), new(3.1, y), new(3.1, y + 0.6), new(2.5, y + 0.6));
|
||||||
|
Loop(new(12, 5), new(30, 5), new(30, 35), new(12, 35));
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
private const double Gap = 0.25, Width = 10.6, Neck = 5.6, Height = 20, Margin = 0.25;
|
private const double Gap = 0.25, Width = 10.6, Neck = 5.6, Height = 20, Margin = 0.25;
|
||||||
// The slanted shoulders need more horizontal room than the gap.
|
// The slanted shoulders need more horizontal room than the gap.
|
||||||
private const double Pitch = (Width + Neck) / 2 + 2 * Gap;
|
private const double Pitch = (Width + Neck) / 2 + 2 * Gap;
|
||||||
|
|||||||
@@ -79,6 +79,43 @@ public class CuttingDependencyTests
|
|||||||
&& f.Message.Contains("before the sheet is separated"));
|
&& f.Message.Contains("before the sheet is separated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A column of two wide parts under three narrow ones, beside a sheet separator at X 12.5
|
||||||
|
// pierced at (12.5, 0). Climbing the column ends at the top, where the straight approach to
|
||||||
|
// that pierce crosses the wide parts, and the separator cannot be cut any earlier. The search
|
||||||
|
// learns the other way round, the part the approach left from before the parts it crossed,
|
||||||
|
// and so finishes beside the pierce. It used to learn nothing and spend its whole budget.
|
||||||
|
[Fact]
|
||||||
|
public void Plan_ApproachIntoASeparatorCrossingCutParts_LearnsToLeaveFromBeyondThem()
|
||||||
|
{
|
||||||
|
var (plate, column, separator) = ColumnBesideSeparator();
|
||||||
|
|
||||||
|
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate,
|
||||||
|
confirmedParameters: ExplicitContourTests.Parameters()));
|
||||||
|
|
||||||
|
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||||
|
var order = result.ProposedOrder.Select(o => o.SourcePart).ToList();
|
||||||
|
Assert.Same(separator, order[^1]);
|
||||||
|
Assert.Contains(order[^2], column[..2]);
|
||||||
|
Assert.True(result.Expansions < 400, Describe(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (Plate, Part[] Column, Part Separator) ColumnBesideSeparator()
|
||||||
|
{
|
||||||
|
var nest = new Nest();
|
||||||
|
var plate = nest.CreatePlate();
|
||||||
|
plate.Size = new Size(100, 100);
|
||||||
|
Part[] column =
|
||||||
|
[
|
||||||
|
Rectangle(2, 2, 10, 10), Rectangle(2, 14, 10, 10),
|
||||||
|
Rectangle(2, 26, 5, 10), Rectangle(2, 38, 5, 10), Rectangle(2, 50, 5, 10),
|
||||||
|
];
|
||||||
|
foreach (var part in column)
|
||||||
|
plate.Parts.Add(part);
|
||||||
|
plate.CutOffs.Add(new CutOff(new Vector(12.5, 0), CutOffAxis.Vertical));
|
||||||
|
plate.RegenerateCutOffs(new CutOffSettings());
|
||||||
|
return (plate, column, plate.Parts.Single(p => p.BaseDrawing.IsCutOff));
|
||||||
|
}
|
||||||
|
|
||||||
// The tour cuts B, then the locked rectangle A (leaving right), then the cutoff pierced at
|
// The tour cuts B, then the locked rectangle A (leaving right), then the cutoff pierced at
|
||||||
// (10, 0) below-left of A: that approach crosses A. Replanning B's entries can never fix it,
|
// (10, 0) below-left of A: that approach crosses A. Replanning B's entries can never fix it,
|
||||||
// so the search learns "cutoff first" at once instead of retrying each of B's entries.
|
// so the search learns "cutoff first" at once instead of retrying each of B's entries.
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using OpenNest.Bending;
|
||||||
|
using OpenNest.Controls;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
|
namespace OpenNest.WinForms.Tests.Controls;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bends are kept in the drawing's source frame while its program starts at the origin, so a
|
||||||
|
/// placed part draws them with <c>Source.Offset</c> removed.
|
||||||
|
/// </summary>
|
||||||
|
public class BendLinePlacementTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void BendToWorld_RemovesSourceOffsetBeforePlacing()
|
||||||
|
{
|
||||||
|
var drawing = new Drawing("bent", new CNC.Program());
|
||||||
|
drawing.Source.Offset = new Vector(100, 50);
|
||||||
|
// In the source frame the bend runs across the part at local x = 5.
|
||||||
|
var bend = new Bend { StartPoint = new Vector(105, 50), EndPoint = new Vector(105, 60) };
|
||||||
|
var part = new Part(drawing, new Vector(10, 20));
|
||||||
|
|
||||||
|
Assert.Equal(new Vector(15, 20), PlateRenderer.BendToWorld(part, bend.StartPoint));
|
||||||
|
Assert.Equal(new Vector(15, 30), PlateRenderer.BendToWorld(part, bend.EndPoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BendToWorld_RotatesAboutTheProgramOrigin()
|
||||||
|
{
|
||||||
|
var drawing = new Drawing("bent", new CNC.Program());
|
||||||
|
drawing.Source.Offset = new Vector(100, 50);
|
||||||
|
var part = new Part(drawing, new Vector(10, 20));
|
||||||
|
part.Rotate(Angle.HalfPI, part.Location);
|
||||||
|
|
||||||
|
var world = PlateRenderer.BendToWorld(part, new Vector(105, 50));
|
||||||
|
|
||||||
|
Assert.Equal(10, world.X, 9);
|
||||||
|
Assert.Equal(25, world.Y, 9);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -465,25 +465,25 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
foreach (var bend in part.BaseDrawing.Bends)
|
foreach (var bend in part.BaseDrawing.Bends)
|
||||||
{
|
{
|
||||||
var start = bend.StartPoint;
|
var pt1 = view.PointWorldToGraph(BendToWorld(part, bend.StartPoint));
|
||||||
var end = bend.EndPoint;
|
var pt2 = view.PointWorldToGraph(BendToWorld(part, bend.EndPoint));
|
||||||
|
|
||||||
if (part.Rotation != 0)
|
|
||||||
{
|
|
||||||
start = start.Rotate(part.Rotation);
|
|
||||||
end = end.Rotate(part.Rotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
start = start + part.Location;
|
|
||||||
end = end + part.Location;
|
|
||||||
|
|
||||||
var pt1 = view.PointWorldToGraph(start);
|
|
||||||
var pt2 = view.PointWorldToGraph(end);
|
|
||||||
|
|
||||||
g.DrawLine(bendPen, pt1, pt2);
|
g.DrawLine(bendPen, pt1, pt2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a bend point to world space. Bends stay in the drawing's source frame while the
|
||||||
|
/// program is moved by -<c>Source.Offset</c>, so the offset comes off before placing.
|
||||||
|
/// </summary>
|
||||||
|
internal static Vector BendToWorld(Part part, Vector point)
|
||||||
|
{
|
||||||
|
var local = point - part.BaseDrawing.Source.Offset;
|
||||||
|
if (part.Rotation != 0)
|
||||||
|
local = local.Rotate(part.Rotation);
|
||||||
|
return local + part.Location;
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawEtchMarks(Graphics g, Part part, bool muted)
|
private void DrawEtchMarks(Graphics g, Part part, bool muted)
|
||||||
{
|
{
|
||||||
if (
|
if (
|
||||||
@@ -501,17 +501,8 @@ namespace OpenNest.Controls
|
|||||||
if (bend.Direction != BendDirection.Up)
|
if (bend.Direction != BendDirection.Up)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var start = bend.StartPoint;
|
var start = BendToWorld(part, bend.StartPoint);
|
||||||
var end = bend.EndPoint;
|
var end = BendToWorld(part, bend.EndPoint);
|
||||||
|
|
||||||
if (part.Rotation != 0)
|
|
||||||
{
|
|
||||||
start = start.Rotate(part.Rotation);
|
|
||||||
end = end.Rotate(part.Rotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
start = start + part.Location;
|
|
||||||
end = end + part.Location;
|
|
||||||
|
|
||||||
var length = bend.Length;
|
var length = bend.Length;
|
||||||
var angle = bend.StartPoint.AngleTo(bend.EndPoint) + part.Rotation;
|
var angle = bend.StartPoint.AngleTo(bend.EndPoint) + part.Rotation;
|
||||||
|
|||||||
@@ -771,10 +771,11 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
renderer.DrawActiveWorkArea(e.Graphics);
|
renderer.DrawActiveWorkArea(e.Graphics);
|
||||||
renderer.DrawDebugRemnants(e.Graphics);
|
renderer.DrawDebugRemnants(e.Graphics);
|
||||||
if (DrawSupport)
|
|
||||||
supportOverlay.Draw(e.Graphics, this);
|
|
||||||
DrawOverlapOverlay(e.Graphics);
|
DrawOverlapOverlay(e.Graphics);
|
||||||
}
|
}
|
||||||
|
// The support overlay follows the displayed plate, so it shows a proposal's tabbed parts too.
|
||||||
|
if (DrawSupport)
|
||||||
|
supportOverlay.Draw(e.Graphics, this);
|
||||||
// Plan Cutting marks findings on its proposal as well as on the live plate.
|
// Plan Cutting marks findings on its proposal as well as on the live plate.
|
||||||
validationOverlay.Draw(e.Graphics);
|
validationOverlay.Draw(e.Graphics);
|
||||||
if (!HasProposalDisplay)
|
if (!HasProposalDisplay)
|
||||||
|
|||||||
@@ -49,8 +49,10 @@ namespace OpenNest.Controls
|
|||||||
using var supportedPen = new Pen(SupportedColor, 2f);
|
using var supportedPen = new Pen(SupportedColor, 2f);
|
||||||
using var tippingPen = new Pen(TippingColor, 2f);
|
using var tippingPen = new Pen(TippingColor, 2f);
|
||||||
using var tabBrush = new SolidBrush(TabColor);
|
using var tabBrush = new SolidBrush(TabColor);
|
||||||
// Tab points keep clear of inside corners as Plan Cutting's do.
|
// Tab points keep clear of inside corners as Plan Cutting's do. A proposal preview has no
|
||||||
var clearance = SupportAnalyzer.InsideCornerClearance(plate.CuttingParameters?.TabConfig?.Size ?? 0);
|
// parameters of its own, so it uses the live plate's.
|
||||||
|
var parameters = plate.CuttingParameters ?? view.Plate?.CuttingParameters;
|
||||||
|
var clearance = SupportAnalyzer.InsideCornerClearance(parameters?.TabConfig?.Size ?? 0);
|
||||||
foreach (var part in plate.Parts)
|
foreach (var part in plate.Parts)
|
||||||
{
|
{
|
||||||
if (part?.BaseDrawing == null || part.BaseDrawing.IsCutOff)
|
if (part?.BaseDrawing == null || part.BaseDrawing.IsCutOff)
|
||||||
|
|||||||
Generated
+1
-1
@@ -267,7 +267,7 @@
|
|||||||
this.updateGroupBox.Size = new System.Drawing.Size(684, 60);
|
this.updateGroupBox.Size = new System.Drawing.Size(684, 60);
|
||||||
this.updateGroupBox.TabIndex = 3;
|
this.updateGroupBox.TabIndex = 3;
|
||||||
this.updateGroupBox.TabStop = false;
|
this.updateGroupBox.TabStop = false;
|
||||||
this.updateGroupBox.Text = "Auto-update feed URL (blank disables update checks; set per machine)";
|
this.updateGroupBox.Text = "Auto-update feed URL (blank uses GitHub Releases; set per machine)";
|
||||||
//
|
//
|
||||||
// updateFeedUrlBox
|
// updateFeedUrlBox
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -252,14 +252,14 @@ namespace OpenNest.Forms
|
|||||||
Settings.Default.UpdateFeedUrl = "";
|
Settings.Default.UpdateFeedUrl = "";
|
||||||
Settings.Default.Save();
|
Settings.Default.Save();
|
||||||
updateFeedUrlBox.Text = "";
|
updateFeedUrlBox.Text = "";
|
||||||
MessageBox.Show(this, "Update feed URL cleared. Update checks are disabled.",
|
MessageBox.Show(this, "Update feed URL cleared. Updates will come from GitHub Releases after a restart.",
|
||||||
"Update Feed URL", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
"Update Feed URL", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!NestStorageSettings.TryNormalizeServerUrl(text, out var url))
|
if (!NestStorageSettings.TryNormalizeServerUrl(text, out var url))
|
||||||
{
|
{
|
||||||
MessageBox.Show(this, "Enter a valid http or https feed URL, or clear the field to disable updates.",
|
MessageBox.Show(this, "Enter a valid http or https feed URL, or clear the field to update from GitHub Releases.",
|
||||||
"Update Feed URL", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
"Update Feed URL", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,18 +307,23 @@ public sealed class SavedNestsForm : Form
|
|||||||
findBar.Controls.Add(nextButton);
|
findBar.Controls.Add(nextButton);
|
||||||
findBar.Controls.Add(summaryLabel);
|
findBar.Controls.Add(summaryLabel);
|
||||||
|
|
||||||
var bottom = new TableLayoutPanel
|
// The find bar sits between the Nests header and the grid it filters, and comes
|
||||||
|
// first in the tab order.
|
||||||
|
var findPanel = new TableLayoutPanel
|
||||||
{
|
{
|
||||||
Dock = DockStyle.Bottom,
|
Dock = DockStyle.Top,
|
||||||
AutoSize = true,
|
AutoSize = true,
|
||||||
ColumnCount = 1,
|
ColumnCount = 1,
|
||||||
Padding = new Padding(3, 3, 3, 6),
|
Padding = new Padding(3, 0, 3, 3),
|
||||||
|
TabIndex = 0,
|
||||||
};
|
};
|
||||||
bottom.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
findPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||||
bottom.Controls.Add(findBar);
|
findPanel.Controls.Add(findBar);
|
||||||
|
nestGrid.TabIndex = 1;
|
||||||
|
split.Panel1.Controls.Add(findPanel);
|
||||||
|
split.Panel1.Controls.SetChildIndex(findPanel, 1);
|
||||||
|
|
||||||
Controls.Add(split);
|
Controls.Add(split);
|
||||||
Controls.Add(bottom);
|
|
||||||
|
|
||||||
Load += (_, _) =>
|
Load += (_, _) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ using System.Windows.Forms;
|
|||||||
namespace OpenNest.Forms
|
namespace OpenNest.Forms
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Background update checks against the per-machine feed URL (Options dialog; blank
|
/// Background update checks (GitHub Releases, or the per-machine feed URL from the
|
||||||
/// disables checks) and the status-bar item that announces a downloaded update. Never
|
/// Options dialog) and the status-bar item that announces a downloaded update. Never
|
||||||
/// restarts on its own: it restarts only when the user clicks the item and confirms.
|
/// restarts on its own: it restarts only when the user clicks the item and confirms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class UpdaterStatus : IDisposable
|
internal sealed class UpdaterStatus : IDisposable
|
||||||
|
|||||||
Generated
+2
-2
@@ -296,8 +296,8 @@ namespace OpenNest.Properties {
|
|||||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
[global::System.Configuration.DefaultSettingValueAttribute("")]
|
[global::System.Configuration.DefaultSettingValueAttribute("")]
|
||||||
// Velopack update feed base URL (e.g. http://host:port). Blank disables
|
// Velopack update feed base URL (e.g. http://host:port). Blank uses GitHub
|
||||||
// update checks; each machine sets its own value via Options, never committed.
|
// Releases; each machine sets its own value via Options, never committed.
|
||||||
public string UpdateFeedUrl {
|
public string UpdateFeedUrl {
|
||||||
get {
|
get {
|
||||||
return ((string)(this["UpdateFeedUrl"]));
|
return ((string)(this["UpdateFeedUrl"]));
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using OpenNest.Properties;
|
using OpenNest.Properties;
|
||||||
using Velopack;
|
using Velopack;
|
||||||
|
using Velopack.Locators;
|
||||||
using Velopack.Sources;
|
using Velopack.Sources;
|
||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
@@ -15,14 +16,15 @@ namespace OpenNest
|
|||||||
/// <see cref="ApplyAndRestart"/> once the user agrees to restart.
|
/// <see cref="ApplyAndRestart"/> once the user agrees to restart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The feed URL is never hard-coded: it is read from the per-machine
|
/// A blank <see cref="Settings.UpdateFeedUrl"/> (the default) checks the
|
||||||
/// <see cref="Settings.UpdateFeedUrl"/> setting (Options dialog), which is
|
/// public GitHub Releases for full, non-pre-release versions. A URL set in
|
||||||
/// user-scoped and lives outside source control. A blank URL disables
|
/// the Options dialog points this machine at a self-hosted feed instead;
|
||||||
/// update checks entirely, which is the default for every install.
|
/// that URL is operator infrastructure and never lives in source control.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class UpdaterService : IDisposable
|
public sealed class UpdaterService : IDisposable
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(30);
|
private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(30);
|
||||||
|
private const string GitHubRepositoryUrl = "https://github.com/ajisaacs/OpenNest";
|
||||||
|
|
||||||
private readonly UpdateManager _manager;
|
private readonly UpdateManager _manager;
|
||||||
private Timer _timer;
|
private Timer _timer;
|
||||||
@@ -36,10 +38,15 @@ namespace OpenNest
|
|||||||
|
|
||||||
public UpdaterService(string feedUrl)
|
public UpdaterService(string feedUrl)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(feedUrl))
|
// Only a process started through VelopackApp (MainApp) has a locator;
|
||||||
|
// test hosts and other embedders get no update checks.
|
||||||
|
if (!VelopackLocator.IsCurrentSet)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_manager = new UpdateManager(new SimpleWebSource(feedUrl));
|
IUpdateSource source = string.IsNullOrWhiteSpace(feedUrl)
|
||||||
|
? new GithubSource(GitHubRepositoryUrl, null, false)
|
||||||
|
: new SimpleWebSource(feedUrl);
|
||||||
|
_manager = new UpdateManager(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>True once a downloaded update is waiting for <see cref="ApplyAndRestart"/>.</summary>
|
/// <summary>True once a downloaded update is waiting for <see cref="ApplyAndRestart"/>.</summary>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ A Windows desktop application for CNC nesting — imports DXF drawings, arranges
|
|||||||
- Windows 10+ for the desktop app; the console, API, and most test projects build on Linux/macOS too.
|
- Windows 10+ for the desktop app; the console, API, and most test projects build on Linux/macOS too.
|
||||||
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) to build from source.
|
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) to build from source.
|
||||||
|
|
||||||
Prebuilt Windows downloads are on [GitHub Releases](https://github.com/ajisaacs/OpenNest/releases), or [build from source](#build-test-run) to run the latest code. Release ZIPs are self-contained: extract the entire archive into a new folder and run `OpenNest.exe`; no separate .NET installation is needed. To update, close OpenNest and extract the new release into a new folder; ZIP releases do not auto-update (optional installer-based updates need a separately hosted [Velopack feed](docs/auto-update.md)). All nesting engines (Default, Rectangles, Irregular, Circles, FlatBar and the fill strategies) are built in; see [nesting engines](docs/nesting-engines.md).
|
Prebuilt Windows downloads are on [GitHub Releases](https://github.com/ajisaacs/OpenNest/releases), or [build from source](#build-test-run) to run the latest code. Release ZIPs are self-contained: extract the entire archive into a new folder and run `OpenNest.exe`; no separate .NET installation is needed. For automatic updates, install with `OpenNest-win-Setup.exe` or extract `OpenNest-win-Portable.zip` instead: those copies download new full releases from GitHub in the background and restart only when you agree ([auto-update](docs/auto-update.md)). The plain ZIP does not auto-update; to update it, close OpenNest and extract the new release into a new folder. All nesting engines (Default, Rectangles, Irregular, Circles, FlatBar and the fill strategies) are built in; see [nesting engines](docs/nesting-engines.md).
|
||||||
|
|
||||||
## Build, Test, Run
|
## Build, Test, Run
|
||||||
|
|
||||||
|
|||||||
+34
-16
@@ -1,32 +1,49 @@
|
|||||||
# Desktop auto-update
|
# Desktop auto-update
|
||||||
|
|
||||||
The Windows desktop app (`OpenNest.exe`) can self-update via
|
The Windows desktop app (`OpenNest.exe`) can self-update via
|
||||||
[Velopack](https://velopack.io): `OpenNest.UpdaterService` polls a static
|
[Velopack](https://velopack.io): `OpenNest.UpdaterService` polls its update
|
||||||
file feed every 30 minutes, downloads a newer release in the background, and
|
source every 30 minutes, downloads a newer release in the background, and
|
||||||
only restarts to apply it after the user confirms (a clickable status-bar
|
only restarts to apply it after the user confirms (a clickable status-bar
|
||||||
item appears once a download finishes). It never force-restarts; an open,
|
item appears once a download finishes). It never force-restarts; an open,
|
||||||
unsaved nest is never discarded automatically.
|
unsaved nest is never discarded automatically.
|
||||||
|
|
||||||
## Configuring the feed
|
## Where updates come from
|
||||||
|
|
||||||
The feed URL is saved for the current Windows user
|
By default an installed copy checks the public
|
||||||
(`OpenNest.Properties.Settings.UpdateFeedUrl`), set from **Tools > Options >
|
[GitHub Releases](https://github.com/ajisaacs/OpenNest/releases) for full,
|
||||||
Auto-update feed URL**. It is blank by default, which disables update checks
|
non-pre-release versions (`GithubSource`, unauthenticated: GitHub allows 60 API
|
||||||
entirely — this is intentional: the feed URL is operator infrastructure, not
|
requests per hour per IP address, ample for a 30-minute poll but shared by every
|
||||||
something that belongs in source control, so no default ships in the repo.
|
machine behind one address). Pre-releases are never offered.
|
||||||
Enter a feed's base URL (`http://host:port`) and restart the app to pick it
|
|
||||||
up.
|
|
||||||
|
|
||||||
Velopack only applies updates to an installed copy (`UpdateManager.IsInstalled`):
|
To use a self-hosted feed instead, set **Tools > Options > Auto-update feed URL**
|
||||||
one installed with the `Setup.exe` that `Deploy-OpenNest.ps1` produces. The ZIPs on
|
(`OpenNest.Properties.Settings.UpdateFeedUrl`, saved for the current Windows
|
||||||
GitHub Releases and ordinary build output are not Velopack installations and never
|
user) to the feed's base URL (`http://host:port`) and restart. A self-hosted
|
||||||
auto-update, even with a feed URL configured.
|
feed URL is operator infrastructure and never belongs in source control; clearing
|
||||||
|
the field returns to GitHub Releases.
|
||||||
|
|
||||||
|
Velopack only applies updates to a Velopack installation (`UpdateManager.IsInstalled`):
|
||||||
|
a copy installed with `OpenNest-win-Setup.exe` or extracted from
|
||||||
|
`OpenNest-win-Portable.zip`. The plain `OpenNest.vX.Y.Z.win-x64.zip` and ordinary
|
||||||
|
build output contain no `Update.exe` and never auto-update; moving from one of
|
||||||
|
them to an updating copy is a one-time manual install.
|
||||||
|
|
||||||
|
## Release packages
|
||||||
|
|
||||||
|
`scripts/Publish-Windows.ps1` packs the same smoke-tested folder as the plain ZIP
|
||||||
|
with the `vpk` CLI pinned in `.config/dotnet-tools.json` (keep it equal to the
|
||||||
|
`Velopack` package version), into `velopack/`: `OpenNest-win-Setup.exe`,
|
||||||
|
`OpenNest-win-Portable.zip`, `OpenNest-X.Y.Z-full.nupkg`, `releases.win.json`
|
||||||
|
and `RELEASES`, with their hashes in `OpenNest.vX.Y.Z.velopack.sha256`. Installed
|
||||||
|
copies find an update through the `releases.win.json` and full package attached to
|
||||||
|
the newest full GitHub Release, so all five must be uploaded with it (see
|
||||||
|
[releasing](releasing.md#publish)). Packages are full only; delta packages would
|
||||||
|
need the previous release's packages beside the output before `vpk pack`.
|
||||||
|
|
||||||
## Hosting a feed
|
## Hosting a feed
|
||||||
|
|
||||||
Any static file server that can serve a `vpk pack` output directory works.
|
Any static file server that can serve a `vpk pack` output directory works.
|
||||||
`scripts/Deploy-OpenNest.ps1` builds a self-contained `win-x64` publish,
|
`scripts/Deploy-OpenNest.ps1` builds a self-contained `win-x64` publish,
|
||||||
packs it with the `vpk` CLI (`dotnet tool install -g vpk`), and `scp`s the
|
packs it with the pinned `vpk` CLI, and `scp`s the
|
||||||
result to a directory you provide:
|
result to a directory you provide:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -36,4 +53,5 @@ result to a directory you provide:
|
|||||||
Bump `<Version>` in `OpenNest/OpenNest.csproj` before each run. This is
|
Bump `<Version>` in `OpenNest/OpenNest.csproj` before each run. This is
|
||||||
independent of [the GitHub release process](releasing.md) and of
|
independent of [the GitHub release process](releasing.md) and of
|
||||||
`Publish-Windows.ps1` — the latter always overrides `<Version>` via
|
`Publish-Windows.ps1` — the latter always overrides `<Version>` via
|
||||||
`-p:Version`, so the two don't conflict.
|
`-p:Version`, so the two don't conflict. Machines on a self-hosted feed only see
|
||||||
|
versions pushed to that feed.
|
||||||
+16
-5
@@ -218,7 +218,10 @@ from the start point. Its 2-opt and Or-opt passes would grow with the square of
|
|||||||
part count. Replans after a learned rule use the plain tour. If a part on that order cannot be
|
part count. Replans after a learned rule use the plain tour. If a part on that order cannot be
|
||||||
reached without crossing parts already cut, the search learns "cut this part
|
reached without crossing parts already cut, the search learns "cut this part
|
||||||
before those", backs up to just before the earliest of them and re-plans the rest
|
before those", backs up to just before the earliest of them and re-plans the rest
|
||||||
from the tool position there; parts cut before that point are kept. The first pass
|
from the tool position there; parts cut before that point are kept. When the part
|
||||||
|
cannot come earlier (a sheet separator is cut after everything), it learns the other
|
||||||
|
way round, "cut the part the approach left from before those", so the tool arrives
|
||||||
|
from beyond them. The first pass
|
||||||
along an order that teaches such a rule ends the round; the directed-recovery and
|
along an order that teaches such a rule ends the round; the directed-recovery and
|
||||||
full passes run only when the lighter pass taught nothing. An attempt
|
full passes run only when the lighter pass taught nothing. An attempt
|
||||||
stops backtracking after a stall of 8 x entries x contours expansions without
|
stops backtracking after a stall of 8 x entries x contours expansions without
|
||||||
@@ -227,7 +230,8 @@ parts before it. The contours are the larger count of the blocked part and the p
|
|||||||
cut just before it, whose departure a retry changes, so one many-holed part elsewhere
|
cut just before it, whose departure a retry changes, so one many-holed part elsewhere
|
||||||
on the plate does not lengthen every stall. A cutoff is the exception: it is a fixed program and cutting it earlier never
|
on the plate does not lengthen every stall. A cutoff is the exception: it is a fixed program and cutting it earlier never
|
||||||
makes its own cut harder, so an approach into a non-separating cutoff that crosses parts already
|
makes its own cut harder, so an approach into a non-separating cutoff that crosses parts already
|
||||||
cut teaches "cut the cutoff before them" at once, without that stall. A rule that would contradict an order already required is skipped.
|
cut teaches "cut the cutoff before them" at once, without that stall, and one into a sheet
|
||||||
|
separator teaches its other-way-round rule at once. A rule that would contradict an order already required is skipped.
|
||||||
"Cut before" rules are a heuristic (a part blocked straight after another may be
|
"Cut before" rules are a heuristic (a part blocked straight after another may be
|
||||||
reachable via a third), so once nothing new can be learned the remaining budget
|
reachable via a third), so once nothing new can be learned the remaining budget
|
||||||
first tries sequence-free recovery, visiting dependency-ready parts lazily by
|
first tries sequence-free recovery, visiting dependency-ready parts lazily by
|
||||||
@@ -247,7 +251,14 @@ segment that must precede some part (`CutOffInsertion`) goes as late as it can b
|
|||||||
the first part in its span of the line, into the latest gap where both rapids cross
|
the first part in its span of the line, into the latest gap where both rapids cross
|
||||||
nothing cut before that point: the part after it keeps the program it was planned with,
|
nothing cut before that point: the part after it keeps the program it was planned with,
|
||||||
and only its incoming rapid is checked again from the segment's end. Segments due in the
|
and only its incoming rapid is checked again from the segment's end. Segments due in the
|
||||||
same gap are chained in the order placed, latest-due first. This keeps cutting local:
|
same gap are chained in the order placed, latest-due first. A holed part cut straight
|
||||||
|
after a segment was planned from where the tool was before the segment, but the tool is
|
||||||
|
at the segment's end, often across the part from there: its holes were routed toward the
|
||||||
|
wrong side and the rapid onto its first hole could cross the rapids between the others.
|
||||||
|
So once the segments are placed, each such part keeps its outside entry and its place in
|
||||||
|
the order, and takes the hole route and preferred hole entries planned from the
|
||||||
|
segment's end; it keeps its planned holes when that route has no preferred entry for
|
||||||
|
every hole or fails the checks. This keeps cutting local:
|
||||||
the segment below a row of parts is cut while the route passes it, not up front, and
|
the segment below a row of parts is cut while the route passes it, not up front, and
|
||||||
never before a part it need not precede unless the geometry allows nothing later (a
|
never before a part it need not precede unless the geometry allows nothing later (a
|
||||||
segment whose pierce is walled off by every part cut after a point goes before that
|
segment whose pierce is walled off by every part cut after a point goes before that
|
||||||
@@ -258,8 +269,8 @@ separator or a line through open scrap precedes nothing and is planned like a pa
|
|||||||
separator after everything, the other at its tour position.
|
separator after everything, the other at its tour position.
|
||||||
|
|
||||||
Wherever a segment sits in an order, the search looks through it: the part before it
|
Wherever a segment sits in an order, the search looks through it: the part before it
|
||||||
faces the next part, not the segment, and the part after it ranks its entries and hole
|
faces the next part, not the segment, and the part after it ranks its outside entry
|
||||||
route from where the tool was before the segment, while every rapid is still checked
|
from where the tool was before the segment, while its hole route and every rapid start
|
||||||
from the segment's end. A kept order with segments where a plan put them (an applied
|
from the segment's end. A kept order with segments where a plan put them (an applied
|
||||||
plan replanned with `Keep the current part order`) therefore gets the entries and
|
plan replanned with `Keep the current part order`) therefore gets the entries and
|
||||||
routes the plan had, and replays Ready again.
|
routes the plan had, and replays Ready again.
|
||||||
|
|||||||
+7
-1
@@ -27,7 +27,8 @@ pulls from GitHub every hour; it refuses pushes.
|
|||||||
(see [desktop help](desktop-help.md)), and `build-info.json` identifying the exact
|
(see [desktop help](desktop-help.md)), and `build-info.json` identifying the exact
|
||||||
source commit and help version. The nesting engines
|
source commit and help version. The nesting engines
|
||||||
are built into `OpenNest.Engine.dll`; no external engine repository or plug-in
|
are built into `OpenNest.Engine.dll`; no external engine repository or plug-in
|
||||||
DLL is packaged. `scripts/ReleaseSmoke` loads the packaged engine assembly and
|
DLL is packaged. Its `velopack/` folder holds the self-updating packages built
|
||||||
|
from the same folder; see [desktop auto-update](auto-update.md#release-packages). `scripts/ReleaseSmoke` loads the packaged engine assembly and
|
||||||
checks that every built-in engine instantiates from it, that the renamed
|
checks that every built-in engine instantiates from it, that the renamed
|
||||||
`Opus55NestingEngine` selection resolves to Irregular, and that an unknown
|
`Opus55NestingEngine` selection resolves to Irregular, and that an unknown
|
||||||
engine name is rejected.
|
engine name is rejected.
|
||||||
@@ -54,6 +55,11 @@ After the candidate passes, integrate the release tooling into the chosen branch
|
|||||||
create an annotated `vX.Y.Z` tag on the exact release commit and push it to
|
create an annotated `vX.Y.Z` tag on the exact release commit and push it to
|
||||||
GitHub. Use the successful **tag build's** artifacts
|
GitHub. Use the successful **tag build's** artifacts
|
||||||
for the GitHub Release; do not substitute packages built from another commit.
|
for the GitHub Release; do not substitute packages built from another commit.
|
||||||
|
Upload the plain ZIP, its `.sha256`, `OpenNest.vX.Y.Z.velopack.sha256` and all five
|
||||||
|
files in `velopack/` as release assets, keeping their names. Installed copies update
|
||||||
|
only from the newest full (non-pre-release) Release that carries
|
||||||
|
`releases.win.json` and its `-full.nupkg`; a full Release without them leaves every
|
||||||
|
installation on its current version.
|
||||||
Verify the public asset names, sizes, download/checksum, and release status after
|
Verify the public asset names, sizes, download/checksum, and release status after
|
||||||
upload. Do not overwrite an existing release/tag or asset silently.
|
upload. Do not overwrite an existing release/tag or asset silently.
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ inside corner. Only when every outline point is too close does the clearance giv
|
|||||||
View > Draw Support shows the grid or the slats, cut off to the sheet (while its points or slats
|
View > Draw Support shows the grid or the slats, cut off to the sheet (while its points or slats
|
||||||
are at least 6 pixels apart), each part's
|
are at least 6 pixels apart), each part's
|
||||||
contacts, and its center of gravity: green when supported, red when it can tip, with an orange
|
contacts, and its center of gravity: green when supported, red when it can tip, with an orange
|
||||||
diamond on a red part's best tab point (keeping the plate's tab-size clearance).
|
diamond on a red part's best tab point (keeping the plate's tab-size clearance). While Plan
|
||||||
|
Cutting inspects a proposal, the overlay shows the proposal's parts on the same bed.
|
||||||
|
|
||||||
## Support tabs in Plan Cutting
|
## Support tabs in Plan Cutting
|
||||||
|
|
||||||
|
|||||||
@@ -42,12 +42,10 @@ if ($csprojContent -notmatch '<Version>(?<v>[\d.]+)</Version>') { throw "Could n
|
|||||||
$Version = $Matches.v
|
$Version = $Matches.v
|
||||||
Write-Host "Packaging OpenNest v$Version"
|
Write-Host "Packaging OpenNest v$Version"
|
||||||
|
|
||||||
# Ensure the vpk (Velopack) CLI is available
|
# The vpk (Velopack) CLI is pinned in .config/dotnet-tools.json to match the Velopack package
|
||||||
if (-not (Get-Command vpk -ErrorAction SilentlyContinue)) {
|
Push-Location $RepoRoot
|
||||||
Write-Host "Installing vpk CLI (dotnet tool)..."
|
try { & dotnet tool restore } finally { Pop-Location }
|
||||||
dotnet tool install -g vpk
|
if ($LASTEXITCODE -ne 0) { throw "dotnet tool restore failed" }
|
||||||
if ($LASTEXITCODE -ne 0) { throw "dotnet tool install vpk failed" }
|
|
||||||
}
|
|
||||||
|
|
||||||
$PublishDir = Join-Path $RepoRoot 'OpenNest\bin\publish-tmp'
|
$PublishDir = Join-Path $RepoRoot 'OpenNest\bin\publish-tmp'
|
||||||
$ReleasesDir = Join-Path $RepoRoot 'OpenNest\bin\releases'
|
$ReleasesDir = Join-Path $RepoRoot 'OpenNest\bin\releases'
|
||||||
@@ -61,7 +59,8 @@ $helpVersion = & (Join-Path $ScriptDir 'Install-Help.ps1') -Destination (Join-Pa
|
|||||||
Write-Host "Packaged help $helpVersion"
|
Write-Host "Packaged help $helpVersion"
|
||||||
|
|
||||||
Write-Host "Packing Velopack release v$Version..."
|
Write-Host "Packing Velopack release v$Version..."
|
||||||
& vpk pack -u OpenNest -v $Version -p $PublishDir -e OpenNest.exe -o $ReleasesDir
|
Push-Location $RepoRoot
|
||||||
|
try { & dotnet tool run vpk pack -u OpenNest -v $Version -p $PublishDir -e OpenNest.exe -o $ReleasesDir } finally { Pop-Location }
|
||||||
if ($LASTEXITCODE -ne 0) { throw "vpk pack failed with exit code $LASTEXITCODE" }
|
if ($LASTEXITCODE -ne 0) { throw "vpk pack failed with exit code $LASTEXITCODE" }
|
||||||
|
|
||||||
Write-Host "Pushing release feed to $FeedHost..."
|
Write-Host "Pushing release feed to $FeedHost..."
|
||||||
|
|||||||
@@ -114,5 +114,32 @@ try {
|
|||||||
$hash = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant()
|
$hash = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
"$hash $zipName" | Set-Content "$zip.sha256" -Encoding ascii
|
"$hash $zipName" | Set-Content "$zip.sha256" -Encoding ascii
|
||||||
Write-Host "Verified $zipName ($hash), source $commit"
|
Write-Host "Verified $zipName ($hash), source $commit"
|
||||||
|
|
||||||
|
# Velopack packages of the same verified folder: Setup.exe and the portable ZIP
|
||||||
|
# self-update from GitHub Releases; the plain ZIP above never does.
|
||||||
|
dotnet tool restore
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'dotnet tool restore failed.' }
|
||||||
|
$velopack = Join-Path $output 'velopack'
|
||||||
|
dotnet tool run vpk pack -u OpenNest -v $Version -p $package -e OpenNest.exe -o $velopack --packTitle OpenNest
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'vpk pack failed.' }
|
||||||
|
|
||||||
|
$assets = @('OpenNest-win-Setup.exe', 'OpenNest-win-Portable.zip', "OpenNest-$Version-full.nupkg", 'releases.win.json', 'RELEASES')
|
||||||
|
foreach ($file in $assets) {
|
||||||
|
if (-not (Test-Path (Join-Path $velopack $file) -PathType Leaf)) { throw "Velopack output missing $file" }
|
||||||
|
}
|
||||||
|
$feed = Get-Content (Join-Path $velopack 'releases.win.json') -Raw | ConvertFrom-Json
|
||||||
|
if (@($feed.Assets | Where-Object { $_.Version -eq $Version -and $_.Type -eq 'Full' }).Count -ne 1) {
|
||||||
|
throw "releases.win.json does not list the $Version full package."
|
||||||
|
}
|
||||||
|
$portable = Join-Path $output 'velopack-portable'
|
||||||
|
Expand-Archive -Path (Join-Path $velopack 'OpenNest-win-Portable.zip') -DestinationPath $portable
|
||||||
|
foreach ($file in @('Update.exe') + ($required | ForEach-Object { "current/$_" })) {
|
||||||
|
if (-not (Test-Path (Join-Path $portable $file) -PathType Leaf)) { throw "Portable ZIP missing $file" }
|
||||||
|
}
|
||||||
|
$sums = foreach ($file in $assets) {
|
||||||
|
"$((Get-FileHash (Join-Path $velopack $file) -Algorithm SHA256).Hash.ToLowerInvariant()) $file"
|
||||||
|
}
|
||||||
|
$sums | Set-Content (Join-Path $output "OpenNest.v$Version.velopack.sha256") -Encoding ascii
|
||||||
|
Write-Host "Packed Velopack release $Version"
|
||||||
}
|
}
|
||||||
finally { Pop-Location }
|
finally { Pop-Location }
|
||||||
Reference in new issue
Block a user