Developer Documentation - NinthDesertDude/Dynamic-Draw GitHub Wiki

Thanks for taking a look at developing in Dynamic Draw. For issues, please include the recent actions you took leading up to the bug, and how to reproduce it if you can. For onboarding, keep reading.

Onboarding

Fork this repository, download the code using git fetch with the URL, and make sure you always run visual studio in administrator mode because builds will automatically move files to the paint.net folder on the C:/ drive which is usually blocked by UAC when not in admin mode.

Note: If you installed paint.net to a special location, you'll have to change the output path in the build steps for yourself or copy the output files manually over. If you don't want to do it manually, you're stuck editing it every time you work with Dynamic Draw -- just be sure not to check in the edit (alternatively, leave the build steps intact and just make a .bat file for yourself that you can double-click to run that copies the files over).

Don't forget to test against a recent version of paint.net. While ideally, the plugin will run on older versions of paint.net, there are no guarantees on how far back a certain version of Dynamic Draw will run.

To debug, make sure paint.net is running with the exact same build of the program. Then select Debug -> Attach to Process and select paintdotnet.exe. It will load symbols. Your breakpoints should be shown as solid afterwards; if not, do this again with the attach type as "Managed (.Net Core, .Net 5+)".

Paint.NET plugin lifecycle

Here's how Paint.NET interacts with the plugin:

  • PDN uses Reflection to load the type information, like the effect name and menu location
  • When the effect runs, PDN calls CreateConfigDialog in EffectPlugin.cs to open the dialog
  • The dialog DynamicDrawWindow.cs stores the final render to RenderSettings.cs when done
  • paint.net calls Render in EffectPlugin.cs to copy that bitmap to the screen

How Loading and Saving Settings Works

SettingsSerialization.cs handles loading and serializing permanent settings to/from JSON

  • LoadSavedSettings in SettingsSerialization.cs calls MigrateSettingsFromRegistry to migrate the oldest saves
  • OnShown in DynamicDrawWindow.cs calls MigrateLegacySettings to migrate all older save types

PersistentSettings.cs is used to preserve temporary settings between consecutive runs of the plugin

  • PDN calls InitialInitToken in DynamicDrawWindow.cs to create the config token
  • PDN calls InitTokenFromDialog to setup persistent settings, and InitDialogFromToken to apply at start

How drawing works

Brush strokes begin in the mouse down event handler. They end in the mouse up event handler. The brush drawing occurs during mouse move (and mouse down for single clicks). It draws as lines between the previous point and the current, or continuously, depending on the brush density setting. When the brush is drawn, DrawBrush() is called to actually perform it. Increasingly often, DrawBrush ends up calling a lockbits method in DrawingUtils.cs. The main reason is that blend modes aren't supported by GDI+ and lockbits affords some special handling.

Brush opacity and blend modes use MergeImage to work. When in use, the current bitmap is copied to merged bitmap as the user begins a brush stroke. The affected rectangle area of each copy to bitmap is added to a list called mergeRegions. In the draw step, these are merged down and removed from the list (much faster than merging the entire image every time). This merge step performs the blend modes and opacity on everything in a staging bitmap (which everything is drawn to, except the eraser tool). On finishing a brush stroke, the staging bitmap is cleared transparent, and the merge bitmap is recomputed for the whole image and replaces the current.

How loading brush images works

InitBrushes() loads/reloads all brushes including defaults. ImportBrushImagesFromDirectories() loads thumbnails from the files. BrushImageLoadingWorker handles image loading asynchronously, displaying an image loading bar until complete. All thumbnails are loaded at once and sent to disk in a temporary folder, drawn only when visible in listviewBrushImagePicker.

Note: Since listviewBrushImagePicker is virtualized, you can't directly modify the images property. You need to change the VirtualListSize property and the associated loadedBrushImages variable.

Brush images are identified by absolute filepath, and secondarily by name (true for all default brushes, which don't have separate filepaths).

How the command system works

CommandTarget.cs names the setting or action a command deals with CommandContext.cs names the contexts that a setting can be enabled or disabled in

Command.cs couples these with the inputs to trigger a command and the callback that gets executed. It also includes action data, which is very important for commands that aren't simple actions because it decides how the command deals with its command target. For example, 50|set for the target Rotation would set rotation to 50. The action data is crucial to know what you're doing with rotation here.

CommandMapping.cs contains all built-in commands, keyed by target, coupled with their display name and action data type. Numeric types also include min/max ranges. This is mainly to associate target, action data, and min/max range info to be used by GUI logic in DynamicDrawWindow.cs.

defaultShortcuts in PersistentSetting.cs is a list of arbitrary instantiated commands that acts as all the default shortcuts for the program. They're all marked as built-in with a unique ID so the user can disable them by ID. Only a few shortcuts, like clicks, are hardcoded in forms. When custom shortcuts are loaded as the plugin starts, the built-in shortcuts are added to them (filtering disabled ones that match IDs in the disabled shortcuts list). Saving shortcuts should always be a list that includes no built-in shortcuts.

InitKeyboardShortcuts in DynamicDrawWindow.cs is called when setting up the shortcuts to bind their OnInvoke callbacks to HandleShortcut, which is what carries out each action, and the GetDataAs* functions it calls parse the action data. Shortcuts are invoked when CommandManager.FireShortcuts is called, which happens in OnKeyDown and OnMouseWheel.

Considerations When Contributing

  • Please add comments to your code with high-level understanding, reasons behind architectural choices, etc. and leave out boilerplate comments that only paraphrase.

  • User-facing English should be in Localization/Strings.resx. Due to difficulty and expected low volume of interest, Right-to-left languages aren't supported.

  • If you add or move controls in the main window, adjust tab indexes and add a mouse enter event for tooltips. Try not to open the main window in the winforms designer; it will auto-change so many things that it likely won't run.

  • Feel free to open an issue to discuss contributions or thoughts, or message the author on the paint.net forums.