Sign in to follow this  
toe_head2001

Mini Tutorials/Tips for Plugin Developers

Recommended Posts

Here are a few Mini Tutorials and Tips that are hopefully helpful for new plugin developers. All of these are geared towards effects in Visual Studio. If you find mistakes, please point them out.
 
Dynamic values for IndirectUI sliders
Sometimes you have a parameter for size, and you're not sure what the Upper Bound (Maximum vale) should be set to. Is 1000px enough? What if some one has a 5000px wide selection/canvas?

Spoiler

I will demonstrate how to get a good value based on the current selection (remember, if there's no selection, the whole canvas is used).
 
First, here's our IndirectUI slider. The default value is hard-code at 100, and the max value is hard-coded at 1000. 


protected override PropertyCollection OnCreatePropertyCollection()
{
    List<Property> props = new List<Property>();
    props.Add(new Int32Property(PropertyNames.Amount1, 100, 1, 1000));

    return new PropertyCollection(props);
}

 Let's create an Size object that represents the Height and Width of the selection. 


protected override PropertyCollection OnCreatePropertyCollection()
{
    Size selSize = EnvironmentParameters.GetSelection(EnvironmentParameters.SourceSurface.Bounds).GetBoundsInt().Size; // Gets the selection dimensions

    List<Property> props = new List<Property>();
    props.Add(new Int32Property(PropertyNames.Amount1, 100, 1, 1000));

    return new PropertyCollection(props);
}

Now we'll want to determine if the Height or Width is the shorter value of the two, and we'll use that for the Upper Bound on the slider. We'll make an int variable to hold the value.


protected override PropertyCollection OnCreatePropertyCollection()
{
    Size selSize = EnvironmentParameters.GetSelection(EnvironmentParameters.SourceSurface.Bounds).GetBoundsInt().Size;
    int upperBound = Math.Min(selSize.Width, selSize.Height); // use the smallest of the two dimensions

    List<Property> props = new List<Property>();
    props.Add(new Int32Property(PropertyNames.Amount1, 100, 1, upperBound)); // Use the upperBound variable here, in place of the 1000

    return new PropertyCollection(props);
}

How about the default value? Let's set that to half the value of the Upper Bound.


protected override PropertyCollection OnCreatePropertyCollection()
{
    Size selSize = EnvironmentParameters.GetSelection(EnvironmentParameters.SourceSurface.Bounds).GetBoundsInt().Size;
    int upperBound = Math.Min(selSize.Width, selSize.Height);
    int default = upperBound / 2; // simply divide upperBound by 2

    List<Property> props = new List<Property>();
    props.Add(new Int32Property(PropertyNames.Amount1, default, 1, upperBound)); // Use the default variable here, in place of the 1000

    return new PropertyCollection(props);
}

 


Copying Surfaces
Sometimes you create an extra Surface or two in your effect. In these examples I'll demonstrate the many ways to set the Destination Surface (dst) to one of these extra surfaces.

 

Spoiler

If you're not doing any pixel processing, and your Surfaces are the same dimensions, you can simply copy like so.


void Render(Surface dst, Surface src, Rectangle rect)
{
    dst.CopySurface(surfaceXYZ, rect.Location, rect);
}

 
Every one should know this one. This if you you doing some pixel processing. Both surfaces need to have the same dimensions.


void Render(Surface dst, Surface src, Rectangle rect)
{
    for (int y = rect.Top; y < rect.Bottom; ++y)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; ++x)
        {
            CurrentPixel = surfaceXYZ[x, y];
            CurrentPixel.A = 128;

            dst[x, y] = CurrentPixel;
        }
    }
}

-------
Ok, say your Surface is smaller than dst, so it does Not have the same dimensions. There are a few options. You can choose one of the following.


CurrentPixel = surfaceXYZ.GetBilinearSample(x, y); // this will place the content of surfaceXYZ at the top left of the canvas
CurrentPixel = surfaceXYZ.GetBilinearSampleClamped(x, y); // this will place the content of surfaceXYZ at the top left of the canvas, and repeat the edge pixel across the reset of the canvas (good if the edges are a solid color)
CurrentPixel = surfaceXYZ.GetBilinearSampleWrapped(x, y); // this will place the content of surfaceXYZ at the top left of the canvas, and then repeat it across the reset of the canvas (aka tiled)

Here's an example


