Compare commits

54 Commits

Author SHA1 Message Date
aj 786b6e2e88 fix: show cutting parameters dialog before assigning lead-ins
Auto-assign lead-ins silently reused existing plate parameters with no
way to change them after the first assignment. Now a dialog with the
full CuttingPanel is shown every time, pre-populated with the current
settings, so the user can review and modify before confirming.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:32:41 -04:00
aj ba89967448 fix: respect suppression state in filter panel and guard DetermineWinding
FilterPanel.LoadItem was hardcoding all layer and line type checkboxes
to checked, ignoring actual visibility state. Now reads Layer.IsVisible
and entity IsVisible to set correct checked state.

Also guard DetermineWinding against shapes with fewer than 3 polygon
points (defaults to CCW) to prevent crash when applying lead-ins.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:58:11 -04:00
aj b566d984b0 fix: preserve suppression state when reopening converter
LoadItem was resetting all entity visibility to true, overriding the
suppression state set by LoadDrawings. Now stores suppressed entity IDs
on FileListItem and re-applies after the reset. Also auto-unchecks
layers where all entities are suppressed, and syncs suppression state
back to the FileListItem when filters change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:32:18 -04:00
aj c1e6092e83 feat: add entity-based suppression with stable GUIDs
Entities now have a Guid Id for stable identity across edit sessions.
Drawing stores the full source entity set (SourceEntities) and a set of
suppressed entity IDs (SuppressedEntityIds), replacing the previous
SuppressedProgram approach. Unchecked entities in the converter are
suppressed rather than permanently removed, so they can be re-enabled
when editing drawings again.

Entities are serialized as JSON in the nest file (entities/entities-N)
alongside the existing G-code programs. Backward compatible with older
nest files that lack entity data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:25:48 -04:00
aj df86d4367b fix: update drawings in-place when editing in converter so parts reflect changes
EditDrawingsInConverter was replacing Drawing objects with new instances,
but Part.BaseDrawing is readonly — parts kept referencing the old drawings
with stale programs (e.g. etch lines that were removed). Now matches by
name and updates existing drawings in-place, then refreshes all parts.

Also fixes Part.Update() which applied rotation backwards and was missing
UpdateBounds() and lead-in state reset.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:58:51 -04:00
aj 40026ab4dc test: add SpatialQuery DirectionalDistance tests for circles, squares, and rounded rects
24 tests covering circle-to-circle, square-to-square, rounded rectangle,
mixed shape types, PushDirection overload, edge cases, and symmetry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:10:32 -04:00
aj b18a82df7a refactor: clean up SpatialQuery duplications and redundancies
Extract ArcToLineClosestDistance helper to eliminate duplicate Phase 3
arc-to-line loops, remove redundant MaxValue guard in curve-to-curve
check, consolidate CollectVertices overloads, and add entity-based
PushDirection overload for API consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 09:33:29 -04:00
aj f090a2e299 fix: add arc-to-line closest-point check in DirectionalDistance
Corner arcs from offset perimeters could slip past vertex sampling,
causing compactor push to undershoot by ~halfSpacing. Use ClosestPointTo
to find the actual nearest point on each arc to each line before firing
the directional ray.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 08:52:30 -04:00
aj 55192a4888 chore: update ShapeLibraryForm designer layout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 08:37:42 -04:00
aj 7c28a35ad8 feat: add Edit Drawings in Converter button to reopen nest drawings in CadConverterForm
Adds a toolbar button on the Drawings tab that opens the CAD converter
pre-populated with the current nest drawings, allowing users to revisit
layer filtering, quantities, and other settings without re-importing.

Also fixes PlateView stealing focus from text inputs on mouse enter
and FilterPanel crashing when loaded before form handle is created.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 08:37:20 -04:00
aj b2a723ca60 feat: add Shape Library UI with configurable shapes and flange presets
Add a Shape Library dialog (Nest > Shape Library) for creating drawings
from built-in parametric shapes. Supports configuration presets loaded
from JSON files — ships with 136 standard pipe flanges. Parameters use
TextBox inputs with architectural unit parsing (feet/inches, fractions).

- ShapeLibraryForm with split layout: shape list, preview, parameters
- ShapePreviewControl for auto-zoom rendering with info overlay
- ArchUnits utility for parsing architectural measurements
- SetPreviewDefaults() on all ShapeDefinition subclasses
- Convention-based config discovery (Configurations/{ShapeName}.json)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 07:44:03 -04:00
aj 3dca25c601 fix: improve circle nesting with curve-to-curve distance and min copy spacing
Add Phase 3 curve-to-curve direct distance in CpuDistanceComputer to
catch contacts that vertex sampling misses between curved entities.
Enforce minimum copy distance in FillLinear to prevent bounding box
overlap when circumscribed polygon boundaries overshoot true arcs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 00:15:35 -04:00
aj ebc1a5f980 refactor: extract shared helpers in SpatialQuery
Pull duplicated vertex collection, edge conversion, sorting, and
ray-circle solving into reusable private methods. Delegate the
no-offset DirectionalDistance overload to the offset version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 00:15:30 -04:00
aj b729f92cd6 fix: correct compactor circle-to-circle directional distance
The vertex-to-entity approach in DirectionalDistance only sampled 4
cardinal points per circle, missing the true closest contact when
circles are offset diagonally from the push direction. This caused
the distance to be overestimated, pushing circles too far and
creating overlap that worsened with distance from center.

Add a curve-to-curve pass that computes exact contact distance by
treating the problem as a ray from one center to an expanded circle
(radius = r1 + r2) at the other center. Includes arc angular range
validation for arc-to-arc and arc-to-circle cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:51:09 -04:00
aj 5d6e018b81 fix: preserve circle rotation direction through geometry round-trip
Circle.Rotation was lost in three places, causing reversed circles to
still offset inward instead of outward:
- ConvertGeometry.AddCircle hardcoded CCW instead of using circle.Rotation
- ConvertProgram.AddArcMove created Circle without setting Rotation from arc
- Shape.OffsetOutward/OffsetInward copied Circle without setting Rotation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:38:23 -04:00
aj 5163b02f89 fix: increase max zoom and handle GDI+ thread race in PlateView
Raise ViewScaleMax from 3000 to 10000 for deeper zoom. Catch
InvalidOperationException in hoverTimer_Elapsed when GraphicsPath is
concurrently used by the paint thread.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:01:50 -04:00
aj a59911b38a remove MicrotabLeadOut — redundant with normal tabs
MicrotabLeadOut was an unimplemented stub (Generate returned empty list)
that duplicated tab functionality. Existing saved configs with "Microtab"
selected will gracefully fall back to NoLeadOut.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:43:38 -04:00
aj 810e37cacf feat: improve multi-plate nesting with multi-remnant filling and better zone scoring
- Iterate all remnants instead of only the first when packing and filling
- Improve ScoreZone with estimated part count and aspect ratio matching
- Cache bounding boxes in SortItems and remnants in TryPlaceOnExistingPlates
- Make TryConsolidateTailPlates loop until stable, trying all donor/target pairs
- Fix consolidation grouping to use BaseDrawing reference instead of name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:20:29 -04:00
aj 8dfa45c446 refactor: rename PlateResult to PlateProcessingResult
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:20:14 -04:00
aj b223f69572 chore: add missing BendLineDialog designer resource
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:20:11 -04:00
aj 98c574c2ad perf: defer Path.IsVisible hit-test to hover timer callback
Move the expensive per-part hit-test out of OnMouseMove and into
the hoverTimer callback. The hit-test now only runs once after
1000ms of stillness, not on every mouse move event.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:19:48 -04:00
aj 30f1008fa9 feat: show hover tooltip only after 1000ms of mouse stillness
Add a hoverTimer that restarts on each mouse move over a part.
Tooltip only renders after the timer fires, hiding while the
cursor is in motion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:12:02 -04:00
aj 41c20eaf75 feat: make hover tooltip follow the cursor
Update hoverPoint on every mouse move while over a part, not just
when the hovered part changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:59:14 -04:00
aj 3a97253473 perf: add bounding box pre-check before Path.IsVisible in hover detection
Path.IsVisible was consuming 52% of CPU on mouse move. Add a cheap
GetBounds().Contains() check first so only parts under the cursor
hit the expensive GDI+ path test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:55:22 -04:00
aj 3eab3c5946 fix: guard against null actionManager during PlateView construction
Plate setter is called in the constructor before actionManager is
initialized, causing a NullReferenceException on startup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:45:41 -04:00
aj 0e05ad04ea refactor: clean up PlateView after component extraction
Remove dead programIdFont field, unused imports (OpenNest.CNC,
System.ComponentModel, OpenNest.Math, System.Collections.ObjectModel).
PlateView is now 692 lines (down from 1035).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:20:28 -04:00
aj 5ac985dc0f refactor: update PlateRenderer for SelectionManager cut-off list
PlateRenderer now checks Selection.SelectedCutOffs.Contains() instead
of comparing against a single SelectedCutOff property. Remove temporary
SelectedCutOff shim from PlateView and unused Designer assignment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:18:59 -04:00
aj 865754611c refactor: extract PreviewManager from PlateView
Moves preview part lifecycle (stationaryParts, activeParts) into a dedicated
PreviewManager class. PlateView retains forwarding properties and methods for
backward compatibility. Adds Previews property for direct access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 15:18:17 -04:00
aj 9db326ee5d refactor: extract ActionManager from PlateView
Move action lifecycle (currentAction, previousAction, SetAction, ProcessEscapeKey,
RestorePreviousAction, GetDisplayName) into a dedicated ActionManager class.
PlateView retains public forwarding methods and exposes Actions property.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 15:16:15 -04:00
aj 25faba430c refactor: extract CutOffHandler from PlateView
Move cut-off drag interaction mechanics into a dedicated CutOffHandler
class, reducing PlateView complexity and following the same pattern
established by SelectionManager extraction in Task 1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 15:13:42 -04:00
aj 089df67627 refactor: extract SelectionManager from PlateView
Move all selection state and operations (SelectedParts, SelectedCutOffs, DeselectAll, SelectAll, AlignSelected, RotateSelectedParts, PushSelected, GetPartAt*, GetPartsFromWindow, DeleteSelected) into a new internal SelectionManager class. PlateView retains public forwarding methods and properties to preserve the existing API surface. SelectedCutOff property kept public for WinForms designer compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 15:09:08 -04:00
aj 11884e712d fix: clear empty area below items in drawing list on resize
The WM_ERASEBKGND suppression from 3c4d00b left stale artifacts
in the non-item region when the control was resized. Fill only
the area below the last visible item so items still don't flicker.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:23:30 -04:00
aj 6bed736cf0 perf: use actual geometry instead of tessellated polygons for push distance
- Add entity-based DirectionalDistance overload to SpatialQuery that
  uses RayArcDistance/RayCircleDistance instead of tessellating arcs
  and circles into line segments
- Add GetOffsetPartEntities, GetPerimeterEntities, GetPartEntities to
  PartGeometry for non-tessellated entity extraction
- Update Compactor.Push to use native entities instead of tessellated
  lines — 952 circles = 952 entities vs ~47,600 line segments
- Use bounding box containment check to skip cutout entities when no
  obstacle is inside the moving part (perimeter-only for common case)
- Obstacles always use perimeter-only entities since cutout edges are
  inside the solid and cannot block external parts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:06:37 -04:00
aj c20a079874 refactor: clean up MultiPlateNester code smells and duplication
Extract shared patterns into reusable helpers: FitsBounds (fits-normal/
rotated check), OptionWorkArea (edge-spacing subtraction), DecrementQuantity,
TryWithUpgradedSize (upgrade-try-revert), FindSmallestFittingOption.
Add PlateResult.AddParts to consolidate dual parts-list bookkeeping.
Cache sorted plate options and add HasPlateOptions property. Introduce
MultiPlateNestOptions to replace 10-parameter Nest signature with a
clean options object. Fix fragile Drawing.Name matching with reference
equality in PackIntoExistingRemnants.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:43:58 -04:00
aj 804a7fd9c1 fix: check longest side against plate dimensions in best fit filter
The filter only checked ShortestSide against the plate's short dimension,
allowing results where the long side far exceeded the plate length.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:28:59 -04:00
aj 3c4d00baa4 fix: suppress WM_ERASEBKGND to prevent drawing list flicker on quantity change
ListBox is a native Win32 control so ControlStyles.OptimizedDoubleBuffer
had no effect. The erase-then-redraw cycle on each Invalidate() caused
visible flashing. Suppressing WM_ERASEBKGND is safe because OnDrawItem
already fills the complete item bounds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:42:00 -04:00
aj 959ab15491 fix: re-enable delete plate button when changing plate selection
UpdateRemovePlateButton() was only called from PlateListChanged,
not CurrentPlateChanged, so the button stayed disabled after switching
away from the sentinel plate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:26:49 -04:00
aj cca70db547 fix: consolidate tail plates by upgrading instead of creating new plates
Three fixes to TryUpgradeOrNewPlate and a new post-pass:

1. Change ShouldUpgrade from < to <= so upgrade wins when costs are
   tied (e.g., all zero) — previously 0 < 0 was always false

2. Guard against "upgrades" that shrink a dimension — when options are
   sorted by cost and costs are equal, the next option may have a
   smaller length despite higher width (e.g., 72x96 after 60x144)

3. Revert plate size when upgrade fill fails — the plate was being
   resized before confirming parts fit, leaving it at the wrong size

4. Add TryConsolidateTailPlates post-pass: after all nesting, find the
   lowest-utilization new plate and try to absorb its parts into
   another plate via upgrade. Eliminates wasteful tail plates (e.g.,
   a 48x96 plate at 21% util for 2 parts that fit in upgraded space).

Real nest file: 6 plates → 5 plates, all 43 parts placed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:41:49 -04:00
aj 62d9dce0b1 refactor: simplify MultiPlateNester by converting to instance class
- Convert static class to instance with private constructor; shared
  parameters (template, plateOptions, salvageRate, minRemnantSize,
  progress, token) become fields, eliminating parameter threading
  across all private methods (10→3 params on Nest entry point stays
  unchanged; private methods drop from 7-9 params to 1-2)
- Extract FillAndPlace helper consolidating the repeated
  clone→fill→add-to-plate→deduct-quantity pattern (was duplicated
  in 4 call sites)
- Merge FindScrapZones/FindViableRemnants (98% duplicate) into single
  FindRemnants(plate, minRemnantSize, scrapOnly) method
- Extract ScoreZone helper and collapse duplicate normal/rotated
  orientation checks into single conditional
