MLeaderExtra

Short description

AutoCAD/Civil 3D .NET plugin for adding an extra inflection or bend vertex to an existing MLeader, with command-line tools, handle-based editing, modeless WinForms UI, preview/highlight support, logs, and versioned NETLOAD build output.

Command:

MLTOOLS – open the MLeader Vertex Tool panel.
MLADDVERT – select an MLeader and pick a new bend/inflection vertex.
MLADDVERT_HANDLE – add a vertex by entering an MLeader handle.
MLVERSION – print the loaded plugin version and build metadata.

Description:

MLeaderVertexTool is a Civil 3D-ready AutoCAD .NET Framework plugin for editing multileader geometry by adding a bend or inflection vertex to an existing MLeader. The service reads the selected MLeader, identifies the editable leader and leader-line index, appends a duplicate of the tail vertex when needed, replaces the previous tail with the picked bend point, regenerates the drawing, and reports success or failure. The package also includes a modeless WinForms UI with selection, preview/highlight, log viewing, copy log tools, and build metadata designed to avoid AutoCAD DLL-lock rebuild problems.

Helper function: (if any)​

Core components include Commands.cs, MLeaderVertexService, PreviewCollector, MLeaderToolForm, PreviewEntity, AppLog, C3DTheme, PluginBootstrap, and BuildInfo. The vertex service uses transactions, DocumentLock, MLeader.VerticesCount, GetVertex, AddLastVertex, SetVertex, RecordGraphicsModified, Regen, and handle-to-ObjectId conversion.

Functionalities:

• Opens a dedicated MLeader editing panel with MLTOOLS.
• Adds a new bend/inflection vertex interactively with MLADDVERT.
• Supports handle-based editing with MLADDVERT_HANDLE.
• Validates that the selected object is an AutoCAD MLeader.
• Works through locked documents and AutoCAD transactions.
• Updates MLeader geometry using AddLastVertex and SetVertex.
• Regenerates the drawing after successful edits.
• Provides preview/highlight and logging tools in a WinForms interface.
• Includes version metadata and a build structure that separates compile output from NETLOAD output.

Result:

The plugin lets CAD users add an extra bend point to MLeader annotation without recreating the leader, improving annotation routing around drawing geometry.

Images, animations etc.

Pixel

XAML code:

<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:Autodesk.Windows;assembly=AdWindows">

<src:RibbonToolTip x:Key="MLeaderExtra">
<src:RibbonToolTip.ExpandedContent>
<StackPanel>
<TextBlock Background="AntiqueWhite" TextAlignment="Left">
<Bold>Function Syntax: MLTOOLS / MLADDVERT / MLADDVERT_HANDLE / MLVERSION</Bold>
<LineBreak/>
<Bold>Version: 1.0 Date: 01.07.2026 10:33:18</Bold>
<LineBreak/>
<Bold>Version: 1.1 Date: 01.07.2026 12:16:11PM</Bold>
<LineBreak/>
<Hyperlink>N/A</Hyperlink>
<LineBreak/>
<Bold>Description</Bold><LineBreak/>
<Run Foreground="White">This program is an AutoCAD and Civil 3D .NET plugin used to inspect, preview, select, highlight, and modify existing MLeader objects.</Run><LineBreak/>
<Run Foreground="White">The main purpose of the tool is to let the user select an existing MLeader label and add or update an inflection vertex on the leader arrow line.</Run><LineBreak/>
<Run Foreground="White">The plugin works inside AutoCAD or Civil 3D after the DLL is loaded with NETLOAD.</Run><LineBreak/>
<Bold>General workflow</Bold><LineBreak/>
<Run Foreground="Gainsboro"> The user loads the compiled DLL in AutoCAD or Civil 3D using </Run><Run Foreground="Orange" FontWeight="Bold">NETLOAD</Run><Run Foreground="Gainsboro">.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The program displays its version and build stamp, for example </Run><Run Foreground="LightSkyBlue" FontWeight="Bold">1.0.008_01.07.2026_09.12.41</Run><Run Foreground="Gainsboro">.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The user opens the modeless MLeader tool window.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The user clicks </Run><Run Foreground="LightGreen" FontWeight="Bold">Pick MLeader</Run><Run Foreground="Gainsboro"> and selects an existing MLeader object from the drawing.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The selected MLeader is highlighted in the AutoCAD viewport.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The tool reads the leader indexes, leader line indexes, vertex count, text location, and leader geometry.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The existing leader geometry is displayed in the preview panel.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The user can pick or define a new bend point.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The user clicks </Run><Run Foreground="LightGreen" FontWeight="Bold">Update MLeader</Run><Run Foreground="Gainsboro"> to apply the new inflection vertex to the selected MLeader.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The user can click </Run><Run Foreground="Tomato" FontWeight="Bold">Clear All</Run><Run Foreground="Gainsboro"> to reset the form state, remove selected rows, clear the preview, and remove the viewport highlight.</Run><LineBreak/>
<Bold>Functionalities</Bold><LineBreak/>
<Run Foreground="Gainsboro"> Selects an existing </Run><Run Foreground="LightSkyBlue" FontWeight="Bold">Autodesk.AutoCAD.DatabaseServices.MLeader</Run><Run Foreground="Gainsboro"> object from the drawing.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Opens the selected MLeader for write access before editing it.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Reads the real leader index and leader line index instead of assuming index zero.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Reads the existing MLeader vertices and displays them in a grid.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Draws a 2D preview of the selected leader geometry in the form.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Uses a world-to-preview transform so the leader geometry fits inside the preview panel.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Highlights the selected MLeader in the AutoCAD viewport using </Run><Run Foreground="Orange" FontWeight="Bold">Editor.SetImpliedSelection</Run><Run Foreground="Gainsboro">.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Updates the AutoCAD viewport after selection changes using </Run><Run Foreground="Orange" FontWeight="Bold">Editor.UpdateScreen</Run><Run Foreground="Gainsboro">.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Adds a new inflection vertex by appending the old tail point and converting the previous tail into the new bend point.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Keeps the original end of the leader line stable after inserting the new bend point.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Allows the user to clear the current selection and pick another MLeader without restarting the command.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Uses a modeless WinForms dialog so the user can keep the form open while working in Model Space or Paper Space.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Uses a dark C3D-style interface with themed buttons, panels, preview area, grid, and command log.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Records command actions in a visible log panel.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Writes log entries with timestamps, level names, source names, and method descriptions.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Stores log files locally so previous tool behavior can be reviewed later.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Generates uniquely named NETLOAD DLL copies to avoid rebuilding into a DLL that AutoCAD is already locking.</Run><LineBreak/>
<Bold>Important commands and buttons</Bold><LineBreak/>
<Run Foreground="LightGreen" FontWeight="Bold">Pick MLeader</Run><Run Foreground="Gainsboro"> selects a new MLeader object from the drawing and loads its leader data into the form.</Run><LineBreak/>
<Run Foreground="LightGreen" FontWeight="Bold">Update MLeader</Run><Run Foreground="Gainsboro"> applies the new bend or inflection vertex to the currently selected MLeader.</Run><LineBreak/>
<Run Foreground="Tomato" FontWeight="Bold">Clear All</Run><Run Foreground="Gainsboro"> clears the grid, preview, selected object, selected point, current highlight, and current form state.</Run><LineBreak/>
<Run Foreground="Orange" FontWeight="Bold">MLVERSION</Run><Run Foreground="Gainsboro"> reports the loaded plugin version, build stamp, and NETLOAD path.</Run><LineBreak/>
<Run Foreground="Orange" FontWeight="Bold">NETLOAD</Run><Run Foreground="Gainsboro"> is the AutoCAD command used to load the generated DLL.</Run><LineBreak/>
<Bold>How the MLeader update works</Bold><LineBreak/>
<Run Foreground="Gainsboro"> The program starts a transaction in the active AutoCAD database.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The selected object is opened as </Run><Run Foreground="LightSkyBlue" FontWeight="Bold">MLeader</Run><Run Foreground="Gainsboro"> with write access.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The program calls </Run><Run Foreground="Orange" FontWeight="Bold">GetLeaderIndexes</Run><Run Foreground="Gainsboro"> to find the actual leader cluster.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The program calls </Run><Run Foreground="Orange" FontWeight="Bold">GetLeaderLineIndexes</Run><Run Foreground="Gainsboro"> to find the actual leader line.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The program calls </Run><Run Foreground="Orange" FontWeight="Bold">VerticesCount</Run><Run Foreground="Gainsboro"> to determine how many points already exist on the leader line.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The program gets the old last vertex so the original leader endpoint can be preserved.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The program calls </Run><Run Foreground="Orange" FontWeight="Bold">AddLastVertex</Run><Run Foreground="Gainsboro"> to duplicate the old tail point at the end of the leader line.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The program calls </Run><Run Foreground="Orange" FontWeight="Bold">SetVertex</Run><Run Foreground="Gainsboro"> to replace the previous tail point with the new inflection point.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The result is a leader line with one additional bend while keeping the original final endpoint.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The transaction is committed and the graphics are marked as modified.</Run><LineBreak/>
<Bold>Viewport highlight behavior</Bold><LineBreak/>
<Run Foreground="Gainsboro"> When a row or selected MLeader changes, the corresponding AutoCAD object is highlighted in the drawing.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The highlight is performed with </Run><Run Foreground="Orange" FontWeight="Bold">Editor.SetImpliedSelection</Run><Run Foreground="Gainsboro">.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The screen is refreshed with </Run><Run Foreground="Orange" FontWeight="Bold">Editor.UpdateScreen</Run><Run Foreground="Gainsboro">.</Run><LineBreak/>
<Run Foreground="Gainsboro"> When </Run><Run Foreground="Tomato" FontWeight="Bold">Clear All</Run><Run Foreground="Gainsboro"> is used, the implied selection is cleared so the previous MLeader is no longer highlighted.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Before selecting a new label, the old highlight is cleared to avoid confusing the old object with the new one.</Run><LineBreak/>
<Bold>Preview panel behavior</Bold><LineBreak/>
<Run Foreground="Gainsboro"> The preview panel does not use AutoCAD drawing APIs after the points are collected.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The program converts world coordinates into panel pixel coordinates.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The scale is calculated from the available panel width and height.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The preview keeps the leader geometry centered and visible.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The leader line is drawn with standard WinForms graphics drawing.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Important vertices are shown with visual markers so the user can recognize the bend points.</Run><LineBreak/>
<Bold>User interface behavior</Bold><LineBreak/>
<Run Foreground="Gainsboro"> The form uses a dark Civil 3D-inspired theme.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The form uses </Run><Run Foreground="LightSkyBlue" FontWeight="Bold">SizableToolWindow</Run><Run Foreground="Gainsboro"> so it stays compact and suitable for AutoCAD work.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The form is modeless, so the user can continue working in the drawing while it remains open.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Pressing Escape is intercepted so the form is not closed accidentally while the user works in the viewport.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The interface includes a header, toolbar, grid, preview panel, command log, and status area.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Important buttons use different colors based on their role.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Update actions use green styling because they perform the main positive operation.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Clear or destructive actions use red or dark red styling because they reset state or remove selection.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Informational actions use blue or orange styling because they show state, version, or command information.</Run><LineBreak/>
<Bold>Logging behavior</Bold><LineBreak/>
<Run Foreground="Gainsboro"> The tool displays a live command log inside the form.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Each log entry includes the current time, severity level, component name, and action description.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Example log format is </Run><Run Foreground="LightSkyBlue" FontWeight="Bold">05:29:43 [INFO] [PvSplitApi] SplitDataLines.Add(Double rawStation, Double datum)</Run><Run Foreground="Gainsboro">.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Informational messages are used for normal selection, loading, preview, and update operations.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Warning messages are used when a selection is missing or an operation cannot continue.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Error messages are used when AutoCAD rejects the operation or the selected object cannot be edited.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Log files are stored locally so previous behavior can be inspected after the session.</Run><LineBreak/>
<Bold>Build and loading behavior</Bold><LineBreak/>
<Run Foreground="Gainsboro"> The project targets </Run><Run Foreground="LightSkyBlue" FontWeight="Bold">x64</Run><Run Foreground="Gainsboro"> because AutoCAD, Civil 3D, and ObjectARX managed references are AMD64.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The project imports the Civil 3D and ObjectARX reference file from the user path.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Intermediate build files are redirected outside the OneDrive project folder to avoid Windows path-length issues.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Compile output and NETLOAD output are separated.</Run><LineBreak/>
<Run Foreground="Gainsboro"> Each build creates a uniquely named DLL copy with a build stamp.</Run><LineBreak/>
<Run Foreground="Gainsboro"> This avoids the common AutoCAD problem where a loaded DLL cannot be overwritten because AutoCAD keeps it locked.</Run><LineBreak/>
<Run Foreground="Gainsboro"> The build prints the exact DLL path that should be loaded with NETLOAD.</Run><LineBreak/>
<Bold>Important limitations</Bold><LineBreak/>
<Run Foreground="DarkRed" FontWeight="Bold"> The selected object must be an MLeader.</Run><LineBreak/>
<Run Foreground="DarkRed" FontWeight="Bold"> The MLeader must be opened for write access before it can be modified.</Run><LineBreak/>
<Run Foreground="DarkRed" FontWeight="Bold"> The MLeader style must allow enough leader points for an additional vertex.</Run><LineBreak/>
<Run Foreground="DarkRed" FontWeight="Bold"> The tool must run on the AutoCAD main thread when highlighting objects in the viewport.</Run><LineBreak/>
<Run Foreground="DarkRed" FontWeight="Bold"> If AutoCAD already loaded a previous DLL, rebuilds must use the uniquely generated NETLOAD DLL path instead of overwriting the locked DLL.</Run><LineBreak/>
<Run Foreground="DarkRed" FontWeight="Bold"> If the selected leader has unusual geometry or no leader line indexes, the update operation cannot continue safely.</Run><LineBreak/>
<Bold>Result</Bold><LineBreak/>
<Run Foreground="White">This tool gives the user a controlled graphical workflow for editing MLeader arrow geometry without manually using grips in AutoCAD.</Run><LineBreak/>
<Run Foreground="White">It improves recognition of selected labels through viewport highlighting, improves confidence through the preview panel, and improves debugging through persistent command logging.</Run><LineBreak/>
<Run Foreground="White">The final result is an updated MLeader with an additional inflection vertex on the leader arrow line.</Run><LineBreak/>

