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.
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; }
}
}