void Render(Surface dst, Surface src, Rectangle rect)
{
    for (int y = rect.Top; y < rect.Bottom; ++y)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; ++x)
        {
            CurrentPixel = surfaceXYZ.GetBilinearSampleWrapped(x, y)

            dst[x, y] = CurrentPixel;
        }
    }
}

-------
Another option is to resize your Surface so that it has the same dimensions as dst. In many cases, this will stretch/smash your image due to the aspect ratio changing.
(It is possible write some code to calculate the aspect ratio, and clip either the height or width. It more complex than I care to demonstrate. Look at my Blur Fill plugin, if you're interested.)


dst.FitSurface(ResamplingAlgorithm.Bicubic, surfaceXYZ); // Bicubic is usually used to enlarge images
dst.FitSurface(ResamplingAlgorithm.Fant, surfaceXYZ); // Fant is usually used to downsize images

Here's an example


void Render(Surface dst, Surface src, Rectangle rect)
{
    dst.FitSurface(ResamplingAlgorithm.Bicubic, surfaceXYZ);
}

 
Use a Surface as an inverted Alpha Mask
This allows you to set the Alpha channel of one Surface based on the Alpha channel of another. I use this method in my Jigsaw Puzzle plugin, so I refer to that in my example.

 

Spoiler

The puzzle cut lines are drawn on a separate transparent Surface. Then the Alpha channel of the Source Surface is subtracted from the Alpha value of the puzzle Surface.
 


void Render(Surface dst, Surface src, Rectangle rect)
{
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; x++)
        {
            puzzlePixel = puzzleSurface.GetBilinearSample(x, y); // Grab the current pixel from the puzzle surface

            sourcePixel = src[x, y]; // Grab the current pixel from the Source surface
            sourcePixel.A = Int32Util.ClampToByte(sourcePixel.A - puzzlePixel.A); // Change the Alpha Channel on the Source pixel. ClampToByte ensures the value stays between 0 and 255.

            dst[x, y] = sourcePixel;
        }
    }
}


Unneed unsafe?
The master template in CodeLab places 'unsafe' in the OnRender function, but many times it's not needed.

Spoiler

protected override unsafe void OnRender(Rectangle[] rois, int startIndex, int length)

If your plugin doesn't use any unsafe code, you can shave off 0.5KB from the file size of your compiled DLL.
Delete the 'unsafe' from the OnRender function, and disable Unsafe Code in the project properties (Release and Debug).

 

 

 

Edited by toe_head2001
  • Like 1
  • Upvote 1

Share this post


Link to post
Share on other sites

This:

void Render(Surface dst, Surface src, Rectangle rect)
{
    dst = surfaceXYZ;
}

doesn't actually do anything. You're assigning something that doesn't exist to one of the function's parameters?

Share this post


Link to post
Share on other sites

Question about Dynamic values for IndirectUI sliders.

 

After running the MoveLines plugin below on a standard 800x600 canvas and then choosing a New size canvas 2400x1800 (in the same working session), the selSize is not updated and retains the old size 800x600.
Please, tell me what is wrong with my code?

 

https://www.mediafire.com/file/3osg1n58q319efd/MoveLinesEffect.zip/file

 

Edited by xod

Share this post


Link to post
Share on other sites
10 minutes ago, xod said:

After running the MoveLines plugin below on a standard 800x600 canvas and then choosing a New size canvas 2400x1800 (in the same working session), the selSize is not updated and retains the old size 800x600.

 

Hmm, it seems my Radius Corners plugin also has that issue. 🤔

I'll look into it, and let you know.

  • Like 1

Share this post


Link to post
Share on other sites
57 minutes ago, xod said:

the selSize is not updated and retains the old size 800x600.

 

The selection size is updated correctly; that's not the issue.

 

IndirectUI is not using the new MaxValue. That is to say, even when you use a different value for the maxValue parameter, it retains the old value from the previous plugin run.

(EDIT: to clarify, the UI Control uses the new MaxValue, but the underlying property retains the old one)