</TextBlock>
<Grid>
<Image Source="PNG" Stretch="Uniform"/>
</Grid>
<Grid>
<MediaElement Source="GIF" Stretch="Uniform" Visibility="Visible"/>
</Grid>
</StackPanel>
</src:RibbonToolTip.ExpandedContent>
</src:RibbonToolTip>
</ResourceDictionary>

C# code:

// ===== FILE: Commands.cs =====
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using MLeaderVertexTool.Services;
using MLeaderVertexTool.UI;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

namespace MLeaderVertexTool
{
    public sealed class Commands
    {
        private static MLeaderToolForm _form;

        [CommandMethod("MLTOOLS", CommandFlags.Session)]
        public static void ShowTools()
        {
            const string Source = "Commands";
            AppLog.CommandStart(Source, "MLTOOLS ShowTools()");

            Document doc = AcAp.DocumentManager.MdiActiveDocument;
            if (doc == null)
            {
                return;
            }

            if (_form == null || _form.IsDisposed)
            {
                _form = new MLeaderToolForm(doc);
                AcAp.ShowModelessDialog(_form);
                AppLog.Info(Source, "Opened modeless MLeader tool form.");
            }
            else
            {
                _form.Activate();
                _form.RefreshDocument(doc);
                AppLog.Info(Source, "Activated existing modeless MLeader tool form.");
            }
        }

        [CommandMethod("MLADDVERT")]
        public static void AddVertexBySelection()
        {
            const string Source = "Commands";
            AppLog.CommandStart(Source, "MLADDVERT AddVertexBySelection()");

            Document doc = AcAp.DocumentManager.MdiActiveDocument;
            if (doc == null)
            {
                return;
            }

            Editor ed = doc.Editor;

            PromptEntityOptions entityOptions = new PromptEntityOptions("\nSelect MLeader: ");
            entityOptions.SetRejectMessage("\nObject must be an MLeader.");
            entityOptions.AddAllowedClass(typeof(MLeader), false);

            PromptEntityResult entityResult = ed.GetEntity(entityOptions);
            if (entityResult.Status != PromptStatus.OK)
            {
                AppLog.Info(Source, "Selection cancelled.");
                return;
            }

            PromptPointResult pointResult = ed.GetPoint("\nPick new inflection/bend vertex: ");
            if (pointResult.Status != PromptStatus.OK)
            {
                AppLog.Info(Source, "Point pick cancelled.");
                return;
            }

            Point3d bendPointWcs = pointResult.Value.TransformBy(ed.CurrentUserCoordinateSystem);
            string message;
            bool ok = MLeaderVertexService.AddInflectionVertex(doc, entityResult.ObjectId, bendPointWcs, out message);
            ed.WriteMessage("\n[MLeaderVertexTool] {0}\n", message);

            if (!ok)
            {
                AppLog.Warn(Source, message);
            }
        }

        [CommandMethod("MLADDVERT_HANDLE")]
        public static void AddVertexByHandle()
        {
            const string Source = "Commands";
            AppLog.CommandStart(Source, "MLADDVERT_HANDLE AddVertexByHandle()");

            Document doc = AcAp.DocumentManager.MdiActiveDocument;
            if (doc == null)
            {
                return;
            }

            Editor ed = doc.Editor;

            PromptStringOptions stringOptions = new PromptStringOptions("\nMLeader handle <36ED4>: ");
            stringOptions.AllowSpaces = false;
            stringOptions.DefaultValue = "36ED4";
            stringOptions.UseDefaultValue = true;

            PromptResult stringResult = ed.GetString(stringOptions);
            if (stringResult.Status != PromptStatus.OK)
            {
                AppLog.Info(Source, "Handle prompt cancelled.");
                return;
            }

            string handleText = stringResult.StringResult;

            ObjectId id;
            string message;
            if (!MLeaderVertexService.TryGetObjectIdFromHandle(doc.Database, handleText, out id, out message))
            {
                ed.WriteMessage("\n[MLeaderVertexTool] {0}\n", message);
                return;
            }

            PromptPointResult pointResult = ed.GetPoint("\nPick new inflection/bend vertex: ");
            if (pointResult.Status != PromptStatus.OK)
            {
                AppLog.Info(Source, "Point pick cancelled.");
                return;
            }

            Point3d bendPointWcs = pointResult.Value.TransformBy(ed.CurrentUserCoordinateSystem);
            bool ok = MLeaderVertexService.AddInflectionVertex(doc, id, bendPointWcs, out message);
            ed.WriteMessage("\n[MLeaderVertexTool] {0}\n", message);

            if (!ok)
            {
                AppLog.Warn(Source, message);
            }
        }

