Beep.Skia extensibility lets third-party developers package diagram families, custom components and commands as standalone extensions. Extensions are loaded at runtime, registered with the component registry, and can be distributed as .beepkg packages through the marketplace.

Extension Model

An extension is a class implementing ISkiaExtension. It declares an identity and the component types it contributes, then optionally registers them with palette categories and commands:

Minimal Extension

using Beep.Skia.Extensions;

public sealed class PnIDExtension : ISkiaExtension
{
    public string Id => "com.acme.beep.pnid";
    public string Name => "Piping & Instrumentation";
    public string Version => "1.0.0";
    public string Description => "ISA S5.1 P&ID node family";

    public IEnumerable<Type> GetComponentTypes() => new[]
    {
        typeof(ValveNode),
        typeof(PumpNode),
        typeof(TankNode),
        typeof(PipeNode)
    };

    public void Initialize(ISkiaExtensionContext context)
    {
        context.RegisterComponent(typeof(ValveNode), "P&ID", "Valve");
        context.RegisterComponent(typeof(PumpNode), "P&ID", "Pump");
        context.RegisterCommand("Reverse Flow", component => { /* ... */ });
        context.Log("P&ID extension initialized");
    }
}

Component types must derive from SkiaComponent and expose a public parameterless constructor. The ISkiaExtensionContext surface is deliberately small: RegisterComponent, RegisterCommand and Log. See Extension SDK for the full walkthrough.

SkiaComponentRegistry

SkiaComponentRegistry is the central catalog of component types. It stores discovered AssemblyClassDefinition entries and serves the palette, serialization and instantiation paths:

MemberPurpose
AddComponent / AddComponents / SetComponentsRegister discovered component definitions
GetComponents() / GetComponentsOrdered()Enumerate all registered components (ordered for palette display)
GetComponentByName / GetComponentByGuidLookups by class name or identifier
GetComponentsByAssembly / GetComponentsByTypeFilter registrations
CreateComponentInstanceInstantiate a component from a definition or class name
IsComponentRegistered / GetAvailableNamespacesDiagnostics and validation
Clear()Reset the registry (used by tests)

Registry Usage

// Enumerate everything the palette will show
foreach (var def in SkiaComponentRegistry.GetComponentsOrdered())
    Console.WriteLine(def.className);

// Create an instance on demand
var component = SkiaComponentRegistry.CreateComponentInstance("ProcessNode");

if (SkiaComponentRegistry.IsComponentRegistered("ProcessNode"))
    Console.WriteLine("ProcessNode is available");

Extension Host

SkiaExtensionHost discovers and activates extensions. It scans assemblies or a plugin directory, instantiates each ISkiaExtension, calls Initialize with a registration context, and exposes the aggregate surface to the host application:

Loading Extensions

var host = new SkiaExtensionHost();

// From already-referenced assemblies
int loaded = host.LoadFromAssemblies(new[] { typeof(PnIDExtension).Assembly });

// Or scan a plugin folder
loaded += host.LoadFromDirectory(@"C:\plugins\beep-skia", "*.dll");

// Create a component contributed by an extension
var gauge = host.CreateComponent("Acme.Widgets.CustomGaugeNode, Acme.Widgets");

// Invoke a command registered by an extension
host.InvokeCommand("Reverse Flow", gauge);

AssemblyHandler Integration

Desktop hosts can combine the extension SDK with the shared BeepDM AssemblyHandler to resolve and load plugin assemblies (including NuGet-backed packages) before calling SkiaExtensionHost.LoadFromAssemblies. The extension SDK itself has no dependency on that infrastructure, so headless and server hosts can load extensions directly from files.

ComponentPalette

ComponentPalette is the visual toolbox. It reads the registry, groups components into categories and exposes drag-and-drop events for the host canvas:

  1. LoadComponentsFromRegistry() builds the Categories collection from SkiaComponentRegistry.GetComponentsOrdered().
  2. Each ComponentCategory exposes its Name, Icon and a collection of ComponentItem entries.
  3. Dragging raises ComponentDragStarted, ComponentDragging and ComponentDragEnded with the item and pointer position.
  4. The host creates the instance with SkiaComponentRegistry.CreateComponentInstance(), positions it and adds it to the DrawingManager.

Wiring the Palette to the Canvas

var palette = new ComponentPalette { X = 10, Y = 50, Width = 320, Height = 600 };
palette.LoadComponentsFromRegistry();

palette.ComponentDragEnded += (s, args) =>
{
    if (args.State != ComponentDragState.Ended) return;

    var component = SkiaComponentRegistry.CreateComponentInstance(args.Component.ClassDefinition);
    if (component == null) return;

    component.X = args.MousePosition.X;
    component.Y = args.MousePosition.Y;

    drawingManager.AddComponent(component);
    drawingManager.RequestRedraw();
};

drawingManager.AddComponent(palette);

Marketplace & Packages

Extensions are distributed as .beepkg packages and installed through ExtensionPackageManager. The manager validates the package (including archive safety checks), tracks installed versions and exposes the directories that the extension host should scan:

MemberPurpose
Install(packagePath, force)Install a .beepkg from disk
InstallFromRegistry(id, version, force)Install from the local registry
Update(id, force)Upgrade an installed extension
Uninstall(id)Remove an installed extension
IsInstalled(id, version) / GetInstalled(id)Query installation state
GetLoadDirectories()Directories to pass to the extension host

Installing an Extension Package

var packages = new ExtensionPackageManager(extensionsRoot);

var result = packages.Install(@"C:\downloads\acme.pnid-1.0.0.beepkg");
if (!result.Success)
    Console.WriteLine(result.Error);

// Load everything that is installed
var host = new SkiaExtensionHost();
foreach (var dir in packages.GetLoadDirectories())
    host.LoadFromDirectory(dir);

Packaging Best Practices

  • Give the package a stable reverse-DNS id (for example com.acme.beep.pnid) and use semantic versions.
  • Keep extension assemblies pure managed code so they load on every supported TFM.
  • Declare component types in GetComponentTypes() and register them in Initialize() so the palette has categories on first load.
  • Never rename component classes after release — serialized diagrams resolve types by name.

For the full packaging format, dependency rules and safety gates, see the Extension Marketplace guide.