So when you move the slider, the value gets clamped to the old MaxValue (since it didn't actually get changed to the new MaxValue).

 

This may or may not be intentional behavior.

 

@Rick Brewster

Edited by toe_head2001
  • Like 1

Share this post


Link to post
Share on other sites

A long time ago, I wrote a test program to see if I could dynamically change the max value. I just recompiled and ran it. It seems, from what I can tell so far, to work correctly. I'm not sure what's going on.

 

        protected override PropertyCollection OnCreatePropertyCollection()
        {
            List<Property> props = new List<Property>();

            int w = EnvironmentParameters.SourceSurface.Width;
            int h = EnvironmentParameters.SourceSurface.Height;
            props.Add(new Int32Property(PropertyNames.Amount1, 1, 1, w));
            props.Add(new Int32Property(PropertyNames.Amount2, 0, 0, h));
            props.Add(new Int32Property(PropertyNames.Amount3, 0, 0, w + h));

            return new PropertyCollection(props);
        }

        protected override ControlInfo OnCreateConfigUI(PropertyCollection props)
        {
            ControlInfo configUI = CreateDefaultConfigUI(props);

            configUI.SetPropertyControlValue(PropertyNames.Amount1, ControlInfoPropertyNames.DisplayName, "Sub-Window Width");
            configUI.SetPropertyControlValue(PropertyNames.Amount2, ControlInfoPropertyNames.DisplayName, "Sub-Window Height");
            configUI.SetPropertyControlValue(PropertyNames.Amount3, ControlInfoPropertyNames.DisplayName, "Sub-Window Width Plus Height");

            return configUI;
        }

 

Share this post


Link to post
Share on other sites

BTW, I'm using PDN version 4.1.5. Are you perhaps using the beta version?

Share this post


Link to post
Share on other sites

Here's a reduced code sample to demonstrate the issue.

 

1) Run the plugin (be sure to click on OK)

2) Open another image with different dimensions

3) Open the plugin again. It will fail on the Assert.

 

Spoiler

using System.Drawing;
using System.Collections.Generic;
using PaintDotNet;
using PaintDotNet.Effects;
using PaintDotNet.PropertySystem;
using System.Diagnostics;

namespace BrokenPropertyEffect
{
    public class BrokenPropertyEffectPlugin : PropertyBasedEffect
    {
        int maxValue;

        public BrokenPropertyEffectPlugin()
            : base("Broken Property", null, null, new EffectOptions() { Flags = EffectFlags.Configurable })
        {
        }

        private enum PropertyNames
        {
            Amount1
        }

        protected override PropertyCollection OnCreatePropertyCollection()
        {
            this.maxValue = EnvironmentParameters.SourceSurface.Width;

            List<Property> props = new List<Property>
            {
                new Int32Property(PropertyNames.Amount1, 1, 0, this.maxValue),
            };

            return new PropertyCollection(props);
        }

        protected override void OnSetRenderInfo(PropertyBasedEffectConfigToken newToken, RenderArgs dstArgs, RenderArgs srcArgs)
        {
            int propMaxValue = newToken.GetProperty<Int32Property>(PropertyNames.Amount1).MaxValue;
            Debug.Assert(propMaxValue == this.maxValue);

            base.OnSetRenderInfo(newToken, dstArgs, srcArgs);
        }

        protected override void OnRender(Rectangle[] renderRects, int startIndex, int length)
        {
            if (length == 0) return;
            for (int i = startIndex; i < startIndex + length; ++i)
            {
                Render(DstArgs.Surface, SrcArgs.Surface, renderRects[i]);
            }
        }

        private void Render(Surface dst, Surface src, Rectangle rect)
        {
            dst.CopySurface(src, rect.Location, rect);
        }
    }
}

 

 

 

16 minutes ago, MJW said:

BTW, I'm using PDN version 4.1.5. Are you perhaps using the beta version? 

 

This is not a new issue with the beta. It has existed for quite some time.

 

24 minutes ago, MJW said:

It seems, from what I can tell so far, to work correctly.

 

The UI Control uses the new MaxValue, but the underlying property retains the old one.

 

Share this post


Link to post
Share on other sites

My plugin has never crashed, but when I added render code that depends on the controls, I discovered that the control's value is sometimes clamped to the wrong max value. Presumably if I'd used the controls for other purposes, it could crash with an exception.

 

  void Render(Surface dst, Surface src, Rectangle rect)
        {
            for (int y = rect.Top; y < rect.Bottom; y++)
            {
                if (IsCancelRequested) return;
                for (int x = rect.Left; x < rect.Right; x++)
                {
                    dst[x, y] = ((x < Amount1) && (y < Amount2)) ? ColorBgra.Red :ColorBgra.White;
                }
            }
        }
        

 

Share this post


Link to post
Share on other sites
42 minutes ago, xod said:

It seems that OptionBasedLibrary does not have this issue.

 

That does not surprise me. Like I said before, it's an issue with IndirectUI.

Share this post


Link to post
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.

Sign in to follow this