        [CommandMethod("MLVERSION")]
        public static void Version()
        {
            const string Source = "Commands";
            AppLog.CommandStart(Source, "MLVERSION Version()");

            Document doc = AcAp.DocumentManager.MdiActiveDocument;
            if (doc != null)
            {
                doc.Editor.WriteMessage("\n[MLeaderVertexTool] Version: {0}", BuildInfo.LoadVersion);
                if (!string.IsNullOrWhiteSpace(BuildInfo.BuildStamp))
                {
                    doc.Editor.WriteMessage("\n[MLeaderVertexTool] BuildStamp: {0}", BuildInfo.BuildStamp);
                }
                if (!string.IsNullOrWhiteSpace(BuildInfo.NetLoadPath))
                {
                    doc.Editor.WriteMessage("\n[MLeaderVertexTool] NETLOAD path: {0}", BuildInfo.NetLoadPath);
                }
                doc.Editor.WriteMessage("\n[MLeaderVertexTool] Logs: {0}\n", AppLog.LogsFolder);
            }
        }
    }
}


// ===== FILE: PluginBootstrap.cs =====
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using MLeaderVertexTool.Services;

namespace MLeaderVertexTool
{
    public sealed class PluginBootstrap : IExtensionApplication
    {
        public void Initialize()
        {
            AppLog.Loaded("PluginBootstrap");

            Document doc = Application.DocumentManager.MdiActiveDocument;
            if (doc != null)
            {
                Editor ed = doc.Editor;
                ed.WriteMessage("\n[MLeaderVertexTool] Loaded {0}", BuildInfo.LoadVersion);
                if (!string.IsNullOrWhiteSpace(BuildInfo.BuildStamp))
                {
                    ed.WriteMessage("\n[MLeaderVertexTool] BuildStamp: {0}", BuildInfo.BuildStamp);
                }
                if (!string.IsNullOrWhiteSpace(BuildInfo.NetLoadPath))
                {
                    ed.WriteMessage("\n[MLeaderVertexTool] NETLOAD path: {0}", BuildInfo.NetLoadPath);
                }
                ed.WriteMessage("\n[MLeaderVertexTool] Commands: MLTOOLS, MLADDVERT, MLADDVERT_HANDLE, MLVERSION");
                ed.WriteMessage("\n[MLeaderVertexTool] Logs: {0}\n", AppLog.LogsFolder);
            }
        }

        public void Terminate()
        {
            AppLog.Info("PluginBootstrap", "Terminate() " + BuildInfo.LoadVersion);
        }
    }
}


// ===== FILE: BuildInfo.cs =====
using System.Linq;
using System.Reflection;

namespace MLeaderVertexTool
{
    internal static class BuildInfo
    {
        // Package version is updated for each delivered ZIP.
        public const string SemanticVersion = "1.0.008";
        public const string PackageCreatedAt = "01.07.2026_09.12.41";

        // These are generated at compile time by Directory.Build.targets.
        // If the generated metadata is missing, the packaged timestamp remains the fallback.
        public static string CreatedAt { get { return AssemblyMetadata("BuildCreatedAt", PackageCreatedAt); } }
        public static string BuildStamp { get { return AssemblyMetadata("BuildStamp", ""); } }
        public static string NetLoadPath { get { return AssemblyMetadata("NetLoadPath", ""); } }
        public static string LoadVersion { get { return SemanticVersion + "_" + CreatedAt; } }

        private static string AssemblyMetadata(string key, string fallback)
        {
            try
            {
                Assembly asm = typeof(BuildInfo).Assembly;
                AssemblyMetadataAttribute found = asm.GetCustomAttributes<AssemblyMetadataAttribute>()
                    .FirstOrDefault(a => a.Key == key);
                return found == null || string.IsNullOrWhiteSpace(found.Value) ? fallback : found.Value;
            }
            catch
            {
                return fallback;
            }
        }
    }
}


// ===== FILE: UI\C3DTheme.cs =====
using System;
using System.Drawing;
using System.Windows.Forms;

namespace MLeaderVertexTool.UI
{
    internal enum C3DButtonRole
    {
        Neutral,
        Primary,
        Refresh,
        Warn,
        Danger
    }

    internal static class C3DTheme
    {
        public static readonly System.Drawing.Color Bg = FromHex("#2b2d3e");
        public static readonly System.Drawing.Color Panel = FromHex("#23253a");
        public static readonly System.Drawing.Color Border = FromHex("#3e4060");
        public static readonly System.Drawing.Color HeaderBg = FromHex("#1e2030");
        public static readonly System.Drawing.Color Bg2 = FromHex("#23253a");
        public static readonly System.Drawing.Color Bg3 = FromHex("#1e2030");
        public static readonly System.Drawing.Color Fg = FromHex("#dde1f0");
        public static readonly System.Drawing.Color Muted = FromHex("#7a7f9a");
        public static readonly System.Drawing.Color Accent = FromHex("#2979d4");
        public static readonly System.Drawing.Color TextWhite = FromHex("#ffffff");
        public static readonly System.Drawing.Color SelectionBg = FromHex("#3a5fa0");
        public static readonly System.Drawing.Color SelectionFg = FromHex("#ffffff");
        public static readonly System.Drawing.Color CodeBg = FromHex("#1e2030");
        public static readonly System.Drawing.Color CodeFg = FromHex("#dde1f0");
        public static readonly System.Drawing.Color Entry = FromHex("#1e2030");
        public static readonly System.Drawing.Color Blue = FromHex("#2979d4");
        public static readonly System.Drawing.Color Green = FromHex("#2a9e52");
        public static readonly System.Drawing.Color Red = FromHex("#c0392b");
        public static readonly System.Drawing.Color Orange = FromHex("#e06c2a");
        public static readonly System.Drawing.Color Purple = FromHex("#7c4dbd");
        public static readonly System.Drawing.Color Grey = FromHex("#3e4060");
        public static readonly System.Drawing.Color Reload = FromHex("#4a5070");

