diff --git a/MatterControlLib/ApplicationView/SceneOperations.cs b/MatterControlLib/ApplicationView/SceneOperations.cs index ca1451b0c..754aa83d6 100644 --- a/MatterControlLib/ApplicationView/SceneOperations.cs +++ b/MatterControlLib/ApplicationView/SceneOperations.cs @@ -294,7 +294,7 @@ namespace MatterHackers.MatterControl var sceneItem = scene.SelectedItem; var imageObject = sceneItem.Clone() as ImageObject3D; - var path = new ImageToPathObject3D(); + var path = new ImageToPathObject3D_2(); path.Children.Add(imageObject); var smooth = new SmoothPathObject3D(); @@ -315,10 +315,10 @@ namespace MatterHackers.MatterControl ComponentID = "4D9BD8DB-C544-4294-9C08-4195A409217A", SurfacedEditors = new List { - "$.Children.Children.Children.Children.Children", + "$.Children.Children.Children.Children.Children", "$.Children.Children.Height", "$.Children.Children.Children.SmoothDistance", - "$.Children.Children.Children.Children", + "$.Children.Children.Children.Children", "$.Children", } }; @@ -346,7 +346,7 @@ namespace MatterHackers.MatterControl { OperationType = typeof(ImageObject3D), TitleResolver = () => "Image to Path".Localize(), - ResultType = typeof(ImageToPathObject3D), + ResultType = typeof(ImageToPathObject3D_2), Action = (sceneContext) => { var scene = sceneContext.Scene; @@ -354,9 +354,9 @@ namespace MatterHackers.MatterControl if (sceneItem is IObject3D imageObject) { // TODO: make it look like this (and get rid of all the other stuff) - // scene.Replace(sceneItem, new ImageToPathObject3D(sceneItem.Clone())); + // scene.Replace(sceneItem, new ImageToPathObject3D_2(sceneItem.Clone())); - var path = new ImageToPathObject3D(); + var path = new ImageToPathObject3D_2(); var itemClone = sceneItem.Clone(); path.Children.Add(itemClone); @@ -896,7 +896,7 @@ namespace MatterHackers.MatterControl PrimaryOperations.Add(typeof(ImageObject3D), new List { SceneOperations.ById("ImageConverter"), SceneOperations.ById("ImageToPath"), }); // path operations - PrimaryOperations.Add(typeof(ImageToPathObject3D), new List + PrimaryOperations.Add(typeof(ImageToPathObject3D_2), new List { SceneOperations.ById("LinearExtrude"), SceneOperations.ById("Revolve"), SceneOperations.ById("SmoothPath") }); diff --git a/MatterControlLib/DesignTools/EditorTools/DesignApps/Parts/ImageCoinObject3D.cs b/MatterControlLib/DesignTools/EditorTools/DesignApps/Parts/ImageCoinObject3D.cs index afce801f1..0459f5bf1 100644 --- a/MatterControlLib/DesignTools/EditorTools/DesignApps/Parts/ImageCoinObject3D.cs +++ b/MatterControlLib/DesignTools/EditorTools/DesignApps/Parts/ImageCoinObject3D.cs @@ -140,7 +140,7 @@ namespace MatterHackers.MatterControl.DesignTools coinBlank = coinBlank.Plus(hook); } - var imageToPath = new ImageToPathObject3D(); + var imageToPath = new ImageToPathObject3D_2(); imageToPath.Children.Add(imageObject); diff --git a/MatterControlLib/DesignTools/Operations/Image/ImageToPathObject3D.cs b/MatterControlLib/DesignTools/Operations/Image/ImageToPathObject3D.cs index 7fa6ce390..47fd2ff6e 100644 --- a/MatterControlLib/DesignTools/Operations/Image/ImageToPathObject3D.cs +++ b/MatterControlLib/DesignTools/Operations/Image/ImageToPathObject3D.cs @@ -27,6 +27,11 @@ of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project. */ +/*********************************************************************/ +/**************************** OBSOLETE! ******************************/ +/************************ USE NEWER VERSION **************************/ +/*********************************************************************/ + using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -40,7 +45,6 @@ using MatterHackers.Agg.Transform; using MatterHackers.Agg.UI; using MatterHackers.Agg.VertexSource; using MatterHackers.DataConverters3D; -using MatterHackers.DataConverters3D.UndoCommands; using MatterHackers.Localizations; using MatterHackers.MarchingSquares; using MatterHackers.MatterControl.DesignTools.Operations; @@ -52,6 +56,7 @@ using Polygons = System.Collections.Generic.List; +using Polygons = System.Collections.Generic.List>; + +namespace MatterHackers.MatterControl.DesignTools +{ + public class ImageToPathObject3D_2 : Object3D, IPathObject, ISelectedEditorDraw, IObject3DControlsProvider + { + private ThresholdFunctions _featureDetector = ThresholdFunctions.Intensity; + + private ImageBuffer _histogramRawCache = null; + private ImageBuffer _histogramDisplayCache = null; + + public ImageToPathObject3D_2() + { + Name = "Image to Path".Localize(); + } + + public enum ThresholdFunctions + { + Transparency, + Colors, + Intensity, + } + + [JsonIgnore] + public ImageBuffer ImageWithAlpha + { + get + { + var imageObject = (ImageObject3D)Children.Where(i => i is ImageObject3D).FirstOrDefault(); + + return imageObject.Image; + } + + set + { + } + } + + + [EnumDisplay(Mode = EnumDisplayAttribute.PresentationMode.Tabs)] + public ThresholdFunctions FeatureDetector + { + get + { + return _featureDetector; + } + + set + { + if (value != _featureDetector) + { + _histogramRawCache = null; + _featureDetector = value; + // make sure we create it + var _ = this.Histogram; + } + } + } + + [JsonIgnore] + public ImageBuffer Histogram + { + get + { + if (_histogramRawCache == null) + { + _histogramRawCache = new ImageBuffer(256, 100); + var image = Image; + if (image != null) + { + var counts = new int[_histogramRawCache.Width]; + var function = ThresholdFunction; + + byte[] buffer = image.GetBuffer(); + int strideInBytes = image.StrideInBytes(); + for (int y = 0; y < image.Height; y++) + { + int imageBufferOffset = image.GetBufferOffsetY(y); + int thresholdBufferOffset = y * image.Width; + + for (int x = 0; x < image.Width; x++) + { + int imageBufferOffsetWithX = imageBufferOffset + x * 4; + var color = GetRGBA(buffer, imageBufferOffsetWithX); + counts[(int)(function.Transform(color) * (_histogramRawCache.Width - 1))]++; + } + } + + double max = counts.Select((value, index) => new { value, index }) + .OrderByDescending(vi => vi.value) + .First().value; + var graphics2D2 = _histogramRawCache.NewGraphics2D(); + graphics2D2.Clear(Color.White); + for (int i = 0; i < 256; i++) + { + graphics2D2.Line(i, 0, i, Easing.Exponential.Out(counts[i] / max) * _histogramRawCache.Height, Color.Black); + } + } + } + + if (_histogramDisplayCache == null) + { + _histogramDisplayCache = new ImageBuffer(_histogramRawCache); + } + + UpdateHistogramDisplay(); + + return _histogramDisplayCache; + } + + set + { + } + } + + private void UpdateHistogramDisplay() + { + if (_histogramRawCache != null + && _histogramDisplayCache != null) + { + var graphics2D = _histogramDisplayCache.NewGraphics2D(); + graphics2D.Clear(Color.Transparent); + _histogramDisplayCache.CopyFrom(_histogramRawCache); + var rangeStart = RangeStart.Value(this); + var rangeEnd = RangeEnd.Value(this); + graphics2D.FillRectangle(0, 0, rangeStart * _histogramDisplayCache.Width, _histogramDisplayCache.Height, new Color(Color.Red, 100)); + graphics2D.FillRectangle(rangeEnd * _histogramDisplayCache.Width, 0, 255, _histogramDisplayCache.Height, new Color(Color.Red, 100)); + graphics2D.Line(rangeStart * _histogramDisplayCache.Width, 0, rangeStart * _histogramDisplayCache.Width, _histogramDisplayCache.Height, new Color(Color.LightGray, 200)); + graphics2D.Line(rangeEnd * _histogramDisplayCache.Width, 0, rangeEnd * _histogramDisplayCache.Width, _histogramDisplayCache.Height, new Color(Color.LightGray, 200)); + } + } + + [JsonIgnore] + private ImageBuffer Image => this.Descendants().FirstOrDefault()?.Image; + + [Range(0, 1, ErrorMessage = "Value for {0} must be between {1} and {2}.")] + public DoubleOrExpression RangeStart { get; set; } = .1; + + [Range(0, 1, ErrorMessage = "Value for {0} must be between {1} and {2}.")] + public DoubleOrExpression RangeEnd { get; set; } = 1; + + public IVertexSource VertexSource { get; set; } = new VertexStorage(); + + private IThresholdFunction ThresholdFunction + { + get + { + switch (FeatureDetector) + { + case ThresholdFunctions.Intensity: + return new MapOnMaxIntensity(RangeStart.Value(this), RangeEnd.Value(this)); + + case ThresholdFunctions.Transparency: + return new AlphaThresholdFunction(RangeStart.Value(this), RangeEnd.Value(this)); + + case ThresholdFunctions.Colors: + return new HueThresholdFunction(RangeStart.Value(this), RangeEnd.Value(this)); + } + + return new MapOnMaxIntensity(RangeStart.Value(this), RangeEnd.Value(this)); + } + } + + public void AddObject3DControls(Object3DControlsLayer object3DControlsLayer) + { + object3DControlsLayer.AddControls(ControlTypes.Standard2D); + } + + public void DrawEditor(Object3DControlsLayer layer, List transparentMeshes, DrawEventArgs e) + { + this.DrawPath(); + } + + public override bool CanFlatten => true; + + public override void Flatten(UndoBuffer undoBuffer) + { + this.FlattenToPathObject(undoBuffer); + } + + public void GenerateMarchingSquaresAndLines(Action progressReporter, ImageBuffer image, IThresholdFunction thresholdFunction) + { + if (image != null) + { + // Regenerate outline + var marchingSquaresData = new MarchingSquaresByte( + image, + thresholdFunction.ZeroColor, + thresholdFunction.Threshold, + 0); + + progressReporter?.Invoke(0, "Creating Outline"); + + marchingSquaresData.CreateLineSegments(); + progressReporter?.Invoke(.1, null); + + int pixelsToIntPointsScale = 1000; + var lineLoops = marchingSquaresData.CreateLineLoops(pixelsToIntPointsScale); + + progressReporter?.Invoke(.15, null); + + var min = new IntPoint(-10, -10); + var max = new IntPoint(10 + image.Width * pixelsToIntPointsScale, 10 + image.Height * pixelsToIntPointsScale); + + var boundingPoly = new Polygon(); + boundingPoly.Add(min); + boundingPoly.Add(new IntPoint(min.X, max.Y)); + boundingPoly.Add(max); + boundingPoly.Add(new IntPoint(max.X, min.Y)); + + // now clip the polygons to get the inside and outside polys + var clipper = new Clipper(); + clipper.AddPaths(lineLoops, PolyType.ptSubject, true); + clipper.AddPath(boundingPoly, PolyType.ptClip, true); + + var polygonShape = new Polygons(); + progressReporter?.Invoke(.3, null); + + clipper.Execute(ClipType.ctIntersection, polygonShape); + + progressReporter?.Invoke(.55, null); + + polygonShape = Clipper.CleanPolygons(polygonShape, 100); + + progressReporter?.Invoke(.75, null); + + VertexStorage rawVectorShape = polygonShape.PolygonToPathStorage(); + + var aabb = this.VisibleMeshes().FirstOrDefault().GetAxisAlignedBoundingBox(); + var xScale = aabb.XSize / image.Width; + + var affine = Affine.NewScaling(1.0 / pixelsToIntPointsScale * xScale); + affine *= Affine.NewTranslation(-aabb.XSize / 2, -aabb.YSize / 2); + + rawVectorShape.transform(affine); + this.VertexSource = rawVectorShape; + + progressReporter?.Invoke(1, null); + } + } + + public override async void OnInvalidate(InvalidateArgs invalidateArgs) + { + if (invalidateArgs.InvalidateType.HasFlag(InvalidateType.Image) + && invalidateArgs.Source != this + && !RebuildLocked) + { + await Rebuild(); + } + else if ((invalidateArgs.InvalidateType.HasFlag(InvalidateType.Properties) && invalidateArgs.Source == this)) + { + await Rebuild(); + } + else if (SheetObject3D.NeedsRebuild(this, invalidateArgs)) + { + await Rebuild(); + } + + base.OnInvalidate(invalidateArgs); + } + + private Color GetRGBA(byte[] buffer, int offset) + { + return new Color(buffer[offset + 2], buffer[offset + 1], buffer[offset + 0], buffer[offset + 3]); + } + + public override Task Rebuild() + { + this.DebugDepth("Rebuild"); + + UpdateHistogramDisplay(); + bool propertyUpdated = false; + var minSeparation = .01; + var rangeStart = RangeStart.Value(this); + var rangeEnd = RangeEnd.Value(this); + if (rangeStart < 0 + || rangeStart > 1 + || rangeEnd < 0 + || rangeEnd > 1 + || rangeStart > rangeEnd - minSeparation) + { + rangeStart = Math.Max(0, Math.Min(1 - minSeparation, rangeStart)); + rangeEnd = Math.Max(0, Math.Min(1, rangeEnd)); + if (rangeStart > rangeEnd - minSeparation) + { + // values are overlapped or too close together + if (rangeEnd < 1 - minSeparation) + { + // move the end up whenever possible + rangeEnd = rangeStart + minSeparation; + } + else + { + // move the end to the end and the start up + rangeEnd = 1; + RangeStart = 1 - minSeparation; + } + } + + propertyUpdated = true; + } + + var rebuildLock = RebuildLock(); + // now create a long running task to process the image + return ApplicationController.Instance.Tasks.Execute( + "Calculate Path".Localize(), + null, + (reporter, cancellationToken) => + { + var progressStatus = new ProgressStatus(); + this.GenerateMarchingSquaresAndLines( + (progress0to1, status) => + { + progressStatus.Progress0To1 = progress0to1; + progressStatus.Status = status; + reporter.Report(progressStatus); + }, + Image, + ThresholdFunction); + + if (propertyUpdated) + { + UpdateHistogramDisplay(); + this.Invalidate(InvalidateType.Properties); + } + + UiThread.RunOnIdle(() => + { + rebuildLock.Dispose(); + Parent?.Invalidate(new InvalidateArgs(this, InvalidateType.Path)); + }); + + return Task.CompletedTask; + }); + } + } +} \ No newline at end of file diff --git a/MatterControlLib/DesignTools/Primitives/BaseObject3D.cs b/MatterControlLib/DesignTools/Primitives/BaseObject3D.cs index d026998f5..d6fdbde0d 100644 --- a/MatterControlLib/DesignTools/Primitives/BaseObject3D.cs +++ b/MatterControlLib/DesignTools/Primitives/BaseObject3D.cs @@ -90,6 +90,7 @@ namespace MatterHackers.MatterControl.DesignTools [DisplayName("Height")] public DoubleOrExpression ExtrusionHeight { get; set; } = 5; + [DisplayName("")] [ReadOnly(true)] public string NoBaseMessage { get; set; } = "No base is added under your part. Switch to a different base option to create a base.";