- Extract CreateNewPlateResult helper for repeated PlateResult
  construction + PlateOption lookup pattern

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:08:44 -04:00
aj 1f88453d4c fix: recalculate remnants after each fill to prevent overlaps
The consolidation pass was iterating stale remnant lists after placing
parts, causing overlapping placements. Now recalculates remnants from
the plate after each fill operation. Also added plate options to the
real nest file integration test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:55:48 -04:00
aj 0697bebbc2 fix: defer small parts to consolidation pass for shared plates
Small parts no longer create their own plates during the main pass.
Instead they're deferred to the consolidation pass which fills them
into remaining space on existing plates, packing multiple drawing
types together. Drops from 9 plates to 4 on the test nest file.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:47:50 -04:00
aj beadb14acc fix: consolidation pass packs medium/small parts onto shared plates
After the main single-pass placement, leftover items are now packed
together using the engine's multi-item Nest()/PackArea() methods
instead of creating one plate per drawing. First tries packing into
remaining space on existing plates, then creates shared plates for
anything still remaining.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:35:12 -04:00
aj 09f1140f54 fix: allow large parts to use remnant space on existing plates
Previously, parts classified as "Large" skipped all existing plates
and always created new ones. This caused one-unique-part-per-plate
behavior since most parts exceed half the plate dimension. Now large
parts search viable remnants on existing plates before creating new
ones, matching the intended part-first behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:24:17 -04:00
aj 7c918a2378 feat: integrate MultiPlateNester into MainForm auto-nest workflow
Wires part-first mode from AutoNestForm into RunAutoNestAsync: reads
PartFirstMode, SortOrder, MinRemnantSize, and AllowPlateCreation from
the form, passes them through to a new part-first branch that delegates
to MultiPlateNester.Nest instead of the plate-first loop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:16:10 -04:00
aj feb08a5f60 feat: refactor AutoNestForm into Parts/Plates tabs with part-first controls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:13:50 -04:00
aj f1fd211ba5 fix: small parts use FindScrapZones not FindAllRemnants
Small parts must only go into scrap zones (both dims < minRemnantSize)
to preserve viable remnants. The implementer had inverted this, giving
small parts access to all remnants. Also fixed the test to verify
remnant preservation behavior and removed unused FindAllRemnants helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:11:10 -04:00
aj fd3c2462df feat: add MultiPlateNester.Nest orchestration method
Implements the main Nest() method that ties together sorting,
classification, and placement across multiple plates. The method
processes items largest-first, placing medium/small parts into
remnant zones on existing plates before creating new ones. Includes
private helpers: TryPlaceOnExistingPlates, PlaceOnNewPlates,
TryUpgradeOrNewPlate, FindAllRemnants, and CloneItem.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:07:24 -04:00
aj a4773748a1 feat: add plate creation and upgrade-vs-new evaluation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:56:08 -04:00
aj af57153269 feat: add scrap zone identification to MultiPlateNester
Adds IsScrapRemnant(), FindScrapZones(), and FindViableRemnants() to
MultiPlateNester. A remnant is scrap only when both dimensions fall
below the minimum remnant size threshold (AND logic, not OR).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:53:32 -04:00
aj 35e89600d0 feat: add part classification (large/medium/small) to MultiPlateNester
Introduces PartClass enum and Classify() static method that categorizes
parts as Large (exceeds half work area in either dimension), Medium
(area > 1/9 work area), or Small.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:52:27 -04:00
aj 89a4e6b981 feat: add MultiPlateNester with sorting logic
Implements static MultiPlateNester.SortItems with BoundingBoxArea and Size sort orders, covered by two passing xUnit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:48:57 -04:00
aj ebad3577dd feat: add MultiPlateResult type for part-first nesting 2026-04-06 13:46:51 -04:00
aj a8dc275da4 feat: add PartSortOrder enum for part-first nesting 2026-04-06 13:46:49 -04:00
aj d84becdaee fix: add bend detection and etch lines to BOM import path
BOM import was skipping BendDetectorRegistry.AutoDetect and
Bend.UpdateEtchEntities, so parts imported via BOM had no etch
or bend lines. Now matches the CadConverterForm import behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:19:17 -04:00
69 changed files with 6810 additions and 1317 deletions
@@ -309,7 +309,12 @@ namespace OpenNest.CNC.CuttingStrategy
if (shape.Entities.Count == 1 && shape.Entities[0] is Circle circle) if (shape.Entities.Count == 1 && shape.Entities[0] is Circle circle)
return circle.Rotation; return circle.Rotation;
return shape.ToPolygon().RotationDirection(); var polygon = shape.ToPolygon();
if (polygon.Vertices.Count < 3)
return RotationType.CCW;
return polygon.RotationDirection();
} }
private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle) private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle)
@@ -1,16 +0,0 @@
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
{
public class MicrotabLeadOut : LeadOut
{
public double GapSize { get; set; } = 0.03;
public override List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
RotationType winding = RotationType.CW)
{
return new List<ICode>();
}
}
}
+1 -1
View File
@@ -97,7 +97,7 @@ namespace OpenNest.Converters
if (startpt != lastpt) if (startpt != lastpt)
pgm.MoveTo(startpt); pgm.MoveTo(startpt);
pgm.ArcTo(startpt, circle.Center, RotationType.CCW); pgm.ArcTo(startpt, circle.Center, circle.Rotation);
lastpt = startpt; lastpt = startpt;
return lastpt; return lastpt;
+1 -1
View File
@@ -106,7 +106,7 @@ namespace OpenNest.Converters
var layer = ConvertLayer(arcMove.Layer); var layer = ConvertLayer(arcMove.Layer);
if (startAngle.IsEqualTo(endAngle)) if (startAngle.IsEqualTo(endAngle))
geometry.Add(new Circle(center, radius) { Layer = layer, Color = layer.Color }); geometry.Add(new Circle(center, radius) { Layer = layer, Color = layer.Color, Rotation = arcMove.Rotation });
else else
geometry.Add(new Arc(center, radius, startAngle, endAngle, arcMove.Rotation == RotationType.CW) { Layer = layer, Color = layer.Color }); geometry.Add(new Arc(center, radius, startAngle, endAngle, arcMove.Rotation == RotationType.CW) { Layer = layer, Color = layer.Color });
+13
View File
@@ -2,6 +2,7 @@
using OpenNest.CNC; using OpenNest.CNC;
using OpenNest.Converters; using OpenNest.Converters;
using OpenNest.Geometry; using OpenNest.Geometry;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
@@ -90,6 +91,18 @@ namespace OpenNest
public List<Bend> Bends { get; set; } = new List<Bend>(); public List<Bend> Bends { get; set; } = new List<Bend>();
/// <summary>
/// Complete set of source entities with stable GUIDs.
/// Null when the drawing was created from G-code or an older nest file.
/// </summary>
public List<Entity> SourceEntities { get; set; }
/// <summary>
/// IDs of entities in <see cref="SourceEntities"/> that are suppressed (hidden).
/// Suppressed entities are excluded from the active Program but preserved for re-enabling.
/// </summary>
public HashSet<Guid> SuppressedEntityIds { get; set; } = new HashSet<Guid>();
public double Area { get; protected set; } public double Area { get; protected set; }
public void UpdateArea() public void UpdateArea()
+7
View File
@@ -1,4 +1,5 @@
using OpenNest.Math; using OpenNest.Math;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
@@ -10,10 +11,16 @@ namespace OpenNest.Geometry
protected Entity() protected Entity()
{ {
Id = Guid.NewGuid();
Layer = OpenNest.Geometry.Layer.Default; Layer = OpenNest.Geometry.Layer.Default;
boundingBox = new Box(); boundingBox = new Box();
} }
/// <summary>
/// Unique identifier for this entity, stable across edit sessions.
/// </summary>
public Guid Id { get; set; }
/// <summary> /// <summary>
/// Entity color (resolved from DXF ByLayer/ByBlock to actual color). /// Entity color (resolved from DXF ByLayer/ByBlock to actual color).
/// </summary> /// </summary>
+2 -2
View File
@@ -605,7 +605,7 @@ namespace OpenNest.Geometry
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer }); copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
break; break;
case Circle c: case Circle c:
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer }); copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CW });
break; break;
} }
} }
@@ -640,7 +640,7 @@ namespace OpenNest.Geometry
copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer }); copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer });
break; break;
case Circle c: case Circle c:
copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer }); copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CCW });
break; break;
} }
} }
+332 -140
View File
@@ -104,6 +104,39 @@ namespace OpenNest.Geometry
return double.MaxValue; return double.MaxValue;
} }
/// <summary>
/// Solves ray-circle intersection, returning the two parametric t values.
/// Returns false if no real intersection exists.
/// </summary>
[System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
private static bool SolveRayCircle(
double vx, double vy,
double cx, double cy, double r,
double dirX, double dirY,
out double t1, out double t2)
{
var ox = vx - cx;
var oy = vy - cy;
var a = dirX * dirX + dirY * dirY;
var b = 2.0 * (ox * dirX + oy * dirY);
var c = ox * ox + oy * oy - r * r;
var discriminant = b * b - 4.0 * a * c;
if (discriminant < 0)
{
t1 = t2 = double.MaxValue;
return false;
}
var sqrtD = System.Math.Sqrt(discriminant);
var inv2a = 1.0 / (2.0 * a);
t1 = (-b - sqrtD) * inv2a;
t2 = (-b + sqrtD) * inv2a;
return true;
}
/// <summary> /// <summary>
/// Computes the distance from a point along a direction to an arc. /// Computes the distance from a point along a direction to an arc.
/// Solves ray-circle intersection, then constrains hits to the arc's /// Solves ray-circle intersection, then constrains hits to the arc's
@@ -117,25 +150,9 @@ namespace OpenNest.Geometry
double startAngle, double endAngle, bool reversed, double startAngle, double endAngle, bool reversed,
double dirX, double dirY) double dirX, double dirY)
{ {
// Ray: P = (vx,vy) + t*(dirX,dirY) if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
// Circle: (x-cx)^2 + (y-cy)^2 = r^2
var ox = vx - cx;
var oy = vy - cy;
// a = dirX^2 + dirY^2 = 1 for unit direction, but handle general case
var a = dirX * dirX + dirY * dirY;
var b = 2.0 * (ox * dirX + oy * dirY);
var c = ox * ox + oy * oy - r * r;
var discriminant = b * b - 4.0 * a * c;
if (discriminant < 0)
return double.MaxValue; return double.MaxValue;
var sqrtD = System.Math.Sqrt(discriminant);
var inv2a = 1.0 / (2.0 * a);
var t1 = (-b - sqrtD) * inv2a;
var t2 = (-b + sqrtD) * inv2a;
var best = double.MaxValue; var best = double.MaxValue;
if (t1 > -Tolerance.Epsilon) if (t1 > -Tolerance.Epsilon)
@@ -168,27 +185,13 @@ namespace OpenNest.Geometry
double cx, double cy, double r, double cx, double cy, double r,
double dirX, double dirY) double dirX, double dirY)
{ {
var ox = vx - cx; if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2))
var oy = vy - cy;
var a = dirX * dirX + dirY * dirY;
var b = 2.0 * (ox * dirX + oy * dirY);
var c = ox * ox + oy * oy - r * r;
var discriminant = b * b - 4.0 * a * c;
if (discriminant < 0)
return double.MaxValue; return double.MaxValue;
var sqrtD = System.Math.Sqrt(discriminant); if (t1 > Tolerance.Epsilon) return t1;
var t = (-b - sqrtD) / (2.0 * a); if (t1 >= -Tolerance.Epsilon) return 0;
if (t2 > Tolerance.Epsilon) return t2;
if (t > Tolerance.Epsilon) return t; if (t2 >= -Tolerance.Epsilon) return 0;
if (t >= -Tolerance.Epsilon) return 0;
// First root is behind us, try the second
t = (-b + sqrtD) / (2.0 * a);
if (t > Tolerance.Epsilon) return t;
if (t >= -Tolerance.Epsilon) return 0;
return double.MaxValue; return double.MaxValue;
} }
@@ -200,57 +203,7 @@ namespace OpenNest.Geometry
/// </summary> /// </summary>
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, PushDirection direction) public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, PushDirection direction)
{ {
var minDist = double.MaxValue; return DirectionalDistance(movingLines, 0, 0, stationaryLines, direction);
// Case 1: Each moving vertex -> each stationary edge
var movingVertices = new HashSet<Vector>();
for (int i = 0; i < movingLines.Count; i++)
{
movingVertices.Add(movingLines[i].pt1);
movingVertices.Add(movingLines[i].pt2);
}
var stationaryEdges = new (Vector start, Vector end)[stationaryLines.Count];
for (int i = 0; i < stationaryLines.Count; i++)
stationaryEdges[i] = (stationaryLines[i].pt1, stationaryLines[i].pt2);
// Sort edges for pruning if not already sorted (usually they aren't here)
if (direction == PushDirection.Left || direction == PushDirection.Right)
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
else
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
foreach (var mv in movingVertices)
{
var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction);
if (d < minDist) minDist = d;
}
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
var opposite = OppositeDirection(direction);
var stationaryVertices = new HashSet<Vector>();
for (int i = 0; i < stationaryLines.Count; i++)
{
stationaryVertices.Add(stationaryLines[i].pt1);
stationaryVertices.Add(stationaryLines[i].pt2);
}
var movingEdges = new (Vector start, Vector end)[movingLines.Count];
for (int i = 0; i < movingLines.Count; i++)
movingEdges[i] = (movingLines[i].pt1, movingLines[i].pt2);
if (opposite == PushDirection.Left || opposite == PushDirection.Right)
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
else
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
foreach (var sv in stationaryVertices)
{
var d = OneWayDistance(sv, movingEdges, Vector.Zero, opposite);
if (d < minDist) minDist = d;
}
return minDist;
} }
/// <summary> /// <summary>
@@ -265,21 +218,10 @@ namespace OpenNest.Geometry
var movingOffset = new Vector(movingDx, movingDy); var movingOffset = new Vector(movingDx, movingDy);
// Case 1: Each moving vertex -> each stationary edge // Case 1: Each moving vertex -> each stationary edge
var movingVertices = new HashSet<Vector>(); var movingVertices = CollectVertices(movingLines, movingOffset);
for (int i = 0; i < movingLines.Count; i++)
{
movingVertices.Add(movingLines[i].pt1 + movingOffset);
movingVertices.Add(movingLines[i].pt2 + movingOffset);
}
var stationaryEdges = new (Vector start, Vector end)[stationaryLines.Count]; var stationaryEdges = ToEdgeArray(stationaryLines);
for (int i = 0; i < stationaryLines.Count; i++) SortEdgesForPruning(stationaryEdges, direction);
stationaryEdges[i] = (stationaryLines[i].pt1, stationaryLines[i].pt2);
if (direction == PushDirection.Left || direction == PushDirection.Right)
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
else
stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
foreach (var mv in movingVertices) foreach (var mv in movingVertices)
{ {
@@ -289,21 +231,10 @@ namespace OpenNest.Geometry
// Case 2: Each stationary vertex -> each moving edge (opposite direction) // Case 2: Each stationary vertex -> each moving edge (opposite direction)
var opposite = OppositeDirection(direction); var opposite = OppositeDirection(direction);
var stationaryVertices = new HashSet<Vector>(); var stationaryVertices = CollectVertices(stationaryLines, Vector.Zero);
for (int i = 0; i < stationaryLines.Count; i++)
{
stationaryVertices.Add(stationaryLines[i].pt1);
stationaryVertices.Add(stationaryLines[i].pt2);
}
var movingEdges = new (Vector start, Vector end)[movingLines.Count]; var movingEdges = ToEdgeArray(movingLines);
for (int i = 0; i < movingLines.Count; i++) SortEdgesForPruning(movingEdges, opposite);
movingEdges[i] = (movingLines[i].pt1, movingLines[i].pt2);
if (opposite == PushDirection.Left || opposite == PushDirection.Right)
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray();
else
movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray();
foreach (var sv in stationaryVertices) foreach (var sv in stationaryVertices)
{ {
@@ -342,15 +273,11 @@ namespace OpenNest.Geometry
{ {
var minDist = double.MaxValue; var minDist = double.MaxValue;
// Extract unique vertices from moving edges. SortEdgesForPruning(stationaryEdges, direction);
var movingVertices = new HashSet<Vector>();
for (var i = 0; i < movingEdges.Length; i++)
{
movingVertices.Add(movingEdges[i].start + movingOffset);
movingVertices.Add(movingEdges[i].end + movingOffset);
}
// Case 1: Each moving vertex -> each stationary edge // Case 1: Each moving vertex -> each stationary edge
var movingVertices = CollectVertices(movingEdges, movingOffset);
foreach (var mv in movingVertices) foreach (var mv in movingVertices)
{ {
var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction); var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction);
@@ -359,12 +286,9 @@ namespace OpenNest.Geometry
// Case 2: Each stationary vertex -> each moving edge (opposite direction) // Case 2: Each stationary vertex -> each moving edge (opposite direction)
var opposite = OppositeDirection(direction); var opposite = OppositeDirection(direction);
var stationaryVertices = new HashSet<Vector>(); SortEdgesForPruning(movingEdges, opposite);
for (var i = 0; i < stationaryEdges.Length; i++)
{ var stationaryVertices = CollectVertices(stationaryEdges, stationaryOffset);
stationaryVertices.Add(stationaryEdges[i].start + stationaryOffset);
stationaryVertices.Add(stationaryEdges[i].end + stationaryOffset);
}
foreach (var sv in stationaryVertices) foreach (var sv in stationaryVertices)
{ {
@@ -556,12 +480,7 @@ namespace OpenNest.Geometry
var dirX = direction.X; var dirX = direction.X;
var dirY = direction.Y; var dirY = direction.Y;
var movingVertices = new HashSet<Vector>(); var movingVertices = CollectVertices(movingLines, Vector.Zero);
for (var i = 0; i < movingLines.Count; i++)
{
movingVertices.Add(movingLines[i].pt1);
movingVertices.Add(movingLines[i].pt2);
}
foreach (var mv in movingVertices) foreach (var mv in movingVertices)
{ {
@@ -576,12 +495,7 @@ namespace OpenNest.Geometry
var oppX = -dirX; var oppX = -dirX;
var oppY = -dirY; var oppY = -dirY;
var stationaryVertices = new HashSet<Vector>(); var stationaryVertices = CollectVertices(stationaryLines, Vector.Zero);
for (var i = 0; i < stationaryLines.Count; i++)
{
stationaryVertices.Add(stationaryLines[i].pt1);
stationaryVertices.Add(stationaryLines[i].pt2);
}
foreach (var sv in stationaryVertices) foreach (var sv in stationaryVertices)
{ {
@@ -596,6 +510,284 @@ namespace OpenNest.Geometry
return minDist; return minDist;
} }
/// <summary>
/// Computes the minimum translation distance along a push direction
/// before any vertex/edge of movingEntities contacts any vertex/edge of
/// stationaryEntities. Delegates to the Vector-based overload.
/// </summary>
public static double DirectionalDistance(
List<Entity> movingEntities, List<Entity> stationaryEntities, PushDirection direction)
{
return DirectionalDistance(movingEntities, stationaryEntities, DirectionToOffset(direction, 1.0));
}
/// <summary>
/// Computes the minimum translation distance along an arbitrary unit direction
/// before any vertex/edge of movingEntities contacts any vertex/edge of
/// stationaryEntities. Works with native Line, Arc, and Circle entities
/// without tessellation.
/// </summary>
public static double DirectionalDistance(
List<Entity> movingEntities, List<Entity> stationaryEntities, Vector direction)
{
var minDist = double.MaxValue;
var dirX = direction.X;
var dirY = direction.Y;
var movingVertices = ExtractEntityVertices(movingEntities);
for (var v = 0; v < movingVertices.Length; v++)
{
var vx = movingVertices[v].X;
var vy = movingVertices[v].Y;
for (var j = 0; j < stationaryEntities.Count; j++)
{
var d = RayEntityDistance(vx, vy, stationaryEntities[j], dirX, dirY);
if (d < minDist)
{
minDist = d;
if (d <= 0) return 0;
}
}
}
var oppX = -dirX;
var oppY = -dirY;
var stationaryVertices = ExtractEntityVertices(stationaryEntities);
for (var v = 0; v < stationaryVertices.Length; v++)
{
var vx = stationaryVertices[v].X;
var vy = stationaryVertices[v].Y;
for (var j = 0; j < movingEntities.Count; j++)
{
var d = RayEntityDistance(vx, vy, movingEntities[j], oppX, oppY);
if (d < minDist)
{
minDist = d;
if (d <= 0) return 0;
}
}
}
// Phase 3: Arc-to-line closest-point check.
// Phases 1-2 sample arc endpoints and cardinal extremes, but the actual
// closest point on a small corner arc to a straight edge may lie between
// those samples. Use ClosestPointTo to find it and fire a ray from there.
minDist = ArcToLineClosestDistance(movingEntities, stationaryEntities, dirX, dirY, minDist);
if (minDist <= 0) return 0;
minDist = ArcToLineClosestDistance(stationaryEntities, movingEntities, oppX, oppY, minDist);
if (minDist <= 0) return 0;
// Phase 4: Curve-to-curve direct distance.
// The vertex-to-entity approach misses the closest contact between two
// curved entities (circles/arcs) because only a few cardinal vertices are
// sampled. The true closest contact along the push direction is found by
// treating it as a ray from one center to an expanded circle at the other
// center (radius = r1 + r2).
for (var i = 0; i < movingEntities.Count; i++)
{
var me = movingEntities[i];
if (!TryGetCurveParams(me, out var mcx, out var mcy, out var mr))
continue;
for (var j = 0; j < stationaryEntities.Count; j++)
{
var se = stationaryEntities[j];
if (!TryGetCurveParams(se, out var scx, out var scy, out var sr))
continue;
var d = RayCircleDistance(mcx, mcy, scx, scy, mr + sr, dirX, dirY);
if (d >= minDist)
continue;
// For arcs, verify the contact point falls within both arcs' angular ranges.
if (me is Arc || se is Arc)
{
var mx = mcx + d * dirX;
var my = mcy + d * dirY;
var toCx = scx - mx;
var toCy = scy - my;
if (me is Arc mArc)
{
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed))
continue;
}
if (se is Arc sArc)
{
var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx));
if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed))
continue;
}
}
minDist = d;
if (d <= 0) return 0;
}
}
return minDist;
}
private static double ArcToLineClosestDistance(
List<Entity> arcEntities, List<Entity> lineEntities,
double dirX, double dirY, double minDist)
{
for (var i = 0; i < arcEntities.Count; i++)
{
if (arcEntities[i] is Arc arc)
{
for (var j = 0; j < lineEntities.Count; j++)
{
if (lineEntities[j] is Line line)
{
var linePt = line.ClosestPointTo(arc.Center);
var arcPt = arc.ClosestPointTo(linePt);
var d = RayEdgeDistance(arcPt.X, arcPt.Y,
line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y,
dirX, dirY);
if (d < minDist) { minDist = d; if (d <= 0) return 0; }
}
}
}
}
return minDist;
}
private static double RayEntityDistance(
double vx, double vy, Entity entity, double dirX, double dirY)
{
if (entity is Line line)
{
return RayEdgeDistance(vx, vy,
line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y,
dirX, dirY);
}
if (entity is Arc arc)
{
return RayArcDistance(vx, vy,
arc.Center.X, arc.Center.Y, arc.Radius,
arc.StartAngle, arc.EndAngle, arc.IsReversed,
dirX, dirY);
}
if (entity is Circle circle)
{
return RayCircleDistance(vx, vy,
circle.Center.X, circle.Center.Y, circle.Radius,
dirX, dirY);
}
return double.MaxValue;
}
private static Vector[] ExtractEntityVertices(List<Entity> entities)
{
var vertices = new HashSet<Vector>();
for (var i = 0; i < entities.Count; i++)
{
var entity = entities[i];
if (entity is Line line)
{
vertices.Add(line.pt1);
vertices.Add(line.pt2);
}
else if (entity is Arc arc)
{
vertices.Add(arc.StartPoint());
vertices.Add(arc.EndPoint());
AddArcExtremeVertices(vertices, arc);
}
else if (entity is Circle circle)
{
vertices.Add(new Vector(circle.Center.X + circle.Radius, circle.Center.Y));
vertices.Add(new Vector(circle.Center.X - circle.Radius, circle.Center.Y));
vertices.Add(new Vector(circle.Center.X, circle.Center.Y + circle.Radius));
vertices.Add(new Vector(circle.Center.X, circle.Center.Y - circle.Radius));
}
}
return vertices.ToArray();
}
private static void AddArcExtremeVertices(HashSet<Vector> points, Arc arc)
{
var a1 = arc.StartAngle;
var a2 = arc.EndAngle;
if (arc.IsReversed)
Generic.Swap(ref a1, ref a2);
if (Angle.IsBetweenRad(Angle.TwoPI, a1, a2))
points.Add(new Vector(arc.Center.X + arc.Radius, arc.Center.Y));
if (Angle.IsBetweenRad(Angle.HalfPI, a1, a2))
points.Add(new Vector(arc.Center.X, arc.Center.Y + arc.Radius));
if (Angle.IsBetweenRad(System.Math.PI, a1, a2))
points.Add(new Vector(arc.Center.X - arc.Radius, arc.Center.Y));
if (Angle.IsBetweenRad(System.Math.PI * 1.5, a1, a2))
points.Add(new Vector(arc.Center.X, arc.Center.Y - arc.Radius));
}
private static HashSet<Vector> CollectVertices(List<Line> lines, Vector offset)
{
return CollectVertices(ToEdgeArray(lines), offset);
}
private static HashSet<Vector> CollectVertices((Vector start, Vector end)[] edges, Vector offset)
{
var vertices = new HashSet<Vector>();
for (var i = 0; i < edges.Length; i++)
{
vertices.Add(edges[i].start + offset);
vertices.Add(edges[i].end + offset);
}
return vertices;
}
private static (Vector start, Vector end)[] ToEdgeArray(List<Line> lines)
{
var edges = new (Vector start, Vector end)[lines.Count];
for (var i = 0; i < lines.Count; i++)
edges[i] = (lines[i].pt1, lines[i].pt2);
return edges;
}
private static void SortEdgesForPruning((Vector start, Vector end)[] edges, PushDirection direction)
{
if (direction == PushDirection.Left || direction == PushDirection.Right)
System.Array.Sort(edges, (a, b) =>
System.Math.Min(a.start.Y, a.end.Y).CompareTo(System.Math.Min(b.start.Y, b.end.Y)));
else
System.Array.Sort(edges, (a, b) =>
System.Math.Min(a.start.X, a.end.X).CompareTo(System.Math.Min(b.start.X, b.end.X)));
}
private static bool TryGetCurveParams(Entity entity, out double cx, out double cy, out double r)
{
if (entity is Circle circle)
{
cx = circle.Center.X; cy = circle.Center.Y; r = circle.Radius;
return true;
}
if (entity is Arc arc)
{
cx = arc.Center.X; cy = arc.Center.Y; r = arc.Radius;
return true;
}
cx = cy = r = 0;
return false;
}
private static double BoxProjectionMin(Box box, double dx, double dy) private static double BoxProjectionMin(Box box, double dx, double dy)
{ {
var x = dx >= 0 ? box.Left : box.Right; var x = dx >= 0 ? box.Left : box.Right;
+8 -1
View File
@@ -190,7 +190,14 @@ namespace OpenNest
{ {
var rotation = Rotation; var rotation = Rotation;
Program = BaseDrawing.Program.Clone() as Program; Program = BaseDrawing.Program.Clone() as Program;
Program.Rotate(Program.Rotation - rotation);
if (!Math.Tolerance.IsEqualTo(rotation, 0))
Program.Rotate(rotation);
HasManualLeadIns = false;
LeadInsLocked = false;
CuttingParameters = null;
UpdateBounds();
} }
/// <summary> /// <summary>
+85
View File
@@ -61,6 +61,91 @@ namespace OpenNest
return offsetShape.Entities; return offsetShape.Entities;
} }
/// <summary>
/// Returns all entities (perimeter + cutouts) with spacing offset applied,
/// without tessellation. Perimeter is offset outward, cutouts inward.
/// </summary>
public static List<Entity> GetOffsetPartEntities(Part part, double spacing)
{
var geoEntities = ConvertProgram.ToGeometry(part.Program);
var profile = new ShapeProfile(
geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
var entities = new List<Entity>();
var perimeter = profile.Perimeter.OffsetOutward(spacing);
if (perimeter != null)
{
foreach (var entity in perimeter.Entities)
entity.Offset(part.Location);
entities.AddRange(perimeter.Entities);
}
foreach (var cutout in profile.Cutouts)
{
var inset = cutout.OffsetInward(spacing);
if (inset == null) continue;
foreach (var entity in inset.Entities)
entity.Offset(part.Location);
entities.AddRange(inset.Entities);
}
return entities;
}
/// <summary>
/// Returns perimeter entities at the part's world location, without tessellation
/// or spacing offset.
/// </summary>
public static List<Entity> GetPerimeterEntities(Part part)
{
var geoEntities = ConvertProgram.ToGeometry(part.Program);
var profile = new ShapeProfile(
geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
return CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location);
}
/// <summary>
/// Returns all entities (perimeter + cutouts) at the part's world location,
/// without tessellation or spacing offset.
/// </summary>
public static List<Entity> GetPartEntities(Part part)
{
var geoEntities = ConvertProgram.ToGeometry(part.Program);
var profile = new ShapeProfile(
geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
var entities = CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location);
foreach (var cutout in profile.Cutouts)
entities.AddRange(CopyEntitiesAtLocation(cutout.Entities, part.Location));
return entities;
}
private static List<Entity> CopyEntitiesAtLocation(List<Entity> source, Vector location)
{
var result = new List<Entity>(source.Count);
for (var i = 0; i < source.Count; i++)
{
var entity = source[i];
Entity copy;
if (entity is Line line)
copy = new Line(line.StartPoint + location, line.EndPoint + location);
else if (entity is Arc arc)
copy = new Arc(arc.Center + location, arc.Radius, arc.StartAngle, arc.EndAngle, arc.IsReversed);
else if (entity is Circle circle)
copy = new Circle(circle.Center + location, circle.Radius);
else
continue;
result.Add(copy);
}
return result;
}
public static List<Line> GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001, public static List<Line> GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001,
bool perimeterOnly = false) bool perimeterOnly = false)
{ {
+5
View File
@@ -7,6 +7,11 @@ namespace OpenNest.Shapes
{ {
public double Diameter { get; set; } public double Diameter { get; set; }
public override void SetPreviewDefaults()
{
Diameter = 8;
}
public override Drawing GetDrawing() public override Drawing GetDrawing()
{ {
var entities = new List<Entity> var entities = new List<Entity>
+9
View File
@@ -11,6 +11,15 @@ namespace OpenNest.Shapes
public double HolePatternDiameter { get; set; } public double HolePatternDiameter { get; set; }
public int HoleCount { get; set; } public int HoleCount { get; set; }
public override void SetPreviewDefaults()
{
NominalPipeSize = 2;
OD = 7.5;
HoleDiameter = 0.875;
HolePatternDiameter = 5.5;
HoleCount = 8;
}
public override Drawing GetDrawing() public override Drawing GetDrawing()
{ {
var entities = new List<Entity>(); var entities = new List<Entity>();
@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
public double Base { get; set; } public double Base { get; set; }
public double Height { get; set; } public double Height { get; set; }
public override void SetPreviewDefaults()
{
Base = 8;
Height = 10;
}
public override Drawing GetDrawing() public override Drawing GetDrawing()
{ {
var midX = Base / 2.0; var midX = Base / 2.0;
+8
View File
@@ -10,6 +10,14 @@ namespace OpenNest.Shapes
public double LegWidth { get; set; } public double LegWidth { get; set; }
public double LegHeight { get; set; } public double LegHeight { get; set; }
public override void SetPreviewDefaults()
{
Width = 8;
Height = 10;
LegWidth = 3;
LegHeight = 3;
}
public override Drawing GetDrawing() public override Drawing GetDrawing()
{ {
var lw = LegWidth > 0 ? LegWidth : Width / 2.0; var lw = LegWidth > 0 ? LegWidth : Width / 2.0;
+5
View File
@@ -7,6 +7,11 @@ namespace OpenNest.Shapes
{ {
public double Width { get; set; } public double Width { get; set; }
public override void SetPreviewDefaults()
{
Width = 8;
}
public override Drawing GetDrawing() public override Drawing GetDrawing()
{ {
var center = Width / 2.0; var center = Width / 2.0;
+6
View File
@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
public double Length { get; set; } public double Length { get; set; }
public double Width { get; set; } public double Width { get; set; }
public override void SetPreviewDefaults()
{
Length = 12;
Width = 6;
}
public override Drawing GetDrawing() public override Drawing GetDrawing()
{ {
var entities = new List<Entity> var entities = new List<Entity>
@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
public double Width { get; set; } public double Width { get; set; }
public double Height { get; set; } public double Height { get; set; }
public override void SetPreviewDefaults()
{
Width = 8;
Height = 6;
}
public override Drawing GetDrawing() public override Drawing GetDrawing()
{ {
var entities = new List<Entity> var entities = new List<Entity>
+6
View File
@@ -8,6 +8,12 @@ namespace OpenNest.Shapes
public double OuterDiameter { get; set; } public double OuterDiameter { get; set; }
public double InnerDiameter { get; set; } public double InnerDiameter { get; set; }
public override void SetPreviewDefaults()
{
OuterDiameter = 10;
InnerDiameter = 6;
}
public override Drawing GetDrawing() public override Drawing GetDrawing()
{ {
var entities = new List<Entity> var entities = new List<Entity>
@@ -10,6 +10,13 @@ namespace OpenNest.Shapes
public double Width { get; set; } public double Width { get; set; }
public double Radius { get; set; } public double Radius { get; set; }
public override void SetPreviewDefaults()
{
Length = 12;
Width = 6;
Radius = 1;
}
public override Drawing GetDrawing() public override Drawing GetDrawing()
{ {
var r = Radius; var r = Radius;
+2
View File
@@ -26,6 +26,8 @@ namespace OpenNest.Shapes
public abstract Drawing GetDrawing(); public abstract Drawing GetDrawing();
public virtual void SetPreviewDefaults() { }
public static List<T> LoadFromJson<T>(string path) where T : ShapeDefinition public static List<T> LoadFromJson<T>(string path) where T : ShapeDefinition
{ {
var json = File.ReadAllText(path); var json = File.ReadAllText(path);
+8
View File
@@ -10,6 +10,14 @@ namespace OpenNest.Shapes
public double StemWidth { get; set; } public double StemWidth { get; set; }
public double BarHeight { get; set; } public double BarHeight { get; set; }
public override void SetPreviewDefaults()
{
Width = 10;
Height = 8;
StemWidth = 3;
BarHeight = 3;
}
public override Drawing GetDrawing() public override Drawing GetDrawing()
{ {
var sw = StemWidth > 0 ? StemWidth : Width / 3.0; var sw = StemWidth > 0 ? StemWidth : Width / 3.0;
+7
View File
@@ -9,6 +9,13 @@ namespace OpenNest.Shapes
public double BottomWidth { get; set; } public double BottomWidth { get; set; }
public double Height { get; set; } public double Height { get; set; }
public override void SetPreviewDefaults()
{
TopWidth = 6;
BottomWidth = 10;
Height = 6;
}
public override Drawing GetDrawing() public override Drawing GetDrawing()
{ {
var offset = (BottomWidth - TopWidth) / 2.0; var offset = (BottomWidth - TopWidth) / 2.0;
+2 -1
View File
@@ -17,7 +17,8 @@ namespace OpenNest.Engine.BestFit
if (!result.Keep) if (!result.Keep)
continue; continue;
if (result.ShortestSide > System.Math.Min(MaxPlateWidth, MaxPlateHeight)) if (result.ShortestSide > System.Math.Min(MaxPlateWidth, MaxPlateHeight) ||
result.LongestSide > System.Math.Max(MaxPlateWidth, MaxPlateHeight))
{ {
result.Keep = false; result.Keep = false;
result.Reason = "Exceeds plate dimensions"; result.Reason = "Exceeds plate dimensions";
@@ -104,6 +104,9 @@ namespace OpenNest.Engine.BestFit
var allMovingVerts = ExtractVerticesFromEntities(movingEntities); var allMovingVerts = ExtractVerticesFromEntities(movingEntities);
var allStationaryVerts = ExtractVerticesFromEntities(stationaryEntities); var allStationaryVerts = ExtractVerticesFromEntities(stationaryEntities);
var movingCurves = ExtractCurveParams(movingEntities);
var stationaryCurves = ExtractCurveParams(stationaryEntities);
var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>(); var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>();
foreach (var offset in offsets) foreach (var offset in offsets)
@@ -165,12 +168,84 @@ namespace OpenNest.Engine.BestFit
} }
} }
// Phase 3: Curve-to-curve direct distance.
// Vertex sampling misses the true contact between two curved entities
// when the approach angle doesn't align with a sampled vertex.
for (var m = 0; m < movingCurves.Length; m++)
{
var mc = movingCurves[m];
var mcx = mc.Cx + offset.Dx;
var mcy = mc.Cy + offset.Dy;
for (var s = 0; s < stationaryCurves.Length; s++)
{
var sc = stationaryCurves[s];
var d = SpatialQuery.RayCircleDistance(
mcx, mcy, sc.Cx, sc.Cy, mc.Radius + sc.Radius, dirX, dirY);
if (d >= minDist || d == double.MaxValue)
continue;
if (mc.Entity is Arc || sc.Entity is Arc)
{
var mx = mcx + d * dirX;
var my = mcy + d * dirY;
var toCx = sc.Cx - mx;
var toCy = sc.Cy - my;
if (mc.Entity is Arc mArc)
{
var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx));
if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed))
continue;
}
if (sc.Entity is Arc sArc)
{
var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx));
if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed))
continue;
}
}
minDist = d;
if (d <= 0) { results[i] = 0; return; }
}
}
results[i] = minDist; results[i] = minDist;
}); });
return results; return results;
} }
private readonly struct CurveParams
{
public readonly Entity Entity;
public readonly double Cx, Cy, Radius;
public CurveParams(Entity entity, double cx, double cy, double radius)
{
Entity = entity;
Cx = cx;
Cy = cy;
Radius = radius;
}
}
private static CurveParams[] ExtractCurveParams(List<Entity> entities)
{
var curves = new List<CurveParams>();
for (var i = 0; i < entities.Count; i++)
{
if (entities[i] is Circle circle)
curves.Add(new CurveParams(circle, circle.Center.X, circle.Center.Y, circle.Radius));
else if (entities[i] is Arc arc)
curves.Add(new CurveParams(arc, arc.Center.X, arc.Center.Y, arc.Radius));
}
return curves.ToArray();
}
private static double RayEntityDistance( private static double RayEntityDistance(
double vx, double vy, Entity entity, double vx, double vy, Entity entity,
double entityOffsetX, double entityOffsetY, double entityOffsetX, double entityOffsetY,
+26 -12
View File
@@ -11,8 +11,6 @@ namespace OpenNest.Engine.Fill
/// </summary> /// </summary>
public static class Compactor public static class Compactor
{ {
private const double ChordTolerance = 0.001;
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction) public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
{ {
var obstacleParts = plate.Parts var obstacleParts = plate.Parts
@@ -44,7 +42,7 @@ namespace OpenNest.Engine.Fill
var opposite = -direction; var opposite = -direction;
var obstacleBoxes = new Box[obstacleParts.Count]; var obstacleBoxes = new Box[obstacleParts.Count];
var obstacleLines = new List<Line>[obstacleParts.Count]; var obstacleEntities = new List<Entity>[obstacleParts.Count];
for (var i = 0; i < obstacleParts.Count; i++) for (var i = 0; i < obstacleParts.Count; i++)
obstacleBoxes[i] = obstacleParts[i].BoundingBox; obstacleBoxes[i] = obstacleParts[i].BoundingBox;
@@ -61,7 +59,19 @@ namespace OpenNest.Engine.Fill
distance = edgeDist; distance = edgeDist;
var movingBox = moving.BoundingBox; var movingBox = moving.BoundingBox;
List<Line> movingLines = null; List<Entity> movingEntities = null;
// Check if any obstacle is inside the moving part — only then
// do we need cutout entities on the moving part.
var needCutouts = false;
for (var i = 0; i < obstacleBoxes.Length; i++)
{
if (movingBox.Contains(obstacleBoxes[i]))
{
needCutouts = true;
break;
}
}
for (var i = 0; i < obstacleBoxes.Length; i++) for (var i = 0; i < obstacleBoxes.Length; i++)
{ {
@@ -76,15 +86,19 @@ namespace OpenNest.Engine.Fill
if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction)) if (!SpatialQuery.PerpendicularOverlap(movingBox, obstacleBoxes[i], direction))
continue; continue;
movingLines ??= halfSpacing > 0 movingEntities ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance) ? (needCutouts
: PartGeometry.GetPartLines(moving, direction, ChordTolerance); ? PartGeometry.GetOffsetPartEntities(moving, halfSpacing)
: PartGeometry.GetOffsetPerimeterEntities(moving, halfSpacing))
: (needCutouts
? PartGeometry.GetPartEntities(moving)
: PartGeometry.GetPerimeterEntities(moving));
obstacleLines[i] ??= halfSpacing > 0 obstacleEntities[i] ??= halfSpacing > 0
? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance) ? PartGeometry.GetOffsetPerimeterEntities(obstacleParts[i], halfSpacing)
: PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance); : PartGeometry.GetPerimeterEntities(obstacleParts[i]);
var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction); var d = SpatialQuery.DirectionalDistance(movingEntities, obstacleEntities[i], direction);
if (d < distance) if (d < distance)
distance = d; distance = d;
} }
@@ -157,7 +171,7 @@ namespace OpenNest.Engine.Fill
continue; continue;
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction); var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
var d = gap - partSpacing - 2 * ChordTolerance; var d = gap - partSpacing - 0.002;
if (d < 0) d = 0; if (d < 0) d = 0;
if (d < distance) if (d < distance)
distance = d; distance = d;
+5 -4
View File
@@ -119,10 +119,11 @@ namespace OpenNest.Engine.Fill
var maxCopyDistance = FindMaxPairDistance( var maxCopyDistance = FindMaxPairDistance(
patternA.Parts, boundaries, offset, pushDir, opposite, startOffset); patternA.Parts, boundaries, offset, pushDir, opposite, startOffset);
if (maxCopyDistance < Tolerance.Epsilon) // The copy distance must be at least bboxDim + PartSpacing to prevent
return bboxDim + PartSpacing; // bounding box overlap. Cross-pair slides can underestimate when the
// circumscribed polygon boundary overshoots the true arc, creating
return maxCopyDistance; // spurious contacts between diagonal parts in adjacent copies.
return System.Math.Max(maxCopyDistance, bboxDim + PartSpacing);
} }
/// <summary> /// <summary>
+661
View File
@@ -0,0 +1,661 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest
{
public enum PartClass
{
Large,
Medium,
Small,
}
public class MultiPlateNester
{
private readonly Plate _template;
private readonly List<PlateOption> _plateOptions;
private readonly List<PlateOption> _sortedOptions;
private readonly double _salvageRate;
private readonly double _minRemnantSize;
private readonly List<PlateResult> _platePool;
private readonly IProgress<NestProgress> _progress;
private readonly CancellationToken _token;
private readonly MultiPlateNestOptions _options;
private bool HasPlateOptions => _plateOptions != null && _plateOptions.Count > 0;
private MultiPlateNester(
MultiPlateNestOptions options,
List<Plate> existingPlates,
IProgress<NestProgress> progress, CancellationToken token)
{
_options = options;
_template = options.Template;
_plateOptions = options.PlateOptions;
_sortedOptions = options.PlateOptions?.OrderBy(o => o.Cost).ToList();
_salvageRate = options.SalvageRate;
_minRemnantSize = options.MinRemnantSize;
_platePool = InitializePlatePool(existingPlates);
_progress = progress;
_token = token;
}
// --- Static Utility Methods ---
public static bool FitsBounds(Box container, Box part)
{
var fitsNormal = container.Width >= part.Width - Tolerance.Epsilon
&& container.Length >= part.Length - Tolerance.Epsilon;
var fitsRotated = container.Width >= part.Length - Tolerance.Epsilon
&& container.Length >= part.Width - Tolerance.Epsilon;
return fitsNormal || fitsRotated;
}
public static List<NestItem> SortItems(List<NestItem> items, PartSortOrder sortOrder)
{
var withBounds = items.Select(i => (Item: i, Bounds: i.Drawing.Program.BoundingBox())).ToList();
switch (sortOrder)
{
case PartSortOrder.BoundingBoxArea:
return withBounds
.OrderByDescending(x => x.Bounds.Width * x.Bounds.Length)
.Select(x => x.Item)
.ToList();
case PartSortOrder.Size:
return withBounds
.OrderByDescending(x => System.Math.Max(x.Bounds.Width, x.Bounds.Length))
.Select(x => x.Item)
.ToList();
default:
return items.ToList();
}
}
public static PartClass Classify(Box partBounds, Box workArea)
{
var halfWidth = workArea.Width / 2.0;
var halfLength = workArea.Length / 2.0;
if (partBounds.Width > halfWidth || partBounds.Length > halfLength)
return PartClass.Large;
var workAreaArea = workArea.Width * workArea.Length;
var partArea = partBounds.Width * partBounds.Length;
if (partArea > workAreaArea / 9.0)
return PartClass.Medium;
return PartClass.Small;
}
public static bool IsScrapRemnant(Box remnant, double minRemnantSize)
{
return remnant.Width < minRemnantSize && remnant.Length < minRemnantSize;
}
public static List<Box> FindRemnants(Plate plate, double minRemnantSize, bool scrapOnly)
{
var remnants = RemnantFinder.FromPlate(plate).FindRemnants();
return remnants.Where(r => IsScrapRemnant(r, minRemnantSize) == scrapOnly).ToList();
}
public struct UpgradeDecision
{
public bool ShouldUpgrade;
public double UpgradeCost;
public double NewPlateCost;
}
public static Plate CreatePlate(Plate template, List<PlateOption> options, Box minBounds)
{
var plate = new Plate(template.Size)
{
PartSpacing = template.PartSpacing,
Quadrant = template.Quadrant,
};
plate.EdgeSpacing = new Spacing
{
Left = template.EdgeSpacing.Left,
Right = template.EdgeSpacing.Right,
Top = template.EdgeSpacing.Top,
Bottom = template.EdgeSpacing.Bottom,
};
if (options == null || options.Count == 0 || minBounds == null)
return plate;
var sorted = options.OrderBy(o => o.Cost).ToList();
foreach (var option in sorted)
{
if (FitsBounds(OptionWorkArea(option, template), minBounds))
{
plate.Size = new Size(option.Width, option.Length);
return plate;
}
}
return plate;
}
public static UpgradeDecision EvaluateUpgradeVsNew(
PlateOption currentSize,
PlateOption upgradeSize,
PlateOption newPlateSize,
double salvageRate,
double estimatedNewPlateUtilization)
{
var upgradeCost = upgradeSize.Cost - currentSize.Cost;
var newPlateCost = newPlateSize.Cost;
var remnantFraction = 1.0 - estimatedNewPlateUtilization;
var salvageCredit = remnantFraction * newPlateSize.Cost * salvageRate;
var netNewCost = newPlateCost - salvageCredit;
return new UpgradeDecision
{
ShouldUpgrade = upgradeCost <= netNewCost,
UpgradeCost = upgradeCost,
NewPlateCost = netNewCost,
};
}
// --- Main Entry Point ---
public static MultiPlateResult Nest(
List<NestItem> items,
MultiPlateNestOptions options,
List<Plate> existingPlates = null,
IProgress<NestProgress> progress = null,
CancellationToken token = default)
{
var nester = new MultiPlateNester(options, existingPlates, progress, token);
return nester.Run(items, options.SortOrder, options.AllowPlateCreation);
}
// --- Private Helpers ---
private static Box OptionWorkArea(PlateOption option, Plate template)
{
var w = option.Width - template.EdgeSpacing.Left - template.EdgeSpacing.Right;
var h = option.Length - template.EdgeSpacing.Top - template.EdgeSpacing.Bottom;
return new Box(0, 0, w, h);
}
private static double ScoreZone(Box zone, Box partBounds)
{
if (!FitsBounds(zone, partBounds))
return -1;
var cols = (int)(zone.Width / partBounds.Width);
var rows = (int)(zone.Length / partBounds.Length);
var colsR = (int)(zone.Width / partBounds.Length);
var rowsR = (int)(zone.Length / partBounds.Width);
var estimatedCount = System.Math.Max(cols * rows, colsR * rowsR);
var utilization = (estimatedCount * partBounds.Width * partBounds.Length) / zone.Area();
var zoneAspect = zone.Width / zone.Length;
var partAspect = partBounds.Width / partBounds.Length;
var aspectMatch = System.Math.Min(zoneAspect, partAspect) / System.Math.Max(zoneAspect, partAspect);
return utilization * 0.7 + aspectMatch * 0.3;
}
private static void DecrementQuantity(NestItem item, int placed)
{
item.Quantity = System.Math.Max(0, item.Quantity - placed);
}
private int FillAndPlace(PlateResult pr, Box zone, NestItem item)
{
var engine = NestEngineRegistry.Create(pr.Plate);
var clonedItem = CloneItem(item);
var parts = engine.Fill(clonedItem, zone, _progress, _token);
if (parts.Count > 0)
{
pr.AddParts(parts);
DecrementQuantity(item, parts.Count);
}
return parts.Count;
}
private PlateResult CreateNewPlateResult(Plate plate)
{
var pr = new PlateResult { Plate = plate, IsNew = true };
if (HasPlateOptions)
{
pr.ChosenSize = _plateOptions.FirstOrDefault(o =>
o.Width.IsEqualTo(plate.Size.Width) && o.Length.IsEqualTo(plate.Size.Length));
}
return pr;
}
private static NestItem CloneItem(NestItem item)
{
return new NestItem
{
Drawing = item.Drawing,
Priority = item.Priority,
Quantity = item.Quantity,
StepAngle = item.StepAngle,
RotationStart = item.RotationStart,
RotationEnd = item.RotationEnd,
};
}
private static List<PlateResult> InitializePlatePool(List<Plate> existingPlates)
{
var pool = new List<PlateResult>();
if (existingPlates != null)
{
foreach (var plate in existingPlates)
pool.Add(new PlateResult { Plate = plate, IsNew = false });
}
return pool;
}
private bool TryWithUpgradedSize(PlateResult pr, PlateOption upgradeOption, Func<List<Box>, bool> tryFill)
{
var oldSize = pr.Plate.Size;
var oldChosenSize = pr.ChosenSize;
pr.Plate.Size = new Size(upgradeOption.Width, upgradeOption.Length);
pr.ChosenSize = upgradeOption;
var remnants = RemnantFinder.FromPlate(pr.Plate).FindRemnants();
if (remnants.Count > 0 && tryFill(remnants))
return true;
pr.Plate.Size = oldSize;
pr.ChosenSize = oldChosenSize;
return false;
}
private PlateOption FindSmallestFittingOption(Box partBounds)
{
return _sortedOptions?.FirstOrDefault(o => FitsBounds(OptionWorkArea(o, _template), partBounds));
}
// --- Orchestration ---
private MultiPlateResult Run(List<NestItem> items, PartSortOrder sortOrder, bool allowPlateCreation)
{
var result = new MultiPlateResult();
if (items == null || items.Count == 0)
return result;
var sorted = SortItems(items.Where(i => i.Quantity > 0).ToList(), sortOrder);
foreach (var item in sorted)
{
if (_token.IsCancellationRequested || item.Quantity <= 0)
continue;
var bb = item.Drawing.Program.BoundingBox();
TryPlaceOnExistingPlates(item, bb);
var templateClass = Classify(bb, _template.WorkArea());
if (item.Quantity > 0 && allowPlateCreation && templateClass != PartClass.Small)
{
PlaceOnNewPlates(item, bb);
if (item.Quantity > 0 && HasPlateOptions)
TryUpgradeOrNewPlate(item, bb);
}
}
var leftovers = sorted.Where(i => i.Quantity > 0).ToList();
if (leftovers.Count > 0 && allowPlateCreation && !_token.IsCancellationRequested)
{
PackIntoExistingRemnants(leftovers);
CreateSharedPlates(leftovers);
}
if (HasPlateOptions && !_token.IsCancellationRequested)
TryConsolidateTailPlates();
foreach (var item in sorted.Where(i => i.Quantity > 0))
result.UnplacedItems.Add(item);
result.Plates.AddRange(_platePool.Where(p => p.Parts.Count > 0 || p.IsNew));
return result;
}
private void PackIntoExistingRemnants(List<NestItem> leftovers)
{
foreach (var pr in _platePool)
{
if (_token.IsCancellationRequested)
break;
var anyPlaced = true;
while (anyPlaced && !_token.IsCancellationRequested)
{
anyPlaced = false;
var remaining = leftovers.Where(i => i.Quantity > 0).ToList();
if (remaining.Count == 0)
break;
var remnants = RemnantFinder.FromPlate(pr.Plate).FindRemnants();
if (remnants.Count == 0)
break;
var engine = NestEngineRegistry.Create(pr.Plate);
foreach (var remnant in remnants)
{
remaining = leftovers.Where(i => i.Quantity > 0).ToList();
if (remaining.Count == 0)
break;
var cloned = remaining.Select(CloneItem).ToList();
var parts = engine.PackArea(remnant, cloned, _progress, _token);
if (parts.Count > 0)
{
pr.AddParts(parts);
anyPlaced = true;
foreach (var item in remaining)
{
var placed = parts.Count(p => p.BaseDrawing == item.Drawing);
DecrementQuantity(item, placed);
}
}
}
}
}
}
private void CreateSharedPlates(List<NestItem> leftovers)
{
leftovers.RemoveAll(i => i.Quantity <= 0);
while (leftovers.Count > 0 && !_token.IsCancellationRequested)
{
var plate = CreatePlate(_template, _plateOptions, null);
var pr = CreateNewPlateResult(plate);
var placedAny = false;
foreach (var item in leftovers)
{
if (item.Quantity <= 0 || _token.IsCancellationRequested)
continue;
var remnants = !placedAny
? new List<Box> { plate.WorkArea() }
: RemnantFinder.FromPlate(plate).FindRemnants();
if (remnants.Count == 0)
break;
var engine = NestEngineRegistry.Create(plate);
foreach (var remnant in remnants)
{
if (item.Quantity <= 0)
break;
var clonedItem = CloneItem(item);
var parts = engine.Fill(clonedItem, remnant, _progress, _token);
if (parts.Count > 0)
{
pr.AddParts(parts);
DecrementQuantity(item, parts.Count);
placedAny = true;
}
}
}
if (!placedAny)
break;
_platePool.Add(pr);
leftovers.RemoveAll(i => i.Quantity <= 0);
}
}
private bool TryPlaceOnExistingPlates(NestItem item, Box partBounds)
{
var anyPlaced = false;
var remnantCache = new Dictionary<PlateResult, List<Box>>();
PlateResult lastModified = null;
while (item.Quantity > 0 && !_token.IsCancellationRequested)
{
PlateResult bestPlate = null;
Box bestZone = null;
var bestScore = double.MinValue;
foreach (var pr in _platePool)
{
if (_token.IsCancellationRequested)
break;
if (pr == lastModified || !remnantCache.ContainsKey(pr))
{
var workArea = pr.Plate.WorkArea();
var classification = Classify(partBounds, workArea);
remnantCache[pr] = classification == PartClass.Small
? FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: true)
: FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: false);
}
foreach (var zone in remnantCache[pr])
{
var score = ScoreZone(zone, partBounds);
if (score > bestScore)
{
bestPlate = pr;
bestZone = zone;
bestScore = score;
}
}
}
if (bestPlate == null || bestZone == null)
break;
if (FillAndPlace(bestPlate, bestZone, item) == 0)
break;
lastModified = bestPlate;
anyPlaced = true;
}
return anyPlaced;
}
private bool PlaceOnNewPlates(NestItem item, Box partBounds)
{
var anyPlaced = false;
while (item.Quantity > 0 && !_token.IsCancellationRequested)
{
var plate = CreatePlate(_template, _plateOptions, partBounds);
var workArea = plate.WorkArea();
if (!FitsBounds(workArea, partBounds))
break;
var pr = CreateNewPlateResult(plate);
if (FillAndPlace(pr, workArea, item) == 0)
break;
_platePool.Add(pr);
anyPlaced = true;
}
return anyPlaced;
}
private bool TryUpgradeOrNewPlate(NestItem item, Box partBounds)
{
if (!HasPlateOptions)
return false;
foreach (var pr in _platePool.Where(p => p.IsNew && p.ChosenSize != null))
{
var currentOption = pr.ChosenSize;
var currentIdx = _sortedOptions.FindIndex(o =>
o.Width.IsEqualTo(currentOption.Width) && o.Length.IsEqualTo(currentOption.Length));
if (currentIdx < 0 || currentIdx >= _sortedOptions.Count - 1)
continue;
for (var i = currentIdx + 1; i < _sortedOptions.Count; i++)
{
var upgradeOption = _sortedOptions[i];
if (upgradeOption.Width < currentOption.Width - Tolerance.Epsilon
|| upgradeOption.Length < currentOption.Length - Tolerance.Epsilon)
continue;
var smallestNew = FindSmallestFittingOption(partBounds);
if (smallestNew == null)
continue;
var utilEst = pr.Plate.Utilization();
var decision = EvaluateUpgradeVsNew(currentOption, upgradeOption, smallestNew,
_salvageRate, utilEst);
if (decision.ShouldUpgrade)
{
var placed = TryWithUpgradedSize(pr, upgradeOption, remnants =>
{
foreach (var remnant in remnants)
{
if (FillAndPlace(pr, remnant, item) > 0)
return true;
}
return false;
});
if (placed)
return true;
}
}
}
return false;
}
private void TryConsolidateTailPlates()
{
var consolidated = true;
while (consolidated)
{
consolidated = false;
var activePlates = _platePool.Where(p => p.Parts.Count > 0 && p.IsNew).ToList();
if (activePlates.Count < 2)
return;
var donors = activePlates.OrderBy(p => p.Plate.Utilization()).ToList();
foreach (var donor in donors)
{
if (donor.Parts.Count == 0)
continue;
var donorParts = donor.Parts.ToList();
var absorbed = false;
foreach (var target in activePlates)
{
if (target == donor || target.ChosenSize == null || target.Parts.Count == 0)
continue;
var currentOption = target.ChosenSize;
foreach (var upgradeOption in _sortedOptions.Where(o =>
o.Width >= currentOption.Width - Tolerance.Epsilon
&& o.Length >= currentOption.Length - Tolerance.Epsilon
&& (o.Width > currentOption.Width + Tolerance.Epsilon
|| o.Length > currentOption.Length + Tolerance.Epsilon)))
{
absorbed = TryWithUpgradedSize(target, upgradeOption, remnants =>
{
var engine = NestEngineRegistry.Create(target.Plate);
var tempItems = donorParts
.GroupBy(p => p.BaseDrawing)
.Select(g => new NestItem
{
Drawing = g.Key,
Quantity = g.Count(),
})
.ToList();
var totalPlaced = new List<Part>();
foreach (var remnant in remnants)
{
var placed = engine.PackArea(remnant, tempItems, _progress, _token);
totalPlaced.AddRange(placed);
foreach (var ti in tempItems)
{
var count = placed.Count(p => p.BaseDrawing == ti.Drawing);
ti.Quantity = System.Math.Max(0, ti.Quantity - count);
}
if (tempItems.All(ti => ti.Quantity <= 0))
break;
}
if (totalPlaced.Count >= donorParts.Count)
{
target.AddParts(totalPlaced);
foreach (var p in donorParts)
donor.Plate.Parts.Remove(p);
donor.Parts.Clear();
_platePool.Remove(donor);
return true;
}
return false;
});
if (absorbed)
break;
}
if (absorbed)
break;
}
if (absorbed)
{
consolidated = true;
break;
}
}
}
}
}
}
+34
View File
@@ -0,0 +1,34 @@
using System.Collections.Generic;
namespace OpenNest
{
public class MultiPlateNestOptions
{
public Plate Template { get; set; }
public List<PlateOption> PlateOptions { get; set; }
public double SalvageRate { get; set; } = 0.5;
public PartSortOrder SortOrder { get; set; } = PartSortOrder.BoundingBoxArea;
public double MinRemnantSize { get; set; } = 12.0;
public bool AllowPlateCreation { get; set; } = true;
}
public class MultiPlateResult
{
public List<PlateResult> Plates { get; set; } = new();
public List<NestItem> UnplacedItems { get; set; } = new();
}
public class PlateResult
{
public Plate Plate { get; set; }
public List<Part> Parts { get; set; } = new();
public PlateOption ChosenSize { get; set; }
public bool IsNew { get; set; }
public void AddParts(IList<Part> parts)
{
Plate.Parts.AddRange(parts);
Parts.AddRange(parts);
}
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace OpenNest
{
public enum PartSortOrder
{
BoundingBoxArea,
Size,
}
}
@@ -4,7 +4,7 @@ using System.Collections.Generic;
namespace OpenNest.Engine namespace OpenNest.Engine
{ {
public class PlateResult public class PlateProcessingResult
{ {
public List<ProcessedPart> Parts { get; init; } public List<ProcessedPart> Parts { get; init; }
} }
+2 -2
View File
@@ -14,7 +14,7 @@ namespace OpenNest.Engine
public ContourCuttingStrategy CuttingStrategy { get; set; } public ContourCuttingStrategy CuttingStrategy { get; set; }
public IRapidPlanner RapidPlanner { get; set; } public IRapidPlanner RapidPlanner { get; set; }
public PlateResult Process(Plate plate) public PlateProcessingResult Process(Plate plate)
{ {
var sequenced = Sequencer.Sequence(plate.Parts.ToList(), plate); var sequenced = Sequencer.Sequence(plate.Parts.ToList(), plate);
var results = new List<ProcessedPart>(sequenced.Count); var results = new List<ProcessedPart>(sequenced.Count);
@@ -66,7 +66,7 @@ namespace OpenNest.Engine
currentPoint = ToPlateSpace(lastCutLocal, part); currentPoint = ToPlateSpace(lastCutLocal, part);
} }
return new PlateResult { Parts = results }; return new PlateProcessingResult { Parts = results };
} }
private static Vector ToPartLocal(Vector platePoint, Part part) private static Vector ToPartLocal(Vector platePoint, Part part)
+139
View File
@@ -0,0 +1,139 @@
using OpenNest.Geometry;
using System;
using System.Collections.Generic;
using System.Linq;
using static OpenNest.IO.NestFormat;
namespace OpenNest.IO
{
public static class EntitySerializer
{
public static EntitySetDto ToDto(List<Entity> entities, HashSet<Guid> suppressed)
{
return new EntitySetDto
{
Entities = entities.Select(ToEntityDto).ToList(),
Suppressed = suppressed.Select(id => id.ToString()).ToList()
};
}
public static (List<Entity> entities, HashSet<Guid> suppressed) FromDto(EntitySetDto dto)
{
var entities = dto.Entities.Select(FromEntityDto).ToList();
var suppressed = new HashSet<Guid>(dto.Suppressed.Select(Guid.Parse));
return (entities, suppressed);
}
private static EntityDto ToEntityDto(Entity entity)
{
switch (entity.Type)
{
case EntityType.Line:
var line = (Line)entity;
return new EntityDto
{
Id = entity.Id.ToString(),
Type = "line",
Layer = entity.Layer?.Name ?? "",
LineType = entity.LineTypeName ?? "",
X1 = line.StartPoint.X,
Y1 = line.StartPoint.Y,
X2 = line.EndPoint.X,
Y2 = line.EndPoint.Y
};
case EntityType.Arc:
var arc = (Arc)entity;
return new EntityDto
{
Id = entity.Id.ToString(),
Type = "arc",
Layer = entity.Layer?.Name ?? "",
LineType = entity.LineTypeName ?? "",
CX = arc.Center.X,
CY = arc.Center.Y,
R = arc.Radius,
StartAngle = arc.StartAngle,
EndAngle = arc.EndAngle,
Reversed = arc.IsReversed
};
case EntityType.Circle:
var circle = (Circle)entity;
return new EntityDto
{
Id = entity.Id.ToString(),
Type = "circle",
Layer = entity.Layer?.Name ?? "",
LineType = entity.LineTypeName ?? "",
CX = circle.Center.X,
CY = circle.Center.Y,
R = circle.Radius,
Rotation = circle.Rotation == RotationType.CW ? "CW" : "CCW"
};
default:
throw new NotSupportedException($"Entity type {entity.Type} is not supported for serialization.");
}
}
private static Entity FromEntityDto(EntityDto dto)
{
Entity entity;
switch (dto.Type)
{
case "line":
entity = new Line(
new Vector(dto.X1, dto.Y1),
new Vector(dto.X2, dto.Y2));
break;
case "arc":
entity = new Arc(
new Vector(dto.CX, dto.CY),
dto.R,
dto.StartAngle,
dto.EndAngle,
dto.Reversed);
break;
case "circle":
var circle = new Circle(new Vector(dto.CX, dto.CY), dto.R);
circle.Rotation = dto.Rotation == "CW" ? RotationType.CW : RotationType.CCW;
entity = circle;
break;
default:
throw new NotSupportedException($"Entity type '{dto.Type}' is not supported for deserialization.");
}
entity.Id = Guid.Parse(dto.Id);
entity.Layer = ResolveLayer(dto.Layer);
entity.LineTypeName = dto.LineType;
return entity;
}
private static Layer ResolveLayer(string name)
{
if (string.IsNullOrEmpty(name) || name == "0")
return Layer.Default;
if (string.Equals(name, SpecialLayers.Cut.Name, StringComparison.OrdinalIgnoreCase))
return SpecialLayers.Cut;
if (string.Equals(name, SpecialLayers.Rapid.Name, StringComparison.OrdinalIgnoreCase))
return SpecialLayers.Rapid;
if (string.Equals(name, SpecialLayers.Display.Name, StringComparison.OrdinalIgnoreCase))
return SpecialLayers.Display;
if (string.Equals(name, SpecialLayers.Leadin.Name, StringComparison.OrdinalIgnoreCase))
return SpecialLayers.Leadin;
if (string.Equals(name, SpecialLayers.Leadout.Name, StringComparison.OrdinalIgnoreCase))
return SpecialLayers.Leadout;
if (string.Equals(name, SpecialLayers.Scribe.Name, StringComparison.OrdinalIgnoreCase))
return SpecialLayers.Scribe;
return new Layer(name);
}
}
}
+29
View File
@@ -162,6 +162,35 @@ namespace OpenNest.IO
public double Cost { get; init; } public double Cost { get; init; }
} }
public record EntitySetDto
{
public List<EntityDto> Entities { get; init; } = new();
public List<string> Suppressed { get; init; } = new();
}
public record EntityDto
{
public string Id { get; init; } = "";
public string Type { get; init; } = "";
public string Layer { get; init; } = "";
public string LineType { get; init; } = "";
// Line
public double X1 { get; init; }
public double Y1 { get; init; }
public double X2 { get; init; }
public double Y2 { get; init; }
// Arc / Circle
public double CX { get; init; }
public double CY { get; init; }
public double R { get; init; }
public double StartAngle { get; init; }
public double EndAngle { get; init; }
public bool Reversed { get; init; }
public string Rotation { get; init; } = "";
}
public record BestFitSetDto public record BestFitSetDto
{ {
public double PlateWidth { get; init; } public double PlateWidth { get; init; }
+27 -2
View File
@@ -36,7 +36,8 @@ namespace OpenNest.IO
var dto = JsonSerializer.Deserialize<NestDto>(nestJson, JsonOptions); var dto = JsonSerializer.Deserialize<NestDto>(nestJson, JsonOptions);
var programs = ReadPrograms(dto.Drawings.Count); var programs = ReadPrograms(dto.Drawings.Count);
var drawingMap = BuildDrawings(dto, programs); var entitySets = ReadEntitySets(dto.Drawings.Count);
var drawingMap = BuildDrawings(dto, programs, entitySets);
ReadBestFits(drawingMap); ReadBestFits(drawingMap);
var nest = BuildNest(dto, drawingMap); var nest = BuildNest(dto, drawingMap);
@@ -74,7 +75,25 @@ namespace OpenNest.IO
return programs; return programs;
} }
private Dictionary<int, Drawing> BuildDrawings(NestDto dto, Dictionary<int, Program> programs) private Dictionary<int, (List<Entity> entities, HashSet<Guid> suppressed)> ReadEntitySets(int count)
{
var result = new Dictionary<int, (List<Entity>, HashSet<Guid>)>();
for (var i = 1; i <= count; i++)
{
var entry = zipArchive.GetEntry($"entities/entities-{i}");
if (entry == null) continue;
using var entryStream = entry.Open();
using var reader = new StreamReader(entryStream);
var json = reader.ReadToEnd();
var dto = JsonSerializer.Deserialize<EntitySetDto>(json, JsonOptions);
result[i] = EntitySerializer.FromDto(dto);
}
return result;
}
private Dictionary<int, Drawing> BuildDrawings(NestDto dto, Dictionary<int, Program> programs,
Dictionary<int, (List<Entity> entities, HashSet<Guid> suppressed)> entitySets)
{ {
var map = new Dictionary<int, Drawing>(); var map = new Dictionary<int, Drawing>();
foreach (var d in dto.Drawings) foreach (var d in dto.Drawings)
@@ -112,6 +131,12 @@ namespace OpenNest.IO
if (programs.TryGetValue(d.Id, out var pgm)) if (programs.TryGetValue(d.Id, out var pgm))
drawing.Program = pgm; drawing.Program = pgm;
if (entitySets.TryGetValue(d.Id, out var entitySet))
{
drawing.SourceEntities = entitySet.entities;
drawing.SuppressedEntityIds = entitySet.suppressed;
}
map[d.Id] = drawing; map[d.Id] = drawing;
} }
return map; return map;
+19
View File
@@ -41,6 +41,7 @@ namespace OpenNest.IO
WriteNestJson(zipArchive); WriteNestJson(zipArchive);
WritePrograms(zipArchive); WritePrograms(zipArchive);
WriteEntities(zipArchive);
WriteBestFits(zipArchive); WriteBestFits(zipArchive);
return true; return true;
@@ -312,6 +313,24 @@ namespace OpenNest.IO
} }
} }
private void WriteEntities(ZipArchive zipArchive)
{
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
{
var drawing = kvp.Value;
if (drawing.SourceEntities == null || drawing.SourceEntities.Count == 0)
continue;
var dto = EntitySerializer.ToDto(drawing.SourceEntities, drawing.SuppressedEntityIds);
var json = JsonSerializer.Serialize(dto, JsonOptions);
var entry = zipArchive.CreateEntry($"entities/entities-{kvp.Key}");
using var stream = entry.Open();
using var writer = new StreamWriter(stream, Encoding.UTF8);
writer.Write(json);
}
}
private void WriteDrawing(Stream stream, Drawing drawing) private void WriteDrawing(Stream stream, Drawing drawing)
{ {
var program = drawing.Program; var program = drawing.Program;
@@ -0,0 +1,411 @@
using OpenNest.Geometry;
using OpenNest.IO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Xunit;
using Xunit.Abstractions;
namespace OpenNest.Tests.Engine;
public class MultiPlateNesterTests
{
private readonly ITestOutputHelper _output;
public MultiPlateNesterTests(ITestOutputHelper output)
{
_output = output;
}
private static Drawing MakeDrawing(string name, double width, double length)
{
var program = new OpenNest.CNC.Program();
program.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(width, 0)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(width, length)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, length)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
var drawing = new Drawing(name, program);
drawing.UpdateArea();
return drawing;
}
private static NestItem MakeItem(string name, double width, double length, int qty = 1)
{
return new NestItem
{
Drawing = MakeDrawing(name, width, length),
Quantity = qty,
};
}
[Fact]
public void SortByBoundingBoxArea_OrdersLargestFirst()
{
var items = new List<NestItem>
{
MakeItem("small", 10, 10),
MakeItem("large", 40, 60),
MakeItem("medium", 20, 30),
};
var sorted = MultiPlateNester.SortItems(items, PartSortOrder.BoundingBoxArea);
Assert.Equal("large", sorted[0].Drawing.Name);
Assert.Equal("medium", sorted[1].Drawing.Name);
Assert.Equal("small", sorted[2].Drawing.Name);
}
[Fact]
public void SortBySize_OrdersByLongestDimension()
{
var items = new List<NestItem>
{
MakeItem("short-wide", 50, 20), // longest = 50
MakeItem("tall-narrow", 10, 80), // longest = 80
MakeItem("square", 30, 30), // longest = 30
};
var sorted = MultiPlateNester.SortItems(items, PartSortOrder.Size);
Assert.Equal("tall-narrow", sorted[0].Drawing.Name);
Assert.Equal("short-wide", sorted[1].Drawing.Name);
Assert.Equal("square", sorted[2].Drawing.Name);
}
// --- Task 4: Part Classification ---
[Fact]
public void Classify_LargePart_WhenWidthExceedsHalfWorkArea()
{
var workArea = new Box(0, 0, 96, 48);
var bb = new Box(0, 0, 50, 20); // width 50 > half of 96 = 48
var result = MultiPlateNester.Classify(bb, workArea);
Assert.Equal(PartClass.Large, result);
}
[Fact]
public void Classify_LargePart_WhenLengthExceedsHalfWorkArea()
{
var workArea = new Box(0, 0, 96, 48);
var bb = new Box(0, 0, 20, 30); // length 30 > half of 48 = 24
var result = MultiPlateNester.Classify(bb, workArea);
Assert.Equal(PartClass.Large, result);
}
[Fact]
public void Classify_MediumPart_NotLargeButAreaAboveThreshold()
{
var workArea = new Box(0, 0, 96, 48);
// workArea = 4608, 1/9 = 512. bb = 40*15 = 600 > 512
// 40 < 48 (half of 96), 15 < 24 (half of 48) — not Large
var bb = new Box(0, 0, 40, 15);
var result = MultiPlateNester.Classify(bb, workArea);
Assert.Equal(PartClass.Medium, result);
}
[Fact]
public void Classify_SmallPart()
{
var workArea = new Box(0, 0, 96, 48);
// workArea = 4608, 1/9 = 512. bb = 10*10 = 100 < 512
var bb = new Box(0, 0, 10, 10);
var result = MultiPlateNester.Classify(bb, workArea);
Assert.Equal(PartClass.Small, result);
}
// --- Task 5: Scrap Zone Identification ---
[Fact]
public void IsScrapRemnant_BothDimensionsBelowThreshold_ReturnsTrue()
{
var remnant = new Box(0, 0, 10, 8);
Assert.True(MultiPlateNester.IsScrapRemnant(remnant, 12.0));
}
[Fact]
public void IsScrapRemnant_OneDimensionAboveThreshold_ReturnsFalse()
{
// 11 x 120 — narrow but long, should be preserved
var remnant = new Box(0, 0, 11, 120);
Assert.False(MultiPlateNester.IsScrapRemnant(remnant, 12.0));
}
[Fact]
public void IsScrapRemnant_BothDimensionsAboveThreshold_ReturnsFalse()
{
var remnant = new Box(0, 0, 20, 30);
Assert.False(MultiPlateNester.IsScrapRemnant(remnant, 12.0));
}
[Fact]
public void FindRemnants_ScrapOnly_ReturnsOnlyScrapRemnants()
{
// 96x48 plate with a 70x40 part placed at origin
var plate = new Plate(96, 48) { PartSpacing = 0.25 };
var drawing = MakeDrawing("big", 70, 40);
var part = new Part(drawing);
plate.Parts.Add(part);
var scrap = MultiPlateNester.FindRemnants(plate, 12.0, scrapOnly: true);
// All returned zones should have both dims < 12
foreach (var zone in scrap)
{
Assert.True(zone.Width < 12.0 && zone.Length < 12.0,
$"Zone {zone.Width:F1}x{zone.Length:F1} is not scrap — at least one dimension >= 12");
}
}
// --- Task 6: Plate Creation Helper ---
[Fact]
public void CreatePlate_UsesTemplateWhenNoOptions()
{
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing { Left = 1, Right = 1, Top = 1, Bottom = 1 };
var plate = MultiPlateNester.CreatePlate(template, null, null);
Assert.Equal(96, plate.Size.Width);
Assert.Equal(48, plate.Size.Length);
Assert.Equal(0.25, plate.PartSpacing);
Assert.Equal(1, plate.Quadrant);
}
[Fact]
public void CreatePlate_PicksSmallestFittingOption()
{
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing { Left = 1, Right = 1, Top = 1, Bottom = 1 };
var options = new List<PlateOption>
{
new() { Width = 48, Length = 96, Cost = 100 },
new() { Width = 60, Length = 120, Cost = 200 },
new() { Width = 72, Length = 144, Cost = 300 },
};
// Part needs 50x50 work area — 48x96 (after edge spacing: 46x94) — 46 < 50, doesn't fit.
// 60x120 (58x118) does fit.
var minBounds = new Box(0, 0, 50, 50);
var plate = MultiPlateNester.CreatePlate(template, options, minBounds);
Assert.Equal(60, plate.Size.Width);
Assert.Equal(120, plate.Size.Length);
}
[Fact]
public void EvaluateUpgrade_PrefersCheaperOption()
{
var currentOption = new PlateOption { Width = 48, Length = 96, Cost = 100 };
var upgradeOption = new PlateOption { Width = 60, Length = 120, Cost = 160 };
var newPlateOption = new PlateOption { Width = 48, Length = 96, Cost = 100 };
// Upgrade cost = 160 - 100 = 60
// New plate cost with 50% utilization, 50% salvage:
// remnantFraction = 0.5, salvageCredit = 0.5 * 100 * 0.5 = 25
// netNewCost = 100 - 25 = 75
// Upgrade (60) < new plate (75), so upgrade wins
var decision = MultiPlateNester.EvaluateUpgradeVsNew(
currentOption, upgradeOption, newPlateOption, 0.5, 0.5);
Assert.True(decision.ShouldUpgrade);
}
// --- Task 7: Main Orchestration ---
[Fact]
public void Nest_LargePartsGetOwnPlates()
{
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing();
var items = new List<NestItem>
{
MakeItem("big1", 80, 40, 1),
MakeItem("big2", 70, 35, 1),
};
var options = new MultiPlateNestOptions { Template = template };
var result = MultiPlateNester.Nest(items, options);
// Each large part should be on its own plate.
Assert.True(result.Plates.Count >= 2,
$"Expected at least 2 plates, got {result.Plates.Count}");
}
[Fact]
public void Nest_SmallPartsConsolidateOntoSharedPlates()
{
// Small parts should be packed together on shared plates rather than
// each drawing getting its own plate. The consolidation pass fills
// small parts into remaining space on existing plates.
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing();
var items = new List<NestItem>
{
MakeItem("big", 80, 40, 1),
MakeItem("tinyA", 5, 5, 3),
MakeItem("tinyB", 4, 4, 3),
};
var options = new MultiPlateNestOptions { Template = template };
var result = MultiPlateNester.Nest(items, options);
// Both small drawing types should share space — not each on their own plate.
// With consolidation, they pack into remaining space alongside the big part.
Assert.True(result.Plates.Count <= 2,
$"Expected at most 2 plates (small parts consolidated), got {result.Plates.Count}");
Assert.Equal(0, result.UnplacedItems.Count);
}
[Fact]
public void Nest_RespectsAllowPlateCreation()
{
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing();
var items = new List<NestItem>
{
MakeItem("big1", 80, 40, 1),
MakeItem("big2", 70, 35, 1),
};
var options = new MultiPlateNestOptions
{
Template = template,
AllowPlateCreation = false,
};
var result = MultiPlateNester.Nest(items, options);
// No existing plates and no plate creation — nothing can be placed.
Assert.Empty(result.Plates);
Assert.Equal(2, result.UnplacedItems.Count);
}
[Fact]
public void Nest_UsesExistingPlates()
{
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing();
var existingPlate = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
existingPlate.EdgeSpacing = new Spacing();
// Use a part small enough to be classified as Medium on a 96x48 plate.
// Plate WorkArea: Width=96, Length=48. Half: 48, 24.
// Part 24x22: Length=24 (not > 24), Width=22 (not > 48) — not Large.
// Area = 528 > 4608/9 = 512 — Medium.
var items = new List<NestItem>
{
MakeItem("medium", 24, 22, 1),
};
var options = new MultiPlateNestOptions { Template = template };
var result = MultiPlateNester.Nest(items, options,
existingPlates: new List<Plate> { existingPlate });
// Part should be placed on the existing plate, not a new one.
Assert.Single(result.Plates);
Assert.False(result.Plates[0].IsNew);
}
[Fact]
public void Nest_RealNestFile_PartFirst()
{
var nestPath = @"C:\Users\aisaacs\Desktop\4526 A14 - 0.188 AISI 304.nest";
if (!File.Exists(nestPath))
{
_output.WriteLine("SKIP: nest file not found");
return;
}
var nest = new NestReader(nestPath).Read();
var template = nest.PlateDefaults.CreateNew();
_output.WriteLine($"Plate: {template.Size.Width}x{template.Size.Length}, " +
$"spacing={template.PartSpacing}, edge=({template.EdgeSpacing.Left},{template.EdgeSpacing.Bottom},{template.EdgeSpacing.Right},{template.EdgeSpacing.Top})");
var wa = template.WorkArea();
_output.WriteLine($"Work area: {wa.Width:F1}x{wa.Length:F1}");
_output.WriteLine($"Classification thresholds: Large if dim > {wa.Width / 2:F1} or {wa.Length / 2:F1}, " +
$"Medium if area > {wa.Width * wa.Length / 9:F0}");
_output.WriteLine("---");
var items = new List<NestItem>();
foreach (var d in nest.Drawings)
{
var qty = d.Quantity.Required > 0 ? d.Quantity.Required : d.Quantity.Remaining;
if (qty <= 0) qty = 1;
var bb = d.Program.BoundingBox();
var classification = MultiPlateNester.Classify(bb, wa);
_output.WriteLine($" {d.Name,-25} {bb.Width:F1}x{bb.Length:F1} (area={bb.Width * bb.Length:F0}) qty={qty} class={classification}");
items.Add(new NestItem
{
Drawing = d,
Quantity = qty,
StepAngle = d.Constraints.StepAngle,
RotationStart = d.Constraints.StartAngle,
RotationEnd = d.Constraints.EndAngle,
});
}
_output.WriteLine("---");
_output.WriteLine($"Total: {items.Count} drawings, {items.Sum(i => i.Quantity)} parts");
var plateOptions = new List<PlateOption>
{
new() { Width = 48, Length = 96, Cost = 0 },
new() { Width = 48, Length = 120, Cost = 0 },
new() { Width = 48, Length = 144, Cost = 0 },
new() { Width = 60, Length = 96, Cost = 0 },
new() { Width = 60, Length = 120, Cost = 0 },
new() { Width = 60, Length = 144, Cost = 0 },
new() { Width = 72, Length = 96, Cost = 0 },
new() { Width = 72, Length = 120, Cost = 0 },
new() { Width = 72, Length = 144, Cost = 0 },
};
_output.WriteLine($"Plate options: {string.Join(", ", plateOptions.Select(o => $"{o.Width}x{o.Length}"))}");
_output.WriteLine("");
var options = new MultiPlateNestOptions
{
Template = template,
PlateOptions = plateOptions,
};
var result = MultiPlateNester.Nest(items, options);
_output.WriteLine($"=== RESULTS: {result.Plates.Count} plates ===");
for (var i = 0; i < result.Plates.Count; i++)
{
var pr = result.Plates[i];
var groups = pr.Parts.GroupBy(p => p.BaseDrawing.Name)
.Select(g => $"{g.Key} x{g.Count()}")
.ToList();
_output.WriteLine($" Plate {i + 1} ({pr.Plate.Size.Width}x{pr.Plate.Size.Length}): " +
$"{pr.Parts.Count} parts, util={pr.Plate.Utilization():P1} [{string.Join(", ", groups)}]");
}
if (result.UnplacedItems.Count > 0)
{
_output.WriteLine($" Unplaced: {string.Join(", ", result.UnplacedItems.Select(i => $"{i.Drawing.Name} x{i.Quantity}"))}");
}
_output.WriteLine($"\nTotal parts placed: {result.Plates.Sum(p => p.Parts.Count)}");
_output.WriteLine($"Total plates used: {result.Plates.Count}");
}
}
@@ -0,0 +1,130 @@
using OpenNest;
using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using OpenNest.Math;
using Xunit;
using Xunit.Abstractions;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest.Tests.Fill
{
public class FillLinearCircleTests
{
private readonly ITestOutputHelper _output;
public FillLinearCircleTests(ITestOutputHelper output) => _output = output;
private static Drawing MakeCircleDrawing(double radius)
{
var pgm = new Program();
var startPt = new Vector(radius * 2, radius); // rightmost point
pgm.Codes.Add(new RapidMove(startPt));
pgm.Codes.Add(new ArcMove(startPt, new Vector(radius, radius), RotationType.CCW));
return new Drawing("circle", pgm);
}
private static Drawing MakeRingDrawing(double outerRadius, double innerRadius)
{
var pgm = new Program();
// Outer circle (CCW)
var outerStart = new Vector(outerRadius * 2, outerRadius);
pgm.Codes.Add(new RapidMove(outerStart));
pgm.Codes.Add(new ArcMove(outerStart, new Vector(outerRadius, outerRadius), RotationType.CCW));
// Inner circle (CW = hole)
var innerStart = new Vector(outerRadius + innerRadius, outerRadius);
pgm.Codes.Add(new RapidMove(innerStart));
pgm.Codes.Add(new ArcMove(innerStart, new Vector(outerRadius, outerRadius), RotationType.CW));
return new Drawing("ring", pgm);
}
[Theory]
[InlineData(2.0, 0.125)] // 4" diameter circle, 1/8" spacing
[InlineData(1.0, 0.125)] // 2" diameter circle
[InlineData(3.0, 0.0625)] // 6" diameter circle, 1/16" spacing
[InlineData(0.5, 0.25)] // 1" diameter circle, 1/4" spacing
public void CircleFill_OffsetBoundaries_DoNotOverlap(double radius, double spacing)
{
var drawing = MakeCircleDrawing(radius);
var workArea = new Box(0, 0, 48, 48);
var engine = new FillLinear(workArea, spacing);
var parts = engine.Fill(drawing, 0, NestDirection.Horizontal);
_output.WriteLine($"Circle R={radius}, spacing={spacing}: {parts.Count} parts");
AssertNoOffsetOverlap(parts, spacing, radius * 2);
}
[Theory]
[InlineData(2.0, 1.5, 0.125)] // Ring: outer R=2, inner R=1.5
[InlineData(1.5, 1.0, 0.125)] // Ring: outer R=1.5, inner R=1.0
public void RingFill_OffsetBoundaries_DoNotOverlap(double outerR, double innerR, double spacing)
{
var drawing = MakeRingDrawing(outerR, innerR);
var workArea = new Box(0, 0, 48, 48);
var engine = new FillLinear(workArea, spacing);
var parts = engine.Fill(drawing, 0, NestDirection.Horizontal);
_output.WriteLine($"Ring outerR={outerR}, innerR={innerR}, spacing={spacing}: {parts.Count} parts");
AssertNoOffsetOverlap(parts, spacing, outerR * 2);
}
private void AssertNoOffsetOverlap(List<Part> parts, double spacing, double expectedDiameter)
{
if (parts.Count < 2)
{
_output.WriteLine(" Only 1 part placed, skipping overlap check");
return;
}
var halfSpacing = spacing / 2;
var radius = expectedDiameter / 2;
var minGap = double.MaxValue;
var violationCount = 0;
// For circular parts, the center is at Location + (radius, radius).
for (var i = 0; i < parts.Count; i++)
{
var ci = parts[i].Location + new Vector(radius, radius);
for (var j = i + 1; j < parts.Count; j++)
{
var cj = parts[j].Location + new Vector(radius, radius);
var centerDist = ci.DistanceTo(cj);
// Gap between raw circle perimeters
var rawGap = centerDist - expectedDiameter;
// Gap between offset circle perimeters (halfSpacing each side)
var offsetGap = centerDist - expectedDiameter - spacing;
if (rawGap < minGap)
minGap = rawGap;
if (rawGap < spacing - Tolerance.Epsilon)
{
violationCount++;
if (violationCount <= 5)
{
_output.WriteLine($" SPACING VIOLATION parts[{i}] vs parts[{j}]: " +
$"centerDist={centerDist:F6}, rawGap={rawGap:F6}, offsetGap={offsetGap:F6}, " +
$"expected>={spacing:F4}");
}
}
}
}
_output.WriteLine($" Min gap={minGap:F6}, expected>={spacing:F4}, violations={violationCount}");
if (violationCount > 0)
{
var maxDeficit = spacing - minGap;
_output.WriteLine($" Max deficit={maxDeficit:F6}");
Assert.Fail($"{violationCount} pairs violate spacing: min gap={minGap:F6}, expected>={spacing}, deficit={maxDeficit:F6}");
}
}
}
}
@@ -0,0 +1,349 @@
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.Tests.Geometry;
public class SpatialQueryTests
{
#region Helpers
private static List<Entity> MakeSquare(double size)
{
return new List<Entity>
{
new Line(0, 0, size, 0),
new Line(size, 0, size, size),
new Line(size, size, 0, size),
new Line(0, size, 0, 0),
};
}
private static List<Entity> MakeRoundedRect(double length, double width, double r)
{
return new List<Entity>
{
new Line(r, 0, length - r, 0),
new Arc(length - r, r, r, Angle.ToRadians(270), Angle.ToRadians(360)),
new Line(length, r, length, width - r),
new Arc(length - r, width - r, r, Angle.ToRadians(0), Angle.ToRadians(90)),
new Line(length - r, width, r, width),
new Arc(r, width - r, r, Angle.ToRadians(90), Angle.ToRadians(180)),
new Line(0, width - r, 0, r),
new Arc(r, r, r, Angle.ToRadians(180), Angle.ToRadians(270)),
};
}
private static List<Entity> MakeCircle(double cx, double cy, double radius)
{
return new List<Entity> { new Circle(cx, cy, radius) };
}
private static List<Entity> Translate(List<Entity> entities, double dx, double dy)
{
var result = new List<Entity>();
foreach (var e in entities)
{
if (e is Line line)
result.Add(new Line(line.pt1.X + dx, line.pt1.Y + dy, line.pt2.X + dx, line.pt2.Y + dy));
else if (e is Arc arc)
result.Add(new Arc(arc.Center.X + dx, arc.Center.Y + dy, arc.Radius, arc.StartAngle, arc.EndAngle));
else if (e is Circle circle)
result.Add(new Circle(circle.Center.X + dx, circle.Center.Y + dy, circle.Radius));
}
return result;
}
#endregion
#region Circle vs Circle
[Fact]
public void CircleToCircle_Right_ReturnsGap()
{
var a = MakeCircle(0, 0, 5);
var b = MakeCircle(20, 0, 5);
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
Assert.InRange(dist, 9.9, 10.1);
}
[Fact]
public void CircleToCircle_Left_ReturnsGap()
{
var a = MakeCircle(20, 0, 5);
var b = MakeCircle(0, 0, 5);
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(-1, 0));
Assert.InRange(dist, 9.9, 10.1);
}
[Fact]
public void CircleToCircle_Up_ReturnsGap()
{
var a = MakeCircle(0, 0, 5);
var b = MakeCircle(0, 20, 5);
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(0, 1));
Assert.InRange(dist, 9.9, 10.1);
}
[Fact]
public void CircleToCircle_Touching_ReturnsZero()
{
var a = MakeCircle(0, 0, 5);
var b = MakeCircle(10, 0, 5);
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
Assert.InRange(dist, -0.01, 0.01);
}
[Fact]
public void CircleToCircle_NoPath_ReturnsMaxValue()
{
var a = MakeCircle(0, 0, 3);
var b = MakeCircle(0, 20, 3);
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
Assert.Equal(double.MaxValue, dist);
}
[Fact]
public void CircleToCircle_PushDirection_Right()
{
var a = MakeCircle(0, 0, 5);
var b = MakeCircle(20, 0, 5);
var dist = SpatialQuery.DirectionalDistance(a, b, PushDirection.Right);
Assert.InRange(dist, 9.9, 10.1);
}
#endregion
#region Square vs Square
[Fact]
public void SquareToSquare_Right_ReturnsGap()
{
var a = MakeSquare(10);
var b = Translate(MakeSquare(10), 25, 0);
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
Assert.InRange(dist, 14.9, 15.1);
}
[Fact]
public void SquareToSquare_Left_ReturnsGap()
{
var a = Translate(MakeSquare(10), 25, 0);
var b = MakeSquare(10);
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(-1, 0));
Assert.InRange(dist, 14.9, 15.1);
}
[Fact]
public void SquareToSquare_Down_ReturnsGap()
{
var a = Translate(MakeSquare(10), 0, 25);
var b = MakeSquare(10);
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(0, -1));
Assert.InRange(dist, 14.9, 15.1);
}
[Fact]
public void SquareToSquare_Touching_ReturnsZero()
{
var a = MakeSquare(10);
var b = Translate(MakeSquare(10), 10, 0);
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
Assert.InRange(dist, -0.01, 0.01);
}
[Fact]
public void SquareToSquare_NoOverlap_ReturnsMaxValue()
{
var a = MakeSquare(10);
var b = Translate(MakeSquare(10), 0, 20);
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
Assert.Equal(double.MaxValue, dist);
}
[Fact]
public void SquareToSquare_PartialOverlap_Right()
{
var a = MakeSquare(10);
var b = Translate(MakeSquare(10), 20, 5);
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
Assert.InRange(dist, 9.9, 10.1);
}
#endregion
#region Rounded Rectangle
[Fact]
public void RoundedRect_Right_ReturnsGap()
{
var a = MakeRoundedRect(20, 10, 2);
var b = Translate(MakeRoundedRect(20, 10, 2), 30, 0);
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
Assert.InRange(dist, 9.9, 10.1);
}
[Fact]
public void RoundedRect_Up_ReturnsGap()
{
var a = MakeRoundedRect(20, 10, 2);
var b = Translate(MakeRoundedRect(20, 10, 2), 0, 25);
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(0, 1));
Assert.InRange(dist, 14.9, 15.1);
}
[Fact]
public void RoundedRect_Touching_ReturnsZero()
{
var a = MakeRoundedRect(20, 10, 2);
var b = Translate(MakeRoundedRect(20, 10, 2), 20, 0);
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
Assert.InRange(dist, -0.01, 0.01);
}
[Fact]
public void RoundedRect_Diagonal_ReturnsDistance()
{
var dir = new Vector(1 / System.Math.Sqrt(2), 1 / System.Math.Sqrt(2));
var a = MakeRoundedRect(10, 10, 2);
var b = Translate(MakeRoundedRect(10, 10, 2), 20, 20);
var dist = SpatialQuery.DirectionalDistance(a, b, dir);
Assert.True(dist > 0 && dist < double.MaxValue);
}
#endregion
#region Circle vs Square
[Fact]
public void CircleToSquare_Right_ReturnsGap()
{
var circle = MakeCircle(0, 5, 5);
var square = Translate(MakeSquare(10), 15, 0);
var dist = SpatialQuery.DirectionalDistance(circle, square, new Vector(1, 0));
Assert.InRange(dist, 9.9, 10.1);
}
[Fact]
public void SquareToCircle_Right_ReturnsGap()
{
var square = MakeSquare(10);
var circle = MakeCircle(25, 5, 5);
var dist = SpatialQuery.DirectionalDistance(square, circle, new Vector(1, 0));
Assert.InRange(dist, 9.9, 10.1);
}
[Fact]
public void CircleToSquare_Touching_ReturnsZero()
{
var circle = MakeCircle(0, 5, 5);
var square = Translate(MakeSquare(10), 5, 0);
var dist = SpatialQuery.DirectionalDistance(circle, square, new Vector(1, 0));
Assert.InRange(dist, -0.01, 0.01);
}
#endregion
#region Circle vs Rounded Rectangle
[Fact]
public void CircleToRoundedRect_Right_ReturnsGap()
{
var circle = MakeCircle(0, 5, 5);
var rect = Translate(MakeRoundedRect(20, 10, 2), 15, 0);
var dist = SpatialQuery.DirectionalDistance(circle, rect, new Vector(1, 0));
Assert.InRange(dist, 9.9, 10.1);
}
[Fact]
public void RoundedRectToCircle_Left_ReturnsGap()
{
var rect = Translate(MakeRoundedRect(20, 10, 2), 15, 0);
var circle = MakeCircle(0, 5, 5);
var dist = SpatialQuery.DirectionalDistance(rect, circle, new Vector(-1, 0));
Assert.InRange(dist, 9.9, 10.1);
}
#endregion
#region Edge cases
[Fact]
public void EmptyLists_ReturnsMaxValue()
{
var a = new List<Entity>();
var b = new List<Entity>();
var dist = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
Assert.Equal(double.MaxValue, dist);
}
[Fact]
public void Symmetry_LeftRightReturnSameDistance()
{
var a = MakeSquare(10);
var b = Translate(MakeSquare(10), 25, 0);
var right = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
var left = SpatialQuery.DirectionalDistance(b, a, new Vector(-1, 0));
Assert.InRange(System.Math.Abs(right - left), 0, 0.01);
}
[Fact]
public void Symmetry_CirclesLeftRightSame()
{
var a = MakeCircle(0, 0, 5);
var b = MakeCircle(20, 0, 5);
var right = SpatialQuery.DirectionalDistance(a, b, new Vector(1, 0));
var left = SpatialQuery.DirectionalDistance(b, a, new Vector(-1, 0));
Assert.InRange(System.Math.Abs(right - left), 0, 0.01);
}
#endregion
}
+84
View File
@@ -0,0 +1,84 @@
using OpenNest.IO.Bom;
using System;
using System.Drawing;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
namespace OpenNest
{
public static class ArchUnits
{
private static readonly Regex UnitRegex =
new Regex("^(?<Feet>\\d+\\.?\\d*\\s*')?\\s*(?<Inches>\\d+\\.?\\d*\\s*\")?$");
public static double ParseToInches(string input)
{
if (string.IsNullOrWhiteSpace(input))
return 0;
var sb = new StringBuilder(input.Trim().ToLower());
sb.Replace("ft", "'");
sb.Replace("feet", "'");
sb.Replace("foot", "'");
sb.Replace("inches", "\"");
sb.Replace("inch", "\"");
sb.Replace("in", "\"");
input = Fraction.ReplaceFractionsWithDecimals(sb.ToString());
var match = UnitRegex.Match(input);
if (!match.Success)
{
if (!input.Contains("'") && !input.Contains("\""))
{
if (double.TryParse(input.Trim(), out var plainInches))
return System.Math.Round(plainInches, 8);
}
throw new FormatException("Input is not in a valid format.");
}
var feet = match.Groups["Feet"];
var inches = match.Groups["Inches"];
var totalInches = 0.0;
if (feet.Success)
{
var x = double.Parse(feet.Value.Remove(feet.Length - 1));
totalInches += x * 12;
}
if (inches.Success)
{
var x = double.Parse(inches.Value.Remove(inches.Length - 1));
totalInches += x;
}
return System.Math.Round(totalInches, 8);
}
public static double GetLengthInches(TextBox tb)
{
try
{
if (double.TryParse(tb.Text, out var d))
{
tb.ForeColor = SystemColors.WindowText;
return d;
}
var x = ParseToInches(tb.Text);
tb.ForeColor = SystemColors.WindowText;
return x;
}
catch
{
tb.ForeColor = Color.Red;
return double.NaN;
}
}
}
}
File diff suppressed because it is too large Load Diff
+118
View File
@@ -0,0 +1,118 @@
using System;
using System.ComponentModel;
using Action = OpenNest.Actions.Action;
namespace OpenNest.Controls
{
internal class ActionManager
{
private readonly PlateView view;
private Action currentAction;
private Action previousAction;
public ActionManager(PlateView view)
{
this.view = view;
}
public Action CurrentAction => currentAction;
public void SetAction(Type type)
{
var action = Activator.CreateInstance(type, view) as Action;
if (action == null)
return;
if (currentAction != null)
{
if (type == typeof(Actions.ActionSelect) && !(currentAction is Actions.ActionSelect))
previousAction = currentAction;
else
previousAction = null;
currentAction.CancelAction();
currentAction.DisconnectEvents();
currentAction = null;
}
currentAction = action;
view.Status = GetDisplayName(type);
}
public void SetAction(Type type, params object[] args)
{
if (currentAction != null)
{
previousAction = null;
currentAction.CancelAction();
currentAction.DisconnectEvents();
currentAction = null;
}
Array.Resize(ref args, args.Length + 1);
for (var i = args.Length - 2; i >= 0; i--)
args[i + 1] = args[i];
args[0] = view;
var action = Activator.CreateInstance(type, args) as Action;
if (action == null)
return;
currentAction = action;
view.Status = GetDisplayName(type);
}
public void ProcessEscapeKey()
{
if (currentAction.IsBusy())
currentAction.CancelAction();
else if (currentAction is Actions.ActionSelect && previousAction != null)
RestorePreviousAction();
else
view.SetAction(typeof(Actions.ActionSelect));
}
public void RestorePreviousAction()
{
var action = previousAction;
previousAction = null;
currentAction.CancelAction();
currentAction.DisconnectEvents();
action.ConnectEvents();
currentAction = action;
view.Status = GetDisplayName(action.GetType());
}
public void OnPlateChanged()
{
if (currentAction == null || !currentAction.SurvivesPlateChange)
view.SetAction(typeof(Actions.ActionSelect));
else
currentAction.OnPlateChanged();
}
public void Cleanup()
{
if (currentAction != null)
{
currentAction.CancelAction();
currentAction.DisconnectEvents();
currentAction = null;
}
}
private string GetDisplayName(Type type)
{
var attributes = type.GetCustomAttributes(true);
foreach (var attr in attributes)
{
if (attr is DisplayNameAttribute displayNameAttr)
return displayNameAttr.DisplayName;
}
return type.Name;
}
}
}
+81
View File
@@ -0,0 +1,81 @@
using OpenNest.CNC;
using OpenNest.Geometry;
using System.Collections.Generic;
namespace OpenNest.Controls
{
internal class CutOffHandler
{
private readonly PlateView view;
private Dictionary<Part, Geometry.Entity> dragPerimeterCache;
public CutOffHandler(PlateView view)
{
this.view = view;
}
public bool IsDragging { get; private set; }
public CutOff TryStartDrag(Vector point, double tolerance)
{
var hitCutOff = GetCutOffAtPoint(point, tolerance);
if (hitCutOff == null)
return null;
IsDragging = true;
dragPerimeterCache = Plate.BuildPerimeterCache(view.Plate);
return hitCutOff;
}
public void UpdateDrag(Vector currentPoint, CutOff cutOff)
{
if (!IsDragging || cutOff == null)
return;
if (cutOff.Axis == CutOffAxis.Vertical)
cutOff.Position = new Vector(currentPoint.X, cutOff.Position.Y);
else
cutOff.Position = new Vector(cutOff.Position.X, currentPoint.Y);
cutOff.Regenerate(view.Plate, view.CutOffSettings, dragPerimeterCache);
view.Invalidate();
}
public void EndDrag()
{
if (!IsDragging)
return;
IsDragging = false;
dragPerimeterCache = null;
view.Plate.RegenerateCutOffs(view.CutOffSettings);
view.Invalidate();
}
public CutOff GetCutOffAtPoint(Vector point, double tolerance)
{
if (view.Plate?.CutOffs == null)
return null;
foreach (var cutoff in view.Plate.CutOffs)
{
var program = cutoff.Drawing?.Program;
if (program == null)
continue;
for (var i = 0; i < program.Codes.Count - 1; i += 2)
{
if (program.Codes[i] is RapidMove rapid &&
program.Codes[i + 1] is LinearMove linear)
{
var line = new Line(rapid.EndPoint, linear.EndPoint);
if (line.ClosestPointTo(point).DistanceTo(point) <= tolerance)
return cutoff;
}
}
}
return null;
}
}
}
+1 -12
View File
@@ -11,7 +11,7 @@ namespace OpenNest.Controls
{ "None", "Line", "Arc", "Line + Arc", "Clean Hole", "Line + Line" }; { "None", "Line", "Arc", "Line + Arc", "Clean Hole", "Line + Line" };
private static readonly string[] LeadOutTypes = private static readonly string[] LeadOutTypes =
{ "None", "Line", "Arc", "Microtab" }; { "None", "Line", "Arc" };
private readonly TabControl tabControl; private readonly TabControl tabControl;
private readonly ComboBox cboExternalLeadIn, cboExternalLeadOut; private readonly ComboBox cboExternalLeadIn, cboExternalLeadOut;
@@ -424,9 +424,6 @@ namespace OpenNest.Controls
case 2: case 2:
AddNumericField(panel, "Radius:", 0.25, ref y, "Radius"); AddNumericField(panel, "Radius:", 0.25, ref y, "Radius");
break; break;
case 3:
AddNumericField(panel, "Gap Size:", 0.06, ref y, "GapSize");
break;
} }
} }
@@ -513,10 +510,6 @@ namespace OpenNest.Controls
combo.SelectedIndex = 2; combo.SelectedIndex = 2;
SetParam(panel, "Radius", arc.Radius); SetParam(panel, "Radius", arc.Radius);
break; break;
case MicrotabLeadOut microtab:
combo.SelectedIndex = 3;
SetParam(panel, "GapSize", microtab.GapSize);
break;
default: default:
combo.SelectedIndex = 0; combo.SelectedIndex = 0;
break; break;
@@ -572,10 +565,6 @@ namespace OpenNest.Controls
{ {
Radius = GetParam(panel, "Radius", 0.25) Radius = GetParam(panel, "Radius", 0.25)
}, },
3 => new MicrotabLeadOut
{
GapSize = GetParam(panel, "GapSize", 0.06)
},
_ => new NoLeadOut() _ => new NoLeadOut()
}; };
} }
+1 -1
View File
@@ -29,7 +29,7 @@ namespace OpenNest.Controls
{ {
ViewScale = 1.0f; ViewScale = 1.0f;
ViewScaleMin = 0.3f; ViewScaleMin = 0.3f;
ViewScaleMax = 3000; ViewScaleMax = 10000;
origin = new PointF(100, 100); origin = new PointF(100, 100);
} }
+26 -4
View File
@@ -8,16 +8,14 @@ namespace OpenNest.Controls
public class DrawingListBox : ListBox public class DrawingListBox : ListBox
{ {
private const int WM_ERASEBKGND = 0x0014;
private readonly Size imageSize; private readonly Size imageSize;
private readonly Font nameFont; private readonly Font nameFont;
private Point lastClickLocation; private Point lastClickLocation;
public DrawingListBox() public DrawingListBox()
{ {
SetStyle(
ControlStyles.AllPaintingInWmPaint |
ControlStyles.OptimizedDoubleBuffer, true);
DrawMode = DrawMode.OwnerDrawFixed; DrawMode = DrawMode.OwnerDrawFixed;
ItemHeight = 85; ItemHeight = 85;
@@ -149,6 +147,30 @@ namespace OpenNest.Controls
base.OnMouseDown(e); base.OnMouseDown(e);
lastClickLocation = e.Location; lastClickLocation = e.Location;
} }
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_ERASEBKGND)
{
var itemBottom = 0;
if (Items.Count > 0)
{
var lastVisible = System.Math.Min(TopIndex + (ClientSize.Height / ItemHeight), Items.Count - 1);
itemBottom = GetItemRectangle(lastVisible).Bottom;
}
if (itemBottom < ClientSize.Height)
{
using var g = Graphics.FromHdc(m.WParam);
g.FillRectangle(Brushes.White, 0, itemBottom, ClientSize.Width, ClientSize.Height - itemBottom);
}
m.Result = (IntPtr)1;
return;
}
base.WndProc(ref m);
}
} }
public static class PointExtensions public static class PointExtensions
+1
View File
@@ -19,6 +19,7 @@ namespace OpenNest.Controls
public List<Entity> Entities { get; set; } = new(); public List<Entity> Entities { get; set; } = new();
public List<Entity> OriginalEntities { get; set; } public List<Entity> OriginalEntities { get; set; }
public List<Bend> Bends { get; set; } = new(); public List<Bend> Bends { get; set; } = new();
public HashSet<Guid> SuppressedEntityIds { get; set; }
public Box Bounds { get; set; } public Box Bounds { get; set; }
public int EntityCount { get; set; } public int EntityCount { get; set; }
} }
+11 -7
View File
@@ -154,7 +154,10 @@ namespace OpenNest.Controls
Font = new Font("Segoe UI", 9f) Font = new Font("Segoe UI", 9f)
}; };
list.ItemCheck += (s, e) => list.ItemCheck += (s, e) =>
BeginInvoke((Action)(() => FilterChanged?.Invoke(this, EventArgs.Empty))); {
if (IsHandleCreated)
BeginInvoke((Action)(() => FilterChanged?.Invoke(this, EventArgs.Empty)));
};
return list; return list;
} }
@@ -167,10 +170,11 @@ namespace OpenNest.Controls
layersList.Items.Clear(); layersList.Items.Clear();
var layers = entities var layers = entities
.Where(e => e.Layer != null) .Where(e => e.Layer != null)
.Select(e => e.Layer.Name) .Select(e => e.Layer)
.Distinct(); .GroupBy(l => l.Name)
.Select(g => g.First());
foreach (var layer in layers) foreach (var layer in layers)
layersList.Items.Add(layer, true); // checked = visible layersList.Items.Add(layer.Name, layer.IsVisible);
layersPanel.HeaderText = $"Layers ({layersList.Items.Count})"; layersPanel.HeaderText = $"Layers ({layersList.Items.Count})";
@@ -188,10 +192,10 @@ namespace OpenNest.Controls
// Line Types // Line Types
lineTypesList.Items.Clear(); lineTypesList.Items.Clear();
var lineTypes = entities var lineTypes = entities
.Select(e => e.LineTypeName ?? "Continuous") .GroupBy(e => e.LineTypeName ?? "Continuous")
.Distinct(); .Select(g => new { Name = g.Key, Visible = g.Any(e => e.IsVisible) });
foreach (var lt in lineTypes) foreach (var lt in lineTypes)
lineTypesList.Items.Add(lt, true); // checked = visible lineTypesList.Items.Add(lt.Name, lt.Visible);
lineTypesPanel.HeaderText = $"Line Types ({lineTypesList.Items.Count})"; lineTypesPanel.HeaderText = $"Line Types ({lineTypesList.Items.Count})";
+1 -1
View File
@@ -168,7 +168,7 @@ namespace OpenNest.Controls
if (program == null || program.Codes.Count == 0) if (program == null || program.Codes.Count == 0)
continue; continue;
var activePen = cutoff == view.SelectedCutOff ? selectedPen : pen; var activePen = view.Selection.SelectedCutOffs.Contains(cutoff) ? selectedPen : pen;
for (var i = 0; i < program.Codes.Count - 1; i += 2) for (var i = 0; i < program.Codes.Count - 1; i += 2)
{ {
+145 -412
View File
@@ -1,5 +1,4 @@
using OpenNest.Actions; using OpenNest.Actions;
using OpenNest.CNC;
using OpenNest.Collections; using OpenNest.Collections;
using OpenNest.Engine.Fill; using OpenNest.Engine.Fill;
using OpenNest.Forms; using OpenNest.Forms;
@@ -8,7 +7,6 @@ using OpenNest.Math;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing; using System.Drawing;
using System.Drawing.Drawing2D; using System.Drawing.Drawing2D;
@@ -16,31 +14,30 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using Action = OpenNest.Actions.Action;
using Timer = System.Timers.Timer; using Timer = System.Timers.Timer;
namespace OpenNest.Controls namespace OpenNest.Controls
{ {
public class PlateView : DrawControl public class PlateView : DrawControl
{ {
private readonly Font programIdFont;
private readonly Timer redrawTimer; private readonly Timer redrawTimer;
private string status; private string status;
private Plate plate; private Plate plate;
private Action currentAction; private ActionManager actionManager;
private Action previousAction;
private CutOffSettings cutOffSettings = new CutOffSettings(); private CutOffSettings cutOffSettings = new CutOffSettings();
private CutOff selectedCutOff; private SelectionManager selection;
private bool draggingCutOff; private CutOffHandler cutOffHandler;
private Dictionary<Part, Geometry.Entity> dragPerimeterCache; private PreviewManager previewManager;
protected List<LayoutPart> parts; protected List<LayoutPart> parts;
private List<LayoutPart> stationaryParts = new List<LayoutPart>();
private List<LayoutPart> activeParts = new List<LayoutPart>();
private Point middleMouseDownPoint; private Point middleMouseDownPoint;
private Box activeWorkArea; private Box activeWorkArea;
private List<Box> debugRemnants; private List<Box> debugRemnants;
private PlateRenderer renderer; private PlateRenderer renderer;
private LayoutPart hoveredPart;
private Point hoverPoint;
private bool showTooltip;
private Timer hoverTimer;
public Box ActiveWorkArea public Box ActiveWorkArea
{ {
@@ -64,13 +61,23 @@ namespace OpenNest.Controls
public List<int> DebugRemnantPriorities { get; set; } public List<int> DebugRemnantPriorities { get; set; }
public List<LayoutPart> SelectedParts; public List<LayoutPart> SelectedParts => selection.SelectedParts;
public ReadOnlyCollection<LayoutPart> Parts; public ReadOnlyCollection<LayoutPart> Parts => new ReadOnlyCollection<LayoutPart>(parts);
internal SelectionManager Selection => selection;
internal CutOffHandler CutOffs => cutOffHandler;
internal ActionManager Actions => actionManager;
internal PreviewManager Previews => previewManager;
public event EventHandler<ItemAddedEventArgs<Part>> PartAdded; public event EventHandler<ItemAddedEventArgs<Part>> PartAdded;
public event EventHandler<ItemRemovedEventArgs<Part>> PartRemoved; public event EventHandler<ItemRemovedEventArgs<Part>> PartRemoved;
public event EventHandler StatusChanged; public event EventHandler StatusChanged;
public event EventHandler SelectionChanged;
public event EventHandler SelectionChanged
{
add => selection.SelectionChanged += value;
remove => selection.SelectionChanged -= value;
}
public PlateView() public PlateView()
: this(ColorScheme.Default) : this(ColorScheme.Default)
@@ -80,11 +87,11 @@ namespace OpenNest.Controls
public PlateView(ColorScheme colorScheme) public PlateView(ColorScheme colorScheme)
{ {
Plate = new Plate(60, 120); Plate = new Plate(60, 120);
programIdFont = new Font(DefaultFont, FontStyle.Bold | FontStyle.Underline);
origin = new PointF(); origin = new PointF();
parts = new List<LayoutPart>(); parts = new List<LayoutPart>();
Parts = new ReadOnlyCollection<LayoutPart>(parts); selection = new SelectionManager(this);
SelectedParts = new List<LayoutPart>(); cutOffHandler = new CutOffHandler(this);
previewManager = new PreviewManager(this);
redrawTimer = new Timer() redrawTimer = new Timer()
{ {
@@ -94,6 +101,9 @@ namespace OpenNest.Controls
}; };
redrawTimer.Elapsed += redrawTimer_Elapsed; redrawTimer.Elapsed += redrawTimer_Elapsed;
hoverTimer = new Timer() { AutoReset = false, Interval = 1000 };
hoverTimer.Elapsed += hoverTimer_Elapsed;
SetStyle( SetStyle(
ControlStyles.AllPaintingInWmPaint | ControlStyles.AllPaintingInWmPaint |
ControlStyles.OptimizedDoubleBuffer | ControlStyles.OptimizedDoubleBuffer |
@@ -115,7 +125,8 @@ namespace OpenNest.Controls
DrawOffset = false; DrawOffset = false;
FillParts = true; FillParts = true;
renderer = new PlateRenderer(this); renderer = new PlateRenderer(this);
SetAction(typeof(ActionSelect)); actionManager = new ActionManager(this);
actionManager.SetAction(typeof(ActionSelect));
UpdateMatrix(); UpdateMatrix();
} }
@@ -148,14 +159,9 @@ namespace OpenNest.Controls
internal List<LayoutPart> LayoutParts => parts; internal List<LayoutPart> LayoutParts => parts;
internal IReadOnlyList<LayoutPart> PreviewParts => internal IReadOnlyList<LayoutPart> PreviewParts => previewManager.PreviewParts;
activeParts.Count > 0 ? activeParts : stationaryParts; internal Brush PreviewBrush => previewManager.PreviewBrush;
internal Pen PreviewPen => previewManager.PreviewPen;
internal Brush PreviewBrush =>
activeParts.Count > 0 ? ColorScheme.ActivePreviewPartBrush : ColorScheme.PreviewPartBrush;
internal Pen PreviewPen =>
activeParts.Count > 0 ? ColorScheme.ActivePreviewPartPen : ColorScheme.PreviewPartPen;
internal RectangleF GetViewBounds() => internal RectangleF GetViewBounds() =>
new RectangleF(-origin.X, -origin.Y, Width, Height); new RectangleF(-origin.X, -origin.Y, Width, Height);
@@ -173,16 +179,6 @@ namespace OpenNest.Controls
} }
} }
public CutOff SelectedCutOff
{
get => selectedCutOff;
set
{
selectedCutOff = value;
Invalidate();
}
}
public double RotateIncrementAngle { get; set; } public double RotateIncrementAngle { get; set; }
public double OffsetIncrementDistance { get; set; } public double OffsetIncrementDistance { get; set; }
@@ -200,9 +196,8 @@ namespace OpenNest.Controls
plate.PartAdded -= plate_PartAdded; plate.PartAdded -= plate_PartAdded;
plate.PartRemoved -= plate_PartRemoved; plate.PartRemoved -= plate_PartRemoved;
parts.Clear(); parts.Clear();
stationaryParts.Clear(); previewManager.Clear();
activeParts.Clear(); selection.Clear();
SelectedParts.Clear();
} }
plate = p; plate = p;
@@ -212,10 +207,7 @@ namespace OpenNest.Controls
foreach (var part in plate.Parts) foreach (var part in plate.Parts)
parts.Add(LayoutPart.Create(part, this)); parts.Add(LayoutPart.Create(part, this));
if (currentAction == null || !currentAction.SurvivesPlateChange) actionManager?.OnPlateChanged();
SetAction(typeof(ActionSelect));
else
currentAction.OnPlateChanged();
} }
public string Status public string Status
@@ -233,7 +225,6 @@ namespace OpenNest.Controls
protected override void OnMouseEnter(EventArgs e) protected override void OnMouseEnter(EventArgs e)
{ {
base.OnMouseEnter(e); base.OnMouseEnter(e);
if (!Focused) Focus();
} }
protected override void OnDragEnter(DragEventArgs drgevent) protected override void OnDragEnter(DragEventArgs drgevent)
@@ -257,22 +248,25 @@ namespace OpenNest.Controls
protected override void OnMouseDown(MouseEventArgs e) protected override void OnMouseDown(MouseEventArgs e)
{ {
if (!Focused) Focus();
if (e.Button == MouseButtons.Middle) if (e.Button == MouseButtons.Middle)
middleMouseDownPoint = e.Location; middleMouseDownPoint = e.Location;
if (e.Button == MouseButtons.Left && currentAction is ActionSelect) if (e.Button == MouseButtons.Left && actionManager.CurrentAction is ActionSelect)
{ {
var hitCutOff = GetCutOffAtPoint(CurrentPoint, 5.0 / ViewScale); var hitCutOff = cutOffHandler.TryStartDrag(CurrentPoint, 5.0 / ViewScale);
if (hitCutOff != null) if (hitCutOff != null)
{ {
SelectedCutOff = hitCutOff; selection.DeselectParts();
draggingCutOff = true; selection.SelectedCutOffs.Clear();
dragPerimeterCache = Plate.BuildPerimeterCache(Plate); selection.SelectedCutOffs.Add(hitCutOff);
Invalidate();
return; return;
} }
else else
{ {
SelectedCutOff = null; selection.DeselectCutOffs();
} }
} }
@@ -288,17 +282,14 @@ namespace OpenNest.Controls
if (dx * dx + dy * dy < 25) if (dx * dx + dy * dy < 25)
{ {
RotateSelectedParts(Angle.ToRadians(90)); selection.RotateSelectedParts(Angle.ToRadians(90));
Invalidate(); Invalidate();
} }
} }
if (draggingCutOff && selectedCutOff != null) if (cutOffHandler.IsDragging && selection.SelectedCutOffs.Count > 0)
{ {
draggingCutOff = false; cutOffHandler.EndDrag();
dragPerimeterCache = null;
Plate.RegenerateCutOffs(cutOffSettings);
Invalidate();
return; return;
} }
@@ -319,7 +310,7 @@ namespace OpenNest.Controls
var angle = Angle.ToRadians((e.Delta > 0 ? -increment : increment) * multiplier); var angle = Angle.ToRadians((e.Delta > 0 ? -increment : increment) * multiplier);
RotateSelectedParts(angle); selection.RotateSelectedParts(angle);
} }
else else
{ {
@@ -358,18 +349,30 @@ namespace OpenNest.Controls
lastPoint = e.Location; lastPoint = e.Location;
if (draggingCutOff && selectedCutOff != null) if (cutOffHandler.IsDragging && selection.SelectedCutOffs.Count > 0)
{ {
if (selectedCutOff.Axis == CutOffAxis.Vertical) cutOffHandler.UpdateDrag(CurrentPoint, selection.SelectedCutOffs[0]);
selectedCutOff.Position = new Vector(CurrentPoint.X, selectedCutOff.Position.Y);
else
selectedCutOff.Position = new Vector(selectedCutOff.Position.X, CurrentPoint.Y);
selectedCutOff.Regenerate(Plate, cutOffSettings, dragPerimeterCache);
Invalidate();
return; return;
} }
if (e.Button == MouseButtons.None && actionManager.CurrentAction is ActionSelect)
{
hoverPoint = e.Location;
showTooltip = false;
hoverTimer.Stop();
hoverTimer.Start();
if (hoveredPart != null)
Invalidate();
}
else if (hoveredPart != null || showTooltip)
{
hoveredPart = null;
hoverTimer.Stop();
showTooltip = false;
Invalidate();
}
base.OnMouseMove(e); base.OnMouseMove(e);
} }
@@ -386,17 +389,7 @@ namespace OpenNest.Controls
switch (e.KeyCode) switch (e.KeyCode)
{ {
case Keys.Delete: case Keys.Delete:
if (selectedCutOff != null) selection.DeleteSelected();
{
Plate.CutOffs.Remove(selectedCutOff);
selectedCutOff = null;
Plate.RegenerateCutOffs(cutOffSettings);
Invalidate();
}
else
{
RemoveSelectedParts();
}
break; break;
case Keys.F: case Keys.F:
@@ -412,15 +405,7 @@ namespace OpenNest.Controls
} }
} }
public void ProcessEscapeKey() public void ProcessEscapeKey() => actionManager.ProcessEscapeKey();
{
if (currentAction.IsBusy())
currentAction.CancelAction();
else if (currentAction is ActionSelect && previousAction != null)
RestorePreviousAction();
else
SetAction(typeof(ActionSelect));
}
protected override bool ProcessDialogKey(Keys keyData) protected override bool ProcessDialogKey(Keys keyData)
{ {
@@ -440,22 +425,22 @@ namespace OpenNest.Controls
case Keys.X: case Keys.X:
case Keys.Shift | Keys.Left: case Keys.Shift | Keys.Left:
PushSelected(PushDirection.Left); selection.PushSelected(PushDirection.Left);
break; break;
case Keys.Shift | Keys.X: case Keys.Shift | Keys.X:
case Keys.Shift | Keys.Right: case Keys.Shift | Keys.Right:
PushSelected(PushDirection.Right); selection.PushSelected(PushDirection.Right);
break; break;
case Keys.Shift | Keys.Y: case Keys.Shift | Keys.Y:
case Keys.Shift | Keys.Up: case Keys.Shift | Keys.Up:
PushSelected(PushDirection.Up); selection.PushSelected(PushDirection.Up);
break; break;
case Keys.Y: case Keys.Y:
case Keys.Shift | Keys.Down: case Keys.Shift | Keys.Down:
PushSelected(PushDirection.Down); selection.PushSelected(PushDirection.Down);
break; break;
case Keys.Right: case Keys.Right:
@@ -496,229 +481,53 @@ namespace OpenNest.Controls
renderer.DrawDebugRemnants(e.Graphics); renderer.DrawDebugRemnants(e.Graphics);
base.OnPaint(e); base.OnPaint(e);
if (hoveredPart != null && showTooltip)
{
e.Graphics.ResetTransform();
var text = hoveredPart.BasePart.BaseDrawing.Name;
var size = e.Graphics.MeasureString(text, Font);
var x = hoverPoint.X + 16f;
var y = hoverPoint.Y - size.Height - 6f;
if (x + size.Width + 8 > ClientSize.Width)
x = hoverPoint.X - size.Width - 8;
if (y < 0)
y = hoverPoint.Y + 20;
var rect = new RectangleF(x, y, size.Width + 6, size.Height + 4);
using (var bgBrush = new SolidBrush(Color.FromArgb(230, Color.White)))
e.Graphics.FillRectangle(bgBrush, rect);
e.Graphics.DrawRectangle(Pens.DimGray, rect.X, rect.Y, rect.Width, rect.Height);
e.Graphics.DrawString(text, Font, Brushes.Black, x + 3, y + 2);
}
} }
protected override void OnHandleDestroyed(EventArgs e) protected override void OnHandleDestroyed(EventArgs e)
{ {
base.OnHandleDestroyed(e); base.OnHandleDestroyed(e);
actionManager.Cleanup();
if (currentAction != null)
{
currentAction.CancelAction();
currentAction.DisconnectEvents();
currentAction = null;
}
} }
public override void Refresh() public override void Refresh()
{ {
parts.ForEach(p => p.Update(this)); parts.ForEach(p => p.Update(this));
stationaryParts.ForEach(p => p.Update(this)); previewManager.Update();
activeParts.ForEach(p => p.Update(this));
Invalidate(); Invalidate();
} }
public CutOff GetCutOffAtPoint(Vector point, double tolerance) public CutOff GetCutOffAtPoint(Vector point, double tolerance) => cutOffHandler.GetCutOffAtPoint(point, tolerance);
{
if (Plate?.CutOffs == null)
return null;
foreach (var cutoff in Plate.CutOffs) public LayoutPart GetPartAtControlPoint(Point pt) => selection.GetPartAtControlPoint(pt);
{ public LayoutPart GetPartAtGraphPoint(PointF pt) => selection.GetPartAtGraphPoint(pt);
var program = cutoff.Drawing?.Program; public LayoutPart GetPartAtPoint(Vector pt) => selection.GetPartAtPoint(pt);
if (program == null) public IList<LayoutPart> GetPartsFromWindow(RectangleF rect, SelectionType selectionType) => selection.GetPartsFromWindow(rect, selectionType);
continue;
for (var i = 0; i < program.Codes.Count - 1; i += 2) public void SetAction(Type type) => actionManager.SetAction(type);
{ public void SetAction(Type type, params object[] args) => actionManager.SetAction(type, args);
if (program.Codes[i] is RapidMove rapid &&
program.Codes[i + 1] is LinearMove linear)
{
var line = new Geometry.Line(rapid.EndPoint, linear.EndPoint);
if (line.ClosestPointTo(point).DistanceTo(point) <= tolerance)
return cutoff;
}
}
}
return null; public void AlignSelected(AlignType alignType) => selection.AlignSelected(alignType);
} public void AlignSelected(AlignType alignType, LayoutPart fixedPart) => selection.AlignSelected(alignType, fixedPart);
public LayoutPart GetPartAtControlPoint(Point pt)
{
var pt2 = PointControlToGraph(pt);
return GetPartAtGraphPoint(pt2);
}
public LayoutPart GetPartAtGraphPoint(PointF pt)
{
for (int i = parts.Count - 1; i >= 0; --i)
{
if (parts[i].Path.IsVisible(pt))
return parts[i];
}
return null;
}
public LayoutPart GetPartAtPoint(Vector pt)
{
var pt2 = PointWorldToGraph(pt);
return GetPartAtGraphPoint(pt2);
}
public IList<LayoutPart> GetPartsFromWindow(RectangleF rect, SelectionType selectionType)
{
var list = new List<LayoutPart>();
if (selectionType == SelectionType.Intersect)
{
for (int i = 0; i < parts.Count; ++i)
{
var part = parts[i];
var path = part.Path;
var region = new Region(path);
if (region.IsVisible(rect))
list.Add(part);
region.Dispose();
}
}
else
{
for (int i = 0; i < parts.Count; ++i)
{
var part = parts[i];
var path = part.Path;
var bounds = path.GetBounds();
if (rect.Contains(bounds))
list.Add(part);
}
}
return list;
}
public void SetAction(Type type)
{
var action = Activator.CreateInstance(type, this) as Action;
if (action == null)
return;
if (currentAction != null)
{
if (type == typeof(ActionSelect) && !(currentAction is ActionSelect))
previousAction = currentAction;
else
previousAction = null;
currentAction.CancelAction();
currentAction.DisconnectEvents();
currentAction = null;
}
currentAction = action;
Status = GetDisplayName(type);
}
public void SetAction(Type type, params object[] args)
{
if (currentAction != null)
{
previousAction = null;
currentAction.CancelAction();
currentAction.DisconnectEvents();
currentAction = null;
}
Array.Resize(ref args, args.Length + 1);
// shift all elements to the right
for (int i = args.Length - 2; i >= 0; i--)
args[i + 1] = args[i];
// set the first argument to this.
args[0] = this;
var action = Activator.CreateInstance(type, args) as Action;
if (action == null)
return;
currentAction = action;
Status = GetDisplayName(type);
}
private void RestorePreviousAction()
{
var action = previousAction;
previousAction = null;
currentAction.CancelAction();
currentAction.DisconnectEvents();
action.ConnectEvents();
currentAction = action;
Status = GetDisplayName(action.GetType());
}
public void AlignSelected(AlignType alignType)
{
if (SelectedParts.Count == 0)
return;
AlignSelected(alignType, SelectedParts[0]);
}
public void AlignSelected(AlignType alignType, LayoutPart fixedPart)
{
switch (alignType)
{
case AlignType.Bottom:
Align.Bottom(fixedPart.BasePart, SelectedParts.Select(p => p.BasePart).ToList());
break;
case AlignType.Horizontally:
Align.Horizontally(fixedPart.BasePart, SelectedParts.Select(p => p.BasePart).ToList());
break;
case AlignType.Left:
Align.Left(fixedPart.BasePart, SelectedParts.Select(p => p.BasePart).ToList());
break;
case AlignType.Right:
Align.Right(fixedPart.BasePart, SelectedParts.Select(p => p.BasePart).ToList());
break;
case AlignType.Top:
Align.Top(fixedPart.BasePart, SelectedParts.Select(p => p.BasePart).ToList());
break;
case AlignType.Vertically:
Align.Vertically(fixedPart.BasePart, SelectedParts.Select(p => p.BasePart).ToList());
break;
case AlignType.EvenlySpaceHorizontally:
Align.EvenlyDistributeHorizontally(SelectedParts.Select(p => p.BasePart).ToList());
break;
case AlignType.EvenlySpaceVertically:
Align.EvenlyDistributeVertically(SelectedParts.Select(p => p.BasePart).ToList());
break;
default:
return;
}
SelectedParts.ForEach(p => p.IsDirty = true);
Invalidate();
}
public void AddPartFromDrawing(Drawing dwg, Vector location) public void AddPartFromDrawing(Drawing dwg, Vector location)
{ {
@@ -731,51 +540,10 @@ namespace OpenNest.Controls
Plate.Parts.Add(part); Plate.Parts.Add(part);
} }
public void SetStationaryParts(List<Part> parts) public void SetStationaryParts(List<Part> parts) => previewManager.SetStationaryParts(parts);
{ public void SetActiveParts(List<Part> parts) => previewManager.SetActiveParts(parts);
stationaryParts.Clear(); public void ClearPreviewParts() => previewManager.ClearPreviewParts();
activeParts.Clear(); public void AcceptPreviewParts(List<Part> parts) => previewManager.AcceptPreviewParts(parts);
if (parts != null)
{
foreach (var part in parts)
stationaryParts.Add(LayoutPart.Create(part, this));
}
Invalidate();
}
public void SetActiveParts(List<Part> parts)
{
activeParts.Clear();
if (parts != null)
{
foreach (var part in parts)
activeParts.Add(LayoutPart.Create(part, this));
}
Invalidate();
}
public void ClearPreviewParts()
{
stationaryParts.Clear();
activeParts.Clear();
Invalidate();
}
public void AcceptPreviewParts(List<Part> parts)
{
if (parts != null)
{
foreach (var part in parts)
Plate.Parts.Add(part);
}
stationaryParts.Clear();
activeParts.Clear();
}
public async void FillWithProgress(List<Part> groupParts, Box workArea) public async void FillWithProgress(List<Part> groupParts, Box workArea)
{ {
@@ -848,14 +616,7 @@ namespace OpenNest.Controls
} }
} }
public void RemoveSelectedParts() public void RemoveSelectedParts() => selection.RemoveSelectedParts();
{
foreach (var part in SelectedParts)
Plate.Parts.Remove(part.BasePart);
DeselectAll();
Invalidate();
}
private void redrawTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) private void redrawTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
@@ -863,6 +624,35 @@ namespace OpenNest.Controls
Invalidate(); Invalidate();
} }
private void hoverTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
var graphPt = PointControlToGraph(hoverPoint);
LayoutPart hitPart = null;
try
{
for (var i = parts.Count - 1; i >= 0; --i)
{
if (parts[i].Path.GetBounds().Contains(graphPt) &&
parts[i].Path.IsVisible(graphPt))
{
hitPart = parts[i];
break;
}
}
}
catch (InvalidOperationException)
{
// GraphicsPath in use by paint thread — skip this hover tick
return;
}
hoveredPart = hitPart;
showTooltip = hitPart != null;
if (showTooltip)
Invalidate();
}
private void plate_PartAdded(object sender, ItemAddedEventArgs<Part> e) private void plate_PartAdded(object sender, ItemAddedEventArgs<Part> e)
{ {
if (PartAdded != null) if (PartAdded != null)
@@ -880,24 +670,9 @@ namespace OpenNest.Controls
parts.RemoveAll(p => p.BasePart == e.Item); parts.RemoveAll(p => p.BasePart == e.Item);
} }
public void DeselectAll() public void DeselectAll() => selection.DeselectAll();
{ public void SelectAll() => selection.SelectAll();
SelectedParts.ForEach(p => p.IsSelected = false); public void NotifySelectionChanged() => selection.NotifySelectionChanged();
SelectedParts.Clear();
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
public void SelectAll()
{
parts.ForEach(p => p.IsSelected = true);
SelectedParts.AddRange(parts);
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
public void NotifySelectionChanged()
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
public override void ZoomToPoint(Vector pt, float zoomFactor, bool redraw = true) public override void ZoomToPoint(Vector pt, float zoomFactor, bool redraw = true)
{ {
@@ -930,57 +705,15 @@ namespace OpenNest.Controls
ZoomToArea(plate.BoundingBox(false), redraw); ZoomToArea(plate.BoundingBox(false), redraw);
} }
public void PushSelected(PushDirection direction) public void PushSelected(PushDirection direction) => selection.PushSelected(direction);
{
var movingParts = SelectedParts.Select(p => p.BasePart).ToList();
Compactor.Push(movingParts, Plate, direction);
SelectedParts.ForEach(p => p.IsDirty = true);
Invalidate();
}
private string GetDisplayName(Type type) public void RotateSelectedParts(double angle) => selection.RotateSelectedParts(angle);
{
var attributes = type.GetCustomAttributes(true);
foreach (var attr in attributes)
{
var displayNameAttr = attr as DisplayNameAttribute;
if (displayNameAttr != null)
return displayNameAttr.DisplayName;
}
return type.Name;
}
public void RotateSelectedParts(double angle)
{
var parts = SelectedParts.Select(p => p.BasePart).ToList();
var bounds = parts.GetBoundingBox();
var center = bounds.Center;
var anchor = bounds.Location;
for (var i = 0; i < SelectedParts.Count; ++i)
{
var part = SelectedParts[i];
part.BasePart.Rotate(angle, center);
}
var diff = anchor - parts.GetBoundingBox().Location;
for (var i = 0; i < SelectedParts.Count; ++i)
SelectedParts[i].Offset(diff);
if (Plate.CutOffs.Count > 0)
Plate.RegenerateCutOffs(cutOffSettings);
}
protected override void UpdateMatrix() protected override void UpdateMatrix()
{ {
base.UpdateMatrix(); base.UpdateMatrix();
parts.ForEach(p => p.Update(this)); parts.ForEach(p => p.Update(this));
stationaryParts.ForEach(p => p.Update(this)); previewManager.Update();
activeParts.ForEach(p => p.Update(this));
} }
} }
} }
+84
View File
@@ -0,0 +1,84 @@
using System.Collections.Generic;
using System.Drawing;
namespace OpenNest.Controls
{
internal class PreviewManager
{
private readonly PlateView view;
private readonly List<LayoutPart> stationaryParts = new List<LayoutPart>();
private readonly List<LayoutPart> activeParts = new List<LayoutPart>();
public PreviewManager(PlateView view)
{
this.view = view;
}
public IReadOnlyList<LayoutPart> PreviewParts =>
activeParts.Count > 0 ? activeParts : stationaryParts;
public Brush PreviewBrush =>
activeParts.Count > 0 ? view.ColorScheme.ActivePreviewPartBrush : view.ColorScheme.PreviewPartBrush;
public Pen PreviewPen =>
activeParts.Count > 0 ? view.ColorScheme.ActivePreviewPartPen : view.ColorScheme.PreviewPartPen;
public void SetStationaryParts(List<Part> parts)
{
stationaryParts.Clear();
activeParts.Clear();
if (parts != null)
{
foreach (var part in parts)
stationaryParts.Add(LayoutPart.Create(part, view));
}
view.Invalidate();
}
public void SetActiveParts(List<Part> parts)
{
activeParts.Clear();
if (parts != null)
{
foreach (var part in parts)
activeParts.Add(LayoutPart.Create(part, view));
}
view.Invalidate();
}
public void ClearPreviewParts()
{
stationaryParts.Clear();
activeParts.Clear();
view.Invalidate();
}
public void AcceptPreviewParts(List<Part> parts)
{
if (parts != null)
{
foreach (var part in parts)
view.Plate.Parts.Add(part);
}
stationaryParts.Clear();
activeParts.Clear();
}
public void Update()
{
stationaryParts.ForEach(p => p.Update(view));
activeParts.ForEach(p => p.Update(view));
}
public void Clear()
{
stationaryParts.Clear();
activeParts.Clear();
}
}
}
+216
View File
@@ -0,0 +1,216 @@
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
namespace OpenNest.Controls
{
internal class SelectionManager
{
private readonly PlateView view;
private readonly List<LayoutPart> selectedParts = new List<LayoutPart>();
private readonly List<CutOff> selectedCutOffs = new List<CutOff>();
public SelectionManager(PlateView view)
{
this.view = view;
}
public List<LayoutPart> SelectedParts => selectedParts;
public List<CutOff> SelectedCutOffs => selectedCutOffs;
public event EventHandler SelectionChanged;
public void DeselectAll()
{
selectedParts.ForEach(p => p.IsSelected = false);
selectedParts.Clear();
selectedCutOffs.Clear();
SelectionChanged?.Invoke(view, EventArgs.Empty);
}
public void DeselectParts()
{
selectedParts.ForEach(p => p.IsSelected = false);
selectedParts.Clear();
SelectionChanged?.Invoke(view, EventArgs.Empty);
}
public void DeselectCutOffs()
{
selectedCutOffs.Clear();
view.Invalidate();
}
public void SelectAll()
{
var parts = view.LayoutParts;
parts.ForEach(p => p.IsSelected = true);
selectedParts.AddRange(parts);
SelectionChanged?.Invoke(view, EventArgs.Empty);
}
public void NotifySelectionChanged()
{
SelectionChanged?.Invoke(view, EventArgs.Empty);
}
public void DeleteSelected()
{
if (selectedCutOffs.Count > 0)
{
foreach (var cutOff in selectedCutOffs)
view.Plate.CutOffs.Remove(cutOff);
selectedCutOffs.Clear();
view.Plate.RegenerateCutOffs(view.CutOffSettings);
view.Invalidate();
}
else
{
RemoveSelectedParts();
}
}
public void RemoveSelectedParts()
{
foreach (var part in selectedParts)
view.Plate.Parts.Remove(part.BasePart);
DeselectAll();
view.Invalidate();
}
public void AlignSelected(AlignType alignType)
{
if (selectedParts.Count == 0)
return;
AlignSelected(alignType, selectedParts[0]);
}
public void AlignSelected(AlignType alignType, LayoutPart fixedPart)
{
switch (alignType)
{
case AlignType.Bottom:
Align.Bottom(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList());
break;
case AlignType.Horizontally:
Align.Horizontally(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList());
break;
case AlignType.Left:
Align.Left(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList());
break;
case AlignType.Right:
Align.Right(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList());
break;
case AlignType.Top:
Align.Top(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList());
break;
case AlignType.Vertically:
Align.Vertically(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList());
break;
case AlignType.EvenlySpaceHorizontally:
Align.EvenlyDistributeHorizontally(selectedParts.Select(p => p.BasePart).ToList());
break;
case AlignType.EvenlySpaceVertically:
Align.EvenlyDistributeVertically(selectedParts.Select(p => p.BasePart).ToList());
break;
default:
return;
}
selectedParts.ForEach(p => p.IsDirty = true);
view.Invalidate();
}
public void RotateSelectedParts(double angle)
{
var parts = selectedParts.Select(p => p.BasePart).ToList();
var bounds = parts.GetBoundingBox();
var center = bounds.Center;
var anchor = bounds.Location;
for (var i = 0; i < selectedParts.Count; ++i)
selectedParts[i].BasePart.Rotate(angle, center);
var diff = anchor - parts.GetBoundingBox().Location;
for (var i = 0; i < selectedParts.Count; ++i)
selectedParts[i].Offset(diff);
if (view.Plate.CutOffs.Count > 0)
view.Plate.RegenerateCutOffs(view.CutOffSettings);
}
public void PushSelected(PushDirection direction)
{
var movingParts = selectedParts.Select(p => p.BasePart).ToList();
Compactor.Push(movingParts, view.Plate, direction);
selectedParts.ForEach(p => p.IsDirty = true);
view.Invalidate();
}
public LayoutPart GetPartAtControlPoint(Point pt)
{
var pt2 = view.PointControlToGraph(pt);
return GetPartAtGraphPoint(pt2);
}
public LayoutPart GetPartAtGraphPoint(PointF pt)
{
var parts = view.LayoutParts;
for (var i = parts.Count - 1; i >= 0; --i)
{
if (parts[i].Path.IsVisible(pt))
return parts[i];
}
return null;
}
public LayoutPart GetPartAtPoint(Vector pt)
{
var pt2 = view.PointWorldToGraph(pt);
return GetPartAtGraphPoint(pt2);
}
public IList<LayoutPart> GetPartsFromWindow(RectangleF rect, SelectionType selectionType)
{
var list = new List<LayoutPart>();
var parts = view.LayoutParts;
if (selectionType == SelectionType.Intersect)
{
for (var i = 0; i < parts.Count; ++i)
{
var part = parts[i];
var region = new Region(part.Path);
if (region.IsVisible(rect))
list.Add(part);
region.Dispose();
}
}
else
{
for (var i = 0; i < parts.Count; ++i)
{
var part = parts[i];
var bounds = part.Path.GetBounds();
if (rect.Contains(bounds))
list.Add(part);
}
}
return list;
}
public void Clear()
{
selectedParts.Clear();
selectedCutOffs.Clear();
}
}
}
+79
View File
@@ -0,0 +1,79 @@
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace OpenNest.Controls
{
public class ShapePreviewControl : PlateView
{
private string[] infoLines;
public ShapePreviewControl()
{
DrawOrigin = false;
DrawBounds = false;
AllowPan = false;
AllowSelect = false;
AllowZoom = false;
AllowDrop = false;
BackColor = Color.White;
}
public void SetInfo(params string[] lines)
{
infoLines = lines;
Invalidate();
}
public void ShowDrawing(Drawing drawing)
{
Plate.Parts.Clear();
Plate.Size = new Geometry.Size(0, 0);
if (drawing?.Program != null)
{
AddPartFromDrawing(drawing, Geometry.Vector.Zero);
ZoomToFit();
}
else
{
Invalidate();
}
}
protected override void OnResize(System.EventArgs e)
{
base.OnResize(e);
if (Plate.Parts.Count > 0)
ZoomToFit(false);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
e.Graphics.TranslateTransform(origin.X, origin.Y);
Renderer.DrawPlate(e.Graphics);
Renderer.DrawParts(e.Graphics);
e.Graphics.ResetTransform();
PaintInfo(e.Graphics);
}
private void PaintInfo(Graphics g)
{
if (infoLines == null) return;
var lineHeight = Font.GetHeight(g) + 1;
var y = 4f;
foreach (var line in infoLines)
{
if (string.IsNullOrEmpty(line)) continue;
g.DrawString(line, Font, Brushes.Black, 4, y);
y += lineHeight;
}
}
}
}
+149 -61
View File
@@ -17,13 +17,20 @@ namespace OpenNest.Forms
private void InitializeComponent() private void InitializeComponent()
{ {
this.engineLabel = new System.Windows.Forms.Label(); this.tabControl = new System.Windows.Forms.TabControl();
this.engineComboBox = new System.Windows.Forms.ComboBox(); this.partsTab = new System.Windows.Forms.TabPage();
this.partsGroup = new System.Windows.Forms.GroupBox(); this.platesTab = new System.Windows.Forms.TabPage();
this.partsGrid = new System.Windows.Forms.DataGridView(); this.partsGrid = new System.Windows.Forms.DataGridView();
this.summaryLabel = new System.Windows.Forms.Label(); this.summaryLabel = new System.Windows.Forms.Label();
this.optionsGroup = new System.Windows.Forms.GroupBox(); this.engineLabel = new System.Windows.Forms.Label();
this.engineComboBox = new System.Windows.Forms.ComboBox();
this.createNewPlatesAsNeededBox = new System.Windows.Forms.CheckBox(); this.createNewPlatesAsNeededBox = new System.Windows.Forms.CheckBox();
this.partFirstGroup = new System.Windows.Forms.GroupBox();
this.partFirstCheckBox = new System.Windows.Forms.CheckBox();
this.sortOrderLabel = new System.Windows.Forms.Label();
this.sortOrderComboBox = new System.Windows.Forms.ComboBox();
this.minRemnantLabel = new System.Windows.Forms.Label();
this.minRemnantBox = new System.Windows.Forms.TextBox();
this.plateOptimizerGroup = new System.Windows.Forms.GroupBox(); this.plateOptimizerGroup = new System.Windows.Forms.GroupBox();
this.optimizePlateSizeBox = new System.Windows.Forms.CheckBox(); this.optimizePlateSizeBox = new System.Windows.Forms.CheckBox();
this.plateGrid = new System.Windows.Forms.DataGridView(); this.plateGrid = new System.Windows.Forms.DataGridView();
@@ -33,42 +40,53 @@ namespace OpenNest.Forms
this.buttonPanel = new System.Windows.Forms.Panel(); this.buttonPanel = new System.Windows.Forms.Panel();
this.acceptButton = new System.Windows.Forms.Button(); this.acceptButton = new System.Windows.Forms.Button();
this.cancelButton = new System.Windows.Forms.Button(); this.cancelButton = new System.Windows.Forms.Button();
this.tabControl.SuspendLayout();
this.partsTab.SuspendLayout();
this.platesTab.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.partsGrid)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.partsGrid)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.plateGrid)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.plateGrid)).BeginInit();
this.partsGroup.SuspendLayout(); this.partFirstGroup.SuspendLayout();
this.optionsGroup.SuspendLayout();
this.plateOptimizerGroup.SuspendLayout(); this.plateOptimizerGroup.SuspendLayout();
this.buttonPanel.SuspendLayout(); this.buttonPanel.SuspendLayout();
this.SuspendLayout(); this.SuspendLayout();
// //
// engineLabel // tabControl
// //
this.engineLabel.AutoSize = true; this.tabControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
this.engineLabel.Location = new System.Drawing.Point(12, 15); this.tabControl.Controls.Add(this.partsTab);
this.engineLabel.Name = "engineLabel"; this.tabControl.Controls.Add(this.platesTab);
this.engineLabel.Size = new System.Drawing.Size(82, 16); this.tabControl.Location = new System.Drawing.Point(12, 12);
this.engineLabel.TabIndex = 0; this.tabControl.Name = "tabControl";
this.engineLabel.Text = "Nest Engine:"; this.tabControl.SelectedIndex = 0;
this.tabControl.Size = new System.Drawing.Size(556, 490);
this.tabControl.TabIndex = 0;
// //
// engineComboBox // partsTab
// //
this.engineComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; this.partsTab.Controls.Add(this.partsGrid);
this.engineComboBox.Location = new System.Drawing.Point(100, 12); this.partsTab.Controls.Add(this.summaryLabel);
this.engineComboBox.Name = "engineComboBox"; this.partsTab.Location = new System.Drawing.Point(4, 25);
this.engineComboBox.Size = new System.Drawing.Size(200, 24); this.partsTab.Name = "partsTab";
this.engineComboBox.TabIndex = 1; this.partsTab.Padding = new System.Windows.Forms.Padding(6);
this.partsTab.Size = new System.Drawing.Size(548, 461);
this.partsTab.TabIndex = 0;
this.partsTab.Text = "Parts";
this.partsTab.UseVisualStyleBackColor = true;
// //
// partsGroup // platesTab
// //
this.partsGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.platesTab.Controls.Add(this.engineLabel);
this.partsGroup.Controls.Add(this.partsGrid); this.platesTab.Controls.Add(this.engineComboBox);
this.partsGroup.Controls.Add(this.summaryLabel); this.platesTab.Controls.Add(this.createNewPlatesAsNeededBox);
this.partsGroup.Location = new System.Drawing.Point(12, 42); this.platesTab.Controls.Add(this.partFirstGroup);
this.partsGroup.Name = "partsGroup"; this.platesTab.Controls.Add(this.plateOptimizerGroup);
this.partsGroup.Size = new System.Drawing.Size(556, 210); this.platesTab.Location = new System.Drawing.Point(4, 25);
this.partsGroup.TabIndex = 2; this.platesTab.Name = "platesTab";
this.partsGroup.TabStop = false; this.platesTab.Padding = new System.Windows.Forms.Padding(6);
this.partsGroup.Text = "Parts"; this.platesTab.Size = new System.Drawing.Size(548, 461);
this.platesTab.TabIndex = 1;
this.platesTab.Text = "Plates";
this.platesTab.UseVisualStyleBackColor = true;
// //
// partsGrid // partsGrid
// //
@@ -78,43 +96,108 @@ namespace OpenNest.Forms
this.partsGrid.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.partsGrid.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
this.partsGrid.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; this.partsGrid.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.partsGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; this.partsGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.partsGrid.Location = new System.Drawing.Point(10, 22); this.partsGrid.Location = new System.Drawing.Point(10, 10);
this.partsGrid.Name = "partsGrid"; this.partsGrid.Name = "partsGrid";
this.partsGrid.RowHeadersVisible = false; this.partsGrid.RowHeadersVisible = false;
this.partsGrid.AutoGenerateColumns = false; this.partsGrid.AutoGenerateColumns = false;
this.partsGrid.Size = new System.Drawing.Size(536, 160); this.partsGrid.Size = new System.Drawing.Size(528, 420);
this.partsGrid.TabIndex = 0; this.partsGrid.TabIndex = 0;
// //
// summaryLabel // summaryLabel
// //
this.summaryLabel.AutoSize = true; this.summaryLabel.AutoSize = true;
this.summaryLabel.ForeColor = System.Drawing.SystemColors.GrayText; this.summaryLabel.ForeColor = System.Drawing.SystemColors.GrayText;
this.summaryLabel.Location = new System.Drawing.Point(10, 188); this.summaryLabel.Location = new System.Drawing.Point(10, 436);
this.summaryLabel.Name = "summaryLabel"; this.summaryLabel.Name = "summaryLabel";
this.summaryLabel.Size = new System.Drawing.Size(0, 16); this.summaryLabel.Size = new System.Drawing.Size(0, 16);
this.summaryLabel.TabIndex = 1; this.summaryLabel.TabIndex = 1;
// //
// optionsGroup // engineLabel
// //
this.optionsGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.engineLabel.AutoSize = true;
this.optionsGroup.Controls.Add(this.createNewPlatesAsNeededBox); this.engineLabel.Location = new System.Drawing.Point(10, 15);
this.optionsGroup.Location = new System.Drawing.Point(12, 258); this.engineLabel.Name = "engineLabel";
this.optionsGroup.Name = "optionsGroup"; this.engineLabel.Size = new System.Drawing.Size(82, 16);
this.optionsGroup.Size = new System.Drawing.Size(556, 48); this.engineLabel.TabIndex = 0;
this.optionsGroup.TabIndex = 3; this.engineLabel.Text = "Nest Engine:";
this.optionsGroup.TabStop = false; //
this.optionsGroup.Text = "Options"; // engineComboBox
//
this.engineComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.engineComboBox.Location = new System.Drawing.Point(98, 12);
this.engineComboBox.Name = "engineComboBox";
this.engineComboBox.Size = new System.Drawing.Size(200, 24);
this.engineComboBox.TabIndex = 1;
// //
// createNewPlatesAsNeededBox // createNewPlatesAsNeededBox
// //
this.createNewPlatesAsNeededBox.AutoSize = true; this.createNewPlatesAsNeededBox.AutoSize = true;
this.createNewPlatesAsNeededBox.Location = new System.Drawing.Point(10, 22); this.createNewPlatesAsNeededBox.Location = new System.Drawing.Point(10, 44);
this.createNewPlatesAsNeededBox.Name = "createNewPlatesAsNeededBox"; this.createNewPlatesAsNeededBox.Name = "createNewPlatesAsNeededBox";
this.createNewPlatesAsNeededBox.Size = new System.Drawing.Size(202, 20); this.createNewPlatesAsNeededBox.Size = new System.Drawing.Size(202, 20);
this.createNewPlatesAsNeededBox.TabIndex = 0; this.createNewPlatesAsNeededBox.TabIndex = 2;
this.createNewPlatesAsNeededBox.Text = "Create new plates as needed"; this.createNewPlatesAsNeededBox.Text = "Create new plates as needed";
this.createNewPlatesAsNeededBox.UseVisualStyleBackColor = true; this.createNewPlatesAsNeededBox.UseVisualStyleBackColor = true;
// //
// partFirstGroup
//
this.partFirstGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
this.partFirstGroup.Controls.Add(this.partFirstCheckBox);
this.partFirstGroup.Controls.Add(this.sortOrderLabel);
this.partFirstGroup.Controls.Add(this.sortOrderComboBox);
this.partFirstGroup.Controls.Add(this.minRemnantLabel);
this.partFirstGroup.Controls.Add(this.minRemnantBox);
this.partFirstGroup.Location = new System.Drawing.Point(10, 72);
this.partFirstGroup.Name = "partFirstGroup";
this.partFirstGroup.Size = new System.Drawing.Size(528, 80);
this.partFirstGroup.TabIndex = 3;
this.partFirstGroup.TabStop = false;
this.partFirstGroup.Text = " Part-First Mode";
//
// partFirstCheckBox
//
this.partFirstCheckBox.AutoSize = true;
this.partFirstCheckBox.Location = new System.Drawing.Point(10, 0);
this.partFirstCheckBox.Name = "partFirstCheckBox";
this.partFirstCheckBox.Size = new System.Drawing.Size(15, 14);
this.partFirstCheckBox.TabIndex = 0;
this.partFirstCheckBox.UseVisualStyleBackColor = true;
this.partFirstCheckBox.CheckedChanged += new System.EventHandler(this.partFirstCheckBox_CheckedChanged);
//
// sortOrderLabel
//
this.sortOrderLabel.AutoSize = true;
this.sortOrderLabel.Location = new System.Drawing.Point(10, 26);
this.sortOrderLabel.Name = "sortOrderLabel";
this.sortOrderLabel.Size = new System.Drawing.Size(75, 16);
this.sortOrderLabel.TabIndex = 1;
this.sortOrderLabel.Text = "Sort Order:";
//
// sortOrderComboBox
//
this.sortOrderComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.sortOrderComboBox.Location = new System.Drawing.Point(100, 23);
this.sortOrderComboBox.Name = "sortOrderComboBox";
this.sortOrderComboBox.Size = new System.Drawing.Size(180, 24);
this.sortOrderComboBox.TabIndex = 2;
//
// minRemnantLabel
//
this.minRemnantLabel.AutoSize = true;
this.minRemnantLabel.Location = new System.Drawing.Point(10, 54);
this.minRemnantLabel.Name = "minRemnantLabel";
this.minRemnantLabel.Size = new System.Drawing.Size(117, 16);
this.minRemnantLabel.TabIndex = 3;
this.minRemnantLabel.Text = "Min Remnant Size:";
//
// minRemnantBox
//
this.minRemnantBox.Location = new System.Drawing.Point(133, 51);
this.minRemnantBox.Name = "minRemnantBox";
this.minRemnantBox.Size = new System.Drawing.Size(60, 22);
this.minRemnantBox.TabIndex = 4;
this.minRemnantBox.Text = "12";
//
// plateOptimizerGroup // plateOptimizerGroup
// //
this.plateOptimizerGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.plateOptimizerGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
@@ -123,9 +206,9 @@ namespace OpenNest.Forms
this.plateOptimizerGroup.Controls.Add(this.salvageRateLabel); this.plateOptimizerGroup.Controls.Add(this.salvageRateLabel);
this.plateOptimizerGroup.Controls.Add(this.salvageRateBox); this.plateOptimizerGroup.Controls.Add(this.salvageRateBox);
this.plateOptimizerGroup.Controls.Add(this.salvageRatePercentLabel); this.plateOptimizerGroup.Controls.Add(this.salvageRatePercentLabel);
this.plateOptimizerGroup.Location = new System.Drawing.Point(12, 312); this.plateOptimizerGroup.Location = new System.Drawing.Point(10, 158);
this.plateOptimizerGroup.Name = "plateOptimizerGroup"; this.plateOptimizerGroup.Name = "plateOptimizerGroup";
this.plateOptimizerGroup.Size = new System.Drawing.Size(556, 188); this.plateOptimizerGroup.Size = new System.Drawing.Size(528, 188);
this.plateOptimizerGroup.TabIndex = 4; this.plateOptimizerGroup.TabIndex = 4;
this.plateOptimizerGroup.TabStop = false; this.plateOptimizerGroup.TabStop = false;
this.plateOptimizerGroup.Text = " Plate Optimizer"; this.plateOptimizerGroup.Text = " Plate Optimizer";
@@ -150,7 +233,7 @@ namespace OpenNest.Forms
this.plateGrid.Name = "plateGrid"; this.plateGrid.Name = "plateGrid";
this.plateGrid.RowHeadersVisible = false; this.plateGrid.RowHeadersVisible = false;
this.plateGrid.AutoGenerateColumns = false; this.plateGrid.AutoGenerateColumns = false;
this.plateGrid.Size = new System.Drawing.Size(536, 130); this.plateGrid.Size = new System.Drawing.Size(508, 130);
this.plateGrid.TabIndex = 1; this.plateGrid.TabIndex = 1;
// //
// salvageRateLabel // salvageRateLabel
@@ -187,7 +270,7 @@ namespace OpenNest.Forms
this.buttonPanel.Location = new System.Drawing.Point(0, 506); this.buttonPanel.Location = new System.Drawing.Point(0, 506);
this.buttonPanel.Name = "buttonPanel"; this.buttonPanel.Name = "buttonPanel";
this.buttonPanel.Size = new System.Drawing.Size(580, 50); this.buttonPanel.Size = new System.Drawing.Size(580, 50);
this.buttonPanel.TabIndex = 5; this.buttonPanel.TabIndex = 1;
// //
// acceptButton // acceptButton
// //
@@ -217,11 +300,7 @@ namespace OpenNest.Forms
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
this.CancelButton = this.cancelButton; this.CancelButton = this.cancelButton;
this.ClientSize = new System.Drawing.Size(580, 556); this.ClientSize = new System.Drawing.Size(580, 556);
this.Controls.Add(this.engineLabel); this.Controls.Add(this.tabControl);
this.Controls.Add(this.engineComboBox);
this.Controls.Add(this.partsGroup);
this.Controls.Add(this.optionsGroup);
this.Controls.Add(this.plateOptimizerGroup);
this.Controls.Add(this.buttonPanel); this.Controls.Add(this.buttonPanel);
this.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); this.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
@@ -232,28 +311,37 @@ namespace OpenNest.Forms
this.ShowInTaskbar = false; this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "AutoNest"; this.Text = "AutoNest";
this.tabControl.ResumeLayout(false);
this.partsTab.ResumeLayout(false);
this.partsTab.PerformLayout();
this.platesTab.ResumeLayout(false);
this.platesTab.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.partsGrid)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.partsGrid)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.plateGrid)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.plateGrid)).EndInit();
this.partsGroup.ResumeLayout(false); this.partFirstGroup.ResumeLayout(false);
this.partsGroup.PerformLayout(); this.partFirstGroup.PerformLayout();
this.optionsGroup.ResumeLayout(false);
this.optionsGroup.PerformLayout();
this.plateOptimizerGroup.ResumeLayout(false); this.plateOptimizerGroup.ResumeLayout(false);
this.plateOptimizerGroup.PerformLayout(); this.plateOptimizerGroup.PerformLayout();
this.buttonPanel.ResumeLayout(false); this.buttonPanel.ResumeLayout(false);
this.ResumeLayout(false); this.ResumeLayout(false);
this.PerformLayout();
} }
#endregion #endregion
private System.Windows.Forms.Label engineLabel; private System.Windows.Forms.TabControl tabControl;
private System.Windows.Forms.ComboBox engineComboBox; private System.Windows.Forms.TabPage partsTab;
private System.Windows.Forms.GroupBox partsGroup; private System.Windows.Forms.TabPage platesTab;
private System.Windows.Forms.DataGridView partsGrid; private System.Windows.Forms.DataGridView partsGrid;
private System.Windows.Forms.Label summaryLabel; private System.Windows.Forms.Label summaryLabel;
private System.Windows.Forms.GroupBox optionsGroup; private System.Windows.Forms.Label engineLabel;
private System.Windows.Forms.ComboBox engineComboBox;
private System.Windows.Forms.CheckBox createNewPlatesAsNeededBox; private System.Windows.Forms.CheckBox createNewPlatesAsNeededBox;
private System.Windows.Forms.GroupBox partFirstGroup;
private System.Windows.Forms.CheckBox partFirstCheckBox;
private System.Windows.Forms.Label sortOrderLabel;
private System.Windows.Forms.ComboBox sortOrderComboBox;
private System.Windows.Forms.Label minRemnantLabel;
private System.Windows.Forms.TextBox minRemnantBox;
private System.Windows.Forms.GroupBox plateOptimizerGroup; private System.Windows.Forms.GroupBox plateOptimizerGroup;
private System.Windows.Forms.CheckBox optimizePlateSizeBox; private System.Windows.Forms.CheckBox optimizePlateSizeBox;
private System.Windows.Forms.DataGridView plateGrid; private System.Windows.Forms.DataGridView plateGrid;
+44
View File
@@ -22,6 +22,11 @@ namespace OpenNest.Forms
LoadDefaultPlateOptions(); LoadDefaultPlateOptions();
SetPlateOptimizerVisible(false); SetPlateOptimizerVisible(false);
sortOrderComboBox.Items.Add("Bounding Box Area");
sortOrderComboBox.Items.Add("Size");
sortOrderComboBox.SelectedIndex = 0;
SetPartFirstVisible(false);
partsGrid.DataError += PartsGrid_DataError; partsGrid.DataError += PartsGrid_DataError;
} }
@@ -54,6 +59,32 @@ namespace OpenNest.Forms
set { salvageRateBox.Text = (value * 100).ToString("F0"); } set { salvageRateBox.Text = (value * 100).ToString("F0"); }
} }
public bool PartFirstMode
{
get { return partFirstCheckBox.Checked; }
set { partFirstCheckBox.Checked = value; }
}
public PartSortOrder SortOrder
{
get
{
if (sortOrderComboBox.SelectedItem is string s && s == "Size")
return PartSortOrder.Size;
return PartSortOrder.BoundingBoxArea;
}
}
public double MinRemnantSize
{
get
{
if (double.TryParse(minRemnantBox.Text, out var val) && val > 0)
return val;
return 12.0;
}
}
private void LoadEngines() private void LoadEngines()
{ {
foreach (var engine in NestEngineRegistry.AvailableEngines) foreach (var engine in NestEngineRegistry.AvailableEngines)
@@ -242,6 +273,19 @@ namespace OpenNest.Forms
salvageRatePercentLabel.Visible = visible; salvageRatePercentLabel.Visible = visible;
} }
private void partFirstCheckBox_CheckedChanged(object sender, EventArgs e)
{
SetPartFirstVisible(partFirstCheckBox.Checked);
}
private void SetPartFirstVisible(bool visible)
{
sortOrderLabel.Visible = visible;
sortOrderComboBox.Visible = visible;
minRemnantLabel.Visible = visible;
minRemnantBox.Visible = visible;
}
private void UpdateSummary() private void UpdateSummary()
{ {
var gridItems = partsGrid.DataSource as List<DataGridViewItem>; var gridItems = partsGrid.DataSource as List<DataGridViewItem>;
+120
View File
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
+9
View File
@@ -1,7 +1,9 @@
using OpenNest.Bending;
using OpenNest.CNC; using OpenNest.CNC;
using OpenNest.Converters; using OpenNest.Converters;
using OpenNest.Geometry; using OpenNest.Geometry;
using OpenNest.IO; using OpenNest.IO;
using OpenNest.IO.Bending;
using OpenNest.IO.Bom; using OpenNest.IO.Bom;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -470,12 +472,19 @@ namespace OpenNest.Forms
{ {
var result = Dxf.Import(part.DxfPath); var result = Dxf.Import(part.DxfPath);
var bends = new List<Bend>();
if (result.Document != null)
bends = BendDetectorRegistry.AutoDetect(result.Document);
Bend.UpdateEtchEntities(result.Entities, bends);
var drawingName = Path.GetFileNameWithoutExtension(part.DxfPath); var drawingName = Path.GetFileNameWithoutExtension(part.DxfPath);
var drawing = new Drawing(drawingName); var drawing = new Drawing(drawingName);
drawing.Color = Drawing.GetNextColor(); drawing.Color = Drawing.GetNextColor();
drawing.Source.Path = part.DxfPath; drawing.Source.Path = part.DxfPath;
drawing.Quantity.Required = part.Qty ?? 1; drawing.Quantity.Required = part.Qty ?? 1;
drawing.Material = new Material(material); drawing.Material = new Material(material);
if (bends.Count > 0)
drawing.Bends.AddRange(bends);
var normalized = ShapeProfile.NormalizeEntities(result.Entities); var normalized = ShapeProfile.NormalizeEntities(result.Entities);
var pgm = ConvertGeometry.ToProgram(normalized); var pgm = ConvertGeometry.ToProgram(normalized);
+114
View File
@@ -169,6 +169,7 @@ namespace OpenNest.Forms
if (item.Entities.Any(e => e.Layer != null)) if (item.Entities.Any(e => e.Layer != null))
item.Entities.ForEach(e => e.Layer.IsVisible = true); item.Entities.ForEach(e => e.Layer.IsVisible = true);
ReHidePromotedEntities(item.Bends); ReHidePromotedEntities(item.Bends);
ReHideSuppressedEntities(item);
filterPanel.LoadItem(item.Entities, item.Bends); filterPanel.LoadItem(item.Entities, item.Bends);
@@ -245,6 +246,7 @@ namespace OpenNest.Forms
filterPanel.ApplyFilters(item.Entities); filterPanel.ApplyFilters(item.Entities);
ReHidePromotedEntities(item.Bends); ReHidePromotedEntities(item.Bends);
SyncSuppressedState(item);
entityView1.Invalidate(); entityView1.Invalidate();
staleProgram = true; staleProgram = true;
} }
@@ -604,6 +606,61 @@ namespace OpenNest.Forms
#endregion #endregion
#region Load Existing Drawings
public void LoadDrawings(IEnumerable<Drawing> drawings)
{
foreach (var drawing in drawings)
{
List<Entity> entities;
if (drawing.SourceEntities != null)
{
// Use stored entities with stable GUIDs; apply suppression state
entities = new List<Entity>(drawing.SourceEntities);
foreach (var entity in entities)
entity.IsVisible = !drawing.SuppressedEntityIds.Contains(entity.Id);
}
else
{
// Fallback: derive entities from Program (older drawings without source entities)
entities = ConvertProgram.ToGeometry(drawing.Program);
// Re-apply source offset so entities appear in their natural position
if (drawing.Source?.Offset != null && drawing.Source.Offset != Vector.Zero)
{
foreach (var entity in entities)
entity.Offset(drawing.Source.Offset);
}
// Remove rapid traversals — they aren't part of the cut geometry
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
}
var bounds = entities.GetBoundingBox();
var item = new FileListItem
{
Name = drawing.Name,
Entities = entities,
Path = drawing.Source?.Path,
Quantity = drawing.Quantity.Required,
Customer = drawing.Customer ?? string.Empty,
Bends = drawing.Bends?.ToList() ?? new List<Bend>(),
SuppressedEntityIds = drawing.SuppressedEntityIds.Count > 0
? new HashSet<Guid>(drawing.SuppressedEntityIds)
: null,
Bounds = bounds,
EntityCount = entities.Count
};
fileList.AddItem(item);
}
}
#endregion
#region Output #region Output
public List<Drawing> GetDrawings() public List<Drawing> GetDrawings()
@@ -644,6 +701,22 @@ namespace OpenNest.Forms
drawing.Program = programEditor.Program; drawing.Program = programEditor.Program;
else else
drawing.Program = pgm; drawing.Program = pgm;
// Store all entities with stable GUIDs; track suppressed by ID
var bendSources = new HashSet<Entity>(
(item.Bends ?? new List<Bend>())
.Where(b => b.SourceEntity != null)
.Select(b => b.SourceEntity));
drawing.SourceEntities = item.Entities
.Where(e => !bendSources.Contains(e))
.ToList();
drawing.SuppressedEntityIds = new HashSet<Guid>(
drawing.SourceEntities
.Where(e => !(e.Layer.IsVisible && e.IsVisible))
.Select(e => e.Id));
drawings.Add(drawing); drawings.Add(drawing);
Thread.Sleep(20); Thread.Sleep(20);
@@ -666,6 +739,47 @@ namespace OpenNest.Forms
} }
} }
private static void ReHideSuppressedEntities(FileListItem item)
{
if (item.SuppressedEntityIds == null || item.SuppressedEntityIds.Count == 0)
return;
foreach (var entity in item.Entities)
{
if (item.SuppressedEntityIds.Contains(entity.Id))
entity.IsVisible = false;
}
// If all entities on a layer are suppressed, uncheck the layer too
var layerGroups = item.Entities
.Where(e => e.Layer != null)
.GroupBy(e => e.Layer);
foreach (var group in layerGroups)
{
if (group.All(e => !e.IsVisible))
group.Key.IsVisible = false;
}
}
private static void SyncSuppressedState(FileListItem item)
{
var bendSources = new HashSet<Entity>(
(item.Bends ?? new List<Bend>())
.Where(b => b.SourceEntity != null)
.Select(b => b.SourceEntity));
var suppressed = item.Entities
.Where(e => !(e.Layer.IsVisible && e.IsVisible))
.Where(e => !bendSources.Contains(e))
.Select(e => e.Id);
item.SuppressedEntityIds = new HashSet<Guid>(suppressed);
if (item.SuppressedEntityIds.Count == 0)
item.SuppressedEntityIds = null;
}
private static Color GetNextColor() => Drawing.GetNextColor(); private static Color GetNextColor() => Drawing.GetNextColor();
+68
View File
@@ -0,0 +1,68 @@
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Controls;
using System.Drawing;
using System.Windows.Forms;
namespace OpenNest.Forms
{
public class CuttingParametersDialog : Form
{
private readonly CuttingPanel cuttingPanel;
public CuttingParametersDialog()
{
Text = "Cutting Parameters";
Size = new Size(400, 560);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
StartPosition = FormStartPosition.CenterParent;
cuttingPanel = new CuttingPanel
{
Dock = DockStyle.Fill
};
var buttonPanel = new Panel
{
Dock = DockStyle.Bottom,
Height = 40
};
var btnOk = new Button
{
Text = "OK",
DialogResult = DialogResult.OK,
Size = new Size(80, 28),
Location = new Point(220, 6)
};
var btnCancel = new Button
{
Text = "Cancel",
DialogResult = DialogResult.Cancel,
Size = new Size(80, 28),
Location = new Point(305, 6)
};
buttonPanel.Controls.Add(btnOk);
buttonPanel.Controls.Add(btnCancel);
Controls.Add(cuttingPanel);
Controls.Add(buttonPanel);
AcceptButton = btnOk;
CancelButton = btnCancel;
}
public void LoadParameters(CuttingParameters parameters)
{
cuttingPanel.LoadFromParameters(parameters);
}
public CuttingParameters GetParameters()
{
return cuttingPanel.BuildParameters();
}
}
}
@@ -85,7 +85,6 @@ namespace OpenNest.Forms
{ {
LineLeadOut line => new LeadOutDto { Type = "Line", Length = line.Length, ApproachAngle = line.ApproachAngle }, LineLeadOut line => new LeadOutDto { Type = "Line", Length = line.Length, ApproachAngle = line.ApproachAngle },
ArcLeadOut arc => new LeadOutDto { Type = "Arc", Radius = arc.Radius }, ArcLeadOut arc => new LeadOutDto { Type = "Arc", Radius = arc.Radius },
MicrotabLeadOut mt => new LeadOutDto { Type = "Microtab", GapSize = mt.GapSize },
_ => new LeadOutDto { Type = "None" } _ => new LeadOutDto { Type = "None" }
}; };
} }
@@ -97,7 +96,6 @@ namespace OpenNest.Forms
{ {
"Line" => new LineLeadOut { Length = dto.Length, ApproachAngle = dto.ApproachAngle }, "Line" => new LineLeadOut { Length = dto.Length, ApproachAngle = dto.ApproachAngle },
"Arc" => new ArcLeadOut { Radius = dto.Radius }, "Arc" => new ArcLeadOut { Radius = dto.Radius },
"Microtab" => new MicrotabLeadOut { GapSize = dto.GapSize },
_ => new NoLeadOut() _ => new NoLeadOut()
}; };
} }
+21 -2
View File
@@ -47,6 +47,8 @@
drawingListBox1 = new OpenNest.Controls.DrawingListBox(); drawingListBox1 = new OpenNest.Controls.DrawingListBox();
toolStrip2 = new System.Windows.Forms.ToolStrip(); toolStrip2 = new System.Windows.Forms.ToolStrip();
toolStripButton2 = new System.Windows.Forms.ToolStripButton(); toolStripButton2 = new System.Windows.Forms.ToolStripButton();
toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator();
editDrawingsButton = new System.Windows.Forms.ToolStripButton();
toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator(); toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
toolStripButton3 = new System.Windows.Forms.ToolStripButton(); toolStripButton3 = new System.Windows.Forms.ToolStripButton();
toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator(); toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
@@ -175,7 +177,7 @@
// toolStripLabel2 // toolStripLabel2
// //
toolStripLabel2.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; toolStripLabel2.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
toolStripLabel2.Image = Properties.Resources.delete; toolStripLabel2.Image = (System.Drawing.Image)resources.GetObject("toolStripLabel2.Image");
toolStripLabel2.Name = "toolStripLabel2"; toolStripLabel2.Name = "toolStripLabel2";
toolStripLabel2.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0); toolStripLabel2.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
toolStripLabel2.Size = new System.Drawing.Size(34, 24); toolStripLabel2.Size = new System.Drawing.Size(34, 24);
@@ -217,7 +219,7 @@
// //
toolStrip2.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden; toolStrip2.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
toolStrip2.ImageScalingSize = new System.Drawing.Size(20, 20); toolStrip2.ImageScalingSize = new System.Drawing.Size(20, 20);
toolStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { toolStripButton2, toolStripSeparator1, toolStripButton3, toolStripSeparator2, hideNestedButton }); toolStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { toolStripButton2, toolStripSeparator4, editDrawingsButton, toolStripSeparator1, toolStripButton3, toolStripSeparator2, hideNestedButton });
toolStrip2.Location = new System.Drawing.Point(4, 3); toolStrip2.Location = new System.Drawing.Point(4, 3);
toolStrip2.Name = "toolStrip2"; toolStrip2.Name = "toolStrip2";
toolStrip2.Size = new System.Drawing.Size(265, 27); toolStrip2.Size = new System.Drawing.Size(265, 27);
@@ -236,6 +238,21 @@
toolStripButton2.Text = "Import Drawings"; toolStripButton2.Text = "Import Drawings";
toolStripButton2.Click += ImportDrawings_Click; toolStripButton2.Click += ImportDrawings_Click;
// //
// toolStripSeparator4
//
toolStripSeparator4.Name = "toolStripSeparator4";
toolStripSeparator4.Size = new System.Drawing.Size(6, 27);
//
// editDrawingsButton
//
editDrawingsButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
editDrawingsButton.Image = (System.Drawing.Image)resources.GetObject("editDrawingsButton.Image");
editDrawingsButton.Name = "editDrawingsButton";
editDrawingsButton.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
editDrawingsButton.Size = new System.Drawing.Size(34, 24);
editDrawingsButton.Text = "Edit Drawings in Converter";
editDrawingsButton.Click += EditDrawingsInConverter_Click;
//
// toolStripSeparator1 // toolStripSeparator1
// //
toolStripSeparator1.Name = "toolStripSeparator1"; toolStripSeparator1.Name = "toolStripSeparator1";
@@ -312,6 +329,8 @@
private System.Windows.Forms.ColumnHeader utilColumn; private System.Windows.Forms.ColumnHeader utilColumn;
private System.Windows.Forms.ToolStrip toolStrip2; private System.Windows.Forms.ToolStrip toolStrip2;
private System.Windows.Forms.ToolStripButton toolStripButton2; private System.Windows.Forms.ToolStripButton toolStripButton2;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator4;
private System.Windows.Forms.ToolStripButton editDrawingsButton;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1; private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripButton toolStripButton3; private System.Windows.Forms.ToolStripButton toolStripButton3;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2; private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
+101 -37
View File
@@ -52,6 +52,7 @@ namespace OpenNest.Forms
private EditNestForm() private EditNestForm()
{ {
PlateView = new PlateView(); PlateView = new PlateView();
PlateView.MouseEnter += PlateView_MouseEnter;
PlateView.Enter += PlateView_Enter; PlateView.Enter += PlateView_Enter;
PlateView.PartAdded += PlateView_PartAdded; PlateView.PartAdded += PlateView_PartAdded;
PlateView.PartRemoved += PlateView_PartRemoved; PlateView.PartRemoved += PlateView_PartRemoved;
@@ -718,19 +719,17 @@ namespace OpenNest.Forms
var plate = PlateView.Plate; var plate = PlateView.Plate;
if (plate.CuttingParameters == null) var parameters = LoadOrDefaultParameters(plate.CuttingParameters);
{
var json = Properties.Settings.Default.CuttingParametersJson; using var dlg = new CuttingParametersDialog();
if (!string.IsNullOrEmpty(json)) dlg.LoadParameters(parameters);
{
try { plate.CuttingParameters = CuttingParametersSerializer.Deserialize(json); } if (dlg.ShowDialog() != DialogResult.OK)
catch { plate.CuttingParameters = new CuttingParameters(); } return;
}
else parameters = dlg.GetParameters();
{ plate.CuttingParameters = parameters;
plate.CuttingParameters = new CuttingParameters(); SaveCuttingParameters(parameters);
}
}
var assigner = new LeadInAssigner var assigner = new LeadInAssigner
{ {
@@ -781,17 +780,16 @@ namespace OpenNest.Forms
if (Nest == null) if (Nest == null)
return; return;
CuttingParameters parameters; var parameters = LoadOrDefaultParameters(PlateView?.Plate?.CuttingParameters);
var json = Properties.Settings.Default.CuttingParametersJson;
if (!string.IsNullOrEmpty(json)) using var dlg = new CuttingParametersDialog();
{ dlg.LoadParameters(parameters);
try { parameters = CuttingParametersSerializer.Deserialize(json); }
catch { parameters = new CuttingParameters(); } if (dlg.ShowDialog() != DialogResult.OK)
} return;
else
{ parameters = dlg.GetParameters();
parameters = new CuttingParameters(); SaveCuttingParameters(parameters);
}
var assigner = new LeadInAssigner var assigner = new LeadInAssigner
{ {
@@ -839,29 +837,88 @@ namespace OpenNest.Forms
var plate = PlateView.Plate; var plate = PlateView.Plate;
// If no cutting parameters exist, initialize from saved settings or defaults
if (plate.CuttingParameters == null) if (plate.CuttingParameters == null)
{ plate.CuttingParameters = LoadOrDefaultParameters(null);
var json = Properties.Settings.Default.CuttingParametersJson;
if (!string.IsNullOrEmpty(json))
{
try { plate.CuttingParameters = CuttingParametersSerializer.Deserialize(json); }
catch { plate.CuttingParameters = new CuttingParameters(); }
}
else
{
plate.CuttingParameters = new CuttingParameters();
}
}
PlateView.SetAction(typeof(Actions.ActionLeadIn)); PlateView.SetAction(typeof(Actions.ActionLeadIn));
} }
private static CuttingParameters LoadOrDefaultParameters(CuttingParameters existing)
{
if (existing != null)
return existing;
var json = Properties.Settings.Default.CuttingParametersJson;
if (!string.IsNullOrEmpty(json))
{
try { return CuttingParametersSerializer.Deserialize(json); }
catch { /* fall through */ }
}
return new CuttingParameters();
}
private static void SaveCuttingParameters(CuttingParameters parameters)
{
var json = CuttingParametersSerializer.Serialize(parameters);
Properties.Settings.Default.CuttingParametersJson = json;
Properties.Settings.Default.Save();
}
private void ImportDrawings_Click(object sender, EventArgs e) private void ImportDrawings_Click(object sender, EventArgs e)
{ {
Import(); Import();
} }
private void EditDrawingsInConverter_Click(object sender, EventArgs e)
{
if (Nest.Drawings.Count == 0)
return;
var converter = new CadConverterForm();
converter.LoadDrawings(Nest.Drawings);
if (converter.ShowDialog() != DialogResult.OK)
return;
var newDrawings = converter.GetDrawings();
var newByName = newDrawings.ToDictionary(d => d.Name);
// Update existing drawings in-place so parts keep their BaseDrawing references
foreach (var existing in Nest.Drawings.ToList())
{
if (newByName.TryGetValue(existing.Name, out var updated))
{
existing.Program = updated.Program;
existing.SourceEntities = updated.SourceEntities;
existing.SuppressedEntityIds = updated.SuppressedEntityIds;
existing.Source = updated.Source;
existing.Customer = updated.Customer;
existing.Quantity.Required = updated.Quantity.Required;
existing.Bends.Clear();
existing.Bends.AddRange(updated.Bends);
newByName.Remove(existing.Name);
}
else
{
Nest.Drawings.Remove(existing);
}
}
// Add any new drawings that weren't in the original set
foreach (var d in newByName.Values)
Nest.Drawings.Add(d);
// Refresh all parts to use the updated programs
foreach (var plate in Nest.Plates)
foreach (var part in plate.Parts)
if (!part.BaseDrawing.IsCutOff)
part.Update();
UpdateDrawingList();
PlateView.Invalidate();
}
private void CleanUnusedDrawings_Click(object sender, EventArgs e) private void CleanUnusedDrawings_Click(object sender, EventArgs e)
{ {
var result = MessageBox.Show( var result = MessageBox.Show(
@@ -892,6 +949,7 @@ namespace OpenNest.Forms
PlateView.Plate = PlateManager.CurrentPlate; PlateView.Plate = PlateManager.CurrentPlate;
PlateView.ZoomToFit(); PlateView.ZoomToFit();
UpdatePlateHeader(); UpdatePlateHeader();
UpdateRemovePlateButton();
PlateChanged?.Invoke(this, EventArgs.Empty); PlateChanged?.Invoke(this, EventArgs.Empty);
} }
@@ -1025,6 +1083,12 @@ namespace OpenNest.Forms
addPart = true; addPart = true;
} }
private void PlateView_MouseEnter(object sender, EventArgs e)
{
if (!PlateView.Focused)
PlateView.Focus();
}
private void PlateView_Enter(object sender, EventArgs e) private void PlateView_Enter(object sender, EventArgs e)
{ {
if (!addPart) if (!addPart)
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -85,6 +85,7 @@
mnuNest = new System.Windows.Forms.ToolStripMenuItem(); mnuNest = new System.Windows.Forms.ToolStripMenuItem();
mnuNestEdit = new System.Windows.Forms.ToolStripMenuItem(); mnuNestEdit = new System.Windows.Forms.ToolStripMenuItem();
mnuNestImportDrawing = new System.Windows.Forms.ToolStripMenuItem(); mnuNestImportDrawing = new System.Windows.Forms.ToolStripMenuItem();
mnuNestShapeLibrary = new System.Windows.Forms.ToolStripMenuItem();
toolStripMenuItem7 = new System.Windows.Forms.ToolStripSeparator(); toolStripMenuItem7 = new System.Windows.Forms.ToolStripSeparator();
mnuNestFirstPlate = new System.Windows.Forms.ToolStripMenuItem(); mnuNestFirstPlate = new System.Windows.Forms.ToolStripMenuItem();
mnuNestLastPlate = new System.Windows.Forms.ToolStripMenuItem(); mnuNestLastPlate = new System.Windows.Forms.ToolStripMenuItem();
@@ -559,7 +560,7 @@
// //
// mnuNest // mnuNest
// //
mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestImportDrawing, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAssignLeadIns, mnuNestRemoveLeadIns }); mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestImportDrawing, mnuNestShapeLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAssignLeadIns, mnuNestRemoveLeadIns });
mnuNest.Name = "mnuNest"; mnuNest.Name = "mnuNest";
mnuNest.Size = new System.Drawing.Size(43, 20); mnuNest.Size = new System.Drawing.Size(43, 20);
mnuNest.Text = "&Nest"; mnuNest.Text = "&Nest";
@@ -579,6 +580,13 @@
mnuNestImportDrawing.Text = "Import Drawing"; mnuNestImportDrawing.Text = "Import Drawing";
mnuNestImportDrawing.Click += Import_Click; mnuNestImportDrawing.Click += Import_Click;
// //
// mnuNestShapeLibrary
//
mnuNestShapeLibrary.Name = "mnuNestShapeLibrary";
mnuNestShapeLibrary.Size = new System.Drawing.Size(205, 22);
mnuNestShapeLibrary.Text = "Shape Library";
mnuNestShapeLibrary.Click += ShapeLibrary_Click;
//
// toolStripMenuItem7 // toolStripMenuItem7
// //
toolStripMenuItem7.Name = "toolStripMenuItem7"; toolStripMenuItem7.Name = "toolStripMenuItem7";
@@ -1213,6 +1221,7 @@
private System.Windows.Forms.ToolStripMenuItem mnuNest; private System.Windows.Forms.ToolStripMenuItem mnuNest;
private System.Windows.Forms.ToolStripMenuItem mnuNestEdit; private System.Windows.Forms.ToolStripMenuItem mnuNestEdit;
private System.Windows.Forms.ToolStripMenuItem mnuNestImportDrawing; private System.Windows.Forms.ToolStripMenuItem mnuNestImportDrawing;
private System.Windows.Forms.ToolStripMenuItem mnuNestShapeLibrary;
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem7; private System.Windows.Forms.ToolStripSeparator toolStripMenuItem7;
private System.Windows.Forms.ToolStripMenuItem mnuNestFirstPlate; private System.Windows.Forms.ToolStripMenuItem mnuNestFirstPlate;
private System.Windows.Forms.ToolStripMenuItem mnuNestLastPlate; private System.Windows.Forms.ToolStripMenuItem mnuNestLastPlate;
+64 -2
View File
@@ -829,6 +829,20 @@ namespace OpenNest.Forms
activeForm.Import(); activeForm.Import();
} }
private void ShapeLibrary_Click(object sender, EventArgs e)
{
if (activeForm == null) return;
var form = new ShapeLibraryForm();
form.ShowDialog();
var drawings = form.GetDrawings();
if (drawings.Count == 0) return;
drawings.ForEach(d => activeForm.Nest.Drawings.Add(d));
activeForm.UpdateDrawingList();
}
private void EditNest_Click(object sender, EventArgs e) private void EditNest_Click(object sender, EventArgs e)
{ {
if (activeForm == null) return; if (activeForm == null) return;
@@ -932,6 +946,10 @@ namespace OpenNest.Forms
var optimizePlateSize = form.OptimizePlateSize; var optimizePlateSize = form.OptimizePlateSize;
var plateOptions = optimizePlateSize ? form.GetPlateOptions() : null; var plateOptions = optimizePlateSize ? form.GetPlateOptions() : null;
var salvageRate = form.SalvageRate; var salvageRate = form.SalvageRate;
var partFirstMode = form.PartFirstMode;
var sortOrder = form.SortOrder;
var minRemnantSize = form.MinRemnantSize;
var allowPlateCreation = form.AllowPlateCreation;
if (optimizePlateSize) if (optimizePlateSize)
{ {
@@ -960,7 +978,7 @@ namespace OpenNest.Forms
try try
{ {
await RunAutoNestAsync(items, progressForm, progress, nestingCts.Token, await RunAutoNestAsync(items, progressForm, progress, nestingCts.Token,
plateOptions, salvageRate); plateOptions, salvageRate, partFirstMode, sortOrder, minRemnantSize, allowPlateCreation);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -984,8 +1002,52 @@ namespace OpenNest.Forms
IProgress<NestProgress> progress, IProgress<NestProgress> progress,
CancellationToken token, CancellationToken token,
List<PlateOption> plateOptions = null, List<PlateOption> plateOptions = null,
double salvageRate = 0.5) double salvageRate = 0.5,
bool partFirstMode = false,
PartSortOrder sortOrder = PartSortOrder.BoundingBoxArea,
double minRemnantSize = 12.0,
bool allowPlateCreation = true)
{ {
if (partFirstMode)
{
var existingPlates = new List<Plate>();
for (var i = 0; i < activeForm.Nest.Plates.Count; i++)
{
var p = activeForm.Nest.Plates[i];
if (p.Parts.Count > 0)
existingPlates.Add(p);
}
var template = activeForm.PlateView.Plate;
var nestOptions = new MultiPlateNestOptions
{
Template = template,
PlateOptions = plateOptions,
SalvageRate = salvageRate,
SortOrder = sortOrder,
MinRemnantSize = minRemnantSize,
AllowPlateCreation = allowPlateCreation,
};
var result = await Task.Run(() =>
MultiPlateNester.Nest(items, nestOptions, existingPlates, progress, token));
foreach (var pr in result.Plates)
{
if (pr.IsNew)
{
var plate = GetOrCreatePlate(progressForm);
plate.Size = pr.Plate.Size;
plate.Parts.AddRange(pr.Parts);
}
}
activeForm.Nest.UpdateDrawingQuantities();
progressForm.ShowCompleted();
return;
}
const int maxPlates = 100; const int maxPlates = 100;
for (var plateIndex = 0; plateIndex < maxPlates; plateIndex++) for (var plateIndex = 0; plateIndex < maxPlates; plateIndex++)
+1 -1
View File
@@ -427,7 +427,7 @@ namespace OpenNest.Forms
plate1.Quantity = 0; plate1.Quantity = 0;
previewPlateView.Plate = plate1; previewPlateView.Plate = plate1;
previewPlateView.RotateIncrementAngle = 10D; previewPlateView.RotateIncrementAngle = 10D;
previewPlateView.SelectedCutOff = null;
previewPlateView.ShowBendLines = false; previewPlateView.ShowBendLines = false;
previewPlateView.Size = new System.Drawing.Size(356, 341); previewPlateView.Size = new System.Drawing.Size(356, 341);
previewPlateView.Status = "Select"; previewPlateView.Status = "Select";
+338
View File
@@ -0,0 +1,338 @@
namespace OpenNest.Forms
{
partial class ShapeLibraryForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
private void InitializeComponent()
{
ColorScheme colorScheme1 = new ColorScheme();
CutOffSettings cutOffSettings1 = new CutOffSettings();
Plate plate1 = new Plate();
Collections.ObservableList<CutOff> observableList_11 = new Collections.ObservableList<CutOff>();
Collections.ObservableList<Part> observableList_12 = new Collections.ObservableList<Part>();
splitContainer = new System.Windows.Forms.SplitContainer();
shapeListBox = new System.Windows.Forms.ListBox();
layoutTable = new System.Windows.Forms.TableLayoutPanel();
fieldsTable = new System.Windows.Forms.TableLayoutPanel();
nameLabel = new System.Windows.Forms.Label();
nameTextBox = new System.Windows.Forms.TextBox();
qtyLabel = new System.Windows.Forms.Label();
quantityUpDown = new OpenNest.Controls.NumericUpDown();
configLabel = new System.Windows.Forms.Label();
configComboBox = new System.Windows.Forms.ComboBox();
contentPanel = new System.Windows.Forms.Panel();
previewBox = new OpenNest.Controls.ShapePreviewControl();
parametersPanel = new System.Windows.Forms.Panel();
buttonPanel = new System.Windows.Forms.Panel();
addButton = new System.Windows.Forms.Button();
closeButton = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit();
splitContainer.Panel1.SuspendLayout();
splitContainer.Panel2.SuspendLayout();
splitContainer.SuspendLayout();
layoutTable.SuspendLayout();
fieldsTable.SuspendLayout();
((System.ComponentModel.ISupportInitialize)quantityUpDown).BeginInit();
contentPanel.SuspendLayout();
buttonPanel.SuspendLayout();
SuspendLayout();
//
// splitContainer
//
splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
splitContainer.FixedPanel = System.Windows.Forms.FixedPanel.Panel1;
splitContainer.Location = new System.Drawing.Point(0, 0);
splitContainer.Name = "splitContainer";
//
// splitContainer.Panel1
//
splitContainer.Panel1.Controls.Add(shapeListBox);
//
// splitContainer.Panel2
//
splitContainer.Panel2.Controls.Add(layoutTable);
splitContainer.Size = new System.Drawing.Size(750, 520);
splitContainer.SplitterDistance = 150;
splitContainer.TabIndex = 0;
//
// shapeListBox
//
shapeListBox.BorderStyle = System.Windows.Forms.BorderStyle.None;
shapeListBox.Dock = System.Windows.Forms.DockStyle.Fill;
shapeListBox.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawFixed;
shapeListBox.Font = new System.Drawing.Font("Segoe UI", 10F);
shapeListBox.IntegralHeight = false;
shapeListBox.ItemHeight = 32;
shapeListBox.Location = new System.Drawing.Point(0, 0);
shapeListBox.Name = "shapeListBox";
shapeListBox.Size = new System.Drawing.Size(150, 520);
shapeListBox.TabIndex = 0;
//
// layoutTable
//
layoutTable.ColumnCount = 1;
layoutTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
layoutTable.Controls.Add(fieldsTable, 0, 0);
layoutTable.Controls.Add(contentPanel, 0, 1);
layoutTable.Controls.Add(buttonPanel, 0, 2);
layoutTable.Dock = System.Windows.Forms.DockStyle.Fill;
layoutTable.Location = new System.Drawing.Point(0, 0);
layoutTable.Name = "layoutTable";
layoutTable.Padding = new System.Windows.Forms.Padding(6, 4, 6, 0);
layoutTable.RowCount = 3;
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 44F));
layoutTable.Size = new System.Drawing.Size(596, 520);
layoutTable.TabIndex = 0;
//
// fieldsTable
//
fieldsTable.AutoSize = true;
fieldsTable.ColumnCount = 2;
fieldsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
fieldsTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
fieldsTable.Controls.Add(nameLabel, 0, 0);
fieldsTable.Controls.Add(nameTextBox, 1, 0);
fieldsTable.Controls.Add(qtyLabel, 0, 1);
fieldsTable.Controls.Add(quantityUpDown, 1, 1);
fieldsTable.Controls.Add(configLabel, 0, 2);
fieldsTable.Controls.Add(configComboBox, 1, 2);
fieldsTable.Dock = System.Windows.Forms.DockStyle.Fill;
fieldsTable.Location = new System.Drawing.Point(6, 4);
fieldsTable.Margin = new System.Windows.Forms.Padding(0, 0, 0, 4);
fieldsTable.Name = "fieldsTable";
fieldsTable.RowCount = 3;
fieldsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
fieldsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
fieldsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
fieldsTable.Size = new System.Drawing.Size(584, 99);
fieldsTable.TabIndex = 0;
//
// nameLabel
//
nameLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
nameLabel.AutoSize = true;
nameLabel.Location = new System.Drawing.Point(4, 8);
nameLabel.Margin = new System.Windows.Forms.Padding(4, 4, 8, 4);
nameLabel.Name = "nameLabel";
nameLabel.Size = new System.Drawing.Size(46, 17);
nameLabel.TabIndex = 0;
nameLabel.Text = "Name:";
//
// nameTextBox
//
nameTextBox.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
nameTextBox.Location = new System.Drawing.Point(106, 4);
nameTextBox.Margin = new System.Windows.Forms.Padding(4);
nameTextBox.Name = "nameTextBox";
nameTextBox.Size = new System.Drawing.Size(474, 25);
nameTextBox.TabIndex = 1;
//
// qtyLabel
//
qtyLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
qtyLabel.AutoSize = true;
qtyLabel.Location = new System.Drawing.Point(4, 41);
qtyLabel.Margin = new System.Windows.Forms.Padding(4, 4, 8, 4);
qtyLabel.Name = "qtyLabel";
qtyLabel.Size = new System.Drawing.Size(59, 17);
qtyLabel.TabIndex = 2;
qtyLabel.Text = "Quantity:";
//
// quantityUpDown
//
quantityUpDown.Location = new System.Drawing.Point(106, 37);
quantityUpDown.Margin = new System.Windows.Forms.Padding(4);
quantityUpDown.Maximum = new decimal(new int[] { 999999, 0, 0, 0 });
quantityUpDown.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
quantityUpDown.Name = "quantityUpDown";
quantityUpDown.Size = new System.Drawing.Size(100, 25);
quantityUpDown.Suffix = "";
quantityUpDown.TabIndex = 2;
quantityUpDown.Value = new decimal(new int[] { 1, 0, 0, 0 });
//
// configLabel
//
configLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
configLabel.AutoSize = true;
configLabel.Location = new System.Drawing.Point(4, 74);
configLabel.Margin = new System.Windows.Forms.Padding(4, 4, 8, 4);
configLabel.Name = "configLabel";
configLabel.Size = new System.Drawing.Size(90, 17);
configLabel.TabIndex = 3;
configLabel.Text = "Configuration:";
configLabel.Visible = false;
//
// configComboBox
//
configComboBox.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
configComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
configComboBox.Location = new System.Drawing.Point(106, 70);
configComboBox.Margin = new System.Windows.Forms.Padding(4);
configComboBox.Name = "configComboBox";
configComboBox.Size = new System.Drawing.Size(474, 25);
configComboBox.TabIndex = 3;
configComboBox.Visible = false;
//
// contentPanel
//
contentPanel.Controls.Add(previewBox);
contentPanel.Controls.Add(parametersPanel);
contentPanel.Dock = System.Windows.Forms.DockStyle.Fill;
contentPanel.Location = new System.Drawing.Point(9, 110);
contentPanel.Name = "contentPanel";
contentPanel.Size = new System.Drawing.Size(578, 363);
contentPanel.TabIndex = 1;
//
// previewBox
//
previewBox.ActiveWorkArea = null;
previewBox.AllowPan = false;
previewBox.AllowSelect = false;
previewBox.AllowZoom = false;
previewBox.BackColor = System.Drawing.Color.White;
colorScheme1.BackgroundColor = System.Drawing.Color.DarkGray;
colorScheme1.BoundingBoxColor = System.Drawing.Color.FromArgb(128, 128, 255);
colorScheme1.EdgeSpacingColor = System.Drawing.Color.FromArgb(180, 180, 180);
colorScheme1.LayoutFillColor = System.Drawing.Color.WhiteSmoke;
colorScheme1.LayoutOutlineColor = System.Drawing.Color.Gray;
colorScheme1.OriginColor = System.Drawing.Color.Gray;
colorScheme1.PreviewPartColor = System.Drawing.Color.FromArgb(255, 140, 0);
colorScheme1.RapidColor = System.Drawing.Color.DodgerBlue;
previewBox.ColorScheme = colorScheme1;
cutOffSettings1.CutDirection = CutDirection.AwayFromOrigin;
cutOffSettings1.MinSegmentLength = 0.05D;
cutOffSettings1.Overtravel = 0D;
cutOffSettings1.PartClearance = 0.02D;
previewBox.CutOffSettings = cutOffSettings1;
previewBox.DebugRemnantPriorities = null;
previewBox.DebugRemnants = null;
previewBox.Dock = System.Windows.Forms.DockStyle.Fill;
previewBox.DrawBounds = false;
previewBox.DrawCutDirection = false;
previewBox.DrawOffset = false;
previewBox.DrawOrigin = false;
previewBox.DrawPiercePoints = false;
previewBox.DrawRapid = false;
previewBox.FillParts = true;
previewBox.Location = new System.Drawing.Point(0, 0);
previewBox.Name = "previewBox";
previewBox.OffsetIncrementDistance = 10D;
previewBox.OffsetTolerance = 0.001D;
plate1.CutOffs = observableList_11;
plate1.CuttingParameters = null;
plate1.GrainAngle = 0D;
plate1.Parts = observableList_12;
plate1.PartSpacing = 0D;
plate1.Quadrant = 1;
plate1.Quantity = 0;
previewBox.Plate = plate1;
previewBox.RotateIncrementAngle = 10D;
previewBox.ShowBendLines = false;
previewBox.Size = new System.Drawing.Size(318, 363);
previewBox.Status = "Select";
previewBox.TabIndex = 4;
previewBox.TabStop = false;
//
// parametersPanel
//
parametersPanel.AutoScroll = true;
parametersPanel.Dock = System.Windows.Forms.DockStyle.Right;
parametersPanel.Location = new System.Drawing.Point(318, 0);
parametersPanel.Name = "parametersPanel";
parametersPanel.Padding = new System.Windows.Forms.Padding(8, 0, 0, 0);
parametersPanel.Size = new System.Drawing.Size(260, 363);
parametersPanel.TabIndex = 5;
//
// buttonPanel
//
buttonPanel.Controls.Add(addButton);
buttonPanel.Controls.Add(closeButton);
buttonPanel.Dock = System.Windows.Forms.DockStyle.Fill;
buttonPanel.Location = new System.Drawing.Point(9, 479);
buttonPanel.Name = "buttonPanel";
buttonPanel.Size = new System.Drawing.Size(578, 38);
buttonPanel.TabIndex = 2;
//
// addButton
//
addButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
addButton.Location = new System.Drawing.Point(379, 5);
addButton.Name = "addButton";
addButton.Size = new System.Drawing.Size(100, 30);
addButton.TabIndex = 0;
addButton.Text = "Add to Nest";
addButton.UseVisualStyleBackColor = true;
//
// closeButton
//
closeButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
closeButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
closeButton.Location = new System.Drawing.Point(485, 5);
closeButton.Name = "closeButton";
closeButton.Size = new System.Drawing.Size(90, 30);
closeButton.TabIndex = 1;
closeButton.Text = "Close";
closeButton.UseVisualStyleBackColor = true;
//
// ShapeLibraryForm
//
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
CancelButton = closeButton;
ClientSize = new System.Drawing.Size(750, 520);
Controls.Add(splitContainer);
Font = new System.Drawing.Font("Segoe UI", 9.75F);
MinimizeBox = false;
MinimumSize = new System.Drawing.Size(600, 400);
Name = "ShapeLibraryForm";
ShowIcon = false;
ShowInTaskbar = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "Shape Library";
splitContainer.Panel1.ResumeLayout(false);
splitContainer.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer).EndInit();
splitContainer.ResumeLayout(false);
layoutTable.ResumeLayout(false);
layoutTable.PerformLayout();
fieldsTable.ResumeLayout(false);
fieldsTable.PerformLayout();
((System.ComponentModel.ISupportInitialize)quantityUpDown).EndInit();
contentPanel.ResumeLayout(false);
buttonPanel.ResumeLayout(false);
ResumeLayout(false);
}
#endregion
private System.Windows.Forms.SplitContainer splitContainer;
private System.Windows.Forms.ListBox shapeListBox;
private System.Windows.Forms.TableLayoutPanel layoutTable;
private System.Windows.Forms.TableLayoutPanel fieldsTable;
private System.Windows.Forms.Label nameLabel;
private System.Windows.Forms.TextBox nameTextBox;
private System.Windows.Forms.Label qtyLabel;
private Controls.NumericUpDown quantityUpDown;
private System.Windows.Forms.Label configLabel;
private System.Windows.Forms.ComboBox configComboBox;
private System.Windows.Forms.Panel contentPanel;
private Controls.ShapePreviewControl previewBox;
private System.Windows.Forms.Panel parametersPanel;
private System.Windows.Forms.Panel buttonPanel;
private System.Windows.Forms.Button addButton;
private System.Windows.Forms.Button closeButton;
}
}
+322
View File
@@ -0,0 +1,322 @@
using OpenNest.Shapes;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Windows.Forms;
namespace OpenNest.Forms
{
public partial class ShapeLibraryForm : Form
{
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
private readonly List<Drawing> addedDrawings = new List<Drawing>();
private readonly List<ShapeEntry> shapeEntries = new List<ShapeEntry>();
private readonly List<ParameterBinding> parameterBindings = new List<ParameterBinding>();
private ShapeEntry selectedEntry;
private bool suppressPreview;
public ShapeLibraryForm()
{
InitializeComponent();
DiscoverShapes();
PopulateShapeList();
shapeListBox.DrawItem += ShapeListBox_DrawItem;
shapeListBox.SelectedIndexChanged += ShapeListBox_SelectedIndexChanged;
configComboBox.SelectedIndexChanged += ConfigComboBox_SelectedIndexChanged;
addButton.Click += AddButton_Click;
closeButton.Click += (s, e) => Close();
if (shapeListBox.Items.Count > 0)
shapeListBox.SelectedIndex = 0;
}
public List<Drawing> GetDrawings() => addedDrawings;
private void DiscoverShapes()
{
var baseType = typeof(ShapeDefinition);
var shapeTypes = baseType.Assembly.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom(t))
.OrderBy(t => t.Name)
.ToList();
var configDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configurations");
foreach (var type in shapeTypes)
{
var entry = new ShapeEntry { ShapeType = type };
entry.DisplayName = FriendlyName(type.Name);
var configPath = Path.Combine(configDir, type.Name + ".json");
if (File.Exists(configPath))
entry.Configurations = LoadConfigurations(type, configPath);
shapeEntries.Add(entry);
}
}
private List<ShapeDefinition> LoadConfigurations(Type shapeType, string path)
{
try
{
var json = File.ReadAllText(path);
var listType = typeof(List<>).MakeGenericType(shapeType);
var list = JsonSerializer.Deserialize(json, listType, JsonOptions);
return ((System.Collections.IEnumerable)list).Cast<ShapeDefinition>().ToList();
}
catch
{
return null;
}
}
private void PopulateShapeList()
{
foreach (var entry in shapeEntries)
shapeListBox.Items.Add(entry);
}
private void ShapeListBox_DrawItem(object sender, DrawItemEventArgs e)
{
if (e.Index < 0) return;
e.DrawBackground();
var entry = (ShapeEntry)shapeListBox.Items[e.Index];
var textColor = (e.State & DrawItemState.Selected) != 0
? SystemColors.HighlightText
: SystemColors.ControlText;
var text = entry.DisplayName;
if (entry.HasConfigurations)
text += $" ({entry.Configurations.Count})";
using (var brush = new SolidBrush(textColor))
{
var format = new StringFormat { LineAlignment = StringAlignment.Center };
var rect = new RectangleF(8, e.Bounds.Y, e.Bounds.Width - 8, e.Bounds.Height);
e.Graphics.DrawString(text, e.Font, brush, rect, format);
}
e.DrawFocusRectangle();
}
private void ShapeListBox_SelectedIndexChanged(object sender, EventArgs e)
{
if (shapeListBox.SelectedIndex < 0) return;
selectedEntry = (ShapeEntry)shapeListBox.SelectedItem;
suppressPreview = true;
var hasConfigs = selectedEntry.HasConfigurations;
configLabel.Visible = hasConfigs;
configComboBox.Visible = hasConfigs;
if (hasConfigs)
{
configComboBox.Items.Clear();
foreach (var cfg in selectedEntry.Configurations)
configComboBox.Items.Add(cfg.Name);
configComboBox.SelectedIndex = 0;
}
else
{
nameTextBox.Text = selectedEntry.DisplayName;
var defaults = (ShapeDefinition)Activator.CreateInstance(selectedEntry.ShapeType);
defaults.SetPreviewDefaults();
BuildParameterControls(selectedEntry.ShapeType, defaults);
}
suppressPreview = false;
UpdatePreview();
}
private void ConfigComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
if (configComboBox.SelectedIndex < 0 || selectedEntry == null) return;
var config = selectedEntry.Configurations[configComboBox.SelectedIndex];
nameTextBox.Text = config.Name;
suppressPreview = true;
BuildParameterControls(selectedEntry.ShapeType, config);
suppressPreview = false;
UpdatePreview();
}
private void BuildParameterControls(Type shapeType, ShapeDefinition sourceValues)
{
parametersPanel.SuspendLayout();
parametersPanel.Controls.Clear();
parameterBindings.Clear();
var props = shapeType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
.Where(p => p.CanRead && p.CanWrite && p.Name != "Name")
.ToArray();
var panelWidth = parametersPanel.ClientSize.Width - parametersPanel.Padding.Horizontal;
var y = 4;
foreach (var prop in props)
{
var label = new Label
{
Text = FriendlyName(prop.Name),
Location = new Point(parametersPanel.Padding.Left, y),
AutoSize = true
};
y += 18;
var tb = new TextBox
{
Location = new Point(parametersPanel.Padding.Left, y),
Width = panelWidth,
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
};
if (sourceValues != null)
{
if (prop.PropertyType == typeof(int))
tb.Text = ((int)prop.GetValue(sourceValues)).ToString();
else
tb.Text = ((double)prop.GetValue(sourceValues)).ToString("G");
}
tb.TextChanged += (s, ev) => UpdatePreview();
parameterBindings.Add(new ParameterBinding { Property = prop, Control = tb });
parametersPanel.Controls.Add(label);
parametersPanel.Controls.Add(tb);
y += 30;
}
parametersPanel.ResumeLayout(true);
}
private void UpdatePreview()
{
if (suppressPreview || selectedEntry == null) return;
try
{
var shape = CreateShapeFromInputs();
if (shape == null) return;
var drawing = shape.GetDrawing();
previewBox.ShowDrawing(drawing);
if (drawing?.Program != null)
{
var bb = drawing.Program.BoundingBox();
previewBox.SetInfo(
nameTextBox.Text,
string.Format("{0:F3} x {1:F3}", bb.Size.Length, bb.Size.Width));
}
}
catch
{
previewBox.ShowDrawing(null);
}
}
private ShapeDefinition CreateShapeFromInputs()
{
var shape = (ShapeDefinition)Activator.CreateInstance(selectedEntry.ShapeType);
shape.Name = nameTextBox.Text;
foreach (var binding in parameterBindings)
{
var tb = (TextBox)binding.Control;
if (binding.Property.PropertyType == typeof(int))
{
if (int.TryParse(tb.Text, out var intVal))
{
binding.Property.SetValue(shape, intVal);
tb.ForeColor = SystemColors.WindowText;
}
else
{
tb.ForeColor = Color.Red;
return null;
}
}
else
{
var val = ArchUnits.GetLengthInches(tb);
if (double.IsNaN(val))
return null;
binding.Property.SetValue(shape, val);
}
}
return shape;
}
private void AddButton_Click(object sender, EventArgs e)
{
try
{
var shape = CreateShapeFromInputs();
if (shape == null) return;
var drawing = shape.GetDrawing();
drawing.Color = Drawing.GetNextColor();
drawing.Quantity.Required = (int)quantityUpDown.Value;
addedDrawings.Add(drawing);
DialogResult = DialogResult.OK;
addButton.Text = $"Added ({addedDrawings.Count})";
}
catch (Exception ex)
{
MessageBox.Show(
$"Failed to create shape: {ex.Message}",
"Error",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
}
}
private static string FriendlyName(string name)
{
if (name.EndsWith("Shape"))
name = name.Substring(0, name.Length - 5);
return Regex.Replace(name, @"(?<=[a-z0-9])([A-Z])", " $1");
}
private class ShapeEntry
{
public Type ShapeType { get; set; }
public string DisplayName { get; set; }
public List<ShapeDefinition> Configurations { get; set; }
public bool HasConfigurations => Configurations != null && Configurations.Count > 0;
public override string ToString() => DisplayName;
}
private class ParameterBinding
{
public PropertyInfo Property { get; set; }
public Control Control { get; set; }
}
}
}
+120
View File
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
+5
View File
@@ -10,6 +10,11 @@
<ItemGroup> <ItemGroup>
<Compile Remove="Controls\LayoutViewGL.cs" /> <Compile Remove="Controls\LayoutViewGL.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="Configurations\**\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" /> <ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" /> <ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />