mirror of
https://github.com/riegera2412/UVtools.git
synced 2026-07-12 03:22:32 +02:00
ca6f623a9f
- **Layer:**
- (Add) Property: `LayerMatBoundingRectangle`
- (Add) Property: `LayerMatModelBoundingRectangle`
- (Add) Method: `GetLayerMat(roi)`
- **Issues:**
- **Islands:**
- (Improvement) Islands detection performance
- (Improvement) Required area to consider an island is now real area instead of bounding box area
- (Fix) Logic bug when combining with overhangs
- **Overhangs:**
- (Improvement) Overhangs detection performance
- (Improvement) Overhangs are now split and identified as separately in the layer
- (Improvement) Overhangs now shows the correct issue area and able to locate the problem region more precisely
- (Improvement) Compress overhangs into contours instead of using whole pixels, resulting in better render performance and less memory to hold the issue
- (Fix) Bug in overhang logic causing to detect the problem twice when combined with supports
- (Improvement) Touching bounds check logic to spare cycles
- **Tool - Raise platform on print finish:**
- (Add) Preset "Minimum": Sets to the minimum position
- (Add) Preset "Medium": Sets to half-way between minimum and maximum position
- (Add) Preset "Maximum": Sets to the maximum position
- (Add) Wait time: Sets the ensured wait time to stay still on the desired position.
This is useful if the printer firmware always move to top and you want to stay still on the set position for at least the desired time.
Note: The print time calculation will take this wait into consideration and display a longer print time.
- (Add) FileFormat: AnyCubic custom machine (.pwc)
- (Downgrade) OpenCV from 4.5.5 to 4.5.4 due a possible crash while detecting islands (Windows)
576 lines
20 KiB
C#
576 lines
20 KiB
C#
/*
|
|
* GNU AFFERO GENERAL PUBLIC LICENSE
|
|
* Version 3, 19 November 2007
|
|
* Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
* Everyone is permitted to copy and distribute verbatim copies
|
|
* of this license document, but changing it is not allowed.
|
|
*/
|
|
|
|
using Emgu.CV;
|
|
using Emgu.CV.CvEnum;
|
|
using Emgu.CV.Structure;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Drawing;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using UVtools.Core.Extensions;
|
|
using UVtools.Core.FileFormats;
|
|
|
|
namespace UVtools.Core.Operations;
|
|
|
|
[Serializable]
|
|
public sealed class OperationInfill : Operation
|
|
{
|
|
#region Members
|
|
private InfillAlgorithm _infillType = InfillAlgorithm.CubicCrossAlternating;
|
|
private ushort _wallThickness = 64;
|
|
private ushort _infillThickness = 45;
|
|
private ushort _infillSpacing = 300;
|
|
private byte _infillBrightness = 255;
|
|
private bool _reinforceInfill;
|
|
|
|
#endregion
|
|
|
|
#region Overrides
|
|
public override string IconClass => "mdi-checkerboard";
|
|
public override string Title => "Infill";
|
|
|
|
public override string Description =>
|
|
$"Generate infill patterns in the model.\n\nNOTES:\n1) You must exclude floor and ceil layers from the range.\n2) You must take care of drain holes after the operation.";
|
|
|
|
public override string ConfirmationText =>
|
|
$"infill model with {InfillType} from layers {LayerIndexStart} through {LayerIndexEnd}?";
|
|
|
|
public override string ProgressTitle =>
|
|
$"Infill model with {InfillType} from layers {LayerIndexStart} through {LayerIndexEnd}";
|
|
|
|
public override string ProgressAction => "Infilled layers";
|
|
|
|
#endregion
|
|
|
|
#region Enums
|
|
public enum InfillAlgorithm
|
|
{
|
|
//Rhombus,
|
|
[Description("Straight pillars (Weak)")]
|
|
Pillars,
|
|
|
|
[Description("Concentric (Weak)")]
|
|
Concentric,
|
|
|
|
[Description("Waves (Medium)")]
|
|
Waves,
|
|
|
|
[Description("Cubic cross: Fixed pilars with crossing sections (Optimal)")]
|
|
CubicCross,
|
|
|
|
[Description("Cubic alternating cross: Fixed pilars with crossing sections alternating in directions (Optimal)")]
|
|
CubicCrossAlternating,
|
|
|
|
[Description("Cubic star: Fixed pilars with crossing sections in star pattern (Strong)")]
|
|
CubicStar,
|
|
|
|
[Description("Honeycomb (Strong)")]
|
|
Honeycomb,
|
|
|
|
[Description("Gyroid (Strong)")]
|
|
Gyroid,
|
|
}
|
|
#endregion
|
|
|
|
#region Properties
|
|
public InfillAlgorithm InfillType
|
|
{
|
|
get => _infillType;
|
|
set => RaiseAndSetIfChanged(ref _infillType, value);
|
|
}
|
|
|
|
public ushort WallThickness
|
|
{
|
|
get => _wallThickness;
|
|
set => RaiseAndSetIfChanged(ref _wallThickness, value);
|
|
}
|
|
|
|
public byte InfillBrightness
|
|
{
|
|
get => _infillBrightness;
|
|
set => RaiseAndSetIfChanged(ref _infillBrightness, value);
|
|
}
|
|
|
|
public ushort InfillThickness
|
|
{
|
|
get => _infillThickness;
|
|
set => RaiseAndSetIfChanged(ref _infillThickness, value);
|
|
}
|
|
|
|
public ushort InfillSpacing
|
|
{
|
|
get => _infillSpacing;
|
|
set => RaiseAndSetIfChanged(ref _infillSpacing, value);
|
|
}
|
|
|
|
public bool ReinforceInfill
|
|
{
|
|
get => _reinforceInfill;
|
|
set => RaiseAndSetIfChanged(ref _reinforceInfill, value);
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
var result = $"[{_infillType}] [Wall: {_wallThickness}px] [B: {_infillBrightness}px] [T: {_infillThickness}px] [S: {_infillSpacing}px] [R: {_reinforceInfill}]" + LayerRangeString;
|
|
if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}";
|
|
return result;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public OperationInfill() { }
|
|
|
|
public OperationInfill(FileFormat slicerFile) : base(slicerFile) { }
|
|
|
|
#endregion
|
|
|
|
#region Equality
|
|
|
|
private bool Equals(OperationInfill other)
|
|
{
|
|
return _infillType == other._infillType && _wallThickness == other._wallThickness && _infillThickness == other._infillThickness && _infillSpacing == other._infillSpacing && _infillBrightness == other._infillBrightness && _reinforceInfill == other._reinforceInfill;
|
|
}
|
|
|
|
public override bool Equals(object? obj)
|
|
{
|
|
return ReferenceEquals(this, obj) || obj is OperationInfill other && Equals(other);
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return HashCode.Combine((int) _infillType, _wallThickness, _infillThickness, _infillSpacing, _infillBrightness, _reinforceInfill);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
protected override bool ExecuteInternally(OperationProgress progress)
|
|
{
|
|
Mat? mask = null;
|
|
if (_infillType == InfillAlgorithm.Honeycomb)
|
|
{
|
|
mask = GetHoneycombMask(GetRoiSizeOrVolumeSize());
|
|
}
|
|
else if (_infillType == InfillAlgorithm.Concentric)
|
|
{
|
|
mask = GetConcentricMask(GetRoiSizeOrVolumeSize());
|
|
}
|
|
|
|
Parallel.For(LayerIndexStart, LayerIndexEnd + 1, CoreSettings.GetParallelOptions(progress), layerIndex =>
|
|
{
|
|
using var mat = SlicerFile[layerIndex].LayerMat;
|
|
Execute(mat, layerIndex, mask!);
|
|
SlicerFile[layerIndex].LayerMat = mat;
|
|
|
|
progress.LockAndIncrement();
|
|
});
|
|
mask?.Dispose();
|
|
return !progress.Token.IsCancellationRequested;
|
|
}
|
|
|
|
public override bool Execute(Mat mat, params object[]? arguments)
|
|
{
|
|
if (arguments is null || arguments.Length < 1) return false;
|
|
|
|
var kernel = EmguExtensions.Kernel3x3Rectangle;
|
|
uint index = Convert.ToUInt32(arguments[0]);
|
|
uint layerIndex = index - LayerIndexStart;
|
|
var infillColor = new MCvScalar(_infillBrightness);
|
|
|
|
Mat? patternMask = null;
|
|
using Mat erode = new ();
|
|
using Mat diff = new ();
|
|
var target = GetRoiOrVolumeBounds(mat);
|
|
using var mask = GetMask(mat);
|
|
bool disposeTargetMask = true;
|
|
|
|
if (_infillType is InfillAlgorithm.Pillars
|
|
or InfillAlgorithm.CubicCross
|
|
or InfillAlgorithm.CubicCrossAlternating
|
|
or InfillAlgorithm.CubicStar)
|
|
{
|
|
using var infillPattern = EmguExtensions.InitMat(new Size(_infillSpacing, _infillSpacing));
|
|
using var matPattern = mat.NewBlank();
|
|
bool firstPattern = true;
|
|
uint accumulator = 0;
|
|
bool dynamicCenter = false;
|
|
while (accumulator < layerIndex)
|
|
{
|
|
dynamicCenter = !dynamicCenter;
|
|
firstPattern = true;
|
|
accumulator += _infillSpacing;
|
|
|
|
if (accumulator >= layerIndex) break;
|
|
|
|
if (_reinforceInfill)
|
|
{
|
|
firstPattern = false;
|
|
accumulator += _infillThickness;
|
|
}
|
|
}
|
|
|
|
if (firstPattern)
|
|
{
|
|
int thickness = _infillThickness / 2;
|
|
// Top Left
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(0, 0, thickness, thickness),
|
|
infillColor, -1);
|
|
|
|
// Top Right
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(infillPattern.Width - thickness, 0, thickness, thickness),
|
|
infillColor, -1);
|
|
|
|
// Bottom Left
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(0, infillPattern.Height - thickness, thickness, thickness),
|
|
infillColor, -1);
|
|
|
|
// Bottom Right
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(infillPattern.Width - thickness, infillPattern.Height - thickness,
|
|
thickness, thickness),
|
|
infillColor, -1);
|
|
|
|
// Center cross
|
|
int margin = (int) (InfillSpacing - accumulator + layerIndex) - thickness;
|
|
int marginInv = (int) (accumulator - layerIndex) - thickness;
|
|
|
|
if (_infillType == InfillAlgorithm.CubicCross ||
|
|
(_infillType == InfillAlgorithm.CubicCrossAlternating &&
|
|
dynamicCenter) ||
|
|
_infillType == InfillAlgorithm.CubicStar)
|
|
{
|
|
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(margin, margin, _infillThickness, _infillThickness),
|
|
infillColor, -1);
|
|
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(marginInv, marginInv, _infillThickness,
|
|
_infillThickness),
|
|
infillColor, -1);
|
|
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(margin, marginInv, _infillThickness,
|
|
_infillThickness),
|
|
infillColor, -1);
|
|
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(marginInv, margin, _infillThickness,
|
|
_infillThickness),
|
|
infillColor, -1);
|
|
}
|
|
|
|
|
|
if (_infillType == InfillAlgorithm.CubicStar ||
|
|
(_infillType == InfillAlgorithm.CubicCrossAlternating &&
|
|
!dynamicCenter))
|
|
{
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(margin, -thickness, _infillThickness,
|
|
_infillThickness),
|
|
infillColor, -1);
|
|
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(marginInv, -thickness, _infillThickness,
|
|
_infillThickness),
|
|
infillColor, -1);
|
|
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(-thickness, margin, _infillThickness,
|
|
_infillThickness),
|
|
infillColor, -1);
|
|
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(-thickness, marginInv, _infillThickness,
|
|
_infillThickness),
|
|
infillColor, -1);
|
|
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(InfillSpacing - thickness, margin,
|
|
_infillThickness, _infillThickness),
|
|
infillColor, -1);
|
|
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(InfillSpacing - thickness, marginInv,
|
|
_infillThickness, _infillThickness),
|
|
infillColor, -1);
|
|
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(margin, InfillSpacing - thickness,
|
|
_infillThickness, _infillThickness),
|
|
infillColor, -1);
|
|
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(marginInv, InfillSpacing - thickness,
|
|
_infillThickness, _infillThickness),
|
|
infillColor, -1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CvInvoke.Rectangle(infillPattern,
|
|
new Rectangle(0, 0, _infillSpacing, _infillSpacing),
|
|
infillColor, _infillThickness);
|
|
}
|
|
|
|
|
|
CvInvoke.Repeat(infillPattern, target.Rows / infillPattern.Rows + 1,
|
|
target.Cols / infillPattern.Cols + 1, matPattern);
|
|
patternMask = matPattern.Roi(target);
|
|
disposeTargetMask = true;
|
|
}
|
|
else if (_infillType == InfillAlgorithm.Honeycomb)
|
|
{
|
|
if (arguments.Length >= 2)
|
|
{
|
|
patternMask = (Mat)arguments[1];
|
|
disposeTargetMask = false;
|
|
}
|
|
else
|
|
{
|
|
patternMask = GetHoneycombMask(target.Size);
|
|
disposeTargetMask = true;
|
|
}
|
|
}
|
|
else if (_infillType == InfillAlgorithm.Concentric)
|
|
{
|
|
if (arguments.Length >= 2)
|
|
{
|
|
patternMask = (Mat)arguments[1];
|
|
disposeTargetMask = false;
|
|
}
|
|
else
|
|
{
|
|
patternMask = GetConcentricMask(target.Size);
|
|
disposeTargetMask = true;
|
|
}
|
|
}
|
|
else if (_infillType == InfillAlgorithm.Waves)
|
|
{
|
|
var sineHeight = 100;
|
|
var sineWidth = 100;
|
|
var radius = (ushort)(_infillThickness / 2);
|
|
|
|
var points = new List<Point>();
|
|
|
|
bool isHorizontal = true;
|
|
float accumulator = 0;
|
|
for (int i = 0; i <= layerIndex; i++)
|
|
{
|
|
accumulator += SlicerFile[index].RelativePositionZ;
|
|
if (accumulator >= 2)
|
|
{
|
|
isHorizontal = !isHorizontal;
|
|
accumulator = 0;
|
|
}
|
|
}
|
|
|
|
//using var infillPattern = EmguExtensions.InitMat(new Size(_infillSpacing, _infillSpacing));
|
|
//using var matPattern = new Mat();
|
|
using var infillPattern = mat.NewBlank();
|
|
|
|
int maxY = 0;
|
|
|
|
if (isHorizontal)
|
|
{
|
|
for (int x = 0; x < mat.Width; x += radius)
|
|
{
|
|
int y = (int) (Math.Sin((double) x / sineWidth /*+ sineWidth * layerIndex*/) * sineHeight / 2.0 +
|
|
sineHeight / 2.0 + radius);
|
|
points.Add(new Point(x, y));
|
|
maxY = Math.Max(maxY, y);
|
|
}
|
|
var infillPatternRoi = infillPattern.Roi(new Size(infillPattern.Width, maxY + radius + 2 + _infillSpacing));
|
|
CvInvoke.Polylines(infillPatternRoi, points.ToArray(), false, infillColor, _infillThickness);
|
|
|
|
CvInvoke.Repeat(infillPatternRoi, target.Rows / infillPatternRoi.Rows + 1, 1, infillPattern);
|
|
}
|
|
else
|
|
{
|
|
for (int y = 0; y < mat.Height; y += radius)
|
|
{
|
|
int x = (int)(Math.Sin((double)y / sineWidth /*+ sineWidth * layerIndex*/) * sineHeight / 2.0 +
|
|
sineHeight / 2.0 + radius);
|
|
points.Add(new Point(x, y));
|
|
maxY = Math.Max(maxY, x);
|
|
}
|
|
var infillPatternRoi = infillPattern.Roi(new Size(maxY + radius + 2 + _infillSpacing, infillPattern.Height));
|
|
CvInvoke.Polylines(infillPatternRoi, points.ToArray(), false, infillColor, _infillThickness);
|
|
CvInvoke.Repeat(infillPatternRoi, 1, target.Cols / infillPatternRoi.Cols + 1, infillPattern);
|
|
|
|
}
|
|
points.Clear();
|
|
|
|
patternMask = infillPattern.Roi(target);
|
|
disposeTargetMask = true;
|
|
}
|
|
else if (_infillType == InfillAlgorithm.Gyroid)
|
|
{
|
|
patternMask = target.NewBlank();
|
|
|
|
var scaleRatio = 0.0012 / (_infillSpacing + _infillThickness / 2);
|
|
//var scaleX = 0.04 * _infillSpacing * Math.PI / target.Width;
|
|
//var scaleY = 0.04 * _infillSpacing * Math.PI / target.Height;
|
|
var scaleX = scaleRatio * mat.Width;
|
|
var scaleY = scaleRatio * mat.Height;
|
|
|
|
//const double scaleZ = 2.0 * Math.PI;
|
|
//var dz = 2.0 * scaleZ / LayerRangeCount; // z step
|
|
//var zz = 0.05 * (SlicerFile[index].LayerHeight / 0.05f) * (layerIndex + 1);
|
|
float zz = 0;
|
|
for (var i = LayerIndexStart; i <= index; i++)
|
|
{
|
|
zz += SlicerFile[i].RelativePositionZ;
|
|
}
|
|
|
|
for (int y = 0; y < patternMask.Height; y++)
|
|
{
|
|
var span = patternMask.GetRowSpan<byte>(y);
|
|
var yy = y * scaleY; // y position of pixel
|
|
for (int x = 0; x < patternMask.Width; x++)
|
|
{
|
|
var xx = x * scaleX; // x position of pixel
|
|
|
|
var d = Math.Sin(xx) * Math.Cos(yy) // compute gyroid equation
|
|
+ Math.Sin(yy) * Math.Cos(zz)
|
|
+ Math.Sin(zz) * Math.Cos(xx);
|
|
//if (d > 1e-6) continue; // Far from surface
|
|
if (Math.Abs(d) - 0.006*_infillThickness > 0) continue;
|
|
//if (d - 0.05 > 1e-6) continue;
|
|
|
|
span[x] = _infillBrightness;
|
|
}
|
|
}
|
|
|
|
|
|
//using var contours = patternMask.FindContours();
|
|
//CvInvoke.DrawContours(patternMask, contours, -1, infillColor, _infillThickness);
|
|
|
|
disposeTargetMask = true;
|
|
}
|
|
|
|
|
|
//patternMask.Save("D:\\pattern.png");
|
|
CvInvoke.Erode(target, erode, kernel, EmguExtensions.AnchorCenter, WallThickness, BorderType.Reflect101,
|
|
default);
|
|
CvInvoke.Subtract(target, erode, diff);
|
|
|
|
CvInvoke.BitwiseAnd(erode, patternMask, target, mask);
|
|
CvInvoke.Add(target, diff, target, mask);
|
|
|
|
if (disposeTargetMask)
|
|
{
|
|
patternMask!.Dispose();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public Mat GetHoneycombMask(Size targetSize)
|
|
{
|
|
var patternMask = EmguExtensions.InitMat(targetSize);
|
|
|
|
var halfInfillSpacing = _infillSpacing / 2;
|
|
var halfThickenss = _infillThickness / 2;
|
|
int width = (int)Math.Round(4 * (_infillSpacing / 2.0 / Math.Sqrt(3)));
|
|
var infillColor = new MCvScalar(_infillBrightness);
|
|
|
|
for (int col = 0; col <= targetSize.Width / _infillSpacing; col++)
|
|
{
|
|
for (int row = 0; row <= targetSize.Height / _infillSpacing; row++)
|
|
{
|
|
// Move over for the column number.
|
|
int x = (int)Math.Round(col * (width * 0.75f));
|
|
|
|
// Move down the required number of rows.
|
|
int y = row * _infillSpacing;
|
|
|
|
// If the column is odd, move down half a hex more.
|
|
if (col % 2 == 1) y += halfInfillSpacing;
|
|
|
|
var points = new Point[]
|
|
{
|
|
new(x, y),
|
|
new((int) Math.Round(x + width * 0.25f), y - _infillSpacing / 2),
|
|
new((int) Math.Round(x + width * 0.75f), y - _infillSpacing / 2),
|
|
new(x + width, y),
|
|
new((int) Math.Round(x + width * 0.75f), y + _infillSpacing / 2),
|
|
new((int) Math.Round(x + width * 0.25f), y + _infillSpacing / 2),
|
|
};
|
|
|
|
CvInvoke.Polylines(patternMask, points, true, infillColor, _infillThickness);
|
|
}
|
|
}
|
|
|
|
return patternMask;
|
|
}
|
|
|
|
public Mat GetConcentricMask(Size targetSize)
|
|
{
|
|
var patternMask = EmguExtensions.InitMat(targetSize);
|
|
|
|
//var halfInfillSpacing = _infillSpacing / 2;
|
|
//var halfThickenss = _infillThickness / 2;
|
|
int multiplicator = 1;
|
|
byte position = 0;
|
|
|
|
int x = patternMask.Width / 2;
|
|
int y = patternMask.Height / 2;
|
|
|
|
Point[] directions = {
|
|
new(0, -_infillSpacing), // top
|
|
new(_infillSpacing, 0), // right
|
|
new(0, _infillSpacing), // bottom
|
|
new(-_infillSpacing, 0), // left
|
|
};
|
|
|
|
bool[] hitLimits =
|
|
{
|
|
false,
|
|
false,
|
|
false,
|
|
false
|
|
};
|
|
|
|
var points = new List<Point> {new(x, y)};
|
|
|
|
while (hitLimits.Any(hitLimit => !hitLimit))
|
|
{
|
|
x += directions[position].X * multiplicator;
|
|
y += directions[position].Y * multiplicator;
|
|
if (x < 0 || y < 0 || x >= patternMask.Width || y >= patternMask.Height) hitLimits[position] = true;
|
|
points.Add(new Point(x, y));
|
|
position++;
|
|
if (position == 2)
|
|
{
|
|
multiplicator++;
|
|
}
|
|
else if (position == 4)
|
|
{
|
|
multiplicator++;
|
|
position = 0;
|
|
}
|
|
}
|
|
|
|
|
|
CvInvoke.Polylines(patternMask, points.ToArray(), false, new MCvScalar(_infillBrightness), _infillThickness);
|
|
|
|
return patternMask;
|
|
}
|
|
|
|
#endregion
|
|
} |