Jump to content

Use the 'magic wand' tool in a plugin?


572

Recommended Posts

I really need the "magic wand" function to locate a specific area of images in my plugin,

is there any online code I can refer to?

 

Function like : MagicWand (tolerance level, pixel) { --- } will be very useful, yet I couldn't find a really usable one.

Or is there a way I can access the built-in magic wand tool? Thank you

Edited by 572
Link to comment
Share on other sites

Hi, Boltbait tried to explain you that plugins can't neither change the selected area nor any pixels outside this area.

 

What you may do is to write a CodeLab plugin which uses the a selector to define a start point in the selected area of the image (or the whole image if nothing is selected) and write your own MagicWand algorithm. This is not to complex just a bit tricky to be fast.

 

But the basic question is what do you like to do with the area defined with your magic wand selector?

midoras signature.gif

Link to comment
Share on other sites

It kind of depends on what you want to do. I assume you want a Magic Wand-like feature that acts in Contiguous, not Global, mode. Global would be much simpler. It's easier if you want to do something immediately to the pixels (like change their color) than if you want to somehow flag them for some later action. Flagging them would probably require an additional buffer to store the flags. If you want to immediately change the pixels, but in a manner where they still might fit the match criteria, you'd probably need to use flags, since the pixels might get remodified.

 

