mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-28 14:51:06 -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: |
|
||||
artifacts/*.zip
|
||||
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
|
||||
retention-days: 14
|
||||
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
|
||||
- [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.
|
||||
- [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.
|
||||
- [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,
|
||||
PartSpacing = source.PartSpacing,
|
||||
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.
|
||||
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);
|
||||
if (inserted != null)
|
||||
return walk.Ready(inserted);
|
||||
return walk.Ready(walk.FollowSegments(inserted));
|
||||
}
|
||||
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
|
||||
// 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)
|
||||
{
|
||||
if (attempt.Blocked is not int blocked)
|
||||
return false;
|
||||
var learned = false;
|
||||
foreach (var crossed in attempt.Crossed.Order())
|
||||
{
|
||||
if (crossed == blocked || prerequisites[crossed].Contains(blocked) || Precedes(crossed, blocked, prerequisites))
|
||||
continue;
|
||||
prerequisites[crossed].Add(blocked);
|
||||
learned = true;
|
||||
}
|
||||
return learned;
|
||||
var rules = Rules(attempt, prerequisites).ToArray();
|
||||
foreach (var (first, then) in rules)
|
||||
prerequisites[then].Add(first);
|
||||
return rules.Length != 0;
|
||||
}
|
||||
|
||||
// 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) =>
|
||||
attempt.Blocked is int blocked && attempt.Crossed.Any(crossed => crossed != blocked
|
||||
&& !prerequisites[crossed].Contains(blocked) && !Precedes(crossed, blocked, prerequisites));
|
||||
Rules(attempt, prerequisites).Any();
|
||||
|
||||
// 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.
|
||||
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
|
||||
// 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
|
||||
&& snapshot.Placements[blocked] is { IsCutOff: true, SeparatesSheet: false }
|
||||
&& CanLearn(attempt, prerequisites))
|
||||
&& snapshot.Placements[blocked].IsCutOff && CanLearn(attempt, prerequisites))
|
||||
return attempt;
|
||||
if (attempt.Advance(node))
|
||||
progressExpansions = Expansions;
|
||||
@@ -523,8 +532,9 @@ internal static class JointCuttingPlanSearch
|
||||
CountExpansion(source);
|
||||
var checker = node.Checker.Copy();
|
||||
if (Check(cut, cut.Execution, node.Position, checker, attempt))
|
||||
// A segment is transparent: the part after it ranks its entries and hole route
|
||||
// from where the tool was before the segment, as planned without it.
|
||||
// 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. 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,
|
||||
IsSegment(cut) ? node.RoutePosition : null),
|
||||
cut.Execution.RapidDistanceFrom(node.Position), cut.SourceOrdinal, -1, -1);
|
||||
@@ -559,7 +569,7 @@ internal static class JointCuttingPlanSearch
|
||||
}
|
||||
var preference = node.Active?.Preference;
|
||||
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 arrival = node.Active?.Arrival ?? node.Position;
|
||||
var before = node.Active?.Before ?? node.Checker;
|
||||
@@ -1216,12 +1226,13 @@ internal static class JointCuttingPlanSearch
|
||||
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 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 endpoint = perimeter.Point;
|
||||
var entries = new Dictionary<int, ContourChoice>();
|
||||
@@ -1255,6 +1266,72 @@ internal static class JointCuttingPlanSearch
|
||||
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.
|
||||
private HashSet<int> Finished(Node node)
|
||||
{
|
||||
@@ -1568,8 +1645,8 @@ internal static class JointCuttingPlanSearch
|
||||
ContourChoice Perimeter = null, HolePreference Preference = null);
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// routes rank from <see cref="RoutePosition"/>, every rapid is checked from Position.
|
||||
/// when set, is where the tool was before the cut-off segments cut last: outside entries
|
||||
/// rank from <see cref="RoutePosition"/>; hole routes and every rapid start from Position.
|
||||
/// </summary>
|
||||
private sealed record Node(FixedProgramPlacement[] Order, Vector Position, ReleasedContourState Checker,
|
||||
ActivePart Active, Node Previous, Vector? Anchor = null)
|
||||
|
||||
@@ -19,4 +19,17 @@ public class ClampZonePreviewTests
|
||||
Assert.Equal(2, preview.ClampZones.Count);
|
||||
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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
// The slanted shoulders need more horizontal room than the 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"));
|
||||
}
|
||||
|
||||
// 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
|
||||
// (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.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
var start = bend.StartPoint;
|
||||
var end = 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);
|
||||
var pt1 = view.PointWorldToGraph(BendToWorld(part, bend.StartPoint));
|
||||
var pt2 = view.PointWorldToGraph(BendToWorld(part, bend.EndPoint));
|
||||
|
||||
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)
|
||||
{
|
||||
if (
|
||||
@@ -501,17 +501,8 @@ namespace OpenNest.Controls
|
||||
if (bend.Direction != BendDirection.Up)
|
||||
continue;
|
||||
|
||||
var start = bend.StartPoint;
|
||||
var end = 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 start = BendToWorld(part, bend.StartPoint);
|
||||
var end = BendToWorld(part, bend.EndPoint);
|
||||
|
||||
var length = bend.Length;
|
||||
var angle = bend.StartPoint.AngleTo(bend.EndPoint) + part.Rotation;
|
||||
|
||||
@@ -771,10 +771,11 @@ namespace OpenNest.Controls
|
||||
{
|
||||
renderer.DrawActiveWorkArea(e.Graphics);
|
||||
renderer.DrawDebugRemnants(e.Graphics);
|
||||
if (DrawSupport)
|
||||
supportOverlay.Draw(e.Graphics, this);
|
||||
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.
|
||||
validationOverlay.Draw(e.Graphics);
|
||||
if (!HasProposalDisplay)
|
||||
|
||||
@@ -49,8 +49,10 @@ namespace OpenNest.Controls
|
||||
using var supportedPen = new Pen(SupportedColor, 2f);
|
||||
using var tippingPen = new Pen(TippingColor, 2f);
|
||||
using var tabBrush = new SolidBrush(TabColor);
|
||||
// Tab points keep clear of inside corners as Plan Cutting's do.
|
||||
var clearance = SupportAnalyzer.InsideCornerClearance(plate.CuttingParameters?.TabConfig?.Size ?? 0);
|
||||
// Tab points keep clear of inside corners as Plan Cutting's do. A proposal preview has no
|
||||
// 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)
|
||||
{
|
||||
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.TabIndex = 3;
|
||||
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
|
||||
//
|
||||
|
||||
@@ -252,14 +252,14 @@ namespace OpenNest.Forms
|
||||
Settings.Default.UpdateFeedUrl = "";
|
||||
Settings.Default.Save();
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -307,18 +307,23 @@ public sealed class SavedNestsForm : Form
|
||||
findBar.Controls.Add(nextButton);
|
||||
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,
|
||||
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));
|
||||
bottom.Controls.Add(findBar);
|
||||
findPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
findPanel.Controls.Add(findBar);
|
||||
nestGrid.TabIndex = 1;
|
||||
split.Panel1.Controls.Add(findPanel);
|
||||
split.Panel1.Controls.SetChildIndex(findPanel, 1);
|
||||
|
||||
Controls.Add(split);
|
||||
Controls.Add(bottom);
|
||||
|
||||
Load += (_, _) =>
|
||||
{
|
||||
|
||||
@@ -5,8 +5,8 @@ using System.Windows.Forms;
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// Background update checks against the per-machine feed URL (Options dialog; blank
|
||||
/// disables checks) and the status-bar item that announces a downloaded update. Never
|
||||
/// Background update checks (GitHub Releases, or the per-machine feed URL from the
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal sealed class UpdaterStatus : IDisposable
|
||||
|
||||
Generated
+2
-2
@@ -296,8 +296,8 @@ namespace OpenNest.Properties {
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("")]
|
||||
// Velopack update feed base URL (e.g. http://host:port). Blank disables
|
||||
// update checks; each machine sets its own value via Options, never committed.
|
||||
// Velopack update feed base URL (e.g. http://host:port). Blank uses GitHub
|
||||
// Releases; each machine sets its own value via Options, never committed.
|
||||
public string UpdateFeedUrl {
|
||||
get {
|
||||
return ((string)(this["UpdateFeedUrl"]));
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Properties;
|
||||
using Velopack;
|
||||
using Velopack.Locators;
|
||||
using Velopack.Sources;
|
||||
|
||||
namespace OpenNest
|
||||
@@ -15,14 +16,15 @@ namespace OpenNest
|
||||
/// <see cref="ApplyAndRestart"/> once the user agrees to restart.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The feed URL is never hard-coded: it is read from the per-machine
|
||||
/// <see cref="Settings.UpdateFeedUrl"/> setting (Options dialog), which is
|
||||
/// user-scoped and lives outside source control. A blank URL disables
|
||||
/// update checks entirely, which is the default for every install.
|
||||
/// A blank <see cref="Settings.UpdateFeedUrl"/> (the default) checks the
|
||||
/// public GitHub Releases for full, non-pre-release versions. A URL set in
|
||||
/// the Options dialog points this machine at a self-hosted feed instead;
|
||||
/// that URL is operator infrastructure and never lives in source control.
|
||||
/// </remarks>
|
||||
public sealed class UpdaterService : IDisposable
|
||||
{
|
||||
private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(30);
|
||||
private const string GitHubRepositoryUrl = "https://github.com/ajisaacs/OpenNest";
|
||||
|
||||
private readonly UpdateManager _manager;
|
||||
private Timer _timer;
|
||||
@@ -36,10 +38,15 @@ namespace OpenNest
|
||||
|
||||
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;
|
||||
|
||||
_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>
|
||||
|
||||
@@ -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.
|
||||
- [.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
|
||||
|
||||
|
||||
+34
-16
@@ -1,32 +1,49 @@
|
||||
# Desktop auto-update
|
||||
|
||||
The Windows desktop app (`OpenNest.exe`) can self-update via
|
||||
[Velopack](https://velopack.io): `OpenNest.UpdaterService` polls a static
|
||||
file feed every 30 minutes, downloads a newer release in the background, and
|
||||
[Velopack](https://velopack.io): `OpenNest.UpdaterService` polls its update
|
||||
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
|
||||
item appears once a download finishes). It never force-restarts; an open,
|
||||
unsaved nest is never discarded automatically.
|
||||
|
||||
## Configuring the feed
|
||||
## Where updates come from
|
||||
|
||||
The feed URL is saved for the current Windows user
|
||||
(`OpenNest.Properties.Settings.UpdateFeedUrl`), set from **Tools > Options >
|
||||
Auto-update feed URL**. It is blank by default, which disables update checks
|
||||
entirely — this is intentional: the feed URL is operator infrastructure, not
|
||||
something that belongs in source control, so no default ships in the repo.
|
||||
Enter a feed's base URL (`http://host:port`) and restart the app to pick it
|
||||
up.
|
||||
By default an installed copy checks the public
|
||||
[GitHub Releases](https://github.com/ajisaacs/OpenNest/releases) for full,
|
||||
non-pre-release versions (`GithubSource`, unauthenticated: GitHub allows 60 API
|
||||
requests per hour per IP address, ample for a 30-minute poll but shared by every
|
||||
machine behind one address). Pre-releases are never offered.
|
||||
|
||||
Velopack only applies updates to an installed copy (`UpdateManager.IsInstalled`):
|
||||
one installed with the `Setup.exe` that `Deploy-OpenNest.ps1` produces. The ZIPs on
|
||||
GitHub Releases and ordinary build output are not Velopack installations and never
|
||||
auto-update, even with a feed URL configured.
|
||||
To use a self-hosted feed instead, set **Tools > Options > Auto-update feed URL**
|
||||
(`OpenNest.Properties.Settings.UpdateFeedUrl`, saved for the current Windows
|
||||
user) to the feed's base URL (`http://host:port`) and restart. A self-hosted
|
||||
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
|
||||
|
||||
Any static file server that can serve a `vpk pack` output directory works.
|
||||
`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:
|
||||
|
||||
```powershell
|
||||
@@ -36,4 +53,5 @@ result to a directory you provide:
|
||||
Bump `<Version>` in `OpenNest/OpenNest.csproj` before each run. This is
|
||||
independent of [the GitHub release process](releasing.md) and of
|
||||
`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
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
route from where the tool was before the segment, while every rapid is still checked
|
||||
faces the next part, not the segment, and the part after it ranks its outside entry
|
||||
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
|
||||
plan replanned with `Keep the current part order`) therefore gets the entries and
|
||||
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
|
||||
source commit and help version. The nesting engines
|
||||
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
|
||||
`Opus55NestingEngine` selection resolves to Irregular, and that an unknown
|
||||
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
|
||||
GitHub. Use the successful **tag build's** artifacts
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@@ -42,12 +42,10 @@ if ($csprojContent -notmatch '<Version>(?<v>[\d.]+)</Version>') { throw "Could n
|
||||
$Version = $Matches.v
|
||||
Write-Host "Packaging OpenNest v$Version"
|
||||
|
||||
# Ensure the vpk (Velopack) CLI is available
|
||||
if (-not (Get-Command vpk -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "Installing vpk CLI (dotnet tool)..."
|
||||
dotnet tool install -g vpk
|
||||
if ($LASTEXITCODE -ne 0) { throw "dotnet tool install vpk failed" }
|
||||
}
|
||||
# The vpk (Velopack) CLI is pinned in .config/dotnet-tools.json to match the Velopack package
|
||||
Push-Location $RepoRoot
|
||||
try { & dotnet tool restore } finally { Pop-Location }
|
||||
if ($LASTEXITCODE -ne 0) { throw "dotnet tool restore failed" }
|
||||
|
||||
$PublishDir = Join-Path $RepoRoot 'OpenNest\bin\publish-tmp'
|
||||
$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 "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" }
|
||||
|
||||
Write-Host "Pushing release feed to $FeedHost..."
|
||||
|
||||
@@ -114,5 +114,32 @@ try {
|
||||
$hash = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
"$hash $zipName" | Set-Content "$zip.sha256" -Encoding ascii
|
||||
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 }
|
||||
Reference in new issue
Block a user