        public static readonly System.Drawing.Font FontNormal = new System.Drawing.Font("Segoe UI", 9.0f, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
        public static readonly System.Drawing.Font FontSmall = new System.Drawing.Font("Segoe UI", 8.0f, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
        public static readonly System.Drawing.Font FontTitle = new System.Drawing.Font("Segoe UI", 10.0f, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
        public static readonly System.Drawing.Font FontButton = new System.Drawing.Font("Segoe UI", 9.0f, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
        public static readonly System.Drawing.Font FontButtonBold = new System.Drawing.Font("Segoe UI", 9.0f, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
        public static readonly System.Drawing.Font FontCode = new System.Drawing.Font("Consolas", 10.0f, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
        public static readonly System.Drawing.Font FontCodeSmall = new System.Drawing.Font("Consolas", 8.5f, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);

        public static System.Drawing.Color FromHex(string hex)
        {
            return System.Drawing.ColorTranslator.FromHtml(hex);
        }

        public static System.Drawing.Color Lighten(System.Drawing.Color colour, int amount)
        {
            return System.Drawing.Color.FromArgb(
                colour.A,
                Math.Min(255, colour.R + amount),
                Math.Min(255, colour.G + amount),
                Math.Min(255, colour.B + amount));
        }

        public static void ApplyForm(Form form)
        {
            form.BackColor = Bg;
            form.ForeColor = Fg;
            form.Font = FontNormal;
        }

        public static void ApplyTable(TableLayoutPanel table)
        {
            table.BackColor = Bg;
            table.ForeColor = Fg;
        }

        public static void ApplyPanel(Control control)
        {
            control.BackColor = Panel;
            control.ForeColor = Fg;
        }

        public static void ApplyHeaderPanel(Control control)
        {
            control.BackColor = HeaderBg;
            control.ForeColor = TextWhite;
        }

        public static void ApplyToolbar(FlowLayoutPanel toolbar)
        {
            toolbar.BackColor = Panel;
            toolbar.ForeColor = Fg;
        }

        public static void ApplyTitleLabel(Label label)
        {
            label.BackColor = HeaderBg;
            label.ForeColor = TextWhite;
            label.Font = FontTitle;
        }

        public static void ApplyMutedHeaderLabel(Label label)
        {
            label.BackColor = HeaderBg;
            label.ForeColor = Muted;
            label.Font = FontSmall;
        }

        public static void ApplySectionLabel(Label label)
        {
            label.BackColor = Panel;
            label.ForeColor = Accent;
            label.Font = FontButtonBold;
        }

        public static void ApplyStatusLabel(Label label)
        {
            label.BackColor = Panel;
            label.ForeColor = Muted;
            label.Font = FontSmall;
        }

        public static void ApplyButton(Button button, C3DButtonRole role)
        {
            button.FlatStyle = FlatStyle.Flat;
            button.UseVisualStyleBackColor = false;
            button.Cursor = Cursors.Hand;
            button.Font = role == C3DButtonRole.Primary ? FontButtonBold : FontButton;
            button.Height = 28;
            button.Margin = new Padding(4, 4, 4, 4);
            button.FlatAppearance.BorderSize = 0;

            System.Drawing.Color background;
            System.Drawing.Color foreground;
            System.Drawing.Color activeBackground;

            switch (role)
            {
                case C3DButtonRole.Primary:
                    background = Green;
                    foreground = TextWhite;
                    activeBackground = Lighten(Green, 22);
                    break;
                case C3DButtonRole.Refresh:
                    background = Blue;
                    foreground = TextWhite;
                    activeBackground = Lighten(Blue, 22);
                    break;
                case C3DButtonRole.Warn:
                    background = Orange;
                    foreground = TextWhite;
                    activeBackground = Lighten(Orange, 22);
                    break;
                case C3DButtonRole.Danger:
                    background = Panel;
                    foreground = Red;
                    activeBackground = Border;
                    break;
                default:
                    background = Panel;
                    foreground = Fg;
                    activeBackground = Border;
                    break;
            }

            button.BackColor = background;
            button.ForeColor = foreground;
            button.FlatAppearance.MouseOverBackColor = activeBackground;
            button.FlatAppearance.MouseDownBackColor = Lighten(activeBackground, 8);
            button.FlatAppearance.BorderColor = Border;
        }

        public static void ApplyGrid(DataGridView grid)
        {
            grid.BackgroundColor = CodeBg;
            grid.BorderStyle = BorderStyle.None;
            grid.EnableHeadersVisualStyles = false;
            grid.GridColor = Border;
            grid.RowTemplate.Height = 24;

            grid.ColumnHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single;
            grid.ColumnHeadersDefaultCellStyle.BackColor = HeaderBg;
            grid.ColumnHeadersDefaultCellStyle.ForeColor = TextWhite;
            grid.ColumnHeadersDefaultCellStyle.SelectionBackColor = HeaderBg;
            grid.ColumnHeadersDefaultCellStyle.SelectionForeColor = TextWhite;
            grid.ColumnHeadersDefaultCellStyle.Font = FontButtonBold;

            grid.DefaultCellStyle.BackColor = CodeBg;
            grid.DefaultCellStyle.ForeColor = Fg;
            grid.DefaultCellStyle.SelectionBackColor = SelectionBg;
            grid.DefaultCellStyle.SelectionForeColor = SelectionFg;
            grid.DefaultCellStyle.Font = FontCodeSmall;

            grid.AlternatingRowsDefaultCellStyle.BackColor = Bg3;
            grid.AlternatingRowsDefaultCellStyle.ForeColor = Fg;
            grid.AlternatingRowsDefaultCellStyle.SelectionBackColor = SelectionBg;
            grid.AlternatingRowsDefaultCellStyle.SelectionForeColor = SelectionFg;

            grid.RowHeadersDefaultCellStyle.BackColor = HeaderBg;
            grid.RowHeadersDefaultCellStyle.ForeColor = Fg;
        }

        public static void ApplyTextBox(TextBox textBox)
        {
            textBox.BackColor = CodeBg;
            textBox.ForeColor = CodeFg;
            textBox.Font = FontCodeSmall;
            textBox.BorderStyle = BorderStyle.FixedSingle;
        }

        public static void ApplyPreview(PictureBox preview)
        {
            preview.BackColor = CodeBg;
        }
    }
}


// ===== FILE: UI\MLeaderToolForm.cs =====
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using MLeaderVertexTool.Models;
using MLeaderVertexTool.Services;

using AcAp = Autodesk.AutoCAD.ApplicationServices.Application;

namespace MLeaderVertexTool.UI
{
    internal sealed class MLeaderToolForm : Form
    {
        private const int MaxVisibleLogCharacters = 120000;

        private readonly List<PreviewEntity> _entities = new List<PreviewEntity>();
        private Document _doc;
        private DataGridView _grid;
        private PictureBox _preview;
        private TextBox _log;
        private Label _versionLabel;
        private Label _statusLabel;
        private Button _selectButton;
        private Button _pickMLeaderButton;
        private Button _addVertexButton;
        private Button _clearAllButton;
        private Button _clearHighlightButton;
        private Button _openLogsButton;
        private Button _copyLogButton;

        public MLeaderToolForm(Document doc)
        {
            _doc = doc;
            InitializeComponent();

            AppLog.LineWritten += OnLogLineWritten;
            AppLog.Info("MLeaderToolForm", "MLeaderToolForm(Document doc)");
        }

        public void RefreshDocument(Document doc)
        {
            if (doc != null)
            {
                _doc = doc;
                AppLog.Info("MLeaderToolForm", "RefreshDocument(Document doc) active document refreshed.");
            }
        }

        protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
        {
            if (keyData == Keys.Escape)
            {
                AppLog.Debug("MLeaderToolForm", "Escape key swallowed by ProcessCmdKey.");
                return true;
            }

            return base.ProcessCmdKey(ref msg, keyData);
        }

        protected override void OnFormClosed(FormClosedEventArgs e)
        {
            AppLog.LineWritten -= OnLogLineWritten;
            MLeaderVertexService.ClearHighlight(_doc);
            base.OnFormClosed(e);
        }

        private void InitializeComponent()
        {
            Text = "MLeader Vertex Tool";
            Width = 1080;
            Height = 700;
            MinimumSize = new Size(780, 460);
            FormBorderStyle = FormBorderStyle.SizableToolWindow;
            ShowInTaskbar = false;
            KeyPreview = true;
            StartPosition = FormStartPosition.CenterScreen;
            C3DTheme.ApplyForm(this);

            TableLayoutPanel root = new TableLayoutPanel();
            root.Dock = DockStyle.Fill;
            root.RowCount = 4;
            root.ColumnCount = 1;
            root.Padding = new Padding(0);
            root.Margin = new Padding(0);
            root.RowStyles.Add(new RowStyle(SizeType.Absolute, 58));
            root.RowStyles.Add(new RowStyle(SizeType.Absolute, 48));
            root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
            root.RowStyles.Add(new RowStyle(SizeType.Absolute, 166));
            C3DTheme.ApplyTable(root);
            Controls.Add(root);

            Panel header = CreateHeaderPanel();
            root.Controls.Add(header, 0, 0);

            FlowLayoutPanel toolbar = CreateToolbar();
            root.Controls.Add(toolbar, 0, 1);

            SplitContainer split = new SplitContainer();
            split.Dock = DockStyle.Fill;
            split.Orientation = Orientation.Vertical;
            split.SplitterDistance = 470;
            split.BackColor = C3DTheme.Border;
            split.Panel1.BackColor = C3DTheme.Bg;
            split.Panel2.BackColor = C3DTheme.Bg;
            split.Margin = new Padding(8, 0, 8, 0);
            root.Controls.Add(split, 0, 2);

            _grid = CreateGrid();
            split.Panel1.Controls.Add(CreateSection("Selected / picked entities", _grid));

            _preview = new PictureBox();
            _preview.Dock = DockStyle.Fill;
            _preview.Margin = new Padding(0);
            _preview.Paint += Preview_Paint;
            C3DTheme.ApplyPreview(_preview);
            split.Panel2.Controls.Add(CreateSection("2D preview", _preview));

            _log = new TextBox();
            _log.Dock = DockStyle.Fill;
            _log.Multiline = true;
            _log.ReadOnly = true;
            _log.ScrollBars = ScrollBars.Vertical;
            _log.Margin = new Padding(0);
            C3DTheme.ApplyTextBox(_log);
            root.Controls.Add(CreateSection("Command log", _log), 0, 3);

            SetStatus("Ready.");
        }

        private Panel CreateHeaderPanel()
        {
            Panel header = new Panel();
            header.Dock = DockStyle.Fill;
            header.Padding = new Padding(12, 8, 12, 6);
            C3DTheme.ApplyHeaderPanel(header);
            header.Paint += SectionBorder_Paint;

            Label title = new Label();
            title.Dock = DockStyle.Top;
            title.Height = 22;
            title.TextAlign = ContentAlignment.MiddleLeft;
            title.Text = "MLeader Vertex Tool";
            C3DTheme.ApplyTitleLabel(title);
            header.Controls.Add(title);

            _versionLabel = new Label();
            _versionLabel.Dock = DockStyle.Bottom;
            _versionLabel.Height = 18;
            _versionLabel.TextAlign = ContentAlignment.MiddleLeft;
            _versionLabel.Text = "Loaded: " + BuildInfo.LoadVersion;
            C3DTheme.ApplyMutedHeaderLabel(_versionLabel);
            header.Controls.Add(_versionLabel);

            return header;
        }

        private FlowLayoutPanel CreateToolbar()
        {
            FlowLayoutPanel toolbar = new FlowLayoutPanel();
            toolbar.Dock = DockStyle.Fill;
            toolbar.FlowDirection = System.Windows.Forms.FlowDirection.LeftToRight;
            toolbar.Padding = new Padding(8, 6, 8, 6);
            toolbar.Margin = new Padding(0);
            toolbar.WrapContents = false;
            C3DTheme.ApplyToolbar(toolbar);

            _selectButton = CreateButton("Load selection", 112, C3DButtonRole.Refresh, SelectButton_Click);
            toolbar.Controls.Add(_selectButton);

            _pickMLeaderButton = CreateButton("Pick MLeader", 112, C3DButtonRole.Primary, PickMLeaderButton_Click);
            toolbar.Controls.Add(_pickMLeaderButton);

            _addVertexButton = CreateButton("Add vertex", 100, C3DButtonRole.Warn, AddVertexButton_Click);
            toolbar.Controls.Add(_addVertexButton);

            _clearAllButton = CreateButton("Clear all", 86, C3DButtonRole.Danger, ClearAllButton_Click);
            toolbar.Controls.Add(_clearAllButton);

            _clearHighlightButton = CreateButton("Clear highlight", 112, C3DButtonRole.Neutral, ClearHighlightButton_Click);
            toolbar.Controls.Add(_clearHighlightButton);

            _openLogsButton = CreateButton("Open logs", 90, C3DButtonRole.Neutral, OpenLogsButton_Click);
            toolbar.Controls.Add(_openLogsButton);

            _copyLogButton = CreateButton("Copy log", 86, C3DButtonRole.Neutral, CopyLogButton_Click);
            toolbar.Controls.Add(_copyLogButton);

            _statusLabel = new Label();
            _statusLabel.AutoSize = true;
            _statusLabel.TextAlign = ContentAlignment.MiddleLeft;
            _statusLabel.Padding = new Padding(12, 8, 0, 0);
            _statusLabel.Margin = new Padding(8, 4, 0, 0);
            C3DTheme.ApplyStatusLabel(_statusLabel);
            toolbar.Controls.Add(_statusLabel);

            return toolbar;
        }

        private Button CreateButton(string text, int width, C3DButtonRole role, EventHandler clickHandler)
        {
            Button button = new Button();
            button.Text = text;
            button.Width = width;
            button.Click += clickHandler;
            C3DTheme.ApplyButton(button, role);
            return button;
        }

        private DataGridView CreateGrid()
        {
            DataGridView grid = new DataGridView();
            grid.Dock = DockStyle.Fill;
            grid.AllowUserToAddRows = false;
            grid.AllowUserToDeleteRows = false;
            grid.AllowUserToResizeRows = false;
            grid.MultiSelect = false;
            grid.ReadOnly = true;
            grid.RowHeadersVisible = false;
            grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
            grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
            grid.Margin = new Padding(0);
            grid.Columns.Add("Handle", "Handle");
            grid.Columns.Add("Type", "Type");
            grid.Columns.Add("Layer", "Layer");
            grid.Columns.Add("Points", "Points");
            grid.SelectionChanged += Grid_SelectionChanged;
            C3DTheme.ApplyGrid(grid);
            return grid;
        }

        private TableLayoutPanel CreateSection(string title, Control body)
        {
            TableLayoutPanel section = new TableLayoutPanel();
            section.Dock = DockStyle.Fill;
            section.RowCount = 2;
            section.ColumnCount = 1;
            section.Padding = new Padding(8);
            section.Margin = new Padding(0);
            section.RowStyles.Add(new RowStyle(SizeType.Absolute, 26));
            section.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
            C3DTheme.ApplyPanel(section);
            section.Paint += SectionBorder_Paint;

            Label titleLabel = new Label();
            titleLabel.Dock = DockStyle.Fill;
            titleLabel.TextAlign = ContentAlignment.MiddleLeft;
            titleLabel.Text = title;
            C3DTheme.ApplySectionLabel(titleLabel);
            section.Controls.Add(titleLabel, 0, 0);

            body.Dock = DockStyle.Fill;
            section.Controls.Add(body, 0, 1);

            return section;
        }

        private void SectionBorder_Paint(object sender, PaintEventArgs e)
        {
            Control control = sender as Control;
            if (control == null)
            {
                return;
            }

            Rectangle rect = control.ClientRectangle;
            rect.Width -= 1;
            rect.Height -= 1;

            using (Pen pen = new Pen(C3DTheme.Border, 1.0f))
            {
                e.Graphics.DrawRectangle(pen, rect);
            }
        }

        private void SelectButton_Click(object sender, EventArgs e)
        {
            AppLog.CommandStart("MLeaderToolForm", "SelectButton_Click(object sender, EventArgs e)");
            RefreshDocument(AcAp.DocumentManager.MdiActiveDocument);

            IList<PreviewEntity> collected = PreviewCollector.SelectAndCollect(_doc);
            _entities.Clear();
            _entities.AddRange(collected);
            RebuildGrid();
            _preview.Invalidate();
            SetStatus(string.Format("Loaded {0} selected object(s).", _entities.Count));
        }

        private void PickMLeaderButton_Click(object sender, EventArgs e)
        {
            AppLog.CommandStart("MLeaderToolForm", "PickMLeaderButton_Click(object sender, EventArgs e)");
            RefreshDocument(AcAp.DocumentManager.MdiActiveDocument);

            if (_doc == null)
            {
                AppLog.Warn("MLeaderToolForm", "No active AutoCAD document.");
                SetStatus("No active AutoCAD document.");
                return;
            }

            MLeaderVertexService.ClearHighlight(_doc);

            Editor ed = _doc.Editor;
            PromptEntityOptions options = new PromptEntityOptions("\nPick MLeader to edit: ");
            options.SetRejectMessage("\nObject must be an MLeader.");
            options.AddAllowedClass(typeof(MLeader), false);

            PromptEntityResult result = ed.GetEntity(options);
            if (result.Status != PromptStatus.OK)
            {
                AppLog.Info("MLeaderToolForm", "MLeader pick cancelled.");
                SetStatus("MLeader pick cancelled.");
                return;
            }

            IList<PreviewEntity> collected = PreviewCollector.CollectByObjectIds(_doc, new[] { result.ObjectId });
            _entities.Clear();
            _entities.AddRange(collected);
            RebuildGrid();

            if (_grid.Rows.Count > 0)
            {
                _grid.Rows[0].Selected = true;
                _grid.CurrentCell = _grid.Rows[0].Cells[0];
                MLeaderVertexService.Highlight(_doc, result.ObjectId);
            }

            _preview.Invalidate();
            SetStatus("Picked new MLeader.");
            AppLog.Info("MLeaderToolForm", "Picked new MLeader for editing: " + result.ObjectId.ToString());
        }

        private void AddVertexButton_Click(object sender, EventArgs e)
        {
            AppLog.CommandStart("MLeaderToolForm", "AddVertexButton_Click(object sender, EventArgs e)");
            RefreshDocument(AcAp.DocumentManager.MdiActiveDocument);

            PreviewEntity selected = GetSelectedPreviewEntity();
            if (selected == null)
            {
                AppLog.Warn("MLeaderToolForm", "No selected row. Use Load selection or Pick MLeader, then select an MLeader row.");
                SetStatus("No MLeader row selected.");
                MessageBox.Show(this, "Select an MLeader row first.", "MLeader Vertex Tool", MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }

            if (!string.Equals(selected.EntityType, "MLeader", StringComparison.OrdinalIgnoreCase))
            {
                AppLog.Warn("MLeaderToolForm", "Selected row is not an MLeader: " + selected.EntityType);
                SetStatus("Selected row is not an MLeader.");
                MessageBox.Show(this, "The selected row is not an MLeader.", "MLeader Vertex Tool", MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }

            Editor ed = _doc.Editor;
            ed.WriteMessage("\n[MLeaderVertexTool] Pick new inflection/bend vertex for handle {0}: ", selected.Handle);
            PromptPointResult pointResult = ed.GetPoint("\nPick new inflection/bend vertex: ");
            if (pointResult.Status != PromptStatus.OK)
            {
                AppLog.Info("MLeaderToolForm", "Point pick cancelled.");
                SetStatus("Point pick cancelled.");
                return;
            }

            Point3d bendPointWcs = pointResult.Value.TransformBy(ed.CurrentUserCoordinateSystem);
            string message;
            bool ok = MLeaderVertexService.AddInflectionVertex(_doc, selected.ObjectId, bendPointWcs, out message);
            ed.WriteMessage("\n[MLeaderVertexTool] {0}\n", message);
            SetStatus(message);

            if (!ok)
            {
                MessageBox.Show(this, message, "MLeader Vertex Tool", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }

            // Refresh preview after an edit using the same ObjectIds, without asking the user to select again.
            RefreshPreviewFromCurrentEntities(selected.Handle);
        }

        private void ClearAllButton_Click(object sender, EventArgs e)
        {
            AppLog.CommandStart("MLeaderToolForm", "ClearAllButton_Click(object sender, EventArgs e)");
            RefreshDocument(AcAp.DocumentManager.MdiActiveDocument);

            MLeaderVertexService.ClearHighlight(_doc);
            _entities.Clear();

            _grid.SelectionChanged -= Grid_SelectionChanged;
            try
            {
                _grid.ClearSelection();
                _grid.CurrentCell = null;
                _grid.Rows.Clear();
            }
            finally
            {
                _grid.SelectionChanged += Grid_SelectionChanged;
            }

            _preview.Invalidate();
            SetStatus("Cleared entities and viewport highlight.");
            AppLog.Info("MLeaderToolForm", "Cleared preview entities, grid rows, current cell, and viewport highlight.");
        }

        private void ClearHighlightButton_Click(object sender, EventArgs e)
        {
            AppLog.CommandStart("MLeaderToolForm", "ClearHighlightButton_Click(object sender, EventArgs e)");
            MLeaderVertexService.ClearHighlight(_doc);
            _grid.ClearSelection();
            _preview.Invalidate();
            SetStatus("Viewport highlight cleared.");
        }

        private void OpenLogsButton_Click(object sender, EventArgs e)
        {
            AppLog.CommandStart("MLeaderToolForm", "OpenLogsButton_Click(object sender, EventArgs e)");

            try
            {
                System.IO.Directory.CreateDirectory(AppLog.LogsFolder);
                Process.Start("explorer.exe", AppLog.LogsFolder);
                SetStatus("Opened log folder.");
            }
            catch (Exception ex)
            {
                AppLog.Error("MLeaderToolForm", "Failed to open log folder.", ex);
                SetStatus("Open logs failed.");
                MessageBox.Show(this, ex.Message, "Open logs failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private void CopyLogButton_Click(object sender, EventArgs e)
        {
            AppLog.CommandStart("MLeaderToolForm", "CopyLogButton_Click(object sender, EventArgs e)");

            if (!string.IsNullOrEmpty(_log.Text))
            {
                Clipboard.SetText(_log.Text);
                SetStatus("Visible log copied.");
                AppLog.Info("MLeaderToolForm", "Copied visible log text to clipboard.");
            }
        }

        private void Grid_SelectionChanged(object sender, EventArgs e)
        {
            PreviewEntity selected = GetSelectedPreviewEntity();
            if (selected == null)
            {
                _preview.Invalidate();
                return;
            }

            MLeaderVertexService.Highlight(_doc, selected.ObjectId);
            _preview.Invalidate();
            SetStatus("Selected handle " + selected.Handle + ".");
        }

        private void RebuildGrid()
        {
            _grid.SelectionChanged -= Grid_SelectionChanged;
            try
            {
                _grid.Rows.Clear();

                foreach (PreviewEntity entity in _entities)
                {
                    int rowIndex = _grid.Rows.Add(entity.Handle, entity.EntityType, entity.Layer, entity.PointCount);
                    _grid.Rows[rowIndex].Tag = entity;
                }

                if (_grid.Rows.Count > 0)
                {
                    _grid.CurrentCell = _grid.Rows[0].Cells[0];
                    _grid.Rows[0].Selected = true;
                }
            }
            finally
            {
                _grid.SelectionChanged += Grid_SelectionChanged;
            }

            PreviewEntity selected = GetSelectedPreviewEntity();
            if (selected != null)
            {
                MLeaderVertexService.Highlight(_doc, selected.ObjectId);
            }

            AppLog.Info("MLeaderToolForm", "Grid rebuilt with rows: " + _grid.Rows.Count);
        }

        private void RefreshPreviewFromCurrentEntities(string preferredHandle)
        {
            List<ObjectId> ids = new List<ObjectId>();
            foreach (PreviewEntity entity in _entities)
            {
                ids.Add(entity.ObjectId);
            }

            IList<PreviewEntity> refreshed = PreviewCollector.CollectByObjectIds(_doc, ids);
            _entities.Clear();
            _entities.AddRange(refreshed);
            RebuildGrid();

            if (!string.IsNullOrEmpty(preferredHandle))
            {
                foreach (DataGridViewRow row in _grid.Rows)
                {
                    PreviewEntity entity = row.Tag as PreviewEntity;
                    if (entity != null && string.Equals(entity.Handle, preferredHandle, StringComparison.OrdinalIgnoreCase))
                    {
                        row.Selected = true;
                        _grid.CurrentCell = row.Cells[0];
                        break;
                    }
                }
            }

            _preview.Invalidate();
        }

        private PreviewEntity GetSelectedPreviewEntity()
        {
            if (_grid.SelectedRows.Count == 0)
            {
                return null;
            }

            return _grid.SelectedRows[0].Tag as PreviewEntity;
        }

        private void Preview_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
            e.Graphics.Clear(C3DTheme.CodeBg);

            using (Pen borderPen = new Pen(C3DTheme.Border, 1.0f))
            {
                Rectangle border = _preview.ClientRectangle;
                border.Width -= 1;
                border.Height -= 1;
                e.Graphics.DrawRectangle(borderPen, border);
            }

            if (_entities.Count == 0)
            {
                using (Brush brush = new SolidBrush(C3DTheme.Muted))
                {
                    e.Graphics.DrawString("Pick an MLeader or load selection to preview geometry.", C3DTheme.FontNormal, brush, new PointF(12, 12));
                }
                return;
            }

            Rectangle client = _preview.ClientRectangle;
            if (client.Width < 20 || client.Height < 20)
            {
                return;
            }

            double minX;
            double minY;
            double maxX;
            double maxY;
            if (!TryGetExtents(out minX, out minY, out maxX, out maxY))
            {
                return;
            }

            const float margin = 18.0f;
            double worldWidth = Math.Max(maxX - minX, 1.0);
            double worldHeight = Math.Max(maxY - minY, 1.0);
            double scaleX = (client.Width - (2.0 * margin)) / worldWidth;
            double scaleY = (client.Height - (2.0 * margin)) / worldHeight;
            double scale = Math.Min(scaleX, scaleY);

            PreviewEntity selected = GetSelectedPreviewEntity();

            foreach (PreviewEntity entity in _entities)
            {
                if (entity.Points.Count < 2)
                {
                    continue;
                }

                PointF[] screenPoints = new PointF[entity.Points.Count];
                for (int i = 0; i < entity.Points.Count; i++)
                {
                    screenPoints[i] = WorldToScreen(entity.Points[i], minX, minY, scale, margin, client.Height);
                }

                bool isSelected = selected != null && selected.ObjectId == entity.ObjectId;
                using (Pen pen = new Pen(isSelected ? C3DTheme.Accent : C3DTheme.Fg, isSelected ? 2.25f : 1.0f))
                {
                    e.Graphics.DrawLines(pen, screenPoints);
                }

                if (isSelected)
                {
                    using (Brush brush = new SolidBrush(C3DTheme.Accent))
                    {
                        for (int i = 0; i < screenPoints.Length; i++)
                        {
                            e.Graphics.FillEllipse(brush, screenPoints[i].X - 2.5f, screenPoints[i].Y - 2.5f, 5.0f, 5.0f);
                        }
                    }
                }
            }
        }

        private bool TryGetExtents(out double minX, out double minY, out double maxX, out double maxY)
        {
            minX = double.MaxValue;
            minY = double.MaxValue;
            maxX = double.MinValue;
            maxY = double.MinValue;

            bool found = false;
            foreach (PreviewEntity entity in _entities)
            {
                foreach (Point2d point in entity.Points)
                {
                    minX = Math.Min(minX, point.X);
                    minY = Math.Min(minY, point.Y);
                    maxX = Math.Max(maxX, point.X);
                    maxY = Math.Max(maxY, point.Y);
                    found = true;
                }
            }

            return found;
        }

        private PointF WorldToScreen(Point2d point, double minX, double minY, double scale, float margin, int clientHeight)
        {
            float x = (float)(margin + ((point.X - minX) * scale));
            float y = (float)(clientHeight - margin - ((point.Y - minY) * scale));
            return new PointF(x, y);
        }

        private void SetStatus(string message)
        {
            if (_statusLabel != null)
            {
                _statusLabel.Text = message;
            }

            if (_versionLabel != null)
            {
                _versionLabel.Text = "Loaded: " + BuildInfo.LoadVersion;
            }
        }

        private void OnLogLineWritten(object sender, LogLineEventArgs e)
        {
            if (IsDisposed)
            {
                return;
            }

            if (InvokeRequired)
            {
                BeginInvoke(new EventHandler<LogLineEventArgs>(OnLogLineWritten), sender, e);
                return;
            }

            if (_log.TextLength > MaxVisibleLogCharacters)
            {
                string current = _log.Text;
                _log.Text = current.Substring(current.Length - (MaxVisibleLogCharacters / 2));
                _log.SelectionStart = _log.TextLength;
                _log.ScrollToCaret();
            }

            _log.AppendText(e.Line + Environment.NewLine);
        }
    }
}


// ===== FILE: Models\PreviewEntity.cs =====
using System.Collections.Generic;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;

namespace MLeaderVertexTool.Models
{
    internal sealed class PreviewEntity
    {
        public PreviewEntity(ObjectId objectId, string handle, string entityType, string layer, IList<Point2d> points)
        {
            ObjectId = objectId;
            Handle = handle;
            EntityType = entityType;
            Layer = layer;
            Points = new List<Point2d>(points);
        }

        public ObjectId ObjectId { get; private set; }
        public string Handle { get; private set; }
        public string EntityType { get; private set; }
        public string Layer { get; private set; }
        public List<Point2d> Points { get; private set; }

        public int PointCount
        {
            get { return Points.Count; }
        }
    }
}


// ===== FILE: Services\PreviewCollector.cs =====
using System;
using System.Collections.Generic;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using MLeaderVertexTool.Models;

namespace MLeaderVertexTool.Services
{
    internal static class PreviewCollector
    {
        public static IList<PreviewEntity> SelectAndCollect(Document doc)
        {
            const string Source = "PreviewCollector";
            AppLog.CommandStart(Source, "SelectAndCollect(Document doc)");

            List<PreviewEntity> result = new List<PreviewEntity>();

            if (doc == null)
            {
                AppLog.Warn(Source, "No active document.");
                return result;
            }

            Editor ed = doc.Editor;
            PromptSelectionOptions options = new PromptSelectionOptions();
            options.MessageForAdding = "\nSelect curves/MLeaders for preview: ";
            options.AllowDuplicates = false;
            options.SingleOnly = false;

            PromptSelectionResult selectionResult = ed.GetSelection(options);
            if (selectionResult.Status != PromptStatus.OK)
            {
                AppLog.Info(Source, "Selection cancelled or empty.");
                return result;
            }

            using (Transaction tr = doc.Database.TransactionManager.StartTransaction())
            {
                foreach (SelectedObject selectedObject in selectionResult.Value)
                {
                    if (selectedObject == null || selectedObject.ObjectId == ObjectId.Null)
                    {
                        continue;
                    }

                    Entity entity = tr.GetObject(selectedObject.ObjectId, OpenMode.ForRead, false) as Entity;
                    if (entity == null)
                    {
                        continue;
                    }

                    List<Point2d> points = CollectEntityPoints(entity, tr);
                    if (points.Count < 2)
                    {
                        continue;
                    }

                    result.Add(new PreviewEntity(
                        selectedObject.ObjectId,
                        entity.Handle.ToString(),
                        entity.GetType().Name,
                        entity.Layer,
                        points));
                }

                tr.Commit();
            }

            AppLog.Info(Source, "Collected preview entities: " + result.Count);
            return result;
        }


        public static IList<PreviewEntity> CollectByObjectIds(Document doc, IEnumerable<ObjectId> objectIds)
        {
            const string Source = "PreviewCollector";
            AppLog.CommandStart(Source, "CollectByObjectIds(Document doc, IEnumerable<ObjectId> objectIds)");

            List<PreviewEntity> result = new List<PreviewEntity>();

            if (doc == null || objectIds == null)
            {
                return result;
            }

            using (Transaction tr = doc.Database.TransactionManager.StartTransaction())
            {
                foreach (ObjectId objectId in objectIds)
                {
                    if (objectId == ObjectId.Null || objectId.IsErased)
                    {
                        continue;
                    }

                    Entity entity = tr.GetObject(objectId, OpenMode.ForRead, false) as Entity;
                    if (entity == null)
                    {
                        continue;
                    }

                    List<Point2d> points = CollectEntityPoints(entity, tr);
                    if (points.Count < 2)
                    {
                        continue;
                    }

                    result.Add(new PreviewEntity(
                        objectId,
                        entity.Handle.ToString(),
                        entity.GetType().Name,
                        entity.Layer,
                        points));
                }

                tr.Commit();
            }

            AppLog.Info(Source, "Re-collected preview entities by ObjectId: " + result.Count);
            return result;
        }

        private static List<Point2d> CollectEntityPoints(Entity entity, Transaction tr)
        {
            Polyline polyline = entity as Polyline;
            if (polyline != null)
            {
                return CollectPolylinePoints(polyline);
            }

            Polyline2d polyline2d = entity as Polyline2d;
            if (polyline2d != null)
            {
                return CollectPolyline2dPoints(polyline2d, tr);
            }

            Polyline3d polyline3d = entity as Polyline3d;
            if (polyline3d != null)
            {
                return CollectPolyline3dPoints(polyline3d, tr);
            }

            MLeader mleader = entity as MLeader;
            if (mleader != null)
            {
                return CollectMLeaderPoints(mleader);
            }

            Curve curve = entity as Curve;
            if (curve != null)
            {
                return CollectCurveSamplePoints(curve, 64);
            }

            return new List<Point2d>();
        }

        private static List<Point2d> CollectPolylinePoints(Polyline polyline)
        {
            List<Point2d> points = new List<Point2d>();
            for (int i = 0; i < polyline.NumberOfVertices; i++)
            {
                points.Add(polyline.GetPoint2dAt(i));
            }

            if (polyline.Closed && points.Count > 0)
            {
                points.Add(points[0]);
            }

            return points;
        }

        private static List<Point2d> CollectPolyline2dPoints(Polyline2d polyline, Transaction tr)
        {
            List<Point2d> points = new List<Point2d>();
            foreach (ObjectId vertexId in polyline)
            {
                Vertex2d vertex = tr.GetObject(vertexId, OpenMode.ForRead, false) as Vertex2d;
                if (vertex != null)
                {
                    points.Add(ToPoint2d(vertex.Position));
                }
            }

            if (polyline.Closed && points.Count > 0)
            {
                points.Add(points[0]);
            }

            return points;
        }

        private static List<Point2d> CollectPolyline3dPoints(Polyline3d polyline, Transaction tr)
        {
            List<Point2d> points = new List<Point2d>();
            foreach (ObjectId vertexId in polyline)
            {
                PolylineVertex3d vertex = tr.GetObject(vertexId, OpenMode.ForRead, false) as PolylineVertex3d;
                if (vertex != null)
                {
                    points.Add(ToPoint2d(vertex.Position));
                }
            }

            if (polyline.Closed && points.Count > 0)
            {
                points.Add(points[0]);
            }

            return points;
        }

        private static List<Point2d> CollectMLeaderPoints(MLeader mleader)
        {
            List<Point2d> points = new List<Point2d>();

            try
            {
                System.Collections.ArrayList leaderIndexes = mleader.GetLeaderIndexes();
                if (leaderIndexes == null || leaderIndexes.Count == 0)
                {
                    return points;
                }

                int leaderIndex = Convert.ToInt32(leaderIndexes[0]);
                System.Collections.ArrayList leaderLineIndexes = mleader.GetLeaderLineIndexes(leaderIndex);
                if (leaderLineIndexes == null || leaderLineIndexes.Count == 0)
                {
                    return points;
                }

                int leaderLineIndex = Convert.ToInt32(leaderLineIndexes[0]);
                int vertexCount = mleader.VerticesCount(leaderLineIndex);
                for (int i = 0; i < vertexCount; i++)
                {
                    points.Add(ToPoint2d(mleader.GetVertex(leaderLineIndex, i)));
                }
            }
            catch (Exception ex)
            {
                AppLog.Error("PreviewCollector", "Failed to collect MLeader preview points.", ex);
            }

            return points;
        }

        private static List<Point2d> CollectCurveSamplePoints(Curve curve, int sampleCount)
        {
            List<Point2d> points = new List<Point2d>();

            try
            {
                double startParam = curve.StartParam;
                double endParam = curve.EndParam;

                if (double.IsNaN(startParam) || double.IsNaN(endParam))
                {
                    return points;
                }

                if (sampleCount < 2)
                {
                    sampleCount = 2;
                }

                for (int i = 0; i < sampleCount; i++)
                {
                    double t = (double)i / (double)(sampleCount - 1);
                    double param = startParam + ((endParam - startParam) * t);
                    Point3d point = curve.GetPointAtParameter(param);
                    points.Add(ToPoint2d(point));
                }
            }
            catch (Exception ex)
            {
                AppLog.Error("PreviewCollector", "Failed to sample curve points.", ex);
            }

            return points;
        }

        private static Point2d ToPoint2d(Point3d point)
        {
            return new Point2d(point.X, point.Y);
        }
    }
}


// ===== FILE: Services\MLeaderVertexService.cs =====
using System;
using System.Collections;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;

namespace MLeaderVertexTool.Services
{
    internal static class MLeaderVertexService
    {
        public static bool AddInflectionVertex(Document doc, ObjectId mleaderId, Point3d bendPointWcs, out string message)
        {
            const string Source = "MLeaderVertexService";
            AppLog.CommandStart(Source, "AddInflectionVertex(Document doc, ObjectId mleaderId, Point3d bendPointWcs, out string message)");

            message = string.Empty;

            if (doc == null)
            {
                message = "No active AutoCAD document.";
                AppLog.Warn(Source, message);
                return false;
            }

            try
            {
                using (doc.LockDocument())
                using (Transaction tr = doc.Database.TransactionManager.StartTransaction())
                {
                    MLeader mleader = tr.GetObject(mleaderId, OpenMode.ForWrite, false) as MLeader;
                    if (mleader == null)
                    {
                        message = "Selected object is not an MLeader.";
                        AppLog.Warn(Source, message);
                        return false;
                    }

                    int leaderIndex = GetFirstLeaderIndex(mleader);
                    int leaderLineIndex = GetFirstLeaderLineIndex(mleader, leaderIndex);
                    int vertexCount = mleader.VerticesCount(leaderLineIndex);

                    AppLog.Info(Source, string.Format(
                        "Object handle {0}; leaderIndex={1}; leaderLineIndex={2}; vertexCount={3}; pickedPoint={4}",
                        mleader.Handle,
                        leaderIndex,
                        leaderLineIndex,
                        vertexCount,
                        bendPointWcs));

                    if (vertexCount < 1)
                    {
                        message = "MLeader leader line has no editable vertices.";
                        AppLog.Warn(Source, message);
                        return false;
                    }

                    if (vertexCount == 1)
                    {
                        // Rare case. Append the picked point so the leader line has a second point.
                        mleader.AddLastVertex(leaderLineIndex, bendPointWcs);
                    }
                    else
                    {
                        // AutoCAD's MLeader API has AddLastVertex/SetVertex, but no direct InsertVertex.
                        // To add an inflection before the text-side tail:
                        // 1. read the current last vertex,
                        // 2. append a duplicate of that last vertex,
                        // 3. replace the previous last vertex with the new bend point.
                        Point3d oldTail = mleader.GetVertex(leaderLineIndex, vertexCount - 1);
                        mleader.AddLastVertex(leaderLineIndex, oldTail);
                        mleader.SetVertex(leaderLineIndex, vertexCount - 1, bendPointWcs);
                    }

                    mleader.RecordGraphicsModified(true);
                    tr.Commit();
                }

                doc.Editor.Regen();
                message = "MLeader vertex added successfully.";
                AppLog.Info(Source, message);
                return true;
            }
            catch (Autodesk.AutoCAD.Runtime.Exception ex)
            {
                message = "AutoCAD error while adding MLeader vertex: " + ex.Message;
                AppLog.Error(Source, message, ex);
                return false;
            }
            catch (Exception ex)
            {
                message = "Unexpected error while adding MLeader vertex: " + ex.Message;
                AppLog.Error(Source, message, ex);
                return false;
            }
        }

        public static bool TryGetObjectIdFromHandle(Database db, string handleText, out ObjectId objectId, out string message)
        {
            const string Source = "MLeaderVertexService";
            AppLog.CommandStart(Source, "TryGetObjectIdFromHandle(Database db, string handleText, out ObjectId objectId, out string message)");

            objectId = ObjectId.Null;
            message = string.Empty;

            if (db == null)
            {
                message = "Database is null.";
                return false;
            }

            if (string.IsNullOrWhiteSpace(handleText))
            {
                message = "Handle is empty.";
                return false;
            }

            try
            {
                long handleValue = Convert.ToInt64(handleText.Trim(), 16);
                Handle handle = new Handle(handleValue);
                objectId = db.GetObjectId(false, handle, 0);

                if (objectId == ObjectId.Null)
                {
                    message = "No object was found for handle " + handleText + ".";
                    AppLog.Warn(Source, message);
                    return false;
                }

                message = "Resolved handle " + handleText + " to ObjectId.";
                AppLog.Info(Source, message);
                return true;
            }
            catch (Exception ex)
            {
                message = "Invalid or unresolved handle " + handleText + ": " + ex.Message;
                AppLog.Error(Source, message, ex);
                return false;
            }
        }

        public static void Highlight(Document doc, ObjectId objectId)
        {
            const string Source = "MLeaderVertexService";

            if (doc == null || objectId == ObjectId.Null)
            {
                return;
            }

            try
            {
                doc.Editor.SetImpliedSelection(new[] { objectId });
                doc.Editor.UpdateScreen();
                AppLog.Debug(Source, "Highlighted object " + objectId.ToString());
            }
            catch (Exception ex)
            {
                AppLog.Error(Source, "Failed to highlight object.", ex);
            }
        }

        public static void ClearHighlight(Document doc)
        {
            const string Source = "MLeaderVertexService";

            if (doc == null)
            {
                return;
            }

            try
            {
                doc.Editor.SetImpliedSelection(new ObjectId[0]);
                doc.Editor.UpdateScreen();
                AppLog.Debug(Source, "Cleared implied selection highlight.");
            }
            catch (Exception ex)
            {
                AppLog.Error(Source, "Failed to clear highlight.", ex);
            }
        }

        private static int GetFirstLeaderIndex(MLeader mleader)
        {
            ArrayList leaderIndexes = mleader.GetLeaderIndexes();
            if (leaderIndexes == null || leaderIndexes.Count == 0)
            {
                throw new InvalidOperationException("MLeader has no leader clusters.");
            }

            return Convert.ToInt32(leaderIndexes[0]);
        }

        private static int GetFirstLeaderLineIndex(MLeader mleader, int leaderIndex)
        {
            ArrayList leaderLineIndexes = mleader.GetLeaderLineIndexes(leaderIndex);
            if (leaderLineIndexes == null || leaderLineIndexes.Count == 0)
            {
                throw new InvalidOperationException("MLeader has no leader lines for leader index " + leaderIndex + ".");
            }

            return Convert.ToInt32(leaderLineIndexes[0]);
        }
    }
}


// ===== FILE: Properties\AssemblyInfo.cs =====
using System.Reflection;
using System.Runtime.InteropServices;

[assembly: AssemblyTitle("MLeaderVertexTool")]
[assembly: AssemblyDescription("AutoCAD/Civil 3D MLeader vertex editor with preview, highlighting, and persistent logging.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("DB E.C.O. Group")]
[assembly: AssemblyProduct("MLeaderVertexTool")]
[assembly: AssemblyCopyright("Copyright © DB E.C.O. Group 2026")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

[assembly: ComVisible(false)]
[assembly: Guid("c4fae40e-a1d7-4e2a-91e5-2f8f6cb3a471")]

[assembly: AssemblyVersion("1.0.8.0")]
[assembly: AssemblyFileVersion("1.0.8.0")]


// ===== FILE: Services\AppLog.cs =====
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;

namespace MLeaderVertexTool.Services
{
    internal static class AppLog
    {
        private static readonly object SyncRoot = new object();
        private static readonly string RootFolder = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "MLeaderVertexTool");

        private static readonly string LogFolder = Path.Combine(RootFolder, "Logs");
        private static readonly string DailyLogFile = Path.Combine(LogFolder, DateTime.Now.ToString("yyyyMMdd") + ".log");
        private static readonly string LoadHistoryFile = Path.Combine(LogFolder, "load-history.log");

        public static event EventHandler<LogLineEventArgs> LineWritten;

        public static string LogsFolder
        {
            get { return LogFolder; }
        }

        public static void Initialize()
        {
            Directory.CreateDirectory(LogFolder);
        }

        public static void Loaded(string source)
        {
            Initialize();

            string assemblyPath = Assembly.GetExecutingAssembly().Location;
            string acadProcess = Process.GetCurrentProcess().ProcessName;

            Info(source, "Loaded " + BuildInfo.LoadVersion);
            if (!string.IsNullOrWhiteSpace(BuildInfo.BuildStamp))
            {
                Info(source, "BuildStamp = " + BuildInfo.BuildStamp);
            }
            if (!string.IsNullOrWhiteSpace(BuildInfo.NetLoadPath))
            {
                Info(source, "NetLoadPath = " + BuildInfo.NetLoadPath);
            }
            Info(source, "AssemblyPath = " + assemblyPath);
            Info(source, "HostProcess = " + acadProcess);
            WriteLoadHistory(source, assemblyPath, acadProcess);
        }

        public static void Debug(string source, string message)
        {
            Write("DEBUG", source, message, null);
        }

        public static void Info(string source, string message)
        {
            Write("INFO", source, message, null);
        }

        public static void Warn(string source, string message)
        {
            Write("WARN", source, message, null);
        }

        public static void Error(string source, string message, Exception exception)
        {
            Write("ERROR", source, message, exception);
        }

        public static void CommandStart(string source, string signature)
        {
            Info(source, signature);
        }

        private static void WriteLoadHistory(string source, string assemblyPath, string acadProcess)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
            sb.Append("  ");
            sb.Append(BuildInfo.LoadVersion);
            if (!string.IsNullOrWhiteSpace(BuildInfo.BuildStamp))
            {
                sb.Append("  BuildStamp=");
                sb.Append(BuildInfo.BuildStamp);
            }
            sb.Append("  [");
            sb.Append(source);
            sb.Append("]  ");
            sb.Append(acadProcess);
            sb.Append("  ");
            sb.Append(assemblyPath);

            lock (SyncRoot)
            {
                File.AppendAllText(LoadHistoryFile, sb.ToString() + Environment.NewLine, Encoding.UTF8);
            }
        }

        private static void Write(string level, string source, string message, Exception exception)
        {
            Initialize();

            string line = FormatLine(level, source, message);
            if (exception != null)
            {
                line += Environment.NewLine + exception;
            }

            lock (SyncRoot)
            {
                File.AppendAllText(DailyLogFile, line + Environment.NewLine, Encoding.UTF8);
            }

            EventHandler<LogLineEventArgs> handler = LineWritten;
            if (handler != null)
            {
                handler(null, new LogLineEventArgs(line));
            }
        }

        private static string FormatLine(string level, string source, string message)
        {
            // Example: 05:29:43  [INFO]   [PvSplitApi]  SplitDataLines.Add(Double rawStation, Double datum)
            return string.Format(
                "{0}  [{1}]   [{2}]  {3}",
                DateTime.Now.ToString("HH:mm:ss"),
                level,
                source,
                message);
        }
    }

    internal sealed class LogLineEventArgs : EventArgs
    {
        public LogLineEventArgs(string line)
        {
            Line = line;
        }

        public string Line { get; private set; }
    }
}

Additional info:

Share this page:

Page Tag: 🏷️ Annotation Editing

  • MLeaderExtra

    Short descriptionAutoCAD/Civil 3D .NET plugin for adding an extra inflection or bend vertex to an existing MLeader, with command-line tools, handle-based editing, modeless WinForms UI, preview/highlight support, logs, and versioned NETLOAD build output. Command:MLTOOLS – open the MLeader Vertex Tool panel. MLADDVERT – select an MLeader and pick a new bend/inflection vertex. MLADDVERT_HANDLE – add…