Here's a flood-fill routine that might do the kind of thing you want.  I believe it will work, though I had to make a few changes to make it not be a subclass. I've used the routine many times and believe it to be very reliable. (For those who hate "gotos," I offer no apologies. I try to avoid them, but use them here to exit out of inner loops, which I think is entirely legitimate, and better than altering the program's rather clear structure in order to obtain gotoless purity.)

 

The Fill4 routine is very general. It takes a delegate whose arguments are the integer x and y values. The delegate should return true if the pixel matches the match criteria, and false otherwise. The delegate is also responsible for modifying the pixel (which could just be setting a flag in another buffer. Already-flagged pixels would no longer be matching). If only a specific type of change is needed, the delegate can be replaced by a hard-coded test-and-modify routine for better performance.

 

The original version, which uses a linked list for the stack.

 

Spoiler
namespace UtensilsPDN
{
    // Flood fill routine based on "A Seed Fill Algorithm" by Paul Heckbert
    // from "Graphics Gems", Academic Press, 1990
    // Some details improved by MJW, including better handling of the initial span, and
    // elimination of the unnecessary retesting of the pixels to the left and right of the
    // parent span.
    //
    //
    // To use, first create a FllodFill class with:
    //     FloodFill floodFill = new FloodFill();
    // -- or --
    //     FloodFill floodFill = new FloodFill(xMin, yMin, xMax, yMax);
    // if the first version is used, then before doing a fill, the size of the region
    // must be set with:
    //     floodFill.SetRange(xMin, yMin, xMax, yMax);
    // To perform fills, call:
    //     floodFill.Fill4(x, y, testAndModifyPixel);   // 4-connected fill.
    //
    // bool testAndModifyPixel(int x, int y) is a delegate that's passed the x and y
    // coordinates of a pixel. If the pixel matches whatever criteria is required for
    // filling, testAndModifyPixel modifies the pixel to the "filled" condition, and
    // returns true; otherwise, it returns false. For example, to replace the Primary
    // Color with the Secondary Color, testAndModifyPixel would test to see if the
    // pixel's color equaled the Primary Color, and if so, set it to the Secondary
    // Color and return true.
    class FloodFill
    {
        private SpanStack stack;

        public FloodFill()
        {
            stack = new SpanStack();
        }

        public FloodFill(int xMin, int yMin, int xMax, int yMax)
        {
            stack = new SpanStack();
            SetRange(xMin, yMin, xMax, yMax);
        }

        // Set the allowable ranges for X and Y.
        // Must be called before the first fill.
        protected int xMin, yMin, xMax, yMax;
        public void SetRange(int xMin, int yMin, int xMax, int yMax)
        {
            this.xMin = xMin; this.yMin = yMin;
            this.xMax = xMax; this.yMax = yMax;
            stack.SetYRange(yMin, yMax);
        }

        public void DeallocateMemory()
        {
            stack.DeallocateShadows();
        }

        public int GetAllocationCount()
        {
            return stack.ShadowCount;
        }

        private class SpanStack
        {
            private int yMin, yMax;
            private int shadowCount = 0;

            public SpanStack()
            {
                InitShadows();
            }

            public void SetYRange(int yMin, int yMax)
            {
                this.yMin = yMin; this.yMax = yMax;
            }

            // The stack consists of a linked list of shadows.
            private class Shadow
            {
                public int y, yParent, xLeft, xRight;
                public Shadow previous, next;
            }
            private Shadow shadowsHead;
            private Shadow shadows;

            public void DeallocateShadows()
            {
                InitShadows();
            }

            public int ShadowCount
            {
                get { return shadowCount; }
            }

            private void InitShadows()
            {
                shadowsHead = new Shadow();
                shadows = shadowsHead;
                shadowCount = 0;
            }

            private void AddShadow()
            {
                shadows = new Shadow();
                shadowsHead.previous = shadows;
                shadows.next = shadowsHead;
                shadowsHead = shadows;
                shadowCount++;
            }

            public void Push(int y, int yParent, int xLeft, int xRight)
            {
                if ((y >= yMin) && (y <= yMax))
                {
                    if ((shadows = shadows.previous) == null)
                        AddShadow();

                    shadows.y = y;
                    shadows.yParent = yParent;
                    shadows.xLeft = xLeft;
                    shadows.xRight = xRight;
                }
            }

            // Same, except no need to test for inside Y range.
            // The parent Y will always be in range.
            public void PushParent(int y, int yParent, int xLeft, int xRight)
            {
                if ((shadows = shadows.previous) == null)
                    AddShadow();

                shadows.y = y;
                shadows.yParent = yParent;
                shadows.xLeft = xLeft;
                shadows.xRight = xRight;
            }

            public void Pop(out int y, out int yParent,
                            out int xLeft, out int xRight)
            {
                y = shadows.y;
                yParent = shadows.yParent;
                xLeft = shadows.xLeft;
                xRight = shadows.xRight;
                shadows = shadows.next;
            }

            // Replace the right X value so the shadow is extended.
            public void Extend(int newRight)
            {
                shadows.xRight = newRight;
            }

            public bool Empty
            {
                get { return (shadows.next == null); }
            }
        }

        // The delegate must test the pixel and modify it if it fits the match criteria.
        // Return true if the pixel matched (and was therefore modified) and false otherwise.
        public delegate bool TestAndModifyPixel(int x, int y);

        // Fill4: If the pixel at (x, y) fits the match criteria, modify it. Then check all
        // its 4-connected neighbors. Modify any than match, and check their 4-connected neighbors, 
        //	etc. Continue until all the matching connected pixels have been modified. 
        // A 4-connected neighbor is a pixel above, below, left, or right of a pixel.
        public void Fill4(int x, int y, TestAndModifyPixel testAndModifyPixel)
        {
            int left, right, parentLeft, parentRight, parentY;

            // Exit if the position is out of range.
            if ((x < xMin) || (y < yMin) ||
                (x > xMax) || (y > yMax))
                return;

            // Scan the first span as a special case. The first span is unique, since it has
            // no parent span.

            // Scan left starting at the initial pixel.
            left = x;
            while ((left >= xMin) && testAndModifyPixel(left, y))
                left--;

            // If no pixels were modified, just return.
            if (left == x)
                return;

            // Scan right for the end of the seed span, starting at the pixel just right
            // of the initial pixel.
            do
            {
                x++;
            }
            while ((x <= xMax) && testAndModifyPixel(x, y));

            // Push the span for the lines above and below.
            left++; // Adjust left so it contains the starting X coordinate of the span.
            right = x - 1;
            stack.Push(y - 1, y, left, right);
            stack.Push(y + 1, y, left, right);

            // Scan the remaining spans, adding new 'shadow' spans of pixels to test to
            // adjacent lines as we go. Above and below each span is a 'parent' line that
            // generated the span, and an 'other' line that lies on the opposite side.
            // For each group of set pixels in the current line, we must add a corresponding
            // span in the 'other' line.  Spans for the parent line only need to be added
            // for the regions that extend to the left and right of the parent span, since
            // the parent span has already been tested.  The pixels immediately left and
            // right of the parent span were tested while processing the parent span, so
            // they don't need to be retested.
            // 
            // Parent:        aa ppppppppppppppppp aaaa
            // Current:       cccc cccccc  cccc ccccccc
            // Other:         aaaa aaaaaa  aaaa aaaaaaa
            while (!stack.Empty)
            {
                stack.Pop(out y, out parentY, out parentLeft, out parentRight);
                int otherY = y + (y - parentY);

                // Scan left, starting at the leftmost pixel of the parent span, for
                // pixels that need replacing.
                x = right = parentLeft;
                while ((x >= xMin) && testAndModifyPixel(x, y))
                    x--;

                // See if the first span starts at or before the parent span.
                // If it does, scan for the end of the span, and output a non-parent
                // span, and a parent span if necessary.
                if (x != parentLeft)
                {
                    left = x + 1;    // Save X of leftmost modified pixel.
                    // The pixel with the same X as the parent's leftmost pixel
                    // was replaced.  We know the parent's leftmost pixel was modified
                    // and that the pixel to its left was tested but not modified,
                    // so there's no there's no need to retest them.  Only add a new
                    // parent span if we need to test the pixels at least two pixels
                    // to the left of the parent's leftmost pixel.
                    if (left <= parentLeft - 2)
                        stack.PushParent(parentY, y, left, parentLeft - 2);
                    x = parentLeft + 1;

                    // Scan rightward for the end of the first span. When
                    // the end of the span is reached, push the span for the
                    // non-parent neighboring line.
                    while ((x <= xMax) && testAndModifyPixel(x, y))
                        x++;
                    right = x - 1;
                    stack.Push(otherY, y, left, right);
                }

                // Output all the non-parent spans, except for the first span when it
                // begins at or before the parent span (it has already been output).
                // When entering this loop, we are outside a span, and x contains the
                // X coordinate of the last pixel tested (which wasn't filled).
                // Scan for the beginning next span. Once found, scan for the end.
                while (x < parentRight)
                {
                    if (testAndModifyPixel(++x, y))
                    {
                        left = x++;
                        // Scan rightward for the end of the current span. When
                        // the end of the span is reached, push the span for the
                        // non-parent neighboring line.
                        while ((x <= xMax) && testAndModifyPixel(x, y))
                            x++;
                        right = x - 1;
                        stack.Push(otherY, y, left, right);
                    }
                }

                // Either we've reached right boundary, or the current pixel doesn't need
                // filling and we're at or past the end of the parent span. In either
                // case, we're done with the current shadow.
                // See if we need to add a shadow to the parent.
                // We know the parent's rightmost pixel was modified and that the pixel
                // to its right was tested but not modified, so there's no there's no
                // need to retest them. Therefore, if we're less than two pixel's past the
                // parent span, we don't need to add a span. 
                if (right >= parentRight + 2)
                    stack.PushParent(parentY, y, parentRight + 2, right);
            }
        }
    }
}

 

 

A newer version, added May 26, 2022, which uses the Stack class for the stack.

 

Spoiler
using System.Collections.Generic;

namespace UtensilsPDN
{
    // Flood fill routine based on "A Seed Fill Algorithm" by Paul Heckbert
    // from "Graphics Gems", Academic Press, 1990
    // Some details improved by MJW, including better handling of the initial span, and
    // elimination of the unnecessary retesting of the pixels to the left and right of the
    // parent span.
    //
    // To use, first create a FllodFill class with:
    //     FloodFill floodFill = new FloodFill();
    // -- or --
    //     FloodFill floodFill = new FloodFill(xMin, yMin, xMax, yMax);
    // if the first version is used, then before doing a fill, the size of the region
    // must be set with:
    //     floodFill.SetRange(xMin, yMin, xMax, yMax);
    // To perform fills, call:
    //     floodFill.Fill4(x, y, testAndModifyPixel);   // 4-connected fill.
    //
    // bool testAndModifyPixel(int x, int y) is a delegate that's passed the x and y
    // coordinates of a pixel. If the pixel matches whatever criteria is required for
    // filling, testAndModifyPixel modifies the pixel to the "filled" condition, and
    // returns true; otherwise, it returns false. For example, to replace the Primary
    // Color with the Secondary Color, testAndModifyPixel would test to see if the
    // pixel's color equaled the Primary Color, and if so, set it to the Secondary
    // Color and return true.
    class FloodFill
    {
        private SpanStack stack;

        public FloodFill()
        {
            stack = new SpanStack();
        }

        public FloodFill(int xMin, int yMin, int xMax, int yMax)
        {
            stack = new SpanStack();
            SetRange(xMin, yMin, xMax, yMax);
        }

        // Set the allowable ranges for X and Y.
        // Must be called before the first fill.
        protected int xMin, yMin, xMax, yMax;
        public void SetRange(int xMin, int yMin, int xMax, int yMax)
        {
            this.xMin = xMin; this.yMin = yMin;
            this.xMax = xMax; this.yMax = yMax;
            stack.SetYRange(yMin, yMax);
        }

        public void DeallocateMemory()
        {
            stack.DeallocateShadows();
        }

        public int GetAllocationCount()
        {
            return stack.ShadowCount;
        }

        private class SpanStack
        {
            private int yMin, yMax;
            private int shadowCount = 0;

            public SpanStack()
            {
                InitShadows();
            }

            public void SetYRange(int yMin, int yMax)
            {
                this.yMin = yMin; this.yMax = yMax;
            }


            // The stack uses a Stack class.
            private struct Shadow
            {
                public int y, yParent, xLeft, xRight;

                public Shadow(int y, int yParent, int xLeft, int xRight)
                {
                    this.y = y;
                    this.yParent = yParent;
                    this.xLeft = xLeft;
                    this.xRight = xRight;
                }

                public void GetValues(out int y, out int yParent, out int xLeft, out int xRight)
                {
                    y = this.y;
                    yParent = this.yParent;
                    xLeft = this.xLeft;
                    xRight = this.xRight;
                }

                public void SetValues(int y, int yParent, int xLeft, int xRight)
                {
                    this.y = y;
                    this.yParent = yParent;
                    this.xLeft = xLeft;
                    this.xRight = xRight;
                }

                public void ExtendRight(int newRight)
                {
                    xRight = newRight;
                }
            }

            // Theres's no automatic way to keep the count of the maximum allocated shadows.
            // When the flag is true, the value is maintained; otherwise it isn't.
            public const bool MaintainShadowCount = false;

            const int InitialShadowAllocation = 1000;
            Stack<Shadow> shadow;

            public void DeallocateShadows()
            {
                shadow = null;
            }

            // Return the maximum number of shadows allocated.
            public int ShadowCount
            {
                get { return shadowCount; }
            }

            private void InitShadows(int initialShadowAllocation)
            {
                shadow = new Stack<Shadow>(initialShadowAllocation);
                shadowCount = 0;
            }

            private void InitShadows()
            {
                InitShadows(InitialShadowAllocation);
            }

            public void Push(int y, int yParent, int xLeft, int xRight)
            {
                if ((y >= yMin) && (y <= yMax))
                {
                    shadow.Push(new Shadow(y, yParent, xLeft, xRight));

                    if (MaintainShadowCount && (shadowCount < shadow.Count))
                        shadowCount = shadow.Count;
                }
            }

            // Same, except no need to test for inside Y range.
            // The parent Y will always be in range.
            public void PushParent(int y, int yParent, int xLeft, int xRight)
            {
                shadow.Push(new Shadow(y, yParent, xLeft, xRight));

                if (MaintainShadowCount && (shadowCount < shadow.Count))
                        shadowCount = shadow.Count;
            }

            public void Pop(out int y, out int yParent,
                            out int xLeft, out int xRight)
            {
                shadow.Pop().GetValues(out y, out yParent, out xLeft, out xRight);
            }

            // Replace the right X value so the shadow is extended.
            public void Extend(int newRight)
            {
                shadow.Peek().ExtendRight(newRight);
            }

            public bool Empty
            {
                get { return shadow.Count == 0; }
            }
        }

        // The delegate must test the pixel and modify it if it fits the match criteria.
        // Return true if the pixel matched (and was therefore modified) and false otherwise.
        public delegate bool TestAndModifyPixel(int x, int y);

        // Fill4: If the pixel at (x, y) fits the match criteria, modify it. Then check all
        // its 4-connected neighbors. Modify any than match, and check their 4-connected neighbors, 
        //	etc. Continue until all the matching connected pixels have been modified. 
        // A 4-connected neighbor is a pixel above, below, left, or right of a pixel.
        public void Fill4(int x, int y, TestAndModifyPixel testAndModifyPixel)
        {
            int left, right, parentLeft, parentRight, parentY;

            // Exit if the position is out of range.
            if ((x < xMin) || (y < yMin) ||
                (x > xMax) || (y > yMax))
                return;

            // Scan the first span as a special case. The first span is unique, since it has
            // no parent span.

            // Scan left starting at the initial pixel.
            left = x;
            while ((left >= xMin) && testAndModifyPixel(left, y))
                left--;

            // If no pixels were modified, just return.
            if (left == x)
                return;

            // Scan right for the end of the seed span, starting at the pixel just right
            // of the initial pixel.
            do
            {
                x++;
            }
            while ((x <= xMax) && testAndModifyPixel(x, y));

            // Push the span for the lines above and below.
            left++; // Adjust left so it contains the starting X coordinate of the span.
            right = x - 1;
            stack.Push(y - 1, y, left, right);
            stack.Push(y + 1, y, left, right);

            // Scan the remaining spans, adding new 'shadow' spans of pixels to test to
            // adjacent lines as we go. Above and below each span is a 'parent' line that
            // generated the span, and an 'other' line that lies on the opposite side.
            // For each group of set pixels in the current line, we must add a corresponding
            // span in the 'other' line.  Spans for the parent line only need to be added
            // for the regions that extend to the left and right of the parent span, since
            // the parent span has already been tested.  The pixels immediately left and
            // right of the parent span were tested while processing the parent span, so
            // they don't need to be retested.
            // 
            // Parent:        aa ppppppppppppppppp aaaa
            // Current:       cccc cccccc  cccc ccccccc
            // Other:         aaaa aaaaaa  aaaa aaaaaaa
            while (!stack.Empty)
            {
                stack.Pop(out y, out parentY, out parentLeft, out parentRight);
                int otherY = y + (y - parentY);

                // Scan left, starting at the leftmost pixel of the parent span, for
                // pixels that need replacing.
                x = right = parentLeft;
                while ((x >= xMin) && testAndModifyPixel(x, y))
                    x--;

                // See if the first span starts at or before the parent span.
                // If it does, scan for the end of the span, and output a non-parent
                // span, and a parent span if necessary.
                if (x != parentLeft)
                {
                    left = x + 1;    // Save X of leftmost modified pixel.
                    // The pixel with the same X as the parent's leftmost pixel
                    // was replaced.  We know the parent's leftmost pixel was modified
                    // and that the pixel to its left was tested but not modified,
                    // so there's no there's no need to retest them.  Only add a new
                    // parent span if we need to test the pixels at least two pixels
                    // to the left of the parent's leftmost pixel.
                    if (left <= parentLeft - 2)
                        stack.PushParent(parentY, y, left, parentLeft - 2);
                    x = parentLeft + 1;

                    // Scan rightward for the end of the first span. When
                    // the end of the span is reached, push the span for the
                    // non-parent neighboring line.
                    while ((x <= xMax) && testAndModifyPixel(x, y))
                        x++;
                    right = x - 1;
                    stack.Push(otherY, y, left, right);
                }

                // Output all the non-parent spans, except for the first span when it
                // begins at or before the parent span (it has already been output).
                // When entering this loop, we are outside a span, and x contains the
                // X coordinate of the last pixel tested (which wasn't filled).
                // Scan for the beginning next span. Once found, scan for the end.
                while (x < parentRight)
                {
                    if (testAndModifyPixel(++x, y))
                    {
                        left = x++;
                        // Scan rightward for the end of the current span. When
                        // the end of the span is reached, push the span for the
                        // non-parent neighboring line.
                        while ((x <= xMax) && testAndModifyPixel(x, y))
                            x++;
                        right = x - 1;
                        stack.Push(otherY, y, left, right);
                    }
                }

                // Either we've reached right boundary, or the current pixel doesn't need
                // filling and we're at or past the end of the parent span. In either
                // case, we're done with the current shadow.
                // See if we need to add a shadow to the parent.
                // We know the parent's rightmost pixel was modified and that the pixel
                // to its right was tested but not modified, so there's no there's no
                // need to retest them. Therefore, if we're less than two pixel's past the
                // parent span, we don't need to add a span. 
                if (right >= parentRight + 2)
                    stack.PushParent(parentY, y, parentRight + 2, right);
            }
        }
    }
}

 

 

 

EDIT: I don't see how it would be possible to implement a Magic-Wand-like feature in a CodeLab plugin. I think it'd have to be done in VS.

Link to comment
Share on other sites

Wait, you just want the magic wand's algorithm for deciding which pixels to touch? Or you want to incorporate an interactive Magic Wand-like or Paint Bucket-like functionality into your plugin?

 

If you just need the algorithm, look at MJW's reply ^^^

 

Also, look up "flood fill algorithm" (like on Wikipedia).

 

You can also boot up something like .NET Reflector or ILSpy (these are disassemblers, and they're not scary :) ), toss PaintDotNet.exe into it (as in, open Reflector or ILSpy and then drag-and-drop the file into it), and search for a class called FloodFillAlgorithm. You can't call into that class from your code but feel free to study the disassembled code as much as you need (I don't even mind if you copy-paste it into your own code and then modify it as you need).

  • Upvote 1

The Paint.NET Blog: https://blog.getpaint.net/

Donations are always appreciated! https://www.getpaint.net/donate.html

forumSig_bmwE60.jpg

Link to comment
Share on other sites

  • 6 years later...
On 5/22/2022 at 9:08 AM, aliababa2001 said:

I try MJW's reply in my code, but it slow I dont know how to improve it like parallel or multithread, any document for that?

 

Well, my version is, I believe, a quite efficient implementation of one of the most commonly used flood fill algorithms. I have, however, updated the post with a different -- and likely better -- stack implementation.  In the original version I used a linked list; the newer version uses the Stack class. The Stack class does have the disadvantage that if the stack is too small, it gets automatically reallocated, which entails quite a lot of copying.

 

As i mentioned in the original comment, in order to make it general, I use delegates, which naturally makes it run slower than if the test-and-modify routine was inline.  One thing that might improve performance while maintaining generality is to replace delegates with the function pointers that were recently added to C#. Because function pointers must point to static functions, besides the X and Y coordinates, a reference to a class containing the state would also need to be passed. I think this means that ths flood fill routine would need to be made into a generic class, so the type of class could be specified.

 

Parallelization would not be a trivial process, since the separate processes can't run independently -- by the way flood filling works, there must be communication between them. The line fill algorithm I use could probably be parallelized by assigning different sets of lines (rows) to each process. For two cores, one would get the odd lines, the other the even lines; for three, each would get every third line, and so on. While filling a span, "shadow" records are generated for the line above and below. The shadows represent spans of pixels that must be checked. Once the fill of a span of pixels is complete, the  shadow records could be passed to the processes that handle those lines (which, by the way the lines are allocated, will not be the current process). Naturally a lock or something similar must be performed when passing the shadow records. It seems like the linked-list version of that stack might work better for this than the Stack version.

 

EDIT: Another parallelization method is probably the simplest: just have a singe stack which is always locked for access. Each process would simply grab the next span to be processed. That would probably work well for most common cases. However, there could be pathological cases where the performance would be worse than the non-parallelized version. For example, single-width vertical lines with a pixel between them, connected at alternating ends to form a vertical zigzag pattern. A fill started at one of the endpoints would have all of the overhead, but none of the advantages of parallelization.

 

  • Upvote 1
Link to comment
Share on other sites

Paint.NET's implementation has been highly tuned for performance. It's multithreaded, but not parallelized. Instead, it uses prefetching and extremely careful locking / interlocked operations.

 

Each row's data for whether each pixel passes/fails the tolerance check is stored in a cache. When the algorithm goes to process row Y, it will check if the cache has been populated for that row. If not, it will process the whole row and store the results in the cache. And, it will also queue several rows above and below to be prefetched on other threads. By the time the flood fill algorithm gets to those rows, they're usually ready. The # of rows is based on the # of processor cores/threads. I found that this was a good balance between performance and complexity. PDN v4.3 is significantly faster with the Magic Wand and Paint Bucket tools than previous versions.

 

The code for performing the tolerance check is also highly tuned based on advice from experts in advanced, low-level C# performance programming (there's a C# Discord server with an #allow-unsafe-blocks channel full of these folks). It uses the System.Numerics.Vector4 type for speed, as well as generics and [MethodImpl(MethodImplOptions.AggressiveInlining)] to get the best code generation by the JIT. Delegates are not used -- for performance it's better to use value delegates (in C++ you might remember "functors").

  • Upvote 1

The Paint.NET Blog: https://blog.getpaint.net/

Donations are always appreciated! https://www.getpaint.net/donate.html

forumSig_bmwE60.jpg

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...